Angular + ui-router parallel states architecture - angularjs

I'm designing a 'custom portal' and i'm stuck on how to design our application in the right way. The portal has a view as on picture below where:
Shopping carts dropdown where user can select 'current' shopping cart(can be empty)
a. Button that redirect to 'current' shopping cart details page.
Application menu. Where we can navigate f.e to Catalogs
The main application area.
I have the following workflow:
User goes to catalogs menu.
url: domain/catalogs; state: catalogs
User select a catalog and see products in the catalog
url: domain/catalogs/catalog1ID; state:catalogs.detail.
User can click on product tile and go to product detail view
url: domain/catalogs/catalog1ID/product1ID;state:catalogs.detail.product
Looks natural and nothing special for now. Now the main problem how to integrate to this workflow a 'shopping cart' functionality. I mean that if user select a shopping cart the data on views might be different(Because shopping cart related to different customers and we can show different catalogs/price/products for different customers). F.e The price for Product1 from Catalog1 might be different for shopping cart 1 and shopping cart 2 and shopping cart not selected. I'm trying to find answers for next questions:
Where to keep selected shopping cart(state/url)?
How to inform current state that shopping cart changed?
How to make F5/refresh works correctly(f.e if my current state is catalogs.detail.product and i have a shopping cart selected)?

Sounds like a lot of questions need to be answered, I'll try to provide some guidance to point you in the right direction, but of course this will need to be tailored to your application.
Always keep scope in perspective. For example, lets say your top level scope (rootScope) stores shopping cart data. Any other state within your app will have access to those rootScope properties. So if you need data that persists across multiple states, storing in something like a rootScope would give child states access. Now please note, it's probably not a good idea to be storing all your data in the app rootScope, it would make much more sense to store data in individual controllers, and give custom directives access to those controllers. This will keep things much more modular and won't pollute the global rootScope
This can easily be done by creating a function within a directive that modifies the scope that holds the shopping cart data.
Sample Directive:
app.directive('pagedirective',function(){
//directive definition object
return{
restrict:'E'
templateUrl:'path to html template'
link:function(scope){
scope.update_Shopping_Cart_In_Controller = function(item){
scope.shopping_Cart_Array.push(item)
}
}
}
})
Sample Controller:
app.controller('shoppingCartCtrl',function($scope){
//this will get updated from the directive
$scope.shopping_Cart_Arr = [];
})
main page html:
<div ng-controller="shoppingCartCtrl">
<p ng-repeat="items in shopping_Cart_Arr">{{items}}</p>
<pagedirective></pagedirective?
</div>
Directive html:
<div>
<button ng-click="update_Shopping_Cart_In_Controller('new toy')">add item</button>
</div>
Obviously you will want your directive html to be much smarter than this, but I just wanted to the pattern across of passing data to the controller from a directive.
If you refresh your a page in your app, you will lose any data that is not initialized within a controller on load. For example, if you add items to your shopping cart controller on one page, and then refresh your browser, data added to that controller will be lost. Here are a couple approaches to solving that issue.
Saving the data to sessionStorage or localStorage. This data will not be lost during refresh and keep me used to initialize data within a controller.
Save data to a user table within the backend of your app, and then retrieve that data when needed.
Let me know if anything is unclear, but hopefully this points you in the right direction.

Related

Building a cart in AngularJS

I have built a client side cart in angularjs, my application allows me to sell tickets, all kinds of tickets; bus, airline, train etc.
Now i have a page for each type of ticket i want to sell. Each page has it's own controller and scope.
Now my problem is since each page has it's on controller and scope and my cart also has it's own controller and scope whenever i add an item from the ticket page, i store it in a scope object, save it in the localStorage and broadcast a value(CartUpdated) of true or false. Now in the cart controller it watches for that broadcast and gets the cart from localStorage and saves it in a new scope object, after which i loop through it and display the contents in the cart.
My issues here is i keep having to deal with multiple scope objects for a single operation.
I have to update the scope in my ticket controller and then update the scope in my cart controller.
What i want to know is is there a way i can make my cart global within my application? Sort of like a static class with static values easily accessed from any controller at all in my application.
This will mean that all functions: Add, Remove, Update, will be located in the cart controller itself and i will simply call them from all my ticket pages instead of the way i have it now with add on y ticket page and Remove and Update on my cart.

is it right to do using ui-router to activate menu items. Any advices to help me understand this?

What I did is:
When a menu item is clicked, a action will be done, like deleting a user, sending emails to a group, etc. To this end, for each menu item, I define a ui-router state, and use the state url to activate the state via sref. I thought that a menu action is just a UI component for user to let users to do something, which is just a state of UI.
I was advised that I was using ui-router in a wrong way as a state url can not identify an action. For example, to delete a group of users, the state url can not tell you what group of users have been deleted.
In short, I agree with your manager while being an angular newbie myself. Angular routes are designed for managing different views of your app. I.e. define a route and corresponding view template for each view. If you add application logic into the routes, your application structure gets quickly a mess and difficult to keep clear.
To me it is much more natural that the views are managed by the routes, and each action in each view is handled by the controller of that view. If the actions grow "big", then it is worth refactoring parts of the controller into separate services. If you require some sort of "dynamic HTML" depending on the action, e.g. bootstrap modals are handy for doing that within the current view (see http://angular-ui.github.io/bootstrap/).
E.g. in my current project, I don't actually manually edit the routes at all but let yeoman angular generator to do that for me free of charge - i.e. I instantiate each new view in my dev.env using the following command (more info on this from https://github.com/yeoman/generator-angular)
yo angular:route myNewView
More info on angular philosophy can be read from angular documentation for developers: https://docs.angularjs.org/guide/concepts
You should probably be doing this actions via a method on $scope.
$scope.deleteItem = function (items) {
Service.delete(items);
};
// which is the same as:
$scope.deleteItem = Service.delete;
<a ng-click="deleteItem(item)">Delete This Item</a>
Having it in the URL just seems wrong. I mean what does that look like? www.mysite.com/delete/users?

AngularJS and UI-Router multi-level breadcrumbs element

I'm using AngularJS and UI-Router to make a little e-commerce product gallery. It should display a grid of images representing categories of products. When the user selects a category, the grid shall display the child categories of that one, and so on, until the user reaches a level where there are only products. Clicking in a product should display its details. Im using 2 states in UI-Router, one for the gallery and one for the details.
app.config(function($stateProvider){
$stateProvider
// Gallery State
.state('gallery', {
url: '/products/:category',
templateUrl: 'views/gallery.html',
controller: 'galleryCtrl'
})
// Details State
.state('details', {
url: '/details/:id',
templateUrl: 'views/details.html',
controller: 'detailsCtrl'
});
});
In the 'gallery' state, the controller uses the :category parameter to get from the database all subcategories or products of that category to display in the grid. The 'details' state's controller uses an analog strategy to get the product's information with the :id parameter.
Now I want to make a breadcrumb element that shows the 'path' the user went through the gallery. For example, if the user selects the "Computers" category, then the "Notebooks" category and then "Foo Notebook", the breadcrumb should display:
Computers > Notebooks > Foo Notebook
I've found solutions that get the state hierarchy from UI-Router to create the breadcrumb. The problem is, with only the 2 states I created, the breadcrumb wouldn't show all categories, just the last selected one. I don't want to create a state for each category because the number and hierarchy of categories can change. Is there any way to accomplish this?
If you're really interested in documenting the way a user got somewhere: One way to go about this would be to create a service and push each state change to a data store in the service and replicated in a $cookieStore -- you can use the state change events for this.
Generally, though, whenever I've done breadcrumbs / hierarchies in the past - there is a definitive way to get to a location. Your example illustrates that; a specific notebook is under a general list category which is in turn under a parent category. So you can infer all parents when you get to a given item page. Not sure which solution best meets your needs.

Many active states simultaneously with ui-router

What would you like to be able to do for example is:
I have an active state 'order.detail' in shell, in these details would provide a link in each product line that leads to 'product.detail' state that is also a state that can be displayed in the shell.
But this link should display the state ' product.detail' as a frame in a dialog without changing the current location and maintain active state in the shell intact.
Also the 'product.detail' state, to be used as a main view of the shell, and to allow their reuse, your template should be wrapped by 'div' template of dialogue.
What I mean is, allow consult the details of something without leaving the current screen, and do so using the same existing details screen, or simply allow the 'Drill down' by related data with existing views.
Sharing state in AngularJS
One of the great things about Angular is that's it quite easy to keep track of state via providers.
For example consider one index view containing a paged grid table with many filter options. Clicking on one of the entries will take you to details view of the entry. When the user goes back from the details to the index he/she will expect that the UI state of the grid will be exactly the way they left it: same page, same sort by, same filters applied, same everything. With traditional techniques you would have to fallback on cookies, query params and/or server side state(less) magic, which all feels (and actually is) very cumbersome and error prone.
Provider values are singletons in the world of Angular, so when we inject the instance in one of the controllers, it will always be the same instance. Controllers on the other hand will be recreated each time one is requested.
Example
Register an empty object to keep track of controllers:
myApp.value('formState', {});
Create a controller, inject the provider value and expose it on the scope:
myApp.controller('MyController', function($scope, formState) {
$scope.formState = formState;
});
Hook any property of the provider value to input elements via the ng-model directive.
<input type="text" ngModel="formState.searchFilter"/>
Now every time the user will leave and re-enters this view the state of the UI is kept intact. You can add as many data to the state as you see fit and maybe even share it among multiple controllers if needed.
Provider types
There are different ways to create provider values: factory, service, value, constant and provider. If you want more control over the state, eg state management, you could use one of the other options. More info can be found here.
To dialog or not to dialog
In traditional websites displaying the details in a dialog was a "cheap" trick to keep track of the state in the background. Of course this is still an option with Angular, but there's no need for it. From the UX view, dialogs are "not done" and should be avoided if possible, but it also introduces pains in the areas of responsiveness and printing.
Plunker examples
Some code examples sharing state among controllers/views.
http://plnkr.co/edit/MwJrk5?p=preview
http://plnkr.co/edit/bNJtOP?p=preview

Angularjs scope not retaining value

Friends..
For my understanding of how routing works in Angular I have created a simple application. This application has only two pages:
1. The first page will display all rows of the employee table. Upon clicking on a particular row, second page will display a form with details of that employee.
The list that is displayed on the first page uses the following code:
<table>
<tr ng-repeat="employee in employees">
<td>{{employee.firstname}} - {{employee. address}}</td>
<td><span ng-click="getSingleEmployeeDetails(employee.id)">Edit</a></td>
</tr>
</table>
I am using the same controller for both these pages and this controller looks like below:
function EmployeeCtrl($scope,$http,Employee,$location,$routeParams) {
// Get all employee details
var data;
Employee.query().then(function(_data) {
$scope.employees = _data.data;
});
// Get Single Employee Details
$scope.getSingleEmployeeDetails = function(id) {
$scope.employee = scope.employees[id];
$location.path('/editemployee/' + id);
}
}
However the issue I am facing is that when the code gets routed to /editemployee/1
for some reason the $scope.employees looses its values.
In other words the form never gets populated with employee details.
What am I doing wrong here ?
This has to do with scoping. The employees are loaded into the EmployeeCtrl when it is instantiated. Once you perform a routing event in getSingleEmployeeDetails() that causes a different controller to load with a different $scope. A $scope that is separate from the $scope inside EmployeeCtrl. One easy way around this is to let EmployeeCtrl handle the functionality of loading/displaying all employees and a single employee without routing to a new controller. The pros here is that it makes it easier to share information, and you don't have to reload the single employee information when the user clicks on a single employee because you can share that information more easily. The con is that you don't get back button navigation to navigate between selections of single employees.
The other option is to let the SingleEmployeeCtrl reload the information when it navigates. The pro is you get back button access again, but the con is you load the information twice (once for loading the full list, and twice for loading the employee information again). This also allows the user to bookmark single employee records, but who bookmarks things anymore?
Others have already explained the fact that a new controller (and $scope) are created when you change routes. Also note that $scope.employees is populated asynchronously, when the promise is resolved. What is likely happening is that getSingleEmployeeDetails() is being called before the promise is resolved, so the employees array is empty.
To solve the problem, I suggest a different architecture.
You have two views/pages. Each view in Angular typically has its own controller. Models/data are typically stored in services, and an API to retrieve and manipulate those models/data is made available/public by the service. A controller just glues everything together: it injects the service(s) it needs, and then references only the models/data that the associated view needs.
So, even though your app is simple, I suggest the above approach: one service (which stores your employee objects), two controllers, two views. In particular, put the query() call into your service (so it will be called once, when the service is created) and store your data in the service. The service API should define functions/methods that return a promise that will eventually contain the desired data (list of employees, or just one). The controllers should use those methods to get a reference to the desired data.
See also Should services expose their asynchronicity? for an example of how to store the data in the service.

Resources