Preventing parent state from reloading on child state change - angularjs

I am trying to implement a tabbed interface akin to this: http://odetocode.com/blogs/scott/archive/2014/04/14/deep-linking-a-tabbed-ui-with-angularjs.aspx
However, on my state change, the controller of the parent state seems to be reinitialized (or a new $scope is created?)
There are two major differences between the example plunkr and my project.
I use a parameter in my url
I resolve different data on the state change for each tab (removing this does nothing).
I am not using ui-bootstrap for the tabs but am triggering a $state.go on ng-click of the tab.
I experimented with the above plunkr and added a dropdown to the parent state; however the parent dropdown values seem to persist when the child states change. I am not too concerned with the child states and will probably end up using sticky states anyways.
I am using wondering if I am doing something fundamentally wrong before I try and add another package to my project.
here is a rough plunkr of what I am trying to do: http://plnkr.co/edit/TmRQN5K8OEc8vHG84G5z?p=preview
here is my config:
app.config(function($stateProvider, $urlRouterProvider){
$urlRouterProvider.when('/main',
function ($state) {
$state.go('parent.tab1', { main_id: '00008' });
});
$stateProvider
//Handle States Here
.state('parent', {
abstract: true,
url: '/parent?main_id',
templateUrl: "main.html",
controller: 'Main_Controller',
resolve: {
//Calls to API
}
})
.state('parent.tab1', {
url: "/applications",
templateUrl: "tab1.html",
controller:'Tab1Ctrl',
resolve: {
//Get some different data from an API
},
})
.state('parent.tab2', {
url: "/phasing",
templateUrl: "tab2.html",
controller: 'Tab2Ctrl',
resolve: {
//More API Data
}
});
});

I've made your plunker working here
$urlRouterProvider
//.when('/main',
.when('',
function ($state) {
$state.go('parent.tab1', { main_id: '00008' })
});
Also there is a change in main.html, which does not use ng-controller any more. We just have to pass the proper Controller name
$stateProvider
//Handle States Here
.state('parent', {
abstract: true,
url: '/parent?main_id',
templateUrl: "main.html",
controller: 'MainController',
resolve: {
//Calls to API
}
})
...
// MainController
// these two names should fit
app.controller("MainController", function($rootScope, $scope, $state) {
So now, it is working, and let's discuss
I use a parameter in my url
I resolve different data on the state change for each tab (removing this does nothing).
I am not using ui-bootstrap for the tabs but am triggering a $state.go on ng-click of the tab.
Quick answers:
parameter in url exists, e.g. #/parent/tab1?main_id=8000
resolve is trigerred for each controller if controller is reinstantiated. That happens when we navigate to that state (among tabs)
no need to use $state.go, I used:
a snippet:
<a ui-sref="parent.tab1({main_id:'00008'})"> go to tab1 with main_id '00008'</a><br />
<a ui-sref="parent.tab2({main_id:'00008'})"> go to tab2 with main_id '00008'</a><br />
<a ui-sref="parent.tab3({main_id:'00008'})"> go to tab3 with main_id '00008'</a><br />
Check it here

Related

Hide previously opened child views when linking to parent with ui-sref

I have a map with markers on it. When you click a marker a child view opens with the marker details.
.state('map', {
url: '/map',
...
})
.state('map.detail', {
url: '/:markerId',
...
})
If you open a marker's details, navigate away from the map, then back to the map using ui-sref="map" the url will change to /map/<markerId> with the child view open.
I'd like to go straight to /map when clicking on ui-sref="map" but don't want to reload the 'map' just hide any child view.
Is there a simple way to do this? I've tried combinations of cache: false on the two states but that doesn't do what I want.
I am not fully sure, what is your issue. Because this
...I'd like to go straight to /map when clicking on ui-sref="map" but don't want to reload the map just hide any child view...
is the default behavior of UI-Router. There is a working plunker
These links, will not trigger RE-init of parent controller ('map' state)
<a ui-sref="map">
<a ui-sref="map.detail({markerId:1})">
<a ui-sref="map.detail({markerId:22})">
And states like this:
.state('map', {
url: "/map",
templateUrl: 'tpl.html',
controller: 'ParentCtrl',
})
.state('map.detail', {
url: "/:markerId",
templateUrl: 'child.html',
controller: 'ChildCtrl',
})
And both controllers:
.controller('ParentCtrl', ['$scope', function ($scope) {
$scope.timestamp = new Date().getTime();
}])
.controller('ChildCtrl', ['$scope', function ($scope) {
$scope.timestamp = new Date().getTime();
}])
And we can see, that 'map' state is still not changed
Check it here

Can I have a child state that is the index using ui-router?

My routes look like:
$stateProvider.state('repository', {
url: '/:host/:owner/:repository',
views: {
appView: {
templateUrl: '/templates/app/repository.html'
}
}
}).state('repository.analytics', {
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryAnalytics.html'
}
}
}).state('repository.commit', {
url: '/:host/:owner/:repository/commit/:commitHash',
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryCommit.html'
}
}
}).state('repository.file', {
url: '/file?path',
views: {
repositoryView: {
templateUrl: '/templates/app/_repositoryFile.html'
}
}
});
I want the base URL for all repository-like states, that's why I'm specifying the url there. As an example, if I didn't do it this way, I would have to specify everything as it's shown in the commit state. This is verbose and not something I want to do.
So is it possible to have a default child state for repository so that if someone is directed there, then that child view loads?
** UPDATE **
This seems to work just fine if I click through the app, but if I go to the /:host/:owner/:repository URL directly, the child view (analytics) never loads.
I don't know whether you can have a default child state, but you can set subview in that parent state. Like this:
$stateProvider.state('repository', {
url: '/:host/:owner/:repository',
views: {
appView: {
templateUrl: '/templates/app/repository.html'
},
'repositoryView#repository': { // it means the repositoryView of repository state.
templateUrl: '/templates/app/_repositoryAnalytics.html'
}
}
})
Then, when you open with repository state or URL, the analytics page will be loaded in repositoryView view of repo page.
[updated]
This format 'repositoryView#repository' means that, the view 'repositoryView' in the state 'repository'. Because you try to open the state 'repository', with a default sub-view. And the sub view 'repositoryView' is defined in 'repository.html'. If you didn't set the state scope, ui-router will think that the sub-view 'repositoryView' belongs to 'repository' 's parent view.
I don't know whether I explain it clearly, you can check the ui-router wiki
I created working plunker here. One way could be to use eventing to force go to child state, when parent is selected (resolved from url)
.run(['$rootScope', '$state', '$stateParams',
function($rootScope, $state, $stateParams) {
$rootScope.$on('$stateChangeStart', function(event, toState, toPrams) {
if (toState.name === "repository") {
event.preventDefault();
$state.go('repository.analytics', toPrams);
}
});
}
])
Check it here
Some other topics about redirections parent-child:
Angular UI-Router $urlRouterProvider .when not working when I click <a ui-sref="...">
Angular UI-Router $urlRouterProvider .when not working *anymore*

Angular-ui.router: Update URL without view refresh

I have an Angular SPA that presents a variety of recommendation lists, and a Google Map of locations, based on different cuts of some restaurant data (see m.amsterdamfoodie.nl). I want each of these lists to have their own URL. In order for Google to crawl the different lists I use <a> tags for the offcanvas navigation.
At present the <a> tag causes a view refresh, which is very noticeable with the map.
I can prevent this using ng-click and $event.preventDefault() (see code snippets below), but then I need to implement a means of updating the browser URL.
But in trying Angular's $state or the browser's history.pushstate, I end up triggering state changes and the view refresh...!
My question is therefore how can I update a model and the URL, but without refreshing the view? (See also Angular/UI-Router - How Can I Update The URL Without Refreshing Everything?)
I have experimented with a lot of approaches and currently have this html
Budget
In the controller:
this.action = ($event) ->
$event.preventDefault()
params = $event.target.href.match(/criteria\/(.*)\/(.*)$/)
# seems to cause a view refresh
# history.pushState({}, "page 2", "criteria/"+params[1]+"/"+params[2]);
# seems to cause a view refresh
# $state.transitionTo 'criteria', {criteria:params[1], q:params[2]}, {inherit:false}
updateModel(...)
And, what is I think is happening is that I am triggering the $stateProvider code:
angular.module 'afmnewApp'
.config ($stateProvider) ->
$stateProvider
.state 'main',
url: '/'
templateUrl: 'app/main/main.html'
controller: 'MainCtrl'
controllerAs: 'main'
.state 'criteria',
url: '/criteria/:criteria/:q'
templateUrl: 'app/main/main.html'
controller: 'MainCtrl'
controllerAs: 'main'
One possible clue is that with the code below if I load e.g. http://afmnew.herokuapp.com/criteria/cuisine/italian then the view refreshes as you navigate, whereas if I load http://afmnew.herokuapp.com/ there are no refreshes, but no URL updates instead. I don't understand why that is happening at all.
This is an example of the way to go if I understand correctly:
$state.go('my.state', {id:data.id}, {notify:false, reload:false});
//And to remove the id from the url:
$state.go('my.state', {id:undefined}, {notify:false, reload:false});
From user l-liava-l in the issue https://github.com/angular-ui/ui-router/issues/64
You can check the $state API here: http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
Based on our previous discussions, I want to give you some idea, how to use UI-Router here. I believe, I understand your challenge properly... There is a working example. If this not fully suites, please take it as some inspiration
DISCLAIMER: With a plunker, I was not able to achieve this: http://m.amsterdamfoodie.nl/, but the principle should be in that example similar
So, there is a state definition (we have only two states)
$stateProvider
.state('main', {
url: '/',
views: {
'#' : {
templateUrl: 'tpl.layout.html',
controller: 'MainCtrl',
},
'right#main' : { templateUrl: 'tpl.right.html',},
'map#main' : {
templateUrl: 'tpl.map.html',
controller: 'MapCtrl',
},
'list#main' : {
templateUrl: 'tpl.list.html',
controller: 'ListCtrl',
},
},
})
.state('main.criteria', {
url: '^/criteria/:criteria/:value',
views: {
'map' : {
templateUrl: 'tpl.map.html',
controller: 'MapCtrl',
},
'list' : {
templateUrl: 'tpl.list.html',
controller: 'ListCtrl',
},
},
})
}];
This would be our main tpl.layout.html
<div>
<section class="main">
<section class="map">
<div ui-view="map"></div>
</section>
<section class="list">
<div ui-view="list"></div>
</section>
</section>
<section class="right">
<div ui-view="right"></div>
</section>
</div>
As we can see, the main state does target these nested views of the main state: 'viewName#main', e.g. 'right#main'
Also the subview, main.criteria does inject into layout views.
Its url starts with a sign ^ (url : '^/criteria/:criteria/:value'), which allows to have / slash for main and not doubled slash for child
And also there are controllers, they are here a bit naive, but they should show, that on the background could be real data load (based on criteria).
The most important stuff here is, that the PARENT MainCtrl creates the $scope.Model = {}. This property will be (thanks to inheritance) shared among parent and children. That's why this all will work:
app.controller('MainCtrl', function($scope)
{
$scope.Model = {};
$scope.Model.data = ['Rest1', 'Rest2', 'Rest3', 'Rest4', 'Rest5'];
$scope.Model.randOrd = function (){ return (Math.round(Math.random())-0.5); };
})
.controller('ListCtrl', function($scope, $stateParams)
{
$scope.Model.list = []
$scope.Model.data
.sort( $scope.Model.randOrd )
.forEach(function(i) {$scope.Model.list.push(i + " - " + $stateParams.value || "root")})
$scope.Model.selected = $scope.Model.list[0];
$scope.Model.select = function(index){
$scope.Model.selected = $scope.Model.list[index];
}
})
This should get some idea how we can use the features provided for us by UI-Router:
Absolute Routes (^)
Scope Inheritance by View Hierarchy Only
View Names - Relative vs. Absolute Names
Check the above extract here, in the working example
Extend: new plunker here
If we do not want to have map view to be recreated, we can just omit that form the child state def:
.state('main.criteria', {
url: '^/criteria/:criteria/:value',
views: {
// 'map' : {
// templateUrl: 'tpl.map.html',
// controller: 'MapCtrl',
//},
'list' : {
templateUrl: 'tpl.list.html',
controller: 'ListCtrl',
},
},
})
Now our map VIEW will be just recieving changes in the model (could be watched) but view and controller won't be rerendered
ALSO, there is another plunker http://plnkr.co/edit/y0GzHv?p=preview which uses the controllerAs
.state('main', {
url: '/',
views: {
'#' : {
templateUrl: 'tpl.layout.html',
controller: 'MainCtrl',
controllerAs: 'main', // here
},
...
},
})
.state('main.criteria', {
url: '^/criteria/:criteria/:value',
views: {
'list' : {
templateUrl: 'tpl.list.html',
controller: 'ListCtrl',
controllerAs: 'list', // here
},
},
})
and that could be used like this:
<h4>{{main.hello()}}</h4>
<h4>{{list.hello()}}</h4>
The last plunker is here
you can use scope inheritance to update url without refreshing view
$stateProvider
.state('itemList', {
url: '/itemlist',
templateUrl: 'Scripts/app/item/ItemListTemplate.html',
controller: 'ItemListController as itemList'
//abstract: true //abstract maybe?
}).state('itemList.itemDetail', {
url: '/:itemName/:itemID',
templateUrl: 'Scripts/app/item/ItemDetailTemplate.html',
controller: 'ItemDetailController as itemDetail',
resolve: {
'CurrentItemID': ['$stateParams',function ($stateParams) {
return $stateParams['itemID'];
}]
}
})
if child view is inside parent view both controllers share same scope.
so you can place a dummy (or neccessary) ui-view inside parent view which will be populated by child view.
and insert a
$scope.loadChildData = function(itemID){..blabla..};
function in parent controller which will be called by child controller on controller load. so when a user clicks
<a ui-sref="childState({itemID: 12})">bla</a>
only child controller and child view will be refreshed. then you can call parent scope function with necessary parameters.
The short answer ended up being do not put the map inside a view that changes. The accepted answer provides a lot more detail on how to structure a page with sub-views, but the key point is not to make the map part of the view but to connect its behaviour to a view that does change and to use a Controller to update the market icons.

Angular UI-Router modal removes parent state

I'm working on an angular app that has ui-router module.
When entering a certain state of the router, I show a modal dialog, which then replaces my parent view. I would like to keep the parent view and show the modal as overlay. Is there a way to do it with ui-router?
To give an example:
$stateProvider.state("clients.list", {
url: "/list",
templateUrl: "app/client/templates/client-list.tpl.html",
controller: moduleNamespace.clientListController,
resolve: {
clients: function (ClientService) {
return ClientService.all();
}
}
})
// This will replace "clients.list", and show the modal
// I want to just overlay the modal on top of "clients.list"
.state("clients.add", {
url: "/add",
onEnter: function ($stateParams, $rootScope, $state, ngDialog) {
ngDialog.open({
controller: moduleNamespace.clientAddController,
template: "app/client/templates/client-add.tpl.html"
});
$rootScope.$on("ngDialog.closed", function (e, $dialog)
if ($state.current.name !== "clients.list") $state.transitionTo("clients.list");
});
}
})
Thanks
I think the proper solution would be something like:
$stateProvider.state("clients.list", {
url: "/list",
templateUrl: "app/client/templates/client-list.tpl.html",
controller: moduleNamespace.clientListController,
resolve: {
clients: function (ClientService) {
return ClientService.all();
}
}
})
.state("clients.list.add", {
url: "^/add",
})
Important things are I made /add absolute by adding a ^. Most people would have just done /list/add because the default behavior of nested state is to add them together...the ^ bypasses this. You also would need to on state load style this thing so its a "modal" and is on top of other content.
And then inside of clients.list state you would need to update /client-list.tpl.html to have an ng-view that would style itself on top of the parent view.
I can create a plunkr if need be, but if I do that I am basically implementing everything for you.

ui-router: A route with no view template

Is it possible to setup a route in ui-router that only has a controller? The purpose being that at a certain URL, the only thing I'd like to do is take action programatically, and not display anything in terms of a view. I've read through the docs, but I'm not sure if they offer a way to do this.
Yes, I have read this: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state, but that is not quite what I am looking for.
For example, let's just say I have a basic body with view:
<body ui-view></body>
And some basic config:
// Routes
$stateProvider
.state('myaction', {
url: "/go/myaction",
onEnter: function() {
console.log('doing something');
}
});
When /go/myaction is visited, the view is blank. Is it possible to do this?
I was able to solve this problem by redirecting the headless state I was taking programmatic action in, to a state WITH a view at the end of the headless state:
$stateProvider
.state('myaction', {
url: "/go/myaction",
onEnter: function() {
console.log('doing something');
}
controller: function($state) {
$state.go('home');
}
});
You can't have a controller without a view but you can use onEnter instead of a controller. If you don't want to change the current view when accessing this state you can define it as a child state:
$stateProvider
// the parent state with a template
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'HomeCtrl'
})
// child of the 'home' state with no view
.state('home.action', {
url: '/action',
onEnter: function() {
alert('Hi');
},
});
Now in home.html you can do something like this:
<a href ui-sref=".action">Greet me!</a>
From the docs:
Warning: The controller will not be instantiated if template is not defined.
Why don't you use an empty string as a template to overcome this?
Yes, you can do that. Use absolute view names to re-use the <ui-view> of another state.
Take a look at this example:
Users go to my app, but depending on them being authenticated or not, I want to send them to a public or private page. I use the index state purely to see if they're logged in or not, and then redirect them to index.private or index.public.
The child states make use of absolute view names to use the <ui-view> element that corresponds to the index state. This way, I don't need to make a second nested <ui-view>.
$stateProvider.state('index', {
url: "/",
controller: 'IndexCtrl'
}).state('index.private', {
views: {
"#": {
templateUrl: 'private.html',
controller: 'PrivateCtrl'
}
}
}).state('index.public', {
views: {
"#": {
templateUrl: 'public.html',
controller: 'PublicCtrl'
}
}
});
A small note on this example: I'm using the # shortcut here. Normally you would use viewname#statename.
My solution for this was just to include a template (html file) that is blank.

Resources