Building a cart in AngularJS - 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.

Related

Angular + ui-router parallel states architecture

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.

Angular UI nested View with different parameter

angular: "1.3.15"
angular-ui-router: "~0.2.13"
TL;DR: How should I provide a value to a nested view?
For brevity's sake I'm going to greatly simplify the application in an attempt to focus on the problem. I have two main views called Customer and Refunds. Each main view has a shared nested view called Notes. The Notes view loads and displays notes relative to the customer at hand using a customerId. The Customer view's route has a customerId, but the Refunds does not. When viewing the Refunds view the notes section is hidden by default. Once the user selects one of the Refunds in the list I want the Notes view to load notes for the customer selected. Since the Notes controller is using $stateParams.customerId it will be empty when loading on the Refunds view. The Refunds controller knows which customerId was selected, but I need some way to tell the nested Notes view which customerId to load.
So far I have come up with a few options, but I'm not sure I like any of them, but here they are.
When the user selects a customer on the refunds page I can navigate to /refunds/:customerId. Don't like that because the page reloads and I have to set the selected item after the page reloads from the stateParams.
After the user selects a customer, use $provide to provide a customerId which gets injected into the NotesController. Don't like that because it's tacky and requires me to do the same thing on the CustomerController.
Caveat: I'm using ControllerAs syntax so scopes don't inherit each other. This prevents me from setting a customerId on the main scope and referencing it in the nested scope.
In your parent scope, store the customerId in the current scope ($scope.customerId = $stateParams.customerId), and on the child scope, get the customer ID from its parent by$scope.$parent.customerId

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: ng-model not getting applied on Controller instantiation

I am creating a web app planner using Angular and I am having some difficulties with a <select> box that is not changing value based on the variable denoted with ng-model.
My architecture is as follows:
I am using ui-router which gives me different view states, one for each page of my planner. The root HTML page has a Controller called MainController. This is where I set up my JSON model, $scope.Master = {} that I want to use throughout the planner. All pages of the planner should inherit this model and continue to modify/add to it.
I then have my 4 pages of the planner like:
Start -> Accounts -> Settings -> Review
Each page has its own Controller that gets instantiated every time I visit the page. On the Start page, I have a <select> box that has ng-model="$scope.Master.Start.selectedAccount" that gets populated dynamically using the StartController (therefore it gets populated every time I come to the Start page).
This <select> works great on the first time to the page, but if I go to Accounts and then come back, the select box is back to the default value, "Please select an account", instead of the selected account that is in the $scope.Master.Start.selectedAccount model that is bound to the <select> box
I thought I could just do something like $scope.$apply or something in order to re-apply the binding to the DOM object, but that just gave me an error saying it is already digesting.
How can I apply the binding to the <select> box after the page has been loaded 2 or more times?
This is probably because every time you go back to your original page, a new controller is instantiated since it was removed from the DOM when you left it originally. Thus $scope.Master.Start.selectedAccount. To save this, you can either
Use a service / factory singleton on the main app to save this value
https://docs.angularjs.org/guide/services#!
Save $scope.Master.Start.selectedAccount as a global variable
https://docs.angularjs.org/api/ng/service/$rootScope
Put that controller on the outside
Im real sorry... I realized I was populating the <select> using ng-repeat instead of ng-options no idea how I managed that... That was the problem

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