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

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.

Related

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

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.

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.

Angular: ui-router how to pass data object or some parameter to other controller through state

I'm already read about resolve, data for ui-router but still can not get how I can solve my problem?
View 1 HTML:
<a ui-sref="configuration.settings.list"><span>Settings</span></a>
<a ui-sref="configuration.settings.personal({user: userId})"><span >Personal</span></a>
userId is variable which is defined in the controller for current view.
Route definition:
angular.module("app").config(["$stateProvider", "$urlRouterProvider", "$locationProvider",
function($stateProvider, $urlRouterProvider, $locationProvider) {
$stateProvider
.state('configuration.settings.lis', {
url: '/',
templateUrl: 'html/settings',
controller: 'settingsController',
}),
.state('configuration.settings.personal', {
url: '/:userId',
templateUrl: 'html/settings',
controller: 'personalController',
}),
I can access userId in personalController through stateParams.
Question: I want pass object or some parameter from View 1 to personalController without `stateParams. Is it possible?
Something like that:
<a ui-sref="configuration.settings.personal({myDamnCooLObject})"><span >Personal</span></a>
.state('configuration.settings.personal', {
url: '/', <-- NO attributes here!!!
templateUrl: 'html/settings',
controller: 'personalController',
data: {{myDamnCooLObject}} <-- THIS object or parameter shuld be accesseble from 'personalController'
})
$stateParams are only guaranteed to be preserved for target routes that use them. ui-router actually keeps this object clean so you can't just stuff random things into it.
I've had good luck using a service as a manager object to persist items between views. For example, I have an app that has a user profile page and a photo gallery as a separate view. I want a lot of the user's core data over in the gallery because there's a title with their name, links back, a little bio box on the side, etc.
The way I wired this up, both views have a user ID in their URL as a parameter. I have a manager that maintains a cache of user data, and a getter that returns a promise to return it. In both states, I have a resolve block that takes the user ID and asks the manager for the whole data object. The manager makes a Web service call to get the data the first time, and immediately resolves for the second call (doesn't matter who gets there first). It works very well and it's lightweight.
In your example you don't want a parameter in the URL at all, but the model could work the same way. Services are global singletons so you can use them as a dumping ground for all kinds of things to be passed around and persisted through things like state transitions. Obviously this is a slippery slope because there are many bad programming practices that are also enabled once you start doing this - but with care and good patterns, it's a very useful technique.
Note that a lot of people actually use $rootScope for this same purpose - it has basically the same effect. But I personally believe a service is better because that way you have a very explicit, defined wrapper around this data you need to pass around, it's much more testable, and it's easier to document it. With $rootScope you're just dumping raw variables into a huge pile and it becomes a pile of spaghetti to pick apart later, especially if you accidentally re-use a variable name. That's causes all kinds of hard-to-find bugs. With a service you can go so far as to use getters/setters, and you could even wire the state transition ($state.go()) call into the setter.

why angular-ui new state router transitions when only parameters changes?

I'm using the new angular-ui router, the one that has a state-machine included (https://github.com/angular-ui/ui-router). This great router allows a user to specify parameters as part of the URL.
for example:
$stateProvider
.state('contacts.detail', {
url: "/contacts/:contactId",
templateUrl: 'contacts.detail.html',
controller: function ($stateParams) {
// If we got here from a url of /contacts/42
expect($stateParams).toBe({contactId: 42});
}]
})
(see here)
this means, that when the user navigates to /contacts/42, the state is changed to 'contacts.details' and the 42 parameter is injected into the controller
There is a problem though. If ONLY the url parameter changes, the transitionTo function is still being called (could happen if the url is changed manually, for example, or bound to a input box). this in turn leads to the view directive of that state, to be re-created, both a waste of time and a problem if we only wanted to update something in that state.
it seems to be on purpose. from the code:
// Starting from the root of the path, keep all levels that haven't changed
var keep, state, locals = root.locals, toLocals = [];
for (keep = 0, state = toPath[keep];
state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams);
keep++, state = toPath[keep]) {
locals = toLocals[keep] = state.locals;
}
equalForKeys is what compares the params, and return false if there's a difference.
My question: do you know why the author would have written it this way? do you think its safe to change, so that there's no transition when only parameters would change?
thanks very much for reading all the way till here, and for any idea
Lior
EDIT: Seems that this is by design. just found: https://github.com/angular-ui/ui-router/issues/46
I generally solve problems of this nature with abstract states and nesting. Place the pieces that don't change based on the url parameter into the abstract parent state and you'll avoid the extra server hit. I prefer to place them into the resolve or custom data section in the parent state, but if necessary you can retrieve them via scope inheritance. Be sure you read and understand the rules of scope inheritance as there are some things that (at least for me) were unexpected.
You can read more details here: https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views. In addition, the sample application included with angular-ui is a good place to start understanding state nesting.

Resources