Inject resolved property into controller of abstract parent - angularjs

I have defined my UI-Router states like this:
$stateProvider.state('contact', {
url: '/contactDemo',
views: {
'main': {
controller: 'contactMainController',
templateUrl: 'templates/contact.tpl.html'
}
}
}).state('contact.details', {
abstract: true,
controller: 'contactDetailsController',
templateUrl: 'templates/contact.details.tpl.html'
}).state('contact.details.add', {
url: '/add'
}).state('contact.details.filter', {
url: '/filter/{filterId}'
}).state('contact.details.filter.record', {
url: '/record/{recordId}',
resolve: {
record: ['$stateParams', 'Restangular', function($stateParams, Restangular) {
return Restangular.one('records', $stateParams.recordId).get();
}]
}
}).state('contact.details.filter.record.edit', {
url: '/edit'
});
Now I would like to inject my resolved record into contactDetailsController. If I do so, I get an Unknown provider error. I can't move the resolve into the abstract state, because from there I can't access the id inside $stateParams.
If I move the controller property down into the child state, my controller is never invoked.
Does anybody know how I can get the resolved property injected into the controller of an abstract parent state?

As documented here, resolve could be inherited. NOT injected from child to parent.
Inherited Resolved Dependencies
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.
$stateProvider.state('parent', {
resolve:{
resA: function(){
return {'value': 'A'};
}
},
controller: function($scope, resA){
$scope.resA = resA.value;
}
})
.state('parent.child', {
resolve:{
resB: function(resA){
return {'value': resA.value + 'B'};
}
},
controller: function($scope, resA, resB){
$scope.resA2 = resA.value;
$scope.resB = resB.value;
}
Simply solution where:
$stateParam is defined in the child
the resolve dependent on it should be injected into parent
cannot work - because one parent will be shared for many children.
Parent will stay while the children's params will differ..

Related

Angularjs ui-router with nested views is loading parent controller for grandchildren

I'm using $state.go("titulos", params) to load the titulos page, in which case the parent controller (emissao) is not loaded again. But when I call the grandchildren $state.go("titulos.fluxos", params) the parent controller (titulos) is loaded again. I do not want this to happen, because the parent controller is already loaded, this is an unwanted behavior. I do not know why the first child works perfectly without loading the parent, but for the grandchild (titulos.fluxos) unnecessarily load his parent (titulos).
How could I solve this? please.
$stateProvider.state('emissao', {
url: '/emissao',
views: {
'main': {
controller: 'emissaoCtrl',
templateUrl: 'view/emissao.html'
}
},
resolve: {
emissoes: function (cadastroAPI) {
return cadastroAPI.listar('emissao');
}
}
});
$stateProvider.state('titulos', {
parent: 'emissao',
url: '/titulos',
controller: 'titulosCtrl',
templateUrl: 'view/titulo.html',
resolve: {
titulos: function (cadastroAPI, $stateParams) {
return cadastroAPI.listarTitulos($stateParams.parent.obj);
}
},
params: {
parent: null,
container: null
}
});
$stateProvider.state('titulos.fluxos', {
url: '/fluxos',
controller: 'fluxosCtrl',
templateUrl: 'view/fluxo.html',
resolve: {
fluxos: function (fluxoAPI, $stateParams) {
return fluxoAPI.listar($stateParams.parent.obj.Id);
}
},
params: {
parent: null,
container: null
}
});
I discovered the problem! Actually, normal behavior does not load the parent controller, my problem is that the 'params' parameters had the same name 'parent' both in the parent controller 'titulos' and in the child 'fluxos' and this was causing an unnecessary call to the parent controller.

angular ui router how to wait for child states to be loaded

I'm having problems loading my child states on an abstract true parent state.
This is my parent state
.state('main', {
url: '/main',
templateUrl: 'templates/main.html',
abstract: true
})
This is the child states
.state('main.panels', {
views: {
'ticketsPanel': {
templateUrl: 'templates/ticketsPanel.html'
},
'productsPanel': {
templateUrl: 'templates/productsPanel.html'
},
'categoriesPanel': {
templateUrl: 'templates/categoriesPanel.html'
}
}
})
I have a login page after I login I want to load all 3 child views.
This is the code that process the login.
.controller('loginController', function($scope, Authentication, $log, $state){
$scope.formData = {};
$scope.processForm = function(){
Authentication.login($scope.formData);
var promise = Authentication.getEmployee();
promise.then(function(respond){
localStorage.setItem('employee', JSON.stringify(respond));
$state.go('main.panels');
})
}
})
The $state.go('main.panels') activates the child state of the main state parent, but the problem I'm having is that DOM is showing the element have being loaded but I can only see them partially in my view. It's like they didn't got fully loaded.
My question is how can I wait for all the views in the main.panels to be loaded completely before I transition to that view.
We do have 'resolve' property that can be provided in the definition of each and every state (or view) whatever you want to load. So what angular-ui-router does is that it resolves the 'resolve' property first and only then the HTML template is being rendered on the browser.
You can define the child state in the following way :
.state('main.panels', {
views: {
'ticketsPanel': {
templateUrl: 'templates/ticketsPanel.html',
resolve: function(LoginService){return LoginService};
},
'productsPanel': {
templateUrl: 'templates/productsPanel.html',
resolve: function(LoginService){return LoginService};
},
'categoriesPanel': {
templateUrl: 'templates/categoriesPanel.html',
resolve: function(LoginService){return LoginService};
}
}
})
You can even read the following links for more details :
https://github.com/angular-ui/ui-router/wiki
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-resolved-dependencies
http://www.jvandemo.com/how-to-resolve-angularjs-resources-with-ui-router/
It has been explained in detail. Hope this helps :)
nested states also makes the views nested, meaning the router will look for a named ui-view within the parent template and render it there, and since that's not what you're trying to do you have to implicitly state that it's the parent view (absolute vs relative) like so:
.state('main.panels', {
views: {
'ticketsPanel#': {
templateUrl: 'templates/ticketsPanel.html',
resolve: function(LoginService){return LoginService};
},
'productsPanel#': {
templateUrl: 'templates/productsPanel.html',
resolve: function(LoginService){return LoginService};
},
'categoriesPanel#': {
templateUrl: 'templates/categoriesPanel.html',
resolve: function(LoginService){return LoginService};
}
}
})
'productsPanel#' is like saying 'productsPanel' # nothing which mean the upper parent or root view.
https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views#view-names---relative-vs-absolute-names

Using $scope in ui-router resolve

I am using ui-router resolve in order to get some data from a service.
The thing is that I need to get a value from the parent $scope in order to call the service as shown bellow.
resolve: {
contactService: 'contactService',
contacts: function ($scope, contactService) {
return contactService.getContacts($scope.parentCtrl.parentObjectId);
}
}
I keep getting Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Also tried a few desperate attempts such as adding scope to the resolve object as shown bellow with not success.
scope: $scope
Any ideas?
That's impossible, scope hasn't been initialized at that point so you can't use it in the resolve object. You can access the scope in the controller after it's been initialized. The whole point of resolve is that it runs before controller initialization so that you can inject and directly access the resolved items in your scope.
If you need to pass a variable to the next state you can do that by using the $stateParams object which is available for use in resolve. You can add data to it when changing states, eg:
In your template, if you have a objectId in your scope:
<a ui-sref="statename({'id': objectId})">Change states</a>
Or in your controller:
$scope.go('statename', {'id': $scope.objectId});
You can then retrieve that in your resolve by using the $stateParams:
resolve: {
contactService: 'contactService',
contacts: function ($stateParams, contactService) {
return contactService.getContacts($stateParams.id);
}
}
As an alternative to the accepted solution, which requires another round trip to the server for the same resource (if you are getting the value from the server/api) you could $watch the parent from the child controller.
function ParentController($http) {
var vm = this;
$http.get(someResourceUrl).then(function(res) {
vm.someResource = res.data;
});
}
function ChildController($scope) {
// wait untill the parent gets the value
var unwatch = $scope.$watch('parent.someResource', function(newValue) {
if (newValue) {
// the parent has the value, init this controller
init(newValue);
// dispose of the watcher we no longer need
unwatch();
}
});
function init(someResource) {
// ... do something
}
}
function routerConfig($stateProvider) {
$stateProvider
.state('parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'parent',
templateUrl: '...',
})
.state('parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'child',
templateUrl: '...',
});
}

ui router - nested views with shared controller

I have an abstract parent view that is meant to share a controller with its nested views.
.state('edit', {
abstract: true,
url: '/home/edit/:id',
templateUrl: 'app/templates/editView.html',
controller: 'editController'
})
.state('edit.details', {
url: '/details',
templateUrl: 'app/templates/editDetailsView.html'
})
.state('edit.info', {
url: '/info',
templateUrl: 'app/templates/editInfoView.html'
})
The routing works as expected.
The problem is that when I update a $scope variable from one of the nested views, the change is not reflected in the view. When I do the same from the parent view, it works fine. This is not situation that requires an $apply.
My guess is that a new instance of editController is being created for each view, but I'm not sure why or how to fix it.
The issue here would be related to this Q & A: How do I share $scope data between states in angularjs ui-router?.
The way how to solve it is hidden in the:
Understanding Scopes
In AngularJS, a child scope normally prototypically inherits from its parent scope.
...
Having a '.' in your models will ensure that prototypal inheritance is in play.
// So, use
<input type="text" ng-model="someObj.prop1">
// rather than
<input type="text" ng-model="prop1">.
And also this
Scope Inheritance by View Hierarchy Only
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states.
Having that we should do this in edit Controller
controller('editController', function ($scope) {
$scope.Model = $scope.Model || {SomeProperty : "xxx"};
})
And we can even reuse that controller: 'editController' (we can do not have to, because the $scope.Model will be there - thanks to inheritance)
.state('edit', {
abstract: true,
url: '/home/edit/:id',
templateUrl: 'app/templates/editView.html',
controller: 'editController'
})
.state('edit.details', {
url: '/details',
templateUrl: 'app/templates/editDetailsView.html',
controller: 'editController'
})
.state('edit.info', {
url: '/info',
templateUrl: 'app/templates/editInfoView.html',
controller: 'editController'
})
Now, the same controller will be instantiated many times (parent all the children) but the $scope.Model will be initiated only once (inside of parent) and available everywhere
Check this similar working example here
Based on a comment by PilotBob
Is it possible to do this when using the controllerAs pattern giving the child state it's own controller?
I decided to append another solution, using controllerAs while keeping the above/original concept
There is a working plunker
The states would be now having different controllers and parent state will name it "parentCtrl" (to be NOT overwritten in a child scope with child controller)
.state("main", {
controller:'mainController',
controllerAs: "parentCtrl",
...
.state("main.1", {
parent: 'main',
controller:'child1Controller',
controllerAs: "ctrl",
...
.state("main.2", {
parent: 'main',
controller:'child2Controller',
controllerAs: "ctrl",
...
And these are controllers:
.controller('mainController', function ($scope) {
this.Model = {Name : "yyy"};
})
.controller('child1Controller', function ($scope) {
$scope.Model = $scope.parentCtrl.Model;
})
.controller('child2Controller', function ($scope) {
$scope.Model = $scope.parentCtrl.Model;
})
Check it in action here
Another alternative using resolve
.state('edit', {
abstract: true,
url: '/home/edit/:id',
templateUrl: 'app/templates/editView.html',
controller: 'editController',
resolve: {
baseData: function() {
return {};
}
}
})
.state('edit.details', {
url: '/details',
templateUrl: 'app/templates/editDetailsView.html',
controller: 'editController'
})
.state('edit.info', {
url: '/info',
templateUrl: 'app/templates/editInfoView.html',
controller: 'editController'
})
.controller('editController', function (baseData) {
baseData.foo = baseData.foo || 'bar';
});
In the child controller you can do:
angular.extend($scope, $scope.$parent)
In case the controller is used with alias e.g. 'vm' you can do:
let vm = angular.extend(this, $scope.$parent.vm);

Angular ui-router to accomplish a conditional view

I am asking a similar question to this question: UI Router conditional ui views?, but my situation is a little more complex and I cannot seem to get the provided answer to work.
Basically, I have a url that can be rendered two very different ways, depending on the type of entity that the url points to.
Here is what I am currently trying
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$state.transitionTo('home.first');
} else {
$state.transitionTo('home.second');
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I set up a Resolve to fetch the actual entity from a restful service.
Every thing seems to be working until I actually get to the transitionTo based on the type.
The transition seems to work, except the resolve re-fires and the getEntity fails because the id is null.
I've tried to send the id to the transitionTo calls, but then it still tries to do a second resolve, meaning the entity is fetched from the rest service twice.
What seems to be happening is that in the onEnter handler, the state hasn't actually changed yet, so when the transition happens, it thinks it is transitioning to a whole new state rather than to a child state. This is further evidenced because when I remove the entity. from the state name in the transitionTo, it believes the current state is root, rather than home. This also prevents me from using 'go' instead of transitionTo.
Any ideas?
The templateUrl can be a function as well so you check the type and return a different view and define the controller in the view rather than as part of the state configuration. You cannot inject parameters to templateUrl so you might have to use templateProvider.
$stateProvider.state('home', {
templateProvider: ['$stateParams', 'restService' , function ($stateParams, restService) {
restService.getEntity($stateParams.id).then(function(entity) {
if (entity.Type == 'first') {
return '<div ng-include="first.html"></div>;
} else {
return '<div ng-include="second.html"></div>';
}
});
}]
})
You can also do the following :
$stateProvider
.state('home', {
url : '/{id}',
resolve: {
entity: function($stateParams, RestService) {
return RestService.getEntity($stateParams.id);
}
},
template: 'Home Template <ui-view></ui-view>',
onEnter: function($state, entity) {
if (entity.Type == 'first') {
$timeout(function() {
$state.go('home.first');
}, 0);
} else {
$timeout(function() {
$state.go('home.second');
}, 0);
}
}
})
.state('home.first', {
url: '',
templateUrl: 'first.html',
controller: 'FirstController'
})
.state('home.second', {
url: '',
templateUrl: 'second.html',
controller: 'SecondController'
});
I ended up making the home controller a sibling of first and second, rather than a parent, and then had the controller of home do a $state.go to first or second depending on the results of the resolve.
Use verified code for conditional view in ui-route
$stateProvider.state('dashboard.home', {
url: '/dashboard',
controller: 'MainCtrl',
// templateUrl: $rootScope.active_admin_template,
templateProvider: ['$stateParams', '$templateRequest','$rootScope', function ($stateParams, templateRequest,$rootScope) {
var templateUrl ='';
if ($rootScope.current_user.role == 'MANAGER'){
templateUrl ='views/manager_portal/dashboard.html';
}else{
templateUrl ='views/dashboard/home.html';
}
return templateRequest(templateUrl);
}]
});

Resources