Re-link an existing $scope? - angularjs

Let me set some context first;
I am creating a wizard in angular, with these characteristics:
The steps in the wizard are big; each step has it's own controller / $scope / template.
The steps in the wizard are dynamic (step1's output decides whether the next step is step2a or step2b).
Of course, step-state should be preserved. When stepping back from step2 to step1, the data in the step1 view should still be there.
In my implementation, I've found a lot of inspiration in the codebase for ui-router.
Basically my approach is:
When going to a step, create a $scope for this step and use the $controller service to create the controller
$compile the step-template and link this step-template to the step-$scope
This all works out great.
However, I'm having doubts on my implementation to step back in the wizard:
To achieve this, I'm keeping an array of step-$scope objects for each step that has been taken.
Whenever stepping back (for instance from step2 back to step1), I:
look up the $scope for step1
re-$compile the template for step1 and re-link it to the $scope for step1
This seems to work, however:
Why can't I just re-use the compiled/linked template from step 1? (this way, I could keep references to the compiled/linked templates, not to the $scopes)
Is this re-$compiling / re-linking the right approach to take?
Thanks for your time,
Koen

I would use routing, using something along the lines
$routeProvider
.when('/wizard/:stepNr', {...})
(c.f. AngularJS docs: https://docs.angularjs.org/api/ngRoute/service/$route#example)
Your "back" and "next" buttons would just be a href like this:
Prev
Next
The Scopes of all steps will have one common $parent. So, Before going to the next step, you could store the important values there:
$scope.$parent.stepData[currentStepNr] = $scope.stepData;
whenever coming back to a step, the Scope of the left future step is automatically destroyed. All you need to do is
$scope.stepData = $scope.$parent.stepData[currentStepNr]; // load from parent scope into current scope
delete $scope.$parent.stepData[currentStepNr]; // discard in the parent scope
PS: the fact that the Parent Scope is common to all step scopes is a "special case" So you might want to use a Service for storing the data between steps, as suggested by #Bixi
EDIT: In case, the number of the next step is determined by an external web srevice, you can do (assuming a simple http call)
$http.get('/your/service').success(function(responseData){
/* grab nr of next step from responseData;
remember it and
use $location.$path to navigate to #/wizard/nextStep. */
$location.$path('/wizard/' + varContainingNumberOfNextStep);
});

First, how are you storing your data? For this kind of pattern, the best choice would be a service which stores your steps data. This service will be available through all your steps (controllers).
More information about services: https://docs.angularjs.org/guide/services
Next, you're talking about two solutions:
storing data and letting angular construct the screen from scratch again
storing fully compiled templates
The first solution is the angular standard behavior. To maintain a as-lightweight-as-possible DOM, angular removes previous ng-view DOM elements and replaces them with the new ones (new route). No problem here, it's angular's work and it does it well.
The second solution is possible. In my view, storing the compiled DOM would be overkill, instead you could just let it loaded in the browser (but showing/hiding its content via CSS)... But there are some disadvantages unlike method #1 (heavyweight DOM).
I think you should let angular construct your steps using only stored data.

Why not just to have the parent controller with some step flag and make some use of ngInclude capabilities to create new child scope?
mod.controller('WizardParentCtrl', function() {
this.step = '1';
})
Outer HTML:
<div ng-controller="WizardParentCtrl as WizardParent">
<div ng-include="'steps/'+WizardParent.step+'.html'"></div>
</div>
Inner HTML:
<div ng-controller="Step1Ctrl as Step1">
...
</div>
WizardParent will be accessible in the child controllers via scope inheritance, and you can communicate with it in order to save the data or change steps.

Related

Angular JS - differentiating between a save and update with a common template

I am working on a project with a PHP backend and Angular 1.x on the front end. I have a Listings model and I use a common template to create (add) and update (edit) a Listing.
Since eidt and add both use the same front end template I need to be able to detect when a user is creating and when they are editing. It seems there are several ways to do this:
I could pass a paramater in the ng-submit:
ng-submit="saveListing({{isNewListing}}"
Then I could read the value of the paramter in my controller, but I think this is overkill?
When editing a Listing some variables set for the form auto-fill
$scope.listing.id = x;
Therfore I could just check for a value in the above:
$scope.saveListing = function() {
if(listing.id) {
// update action
} else {
// save action
}
};
Is the second option a sound and non-hacky approach. I am not an Angular pro so although it seems the logical approach to me I want to ensure that I am not hot woring this.
I usually do something similar to the second approach. Since editing means you have to "get" the original record in most cases, the record should exist somewhere in the scope. I use ui-router and have a resolve for the record, which means I can check right at the top of the controller:
$scope.isEdit = record != null;
With a scope variable or similar (e.g. controllerAs vm) you can leverage the fact that you're in "edit mode" and change the UI up a bit. Instead of "+ New" on a button you can have "+ Save".
Hope that helps!
We have a large ERP system with angularJs as a front-end framework, and we are using the "check id" approach.
When updating/edit an item there would be existing id for that item.
I think the second approach is good and I don't see any drawbacks.

Cordova - Angular - <select> tag open a new view

I have an Angular/Cordova app and I'm trying to figure out how should I handle the HTML SELECT tag. What I would like to do is to open a new window with all the options in a list, the user picks one and returns with that value.
The problem is when I do that I lose all the data I had in the first screen as I am closing it when I move to the second one.
I am using Angular's UI.ROUTER. One thing, which I am not too convinced to do, is to save all data entered into StateParams, and when I return, place it back.
What would be the best approach?
It really depends on the use case. If you need to be able to "deep link" to the view where a link loads the view with the pop-up active then using ui-router and stateparams makes the most sense. If deep linking isn't a concern and the user must always select something then you can just use a service/factory/value/provider in order to share the data between the controllers during the lifetime of the app.

Dynamic view names with ui-router

I am trying to target dynamic named views in an ng-repeat but can't do so at config phase as views can only be named statically. Is there a way imitate url param matching like '/path/:param' but with view names like views: {'path:param': {...}} ?
I've tried modifying the state config object at run() to see if changing state configuration after config() had any effect:
rootScope.$on('$stateChangeStart', function(e, to, toP, from, fromP) {
//nope
if(toP.itemId) {
to.views['item-'+toP.itemId+'#home'] = to.views['item-:itemId#home'];
delete to.views['item-:itemId#home'];
}
}
plunker: http://plnkr.co/edit/ZkrteD1ls71yd5V10Xub?p=preview
This concept would not be working, in general.
The reason is that stable, solid states should not be driven by the data (which are very dynamic, changing frequently). All states and their views :{} should be defined first. The data (as a list of items) should be injected there, consuming already defined states.
states should be defined once (even if a bit later, once configuration is loaded via $http)
data could change during the application life-time often, frequently. They should be placed into states/views - but not drive them
How to load dynamic states dynamically with $http (using the .run() phase)? Check these with full examples and details:
AngularJs UI-Router - Page Refresh $state.current is now empty
AngularJS - UI-router - How to configure dynamic views
the doc: $urlRouterProvider.deferIntercept(defer)
Small extract from the doc of deferIntercept(defer):
Disables (or enables) deferring location change interception...
So, we can (once) postpone the url / location handling... to the moment, when all states are dynamically loaded. But this feature could be used just once.
Well - all that could still not fit into what we want - place some details into the list.
There is a big detail Q & A, which should give some insight:
How to replace list item with details
Please, have a look at that, because there is detailed explanation and working example...

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

Do we need multiple controllers to implement routes in angularjs?

There is chance that I might not be able to explain my problem properly. Let me try.
I am developing a single page application using angular. This app basically displays the episodes of an online novel series. There is a navigation bar, which has query menus (Like, latest episode, episode of a particular date, episodes with a particular tag, etc). For each of these queries, i want a separate url.
/latest - should display the latest episode
/tag/:tagname - should return all episodes with that tag.
For all these queries, the resultant view is the same (list of episodes). So I will be using the same partial for all routes.
My question is, Should I actually create a new controller for each query? like, LatestEpisodeController, TagController?
Is there anyway I can use the url to determine what the user wants and run that query from within the same controller?
Ofcourse you can use same controller in routing definition, the question is what is the purpose of that? It will be worse to debug it later, if you have a shared functionality it's better to turn it into a factory or service and then use in controllers.
But the answer is YES, you can use same controllers and implement different behaviour basing on i.e. $location.path()
yes you can use single controller for multiple routing..
you can create different functions in controller and in each function do the according job.
In my case I have created different html page for different url and registered same controller for the html pages and in the html page I have called controller method using ng-init in div portion.
You can use same controller and same views as you wish...
$location can help you to get current path or full url if you want and you can call your service depends on your path...
here I write a little example for you to get the idea
PLUNKER

Resources