Sharing resolved data between controllers - UI Router - angularjs

I'm using UI Router as a router for my application, and I have a situation where I need to have all instances of one resource available all the time.
These instances are listed in sidebar of my application.
I starter with something like this in my routes.js file:
.state('auth', {
url: '/',
abstract: true,
controller: RootCtrl,
template: '<div ui-view autoscroll="true"></div>',
resolve: {
campaigns: function(UserCampaignsCollection, activeUser) {
return (new UserCampaignsCollection).query({user_id: activeUser.id});
}
}
})
This means that I had to create separate controller, just for keeping all the campaigns and sharing those between other controllers, like this:
_app.controller('RootCtrl',
function($rootScope, campaigns) {
$rootScope.campaigns = campaigns;
});
This works fine, since I resolve it on only one place, and then all of my other states just inherit base state (as auth.account, auth.inovice etc.), but I would like to avoid need for attaching all of my campaigns on rootScope.
Is there some other way to pass data to other controllers, which would not involve creating dummy controller like this, and attaching data to rootScope?

If all other states inherit from auth then (via https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views#what-do-child-states-inherit-from-parent-states) their controllers can also be injected with resolved campaigns.

Related

With angular component and ui-router, can a single controller have multiple templates?

In a component I would like to associate templates to CRUD actions, but use a single controller that handles data for all all of them. Does this make sense with angular components, or should I use several components ?
How would the ui-router state configuration look like ?
EDIT:
Components have templates, and ui-router states too. I am confused about the articulation of those 2 concepts.
EDIT 2:
trying to clarify my understanding so far :
component are controller + template + bindings. Controller can be omitted.
states are url + template + controller or url + component. Controller can be omitted.
So it seems that components are taking away some of the responsabilities that use to belong to ui-router.
My goal here:
- url1 --> controller foo + template x;
- url2 --> controller foo + template y;
- url3 --> controller foo + template z;
should I do :
components:
component x --> controller foo + template x;
component y --> controller foo + template y;
component z --> controller foo + template z;
and then routing:
url 1 --> component x
url 2 --> component y
url 3 --> component z
?
EDIT 3:
quote from https://docs.angularjs.org/guide/component :
"In a component-based application, every view is a component"
quote from https://ui-router.github.io/guide/ng1/route-to-component :
"The component model enforces separation of concerns and encapsulation by using an isolate scope. Data from parent scopes can no longer be directly accessed. Instead, it needs to be explicitly wired in"
Yes, it is possible. Your ui-router config would look something like this: (Multiple states having same controllers.)
.state('add', {
url: '/add',
templateUrl: 'templates/add.html',
controller: 'contactCtrl'
})
.state('edit', {
url: '/edit',
templateUrl: 'templates/edit.html',
controller: 'contactCtrl'
})
Here's a working example of having multiple states and templates using same controller.
Edit: You don't have to use components. Why create three different extra components while you can achieve the same thing without them? I would still recommend the approach I mentioned above. Getting the same outcome with lesser code should always be chosen. :)
Quoting Eric Elliot on twitter,
Code is temporary. It exists while it is useful.
If code is replaced by better code, good!
If code can be deleted, celebrate!
Your state provider will look like this
JS code
$stateProvider
.state('report', {
views: {
'filters': {
templateUrl: 'report-filters.html',
controller: 'ctrl'
},
'tabledata': {
templateUrl: 'report-table.html',
controller: 'ctrl'
},
'graph': {
templateUrl: 'report-graph.html',
controller: 'ctrl'
}
}
})
in single state you can load multiple views
HTML
<body>
<div ui-view="filters"></div>
<div ui-view="tabledata"></div>
<div ui-view="graph"></div>
</body>
refer multiple views
angular.module('', [])
// Route handler touches this
.component('route1', {
template: `<shared-behaviour>
<route-view-1></route-view-1>
</shared-behaviour>`
})
// This is a purely visual component that links actions with the shared behaviour component
.component('routeView1', {
require: {
shared: '^sharedBehaviour'
},
template: '<button ng-click="$ctrl.onClick()></button>',
controller: class RouteView2Ctrl {
onClick() {
this.shared.onSharedClick()
}
}
})
// Contains the shared behaviour that is shared across the multiple routes
.component('sharedBehaviour', {
// Tell Angular to render children passed to this component
transclude: true,
template: '<div ng-transclude></div>',
controller: class SharedBehaviourCtrl {
onSharedClick() {
/// do something
}
}
})
// Newest version of UI-Router
.state('route1', {
component: 'route1'
})
Further to our comments, you could use something like the above. It's pretty verbose, but that's Angular 1.5 for you. Haven't tested that it works, but it's a basic idea of how it might work.
The main take away being that you have the route1 component which only handles linking route stuff (like the url, resolves, etc) and passes that information to its children (of which there aren't any).
routeView1 just renders how the route would work, and uses require to talk to some parent API that is shared (you could also use one-way bindings for this if you wanted, but with lots of methods this leads to a messy template).
sharedBehaviour only contains the shared behaviour you want renders any child passed to it.
This is admittedly pretty messy and I would prefer that you instead used one-way bindings for route-view-1 and handled the linking in shared-behaviour, but this might be quite a bit of repetition. You could use some transclusion magic to solve that, but it's.. not really worth it.
I would really recommend against sharing controllers like other answerers have suggested. In Angular 2 and above, a controller is the component. You share behaviour there by doing something similar to what I have done (or by using one-way bindings with callbacks) and using composition.
BTW as mentioned in my answer, newer versions of UI-Router for Angular 1.5 allow you to specify a component to be rendered rather than a template. See here for more info.

Angular ui-router: nested views vs multiple views

ui-router is a great alternative to angular's standard router; it supports nested states and views and multiple views.
I am a little confused, though, by the difference between the two. It seems to me that multiple views can almost always be thinked and implemented as nested views of an "higher-order" component. For example, if we consider an app with a sidebar and a content box, we may model them as two "parallel" views or as making the sidebar the parent view and the content-pane its nested child view that depends on the selected sidebar item.
The readme itself seems to suggest the division is not really clear-cut:
Pro Tip: While multiple parallel views are a powerful feature, you'll often be able to manage your interfaces more effectively by nesting your views, and pairing those views with nested states.
When should I use multiple views and when nested views? Is there some criteria that can help you choose most of the time which is the correct way to model the states, nested vs multiple?
In my understanding, the multiple views are primarily for layout purpose, while the nested views are for parent-children hierarchical views. Using the case you mentioned as an example:
The sidebar and the content could be arranged as two distinct views:
$stateProvider.state('main', {
abstract: true,
url: '/main', //base url for the app
views: {
'': {
//this serves as a main frame for the multiple views
templateUrl: 'path/to/the/main/frame/template.html'
},
'sideBar#main': {
templateUrl: 'path/to/the/sidebar/template.html'
},
'content#main': {
templateUrl: 'path/to/the/content/template.html'
}
}
});
The path/to/the/main/frame/template.html template may contain the following frame:
<div class="row"> Header section </div>
<div class="row>
<div class="col-sm-4"><div ui-view="sideBar"></div></div>
<div class="col-sm-8"><div ui-view="content"></div></div>
</div>
Then in the sideBar or the content templates, you can nest their children subview/partials.
Nested states can be used with both nested and parallel views.
Just to note something about nested states.
What makes nested states great is that you can easily share/inherit data from parent to child view.
e.g:
Lets say you have some pages that require a user has logged in.
$stateProvider
.state('admin', {
url: '/admin'
resolve: { authenticate: authenticate }
})
.state('admin.users', {
url: '/users'
})
function authenticate(Auth) {
if (Auth.isLoggedIn()) {
// Resolve the promise successfully
return $q.when();
} else {
$timeout(function() {
// This code runs after the authentication promise has been rejected.
// Go to the log-in page
$state.go('login', {}, {reload:true});
});
// Reject the authentication promise to prevent the state from loading
return $q.reject();
}
Now every state that is a child state of admin has to pass authenticate service ( even if you navigate directly to admin/users/ ).
So basically in the resolve you can put anything and all the child states will inherit from that.
As for parallel views you have complete control over your layouts.
#TonyGW's example is great
You can use them both in your app at the same time,
I mean nested states and parallel views and you also can have parallel nested views. It really depends on the structure of your layout layout.
If you want child states to appear inside the html of the parent state you have to use nested views.

Pass object to child uiView using Angular's uiRouter

I have an Angular application in which I'm creating a header to a page using uiRouter separate views:
Route config:
$stateProvider.state('content', {
url: '/content/{idContent}',
views: {
'header#content': {
templateUrl: 'templates/content/header.partial.html'
},
'mainContent': {
templateUrl: 'templates/content/index.html'
}
}
});
Content template:
<section ng-controller="ContentController">
<div ui-view="header"></div>
{{Here I display the content}}
</section>
Header template:
<section ng-controller="ContentHeaderController">
{{Here I render the header}}
</section>
The problem is, I need to access an object that is resolved in ContentController on the ContentHeaderController. I see that they don't share a parent scope:
ContentController: 4
ContentController's parent: 3
ContentHeaderController: 6
ContentHeaderController's parent: 5
How can I pass an object from the content view to it's header?
The problem of sharing data between controllers can be solved by using events (don't recommend it) and using services as models, which is a recommended way to do that because your data will be synchronized automatically and you will not be using soon to be deprecated API such as $broadcast.
The gist of this approach is to store data in service in an objects, not just as plain primitives on service intself, and then pass reference to this objects into controller where they are bound in template. Check this blog post about model pattern with code examples.

AngularJS: Multiple views with routing without losing scope

I'm trying to implement a classic list/details UI. When clicking an item in the list, I want to display an edit form for that item while still displaying the list. I'm trying to work around Angular's 1-view-per-page limitation and decided to do it by having all URLs routed to the same controller/view. (Perhaps this is the root of my problem and I'm open to alternatives.)
Routing:
$routeProvider
.when('/list', { templateUrl: '/Partials/Users.html', controller: UserController })
.when('/edit/:UserId', { templateUrl: '/Partials/Users.html', controller: UserController })
.otherwise({ redirectTo: '/list' });
The view (/Partials/Users.html):
<!-- List of users -->
<div ng-repeat="user in Users">
Edit {{ user.Name }}
</div>
<!-- Edit form -->
<div>
{{ SelectedUser.Name }}
</div>
Controller:
function UserController($scope, $routeParams) {
// the model for the list
$scope.Users = GetUserListFromService();
// the model for the edit form
if ($routeParams.UserId != null)
$scope.SelectedUser = GetUserFromService($routeParams.UserId);
}
Problems:
When clicking an edit link, the controller is reinstantiated with a new scope, so I have to re-init the Users list. (In a more complex example I could have input from the user stored bound to the model and this would also get lost.) I'd prefer to persist the scope from the previous route.
I'd prefer to use a separate controller (or, as many other Angular developers have complained, the ability to have multiple displayed views!) but that leads to the same issue of losing scope.
Try using ui-router: http://github.com/angular-ui/ui-router.
They have nested views and easier state management than angular default routing :-)
Multiple views are not supported in core AngularJS. You can use this library for this purpose which supports any amount of nested views on the page, where each level is configured independently with its own controller and template:
http://angular-route-segment.com
It is much simpler to use than ui-router. Sample config may look like this:
$routeSegmentProvider.
when('/section1', 's1.home').
when('/section1/prefs', 's1.prefs').
when('/section1/:id', 's1.itemInfo.overview').
when('/section1/:id/edit', 's1.itemInfo.edit').
when('/section2', 's2').
segment('s1', {
templateUrl: 'templates/section1.html',
controller: MainCtrl}).
within().
segment('home', {
templateUrl: 'templates/section1/home.html'}).
segment('itemInfo', {
templateUrl: 'templates/section1/item.html',
controller: Section1ItemCtrl,
dependencies: ['id']}).
within().
segment('overview', {
templateUrl: 'templates/section1/item/overview.html'}).
segment('edit', {
templateUrl: 'templates/section1/item/edit.html'}).
up().
segment('prefs', {
templateUrl: 'templates/section1/prefs.html'}).
up().
segment('s2', {
templateUrl: 'templates/section2.html',
controller: MainCtrl});
I've found Angular Multi View to be a godsend for this scenario. It lets you preserve scope as the route changes and lets multiple controllers share the same route without nesting your views.
I recommend Angular Multi View if you have more than 2 views on your page. Otherwise, when using ui-router, nesting multiple views gets messy really fast.
I came up with the same problem and I personnaly don't like plugins when they aren't absolutely unavoidable. I just moved singleton part to a service.
In my case there are :id[/:mode] routes and I want to react different way if user changes just mode or id too. Thus, I have to know previous id.
So, there is a service with activate method which updates its state. And the scope is reinitialized every time with the following code.
module.controller('MyController', ['$scope', '$routeParams', 'navigator', function($scope, $routeParams, navigator) {
var id = null;
var mode = null;
if (typeof($routeParams.id)!='undefined')
{
id = $routeParams.id;
}
if (typeof($routeParams.mode)!='undefined')
{
mode = $routeParams.mode;
}
navigator.activate(id, mode);
$scope.items = navigator.items;
$scope.activeItem = navigator.activeItem;
$scope.modes = navigator.modes;
$scope.activeMode = navigator.activeMode;
}]);
In activate method I can compare id to the singleton's activeItem.id and react differently.

angularjs ui-router - how to build master state which is global across app

<html ng-app="app">
<head>
...
</head>
<body>
<div id="header"></div>
<div id="notification"></div>
<div id="container"></div>
<div id="footer"></div>
</body>
</html>
With the given structure of the app (derived from angular-app):
header: Here the site navigation, login/out toolbar etc comes in. This is dynamic and has it's own Controller
notification: Global notification container.
container: This used to be my <ng-view>. So this is where all other modules loads in.
footer: Global footer.
How do the state hierarchy looks like? I've gone through the example which shows a single module (contacts) but typically an app would have a global (root) state and inside the root state other module states are rendered.
What I'm thinking is my app module probably have the root state and then each module should have it's own state and I have to inherit from root state. Am I right?
Also from ui-state example, they have used both $routeProvider and $urlRouterProvider as well as $stateProvider has url defined. My understand was $stateProvide also handles routing. If I'm wrong, which provider should I use for routing?
EDIT: http://plnkr.co/edit/wqKsKwFq1nxRQ3H667LU?p=preview
Thanks!
The approach you took in your plunker is close. #ben-schwartz's solution demonstrates how you'd set defaults in your root state for the three essentially-static views. The thing missing in your plunker is that your child states still need to reference the top container view.
.state('root',{
url: '',
views: {
'header': {
templateUrl: 'header.html',
controller: 'HeaderCtrl'
},
'footer':{
templateUrl: 'footer.html'
}
}
})
.state('root.about', {
url: '/about',
views: {
'container#': {
templateUrl: 'about.html'
}
}
});
Note views: { 'container#': ...} instead of just templateUrl: ... in 'root.about'
What you may also be asking about is whether you can have modules define their own state-sets, which you then attach to your app's state hierarchy. A sort of plug-and-play for the routes/states each module provides.
To achieve this you'll have tightly couple your modules to your main app.
In the module:
angular.module('contact', ['ui.router'])
.constant('statesContact', [
{ name: 'root.contact',
options: {
url: 'contact',
views: {
'container#': {
templateUrl: 'contacts.html'
}
},
controller:'ContactController'
}}
])
.config(['$stateProvider', function($stateProvider){
}])
Then, in the app:
var app = angular.module('plunker', ['ui.router', 'contact']);
app.config( ['$stateProvider', 'statesContact',
function($stateProvider, statesContact){
$stateProvider
.state('root',{ ... })
.state('root.home', { ... })
.state('root.about', { ... })
angular.forEach(statesContact, function(state) {
$stateProvider.state(state.name, state.options);
})
}]);
This means all your modules will need to be compatible with this pattern set out in your app. But if you accept this constraint you can then choose include any combination of your modules, and their states magically get added to your app. If you want to get even fancier, you can modify state.options.url in your statesModuleName loop to, for example, prefix your module's url structure.
Also note that the module ui.compat is only necessary when you are transitioning from $routeProvider to $stateProvider. You should normally use ui.state instead.
Also don't forget to adjust in header.html $state.includes('root.contact'))
updated plunker
Although confusing, the FAQ in the ui-router wiki seems to say that this isn't possible: ui-router/wiki/faq
One approach is to allow each feature to define it's own root state (as in this example: AngularJS State Management with ui-router)
Another would be to define the entire state hierarchy in your "myApp" module and just leverage controllers etc. from the dependent modules. This works especially well when you are maintaining a mobile and desktop site; define each site's state hierarchy in a mobileApp and desktopApp module, and have the functionality (e.g. controllers, services, etc.) shared in separate modules.
It depends how you prefer to approach it.
All scopes inherit from rootScope so you may place global state there. The NG literature specifically mentions that you should only put global state there, and not functionality. Functionality required across the system belongs in services, and specifically you should not implement services whose sole purpose is to retain state. All the advice seems to be implicitly shared in the context of how it either facilitates or hampers your ability to easily do end to end testing.

Resources