ui-router: open modal and pass parent scope parameters to it - javascript

I am using this FAQ entry to open a modal dialog in a child state of a certain state: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state
My code is below. When I open a modal dialog, I need to get access to the properties of the parent state's scope. Is this possible?
plnkr: http://plnkr.co/edit/knY87n
.state('edit', {
url: '/{id:[0-9a-f]+}',
views: {
'#': {
templateUrl: 'views/edit.html',
controller: 'editContr'
}
}
})
.state('edit.item', {
url: "/item/new",
onEnter: function($stateParams, $state, $modal) {
$modal.open({
controller: 'itemEditContr',
templateUrl: 'views/edit-item.html',
}).result.then(function (item) {
//
// here I need to insert item into the items
// seen by my parent state. how?
//
$state.go('^');
}, function () {
$state.go('^');
});
}
});
function editContr($scope) {
$scope.items = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
}
function itemEditContr($scope, $modalInstance) {
$scope.submit = function () {
$modalInstance.close($scope.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.item = {name: 'test'};
}

Are you planning to update params set by the parent? You should be able to get the parent state using
$state.$current.parent
For e.g. the name of the parent state will be $state.$current.parent.name
EDIT: Updating since the OP wanted to access the Scope and not the parent state.
You can emit an event from the child and then capture it in the parent.
Untested code:
In Parent:
$scope.$on('ADD_ITEM', function(evt, msg) {
$scope.items.add(msg);
}
In the child state:
$scope.$emit('ADD_ITEM', item);

long story short, yes it is. Reading the angularjs developer guide for scopes, is actually one of their more helpful and very well documented pieces: http://docs.angularjs.org/guide/scope
Aside from that, scope in angular is no different than scope with any javascript object.
You've got one or two things that you're not doing correctly. For one, passing item in your onEnter function won't help unless you're grabbing something from the url as an identifier, or resloving some data that you can inject into the states controller. You're trying to do the latter, but you aren't resolving anything, so you are getting undefined on item.
One trick you can use is to set a truthy value in your your parent, and access it.
//in parent ctrl
$scope.newItem = function(itemname){
return {name:itemname}
}
$scope.save = function(item){
$scope.items.push(item);
}
Then when you open your modal call $scope.getItem() within the controller instead of injecting item into the controller directly
function itemEditContr($scope, $modalInstance) {
$scope.submit = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.item = $scope.newItem('test') //this will look for newItem funciton in the local scope, fail to find it, and walk up the heirarchy until it has found newItem() in the parent
//now a save function, also defined on the parent scope
$scope.save($scope.item);
}
Accessing the parent scope is nothing special, just make sure to get a value, and not overwrite it. so you can access $scope.items from the child controller by assigning it to a variable, or you can push it new values, but never set it, or you will create a new local items object on the child scope instead.

I struggled with this too and found an alternative solution. Instead of using the onEnter function you can use a controller with a very simple view that calls a function to open the modal. I used a
config(['$stateProvider', function($stateProvider) {
// Now set up the states
$stateProvider
.state('exmple.modal', {
url: "/example/modal/",
controller: "ExampleController",
template: '<div ng-init="openModal()"></div>'
});
}]);
In the example controller you an put the openModal() function to open the modal where you have a scope.

Related

combine two scopes in angular

I have two scopes and need to combine them. Does anyone know the best way to do this? below are the two scopes. the second one comes from a service
The first scope, retrieves info from database using PHP SLIM RESTful API
Data.get('posts').then(function(data){
$scope.posts = data.data;
});
The second scope retrieves info from database via a service
dataShare.getconfigs().then(function(data){
$scope.configs = data;
});
UPDATE:
When I open the modal to edit I only get the $scope.posts. I am not currently passing through the $scope.configs
$scope.open = function (p,size) {
var modalInstance = $uibModal.open({
templateUrl: 'views/postsEdit.html',
controller: 'postsEditCtrl',
size: size,
resolve: {
item: function () {
return p;
}
}
}); ...
The problem is that $modal.open will create new isolated scope which will be used inside of modal controller and template. This new scope is going to be a direct child of the $rootScope, and thus it will not inherit from your $scope. However, what you want is to inherit from the $scope object from which you open a modal. For this configure modal like this:
$scope.open = function(p, size) {
var modalInstance = $uibModal.open({
templateUrl: 'views/postsEdit.html',
controller: 'postsEditCtrl',
size: size,
scope: $scope, // <-- use $scope as a parent to for modal scope
resolve: {
item: function() {
return p;
}
}
});
};

angular router does not select child state (nested states)

I'm having a problem with a new state I added to our web site.
Short description
I have a state (/page)'q.explorer' which contains a button; when clicked it should go to a child state (named 'q.explorer.detail') but it does not. However: in the logging I see that it does try to go to that (/state) and the new url is formatted as defined in the child state.
But still the template and controller that are actually used is the 'parent' which contains the button ...
This may be a little confusing to explain; so I have also added some code in the hope that this will clarify my problem.
The setup looks like this:
$stateProvider
.state('q', {
url: '/:locale/app',
data : { ui: "V2" },
views: {
'application' : {template: '<div ui-view=""><page-home-v2></page-home-v2></div>' }
}, controller: function($scope, $stateParams, siteNavigation) {
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer', {
url: '/explorer?:year&:month&:guide',
template: '<page-explorer-v2></page-explorer-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer");
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer.detail', {
url: '/detail',
template: '<page-explorer-detail-v2></page-explorer-detail-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer.detail");
siteNavigation.applyUrlParameters($stateParams);
}
})
angular
.module('q.components')
.service('siteNavigation', function($state, $location) {
var service = this;
service.applyUrlParameters = function($stateParams) {
if (!$stateParams) $stateParams = $state.params;
console.log('Apply state parameters for state: ' + $state.current.name);
console.log('URL >> ' + $location.url());
};
};
Somewhere deep in the template of "q.explorer" there is a button to open the detail view ("q.explorer.detail"). It uses this code:
function goToDetail() {
var ui = $state.current.data.ui;
if (ui === "V1") { /*...*/ }
else if (ui === "V2") {
var params = {
year: Store.getYear(),
month: Store.getMonth(),
locale: Store.getLocale()
}
var guide = Store.getActiveSidebarItem();
if (guide) params.guide = guide.slug;
console.log("go to explorer: " + params.guide);
$state.go('q.explorer.detail', params);
}
else console.log("Unable to go to state because the ui version is not known.");
}
And this is what I see in the console after clicking the link:
go to explorer: ebit
controller: q.explorer
Apply state parameters for state: q.explorer.detail
URL >> /nl/app/explorer/detail?year=2015&month=11&guide=ebit
As you can see, it uses the controller of the 'parent' iso the child page I want to open. Even though $state.current.name is correct ... Or maybe I should say it does not change from state ...
Any help is welcome.
(PS: We are using Angular 1.4.9)
Looks like you are using nested states such that q.explorer.detail is a child of q.explorer. To render the child state's template, you also need a specific ui-view where it can be placed into. And this will be searched in the template of the parent state. Getting the console.log() output just means the controllers are instantiated, but that even happens if the template isn't rendered at all.
So check if you have an ui-view in the template of the q.explorer state. For more details, please see: https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
You could also fix this by not making q.explorer.detail a child of q.explorer. A child state is created as soon as you need the dot notation.
Yoy have to add somewhere in 'q.explorer' state's template entry point for nested view 'q.explorer.detail', otherwise child controller will not be called.
For example:
template: '<page-explorer-v2></page-explorer-v2><ui-view></ui-view>',
instead of
template: '<page-explorer-v2></page-explorer-v2>'
See jsfiddle: http://jsfiddle.net/jcpmsuxj/42/
Upd. As #ajaegle mentioned you should to visit official docs page:
https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views

How to access parameter from nested state in parent state?

I have an example here. How i can access 'itemId' parameter in the parent state controller? Parent view must not be reloaded.
angular.module('app',['ui.router'])
.config(function($stateProvider){
$stateProvider.state('step', {
url: '/orders/:step',
templateUrl: 'parent.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
})
.state('step.item', {
url: '/:itemId',
templateUrl: 'child.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
});
}).controller('SimpleController', function($scope, $state){
$scope.items = [1,2,3,4,5,6];
$scope.steps = ["first", "second"];
})
I see only two ways:
add an service and inject it to both controllers
access a parent scope from a child controller and pass a parameter
But both cause me to add watchers at a parent scope.
Maybe i can accomplish it easier?
First, you need to recognize that a parent state's controller runs only once when you enter the subtree of its child states, so switching between its children would not re-run the controller.
This is to say that if you want the itemId parameter to always be up to date, you'd need a $watch (which you tried to avoid).
For the first time, you could collect the itemId like so in the parent's state:
$scope.itemId = $state.params.itemId;
If you need it to be kept up-to-date, then you'd need to watch for changes, for example:
$scope.$watch(function(){
return $state.params;
}, function(p){
$scope.itemId = p.itemId;
});
plunker
Similarly, if you only need to place it in the view, then set $state on the scope:
$scope.$state = $state;
and in the view:
<div>parent /orders/{{step}}/{{$state.params.itemId}}</div>
EDIT:
I guess another way would be to call a scope-exposed function on the parent to update the value. I'm not a fan of such an approach, since it relies on scope inheritance (which is not always the same as state inheritance) and scope inheritance in a large application is difficult to track. But it removes the need for a $watch:
In the parent controller:
$scope.updateItemId = function(itemId){
$scope.itemId = itemId;
};
and in the child controller:
if ($scope.updateItemId) $scope.updateItemId($stateParams.itemId)
I can see, that you've already found your answer, but I would like to show you different approach. And I would even name it as "the UI-Router built in approach".
It has been shown in the UI-Router example application, where if we go to child state with some ID = 42 like here we can also change the other, than the main view (the hint view in this case).
There is a working example with your scenario, showing that all in action.
What we do use, is the Multiple Named Views
The parent state, now defines root view, which is injected into unnamed ui-view="" inside of the root/index.html.
A new parent.html
<div ui-view="title"></div>
<div ui-view=""></div>
As we can see, it also contains another view target (than unnamed) - e.g. ui-view="title" which we fill with another template immediately... in parent state:
$stateProvider.state('step', {
url: '/orders/:step',
views : {
'': {
templateUrl: 'parent.html',
},
'title#step': {
templateUrl:'parent_title_view.html',
}
}
})
And child? It can continue to handle main area... but also can change the title area. Both views are independent, and belong to child.
.state('step.item', {
url: '/:itemId',
views : {
'': {
templateUrl: 'child.html',
},
'title': {
templateUrl:'child_title_view.html',
}
}
So, there are no watchers, nothing... which is not shipped with the UI-Router by default. We just adjust many places (views) with the child implementation. We can still provide their content with some defaults in parent..
Check that example here. Hope it will help to see it a bit differently...
I'm not sure if this will really solve your problem but you can do
$stateProvider.state('step', {
url: '/orders/:step/:itemId',
templateUrl: 'parent.html',
http://plnkr.co/edit/sYCino1V0NOw3qlEALNj?p=preview
This means the parent state will collect the itemID on the way through.
include $state in resolve function for parent state
.state('parent', {
url: '/parent',
controller: 'parentController',
resolve: {
params: ['$state', function ($state) {
return $state.params;
}]
}
})
then in parent controller inject the resloved vars
app.controller('parentController', ['$scope', 'params',
function ($scope, params) {
$scope.params = params;
}]);
in this way params available in both child and parent.

Named view in angular ui-router not updating, despite being watched

I have a scoped variable $scope.foo that I am keeping a watch on. It could be updated through a text field in a form.
I have two named views A and B on a page that I am rendering using angular ui-router.
The named view A has the text form field that is being watched for changes in a controller through ng-model="foo". When the value of foo is changed by a user it changes the value of another scoped variable $scope.bar, which is an array, in the controller that is being used in the ng-repeat directive on the named view B. The changes in the $scope.bar is made using $scope.$watch method in the controller.
The issue that I am facing is that the when the foo is changed I could see the changes in bar on the named view A but not on the named view B.
Could somebody help me resolve this issue?
Edit:
Here is the plunker for this issue.
There is a plunker, which should show that your scenario is working.
The most important part of that solution is driven by:
Scope Inheritance by View Hierarchy Only (cite:)
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.
Let me express it again: A scope inheritance goes only via the view nesting.
With that we can create this states definitions:
$stateProvider
.state('root', {
url: '/root',
templateUrl: 'tpl.root.html',
controller: 'RootCtrl', // this root scope will be parent
})
.state('root.entity', {
url: '/entity',
views:{
'A': {
templateUrl: 'tpl.a.html',
controller: 'ACtrl', // scope is inherited from Root
},
'B': {
templateUrl: 'tpl.b.html',
controller: 'ACtrl', // scope is inherited from Root
}
}
})
So the state defintion support nested views - let's profit from that and place the $scope.bar collection into the parent. All views involved will then have access to the same collection:
.controller('RootCtrl', function ($scope, $state) {
$scope.bar = ['first', 'second', 'last'];
})
.controller('ACtrl', function ($scope, $state) {
// *) note below
$scope.foo = $scope.bar[0];
$scope.$watch("foo", function(val){$scope.bar[0] = val; });
})
.controller('BCtrl', function ($scope, $state) {
})
*) note: here we do 1) set from bar 2) $watch and 3) set back to bar to follow the question description... but if the array would contain objects, we can work with them directly... without that overhead, but that's another story...
Check here how that works, and that any changes in view A are also visible in B ... because of inherited reference to the array bar declared in parent $scope.
I created the second answer, to follow also the issue in this plunker, which #skip (OP) passed me as the example fo the issue.
Firstly There is an updated working version
of that plunker, which does what we need. There are the main changes:
The original state def:
.state('home', {
url: '/',
views: {
'': { templateUrl: 'home.html' },
'A#home': {
templateUrl: 'a.html',
controller: 'MainCtrl'
},
'B#home': {
templateUrl: 'b.html',
controller: 'MainCtrl'
}
}
Was replaced with the RootCtrl defintion:
.state('home', {
url: '/',
views: {
'': {
templateUrl: 'home.html',
controller: 'RootCtrl' // here we do use parent scoping
},
'A#home': {
templateUrl: 'a.html',
controller: 'MainCtrl'
},
'B#home': {
templateUrl: 'b.html',
controller: 'MainCtrl'
}
}
And this was one controller:
app.controller('MainCtrl', function($scope) {
var fruits = [{"name": "Apple"}, {"name": "Banana"}, {"name": "Carrot"}];
$scope.bar = $scope.bar || [];
$scope.foo = 2;
$scope.$watch('foo',function(value, oldValue){
$scope.bar = [];
getBar(fruits, value);
});
function getBar(fruits, howManyFruits) {
for(var i=0; i < $scope.foo; i++) {
$scope.bar.push(fruits[i]);
}
}
});
But now we do have two (Parent and child):
app.controller('RootCtrl', function($scope) {
$scope.bar = [];
})
app.controller('MainCtrl', function($scope) {
var fruits = [{"name": "Apple"}, {"name": "Banana"}, {"name": "Carrot"}];
//$scope.bar = $scope.bar || [];
$scope.foo = 2;
$scope.$watch('foo',function(value, oldValue){
$scope.bar.length = 0;
getBar(fruits, value);
});
function getBar(fruits, howManyFruits) {
for(var i=0; i < $scope.foo; i++) {
$scope.bar.push(fruits[i]);
}
}
});
Some important parts to mention
I. The least common denominator
We have to move the shared collection (array bar) into the parent. Why?
we have to move the shared reference to the least common denominator - to the parent scope
see
How do I prevent reload on named view, when state changes? AngularJS UI-Router
II. The Reference to array must be unchanged
we have to keep the reference to the Parent $scope.bar unchanged!. This is essential. How to achieve that? see:
Short way to replace content of an array
where instead of creating new reference, we clear the array, keeping the reference to it
// wrong
$scope.bar = [];
// good
$scope.bar.length = 0;
III. Controller can have multiple instances
Also, the fact that both views A and B had the same controller (same controller name in fact), definitely did not mean, that they were the same instance.
No, they were two different instances... not sharing anything. That is I guess, the most critical confusion. see
angularjs guide - Dependency Injection
Controllers are special in that, unlike services, there can be many instances of them in the application. For example, there would be one instance for every ng-controller directive in the template.
Please, observe that all in the updated example

Passing information from child state to parent state in angular-ui-router

I am building an angular-app with ui-router where I have a parent view and a child view.
routerApp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/views/home/home.html',
controller: 'HomeController'
})
.state('new-assignment', {
url: '/new-assignment',
templateUrl: 'new-assignment.html',
controller: 'AssignmentController'
})
.state('new-assignment.new-client', {
url: '/new-client',
templateUrl: 'new-client.html',
controller: 'ClientController'
})
;
});
The child view is used (among other things) to create a new client.
So, by clicking on 'create new client' in the main view. The state is change to 'new-assignment.new-client', and the side view is shown.
When the client is created I want to transition back to the parent view 'new-assignment' and pass along information with what client that have just been created.
The parent view could still have data in its own form (for creating assignment) and should naturally not be touched.
I can detect a change by '$stateChangeSuccess' event:
routerApp.controller('AssignmentController', function($scope, Assignment, Client, $log) {
$scope.clients = Client.clients;
$scope.$on('$stateChangeSuccess', function (e, toState, toParams, fromState, fromParams){
$log.log(fromState.name + ' -> ' + toState.name);
$log.log('toParams ');
$log.log(toParams);
$log.log('fromParams ');
$log.log(fromParams);
});
});
I have tried to pass info through
$state.go('^', data);
but without success...
But I don't understand how to pass data to the parent view. Any ideas?
Child view scope inherits from the scope of the parent view. So just define $scope.clients[] array in the AssignmentController. Then add the new client to that array in the ClientController.
Take a look at this Plunker to see how the nested view scopes get access to their parent view scope.
Rudely I will answer my own question.
I decided to go with event messaging:
app.controller('ClientController', function ($scope, $state, ClientService) {
$scope.save = function (newClientData) {
var ret = ClientService.create(newClientData);
if (ret) {
// Pass newly created object to parent view.
$scope.$emit('clientCreated', newClientData);
// Then go to that state (closing child view)
$log.log($state);
if ($state.current.name === 'new-client')
return $state.go('home');
$state.go('^');
}
};
});
app.controller('AssignmentController', function ($scope, Assignment, ClientService) {
$scope.clients = ClientService.clients;
$scope.$on('clientCreated', function (e, data) {
$scope.clientSelected(data);
});
$scope.clientSelected = function (client) {
$log.log(client);
$scope.data.client = client;
$scope.data.clientName = client.name;
$scope.data.clientId = client.id;
}
});
This code is linked to the code in my question!
yes, various ideas.
You can send the data information using query parameters. Converting your data to JSON and sending like this "$state.go('^', data)" and defining your state url with "new-assignmentww/?data".
If data is assigned, you can get information and convert to object again.
Other, you can access the parent state using "$state.$parent" and assign the value in the controller, then you can call the "$state.go('^')".
You can also create a callback function in controller of the parent state and call in the controller of the children state.

Categories

Resources