My question is two fold:
1 - I have a parent state and several child states using ui.router, there's one object (stored in mongodb) that is need in all states, but it gets updated by all states. In this scenario does it make sense to use the parent state's resolve option to populate the object?
2 - If this is the proper way to do this, how can I update that "reference" (the mock service injector created by the ui.router) to that object from every state.
To help in explain he's a example of the idea (lot's of code ommited)
.state('parent',resolve:{objectX:return x.promise;},...);
.controller('childstateCtrl',$scope,objectX){
$scope.data.objectX = objectX;
$scope.someEvent =function(){
// something updates objectX on the scope
}
}
.controller('otherChildCtrl',$scope,objectX){
// how to get the updated objectX?
}
Thanks in advance
Not fully sure if I can see where is the issue... but if you are searching for a way how to share access to updated reference, it should be easy. There is an example
Let's have these states
$stateProvider
.state('root', {
abstract: true,
template: '<div ui-view></div>',
resolve: {objectX : function() { return {x : 'x', y : 'y'};}},
controller: 'rootController',
})
.state('home', {
parent: "root",
url: '/home',
templateUrl: 'tpl.example.html',
})
.state('search', {
parent: "root",
url: '/search',
templateUrl: 'tpl.example.html',
})
.state('index', {
parent: "root",
url: '/index',
templateUrl: 'tpl.example.html',
})
Working with only one controller (for a root state):
.controller('rootController', function($scope, objectX){
$scope.data = { objectX: objectX };
})
And for this example, this is shared template:
<div>
<h3>{{state.current.name}}</3>
x <input ng-model="data.objectX.x"/>
y <input ng-model="data.objectX.y"/>
</div>
So, in this scenario, parent (root) has injected an object data into $scope. That reference is then inherit as described here:
Scope Inheritance by View Hierarchy Only
Check that example in action here. If you need more details (than in the link above, check this Q&A)
you could store it in a service.
.service("myService", function($q) {
// the actual data is stored in a closure variable
var data = undefined;
return {
getPromise: function() { // promise for some data
if (data === undefined) // nothing set yet, go fetch it
return $http.get('resourceurl').then(function(value) { data = value; return data; });
else
return $q.when(data); // already set, just wrap in a promise.
},
getData: function() { return data; }, // get current data (not wrapped)
setData: function(newDataVal) { data = newDataVal; } // reset current data
}
})
// `parent` wont' enter until getPromise() is resolved.
.state('parent', resolve:{objectX: function(myService) { return myService.getPromise(); } });
.controller('childstateCtrl', $scope, myService) {
$scope.data.objectX = myService.getData();
$scope.someEvent = function(someData){
myService.setData(someData);
}
}
.controller('otherChildCtrl', $scope, myService){
// how to get the updated objectX?
var data = myService.getData();
}
Related
I'm using AngularJS's UI-Router to manage routes for my web application.
I have two states: parent_state and child_state arranged as shown below.
$stateProvider
.state('parent_state', {
abstract: true,
views: {
'#' : {
templateUrl: 'http://example.com/parent.html',
controller: 'ParentCtrl'
}
}
})
.state('child_state', {
parent: 'parent_state',
url: '/child',
params: {
myArg: {value: null}
},
views: {
'mainarea#parent_state': {
templateUrl: 'http://example.com/child.html',
controller: 'ChildCtrl'
}
}
})
From within ChildCtrl, I can access myArg like this:
app.controller("ChildCtrl", function($stateParams) {
console.log('myArg = ', $stateParams.myArg);
});
Is it possible for me to access myArg and have it displayed in the html page parent.html? If so, how can it be done? I see that the ParentCtrl controller for the abstract state is never even called.
This question addresses a related topic. But it doesn't show me how to display a parameter to the child state in a template of the parent state.
The first thing that comes to my mind is to use events for notifying parent after child param change. See the following (you can even run it here).
Child, after rendering, emits an event to the parent with the changed value of the parameter. Parent grabs and displays it in its own template.
angular.module('myApp', ['ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('parent_state', {
abstract: true,
template: "<h1>Parent! Value from child: {{ paramFromChild }}</h1><div ui-view></div>",
controller: function ($scope) {
$scope.$on('childLoaded', function (e, param) {
$scope.paramFromChild = param;
});
}
})
.state('child_state', {
parent: 'parent_state',
url: '/child',
params: {
myArg: {value: null}
},
template: '<h2>Child! Value: {{ param }}</h2>',
controller: function($stateParams, $scope){
$scope.param = $stateParams.myArg;
$scope.$emit('childLoaded', $stateParams.myArg);
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.10/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.20/angular-ui-router.js"></script>
<div ng-app="myApp">
<a ui-sref="child_state({myArg: 'first'})">First link</a>
<a ui-sref="child_state({myArg: 'second'})">First second</a>
<div ui-view></div>
</div>
Is it possible for me to access myArg and have it displayed in the
html page parent.html?
That is against the principle of the UI-Router. Parent params can be consumed in children, but not vice versa. How would parent view know about changes WITHOUT re-initializing the controller? You need something like watching.
The true way is to employ Multiple Named Views. Look at this working plunkr.
Yes, this is possible.
Using $stateChangeSuccess:
You can use $stateChangeSuccess to achieve this.
For example:
.state('main.parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'vm',
templateUrl: 'app/parent.html',
data: {
title: 'Parent'
}
})
.state('main.parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'vm',
templateUrl: 'app/child.html'
})
And in the runblock call it as follows:
$rootScope.$on('$stateChangeSuccess', function (event, toState, fromState) {
var current = $state.$current;
if (current.data.hasOwnProperty('title')) {
$rootScope.title = current.data.title;
} else if(current.parent && current.parent.data.hasOwnProperty('title')) {
$rootScope.title = current.parent.data.title;
} else {
$rootScope.title = null;
}
});
Then you can access the $rootScope.title from the child controller since it is globally available.
Using a Factory or Service:
By writing setters and getters you can pass data between controllers. So, you can set the data from the child controller and get the data from the parent controller.
'use strict';
(function () {
var storeService = function () {
//Getters and Setters to keep the values in session
var headInfo = [];
return {
setData: function (key, data) {
headInfo[key] = data;
},
getData: function (key) {
return headInfo[key];
}
};
};
angular.module('MyApp')
.factory('StoreService', storeService);
})(angular);
Set data from child controller
StoreService.setData('title', $scope.title)
Get data
StoreService.getData('title');
Using events $emit, $on:
You can emit the scope value from the child controller and listen for it in the parent scope.
I'm using $stateProvider for routing in my application. For some requirement, I'd like to use this (or $state itself) to read data of self .state(), in the data attribute, as in below.
...
.state(<state_name>, {
url: <path>,
views: {
<view_name>: {
templateUrl: <template_url>,
controller: <controller_name>
}
},
data: {
selfStateData: (function() {
return this;
})()
}
})
...
When checked for the above returned value by console.log() in $stateChangeSuccess, I'm receiving undefined.
Please help.
Check if .resolve() holds this and update the data attribute in the same .state().
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: '...',
});
}
I'm developing application using Ionic Framework and there's problem I can't solve.
I have few views that are steps and must share data, user can also step back, go to some else views and come back later. His input should be stored until last step is finished then I can persist model and I need new empty one.
I used factory for this, but I can't find a way to clear object that is returned. Is it possible?
What's some other approaches that I can use here?
app.js
.state('topup', {
url: "/topup",
abstract: true,
templateUrl: "templates/topup/topup.html",
controller:'TopupCtrl'
})
.state('topup-1', {
url: "/topup-1",
templateUrl: "templates/topup/topup-1.html",
controller:'TopupCtrl'
})
.state('topup-2', {
url: "/topup-2",
templateUrl: "templates/topup/topup-2.html",
controller:'TopupCtrl'
})
controllers.js
.controller('TopupCtrl', function($scope, $state, TopupData, HistoryData) {
$scope.data = TopupData.getCurrent();
$scope.selectOperator = function(operator) {
$scope.data.Operator = operator;
$state.go("topup-2");
};
$scope.acceptTopup = function(){
HistoryData.getHistory().push($scope.data);
console.log(HistoryData.getHistory());
TopupData.setCurrent({});
$state.go("main");
};
})
services.js
.factory('TopupData', function () {
var service = {};
var Model = {};
service.setCurrent = function(value)
{
Model = value;
};
service.getCurrent = function(value)
{
return Model;
};
return service;
})
Try this way.
console.log(HistoryData.getHistory());
var resetObj = {};
TopupData.setCurrent(resetObj);
$state.go("main");
I'd suggest you to don't define a controller on state level, if they are of the same name, re initializing same controller on state change is not good idea.
HTML
<div ng-controller="TopupCtrl">
<ui-view></ui-view>
</div>
CODE
.state('topup', {
url: "/topup",
abstract: true,
templateUrl: "templates/topup/topup.html"
})
.state('topup-1', {
url: "/topup-1",
templateUrl: "templates/topup/topup-1.html"
})
.state('topup-2', {
url: "/topup-2",
templateUrl: "templates/topup/topup-2.html"
})
Then i don't think so you will need factory anymore, as there no need to share a scope.
Changing code like this, solved my problem:
var resetObj = {};
TopupData.setCurrent(resetObj);
$state.go("main").then(function(){
$ionicHistory.clearHistory();
$ionicHistory.clearCache();
});
I've got a url in my application that needs to load one of two templates based on the results of a resolve call, like so:
app.config(function($routeProvider) {
$routeProvider.
when('/someurl', {
resolve: {
someData: function(dataService) {
var data = dataService.loadData();
// data has a .type field that determines which template should be loaded
return data;
}
},
templateUrl: function(routeParams) {
// return a path based on the value of data.type in the someData resolve block
}
})
});
Is there a way for me to set the templateUrl based on what's returned by the someData resolve?
So I figured out how to do this using ui-router - thanks m59 for pointing me at it.
Here's how you'd do this with ui-router:
app.config(function($stateProvider) {
$stateProvider
.state('someState'
url: '/someurl',
template: '<ui-view>',
resolve: {
someData: function(dataService) {
var data = dataService.loadData();
// data has a .type field that determines which template should be loaded
return data;
}
},
controller: function($state, someData) {
$state.go('someState.' + someData.type);
}
})
.state('someState.type1', {
templateUrl: 'someTemplate1.html',
controller: 'Type1Controller'
})
.state('someState.type2', {
templateUrl: 'someTemplate2.html',
controller: 'Type2Controller'
})
});
There's a parent state that handles resolves, then redirects to the child states based on the information it gets. The child states don't have a url, so they can't be loaded directly.