Angular UI-Router Best Practices: Share Data Between named views - angularjs

I'm migrating from angular-route to ui-router and Im trying to figure out what is the best option for build the following scenario:
I have a generic panel wich is composed of two nested elements showed at the same time: A FORM(rendered on the left) and a simple LIST (rendered on the right).
The form is used to edit or even create a new item for the list (this manipulation is made throught a service makes ajax calls)
The list shows all items and I can select one of them and click on "edit", after that the item goes to the form and I'll be able to manipulate/update it.
Here is my code:
.state('mt.demos', {
abstract: true,
url: '/demos',
views : {
'container#' : {
templateUrl: '....demos.html'
}
}
})
.state('mt.panel', {
url: '',
views: {
'form#mt.demos': {
templateUrl: '...form.html',
controller : 'formController',
controllerAs : 'vm',
},
'list#mt.demos': {
templateUrl: '...list.html',
controller : 'listController',
controllerAs : 'vm',
}
}
})
So far I ended up with those 2 options:
Create a service for the shared data (this seems to be the most acceptable answer due to its Singleton behavior -- but look the NOTE 1 at the end of this post.
Create a parent controller so each nested state can access parent properts like the list or the item to be edited. So when I click on "edit item" the listController updates the parent's scope and the formController will be changed with this data.
NOTE 1: Of course to retrieve the list and to update items I already use one service called demoService. This service is used for ajax requests only. So If the best decision is the options 1 showed above, I belive that I should create a new service with a new proposal, right? Or it isnt bad to store some local variable in this same service.
This new service will have the list and the item itself, so when I click on edit I will update the service "item" property?

I think you already have an answer of using a service to store your data.
About a new service with new proposal, while I'm not entirely sure what a 'proposal' means, I'm assuming you are referring to a different list with different fields with your current one?
In that case that would depends on how reusable your code is. If your forms are similar enough, or you can generalize it, then it might be fine to share the same service so you don't have to rewrite the functions. The lists and selected items can be stored as different variables in the service.
Or, you can also store them in different service, and write their common functions in another service.

Related

Go to a view and trigger a $scope function with ui-router

I realize this ask is for a hack, but here is the motivation:
I am designing a "shortcut" view for my rather complex app. Users want to trigger functions inside different views.
Different users have different opinions on useful shortcuts
For example, one user may want to define a shortcut that "goes to view app.montage and triggers $scope.toggleInfo()"
Another user may want to define a completely different shortcut that invokes a function 3 levels deep inside an existing view's UI
So bottomline, can I devise something like:
$state.go('state name') && then initiate function inside that state?
I know I can define URL routes, but its a rather complex app and its easier to be able to "trigger" actions if I can.
$stateProvider.state('users', {
url: '/users',
controller: 'UsersCtrl',
params: {
obj: null
}
})
function UserCtrl($stateParams,$scope) {
console.log($stateParams);
$stateParams.obj && $stateParams.obj.toggleInfo && $scope.toggleInfo();
}
$state.go('users', {obj:{toggleInfo: true});
For more information, see UI-Router Wiki - Using Parameters without Specifying Them in State URLs.

Passing data from one route view to another

I want to pass some values from one view to another in Angularjs using ui-Router.
I don't want to use $rootScope to save data or create a new services ( as I have many views that pass small bits of data so creating new jsfile for few lines code is not fun). A super-minified example of what I want to do is:
Controller of View 1
$scope.goodFood = 10
$scope.badFood = 2
Controller of View 2
$scope.results = 10 - 2 (from view 1's scope)
Is there any quick way to do these kinds of small operations ?
I don't want to use $rootScope to save data or create a new services ( as I have many views that pass small bits of data so creating new jsfile for few lines code is not fun)
There is no need to create a new service for new bits of data. Simply create a value service with an object:
app.value("viewData", {});
Then simply add new properties as needed:
app.controller("viewCtrl", function(viewData) {
viewData.newProp = "new info";
console.log(viewData.oldProp);
});
As value services are singletons, changes to the contents of the object will survive view changes.
Since you have specified the approach that you wanted to follow, you should create the following
$stateProvider
.state('view2', {
url: "/view2/:param1/:param2",
templateUrl: 'view2.html',
controller: function ($stateParams) {
console.log($state.params.param1+"-"+$state.params.param2);
}
});
and from where ever you want to call
$state.go('view2',{param1:'10', param2:'2'});
But in general it is advised that you keep these values in a service or some where stored. With the application growing you may have to use few more attributes to compute
Easiest way would be to store all the data directly into the $routeParams either through route or query string.
routeUrl: /foods/:quality/:stars
then in your controller you can access
$routeParams.quality - $routeParams.stars

Angularjs: Can i have one route with multiple views?

Consider a certain route let's say myapp\profile
which has two modes (buyer/seller)
What i would like to achieve is:
keep the same route url for both modes
Alternate the view with different HTML files (lets say buyer.html, seller.html), of course each view has it's view model.
Sharing some logic between the two modes.
I would like to have a controller/logic to each mode
What i already considered:
Thought about using ui-router's sub states, but I dont want to change the url.
Thought about creating this 'profile' route and while navigating to it, figure the mode (buyer/seller), and then $state.go to a new state (but again, i would like to keep same route name at the end so it's not ok)
Ideally thought i could navigate to my shared controller and then render the correct view and controller, but this idea kinda messed up me.
Could you share what is a clean way of doing this?
most use cases
Normally, in order to dynamically select a template for a state, you can use a function:
state = {
.....
templateUrl: function($stateParams){
if ($stateParams.isThis === true)
return 'this.html'
else
return 'that.html'
}
}
but...
Unfortunately you can't pass other injectables to the templateUrl function. UI.Router only passes $stateParams. You don't want to alter the URL in anyway so you can't use this.
when you need to inject more than $stateParams
But you can leverage templateProvider instead. With this feature, you can pass a service to your templateProvider function to determine if your user is a buyer or seller. You'll also want to use UI.Router's $templateFactory service to easily pull and cache your template.
state = {
....
templateProvider: function($templateFactory, profileService){
var url = profileService.isBuyer ? 'buyer.html' : 'seller.html';
return $templateFactory.fromUrl(url);
}
}
Here it is working in your plunkr - http://plnkr.co/edit/0gLBJlQrNPUNtkqWNrZm?p=preview
Docs:
https://github.com/angular-ui/ui-router/wiki#templates
http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$templateFactory

Multiple states and urls for the same template

I'm building a web application and I have a screen that consists in five sections, each section represents a level, the areas are the higher level of my tree, when I click in any card of the area, the system should return the skills of that area and so on.
I need to change the url and state according what the user is accessing, for example, if the user access some skill, the url must be
example.com/#/curriculum/skill/<skillId>
and if I access this link it should automatically load the capabilities from this skill and his parent which is area in this case.
I have one controller for area, skill, capability, knowledge and criteria, in each controller I have a action to load the next level of the tree, which looks like that
$scope.loadSkills = function (id) {
Area.loadSkills(...)
$state.go('curriculo.skill', {id: this.current.id}, {nofity: false, reload: false});
}
And these are my states
$stateProvider
.state('curriculum', {
url: '/curriculum',
templateUrl: '/templates/curriculo.html',
})
.state('curriculum.are', {
url: '/area/:id',
template: '',
})
.state('curriculum.skill', {
url: '/skill/:id',
template: '',
})
.state('curriculum.capability', {
url: '/capability/:id',
})
.state('curriculum.knowledge', {
url: '/knowledge/:id',
})
.state('curriculum.criteria', {
url: '/criteria/:id',
});
I'm new in Angular and I not sure about what to do, should I created multiple nested views in this case, and if so, how do I load stuff that I need according the url?
I would suggest to use the capability of multiple named views offered by the ui-router. You can read more about it here. Basically the documentation says the following:
You can name your views so that you can have more than one ui-view per
template.
If you check the example in the documentation, you'll notive that there are similarities between your scenario and the example, because you want to dynamically populate a different views (here named views).
Example
I tried to recreate your scenario in this JSFiddle.
First I created an abstract state which provides the different views like areas, skills etc. This is the template for the abstract state:
<div class="curriculum" ui-view="areas"></div>
<div class="curriculum" ui-view="skills"></div>
Next I created a nested state curriculo.main, which declares the different views (areas, skills etc.) you need. Each view has its own template and controller. Notice that the nested state has a resolve which initially loads the areas from a service called curriculo. If you use resolves remember that the resolve keyword MUST be relative to the state not the views (when using multiple views).
Basically the service is responsible for the business logic, means getting the areas, skills etc. In the JSFiddle I have hard-coded the HTTP results. Replace that with HTTP calls and make use of promises. Since each named view has its own controller we need a mechanism to notify about changes, for example to notify the SkillsController that skills have been loaded. Thus, I created a simple event system (subcribe-notify):
.factory('notifier', function($rootScope) {
return {
subscribe: function(scope, callback, eventName) {
var handler = $rootScope.$on(eventName, callback);
scope.$on('$destroy', handler);
},
notify: function(eventName, data) {
$rootScope.$emit(eventName, data);
}
};
});
The SkillsController can then subscribe to a specific event like so:
notifier.subscribe($scope, function(event, data) {
$scope.skills = data;
}, 'onSkillsLoaded');
The curriculo service calls (at the end of the getSkills()) notifyand provides an event. In this case the same event as you subscribed to in the SkillsController.
notifier.notify('onSkillsLoaded', result);
All in all, that's the magic behind my little example. It's worth mentioning that you need to apply best practices to the code, since this is just to recreate your scenario. For best practices I suggest the Angular Style Guide by John Papa.
Update 1
I updated my example to show you deep linking. I simulate the deep link via
$state.go('.', {area: 2, skill: 5});
This way I can activate a certain state. Now each view has its activate function. Inside this function I do all the work that is neseccary for the initialization, e.g. selecting an area if the query param is set. As you know, you can access the params with the $state service. I had to use a $timeout to delay the init of the areas controller because the subscribe wasn't made yet. Please try to find a better solution to this problem. Maybe you can use promises or register each controller in a service which resolves if all controller have been initialized.
If anything has been selected I also use the go with an additional option to set the notify to false.
$state.go('.', {area: area.id, skill: skillId ? skillId : undefined}, {notify: false});
If notify is set to false it will prevent the controllers from being reinitialized. Thus you can only update the URL and no state change will happen.

UI-Router : Can I get always the same controller?

I'm using UI-Router to have some "menus" in my application.
$stateProvider.state("list"
url: "/Focales"
templateUrl: "/demo/focals.html"
controller: FocalCtrl)
$stateProvider.state("collection"
url: "/Collections"
templateUrl: "/demo/collections.html"
controller: CollectionCtrl)
In my CollectionCtrl, I trigger a processing done on server and just waiting to display the information like this (CoffeeScript)
Progress = $resource('/collections/getProgress')
$scope.getProgress = () ->
prg = Progress.get {}, ->
$scope.currentProgress = prg
$scope.getProgress() if prg.end isnt true
My issue : When the user moves to Focal and goes back to CollectionCtrl, I have a new instance of CollectionCtrl. So as far as I understand, the $scope.getProgress code still receives data but for the PREVIOUS CollectionCtrl (so the display is not updated...)
Is it possible to get the previous Controller rather than a new "instance" of the CollectionCtrl ? Why is a new CollectionCtrl created ?
What's the best approach: So I'm tempted to have a state data to store the current $scope so I could do $state.currentScope = prg rather than $scope.currentProgress = prg.
Is it a good approach ?
Thanks !
The best approach to take here is to define a service which will communicate with the server and possibly cache the results. This service can be injected in the Controllers and the controllers can ask it for the progress, probably using $watch, though providing the service with a callback may also suffice, and update the UI based on that.
Controllers should be ephemeral and one should not rely on them being singleton-ish objects in Angular. Importantly, they should not contain any state apart from that needed by the template they are supporting. The are created again when the route becomes active so that we can write comparatively simpler code, initializing the $scope once and rest assured that it will contains as fresh information (w.r.t the route) for the template as possible. This also avoids memory leaks if a controller is kept around for each route like /app/:object_id and the user switches routes often.

Resources