Update single view where there are many in AngularJs and UI-Router - angularjs

I am a n00b to Angular, and trying to get my head around how ui-routing works. I see the how the concept of multiple routes works, and how nested routes work. How would i do both? Or is this not good practice?
In the example bellow I have app/wh working fine, however when some goes to app/wh/toc/123 I want only the middle view to swap.
.state('app.wh', {
url: "/wh",
views: {
'left' : {
templateUrl: "static/partials/leftPane.html"
},
'middle': {
templateUrl: "static/partials/start.html"
},
'right' : {
templateUrl: "static/partials/rightPane.html",
controller: 'AsideCtrl'
}
}
})
.state('app.wh.toc', {
url: "/toc/:id",
views: {
'middle' : {
templateUrl: "static/partials/toc.html",
controller: function($scope, $stateParams, $state){
$scope.title = $stateParams.id;
}
}
}
})

I am guessing that you want your left and right panels to stay static while the middle content changes through the users actions. Personally I am not a big fan of using multiple routes, and instead simply use ng-include if I want to separate portions of a single view into their own files.
If you do this, then you only need to define your parent, and child state:
.state('app.wh', url: '/wh', abstract: true, controller: ...
Note that if you want separate controllers for left and right, you can use ng-controller in your views. Also If this state is common to multiple middle contents, then you might want to make it an abstract state, so that the user cannot actually load it (it only acts as a parent placeholder).
.state('app.wh.toc', url: /toc/:id ...
Then in your view:
<div ng-include="someUrlRefLeft"></div>
<div ui-view=""></div>
<div ng-include="someUrlRefRight"></div>

Related

ui-router parent state optional parameter

I am trying to manage CRUD for lists and list elements.
I want to have one state, one template and one controller per element for each case(create/edit).
I create the state:
state('list', {
parent: 'root',
url: '/list?list_id',
views: {
'content#root': {
templateUrl: 'list.tmpl.html',
controller: 'ListController',
controllerAs: 'vm',
}
}
})
Which is perfect, in the controller i can check for a list_id and toggle betweend create/edit.
The problem occurs when the state above becomes a parent.
When the child is introduced:
state('list-element', {
parent: 'list',
url: '/element?list_id&element_id',
views: {
'content#root': {
templateUrl: 'element.tmpl.html',
controller: 'ElementController',
controllerAs: 'vm',
}
}
})
I can no longer have the uncertainty i need.
To put it more simple, i want the url structure to look like this:
/list?list_id - if list_id toggle edit
/list/element?list_id&element_id - if element_id toggle edit
Note that when the list element is created, the parent state does not have a parameter.
Now, i can work around this by creating two states for the list:
/list (parent) create
?list_id (child for 1) edit
/element?list_id&element_id (child for 1) create or edit
but this would break the "one state, one template and one controller" that i want.
Anyway that i can achieve to do it the way i want?
I know you're looking for a specific implementation, but I think part of the reason you're hitting trouble is because that approach may not be the best for what you're trying to do, since it seems like you're confusing the role of a route versus a DOM element.
Components (and directives) are designed to help you bundle together controllers and templates explicitly for elements on the page. Routing is just what gets you there, and tells you which ones to load. Here's how you might handle this using a more conventional approach:
stateHelperProvider.state({
name: 'list',
url: '/list'
// This is possible with https://github.com/marklagendijk/ui-router.stateHelper
children: {
name: 'element',
// Example: /list/123
url: '/{list_id:int}?element_id',
resolve: {
item: ['$stateParams', 'FooService', ($stateParams, FooService) => {
// Get whatever info you need, based on the ID.
return FooService.getItem($stateParams.list_id);
}
},
views: {
'content#root': {
// This will use the resolved `item` and pass it into the component.
template: '<list-element item="$resolve.item"></list-element>'
}
}
}
});
// The component
(function () {
'use strict';
angular.module('yourModule')
.component('listElement', {
templateUrl: 'element.tmpl.html',
bindings: {
item: '<'
}
});
})();
// Sample component template
<h2 ng-bind="$ctrl.item.name"></h2>
<div ng-bind="$ctrl.item.description"></div>
For a different route, you may want to check out nested named views in UI Router. This will allow you to specify a different controller, template, and state each. Gist:
<!-- Parent template -->
<div ui-view="editContents"></div>
<div ui-view="previewContents"></div>

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.

UI Router: Multiple Independent Views

I feel like this is a straight forward use case for the ui-router but maybe I'm missing something...
I want to have two separate views next to each other controlled by their own menus. When I click a ui-sref link on one menu (or $state.go for that matter), I would like to update only one of the views. Additionally, only one of the two views needs to be reflected in the url.
I tried defining a few states:
$stateProvider
.state('home', {
url: '/',
views: {
'viewA': {
template: "I'm number A!"
},
'viewB': {
template: "It's good to be B."
}
}
})
.state('shouldOnlyChangeA', {
'url': '',
views: {
'viewA': {
template: 'Check out my new shoes!'
}
}
})
.state('shouldOnlyChangeB', {
'url': '/shouldGoToNewUrl',
views: {
'viewB': {
template: "This probably won't work..."
}
}
});
Now press $state.go('shouldOnlyChangeA') from your favorite controller and watch it change the crap out of viewB. I'd also like to omit the url definition in this state since the url should only change between the first and third states I've defined.
I have each ui-view sitting next to each other in index.html:
...
<div ui-view="viewA"></div>
<div ui-view="viewB"></div>
...
TL;DR
I want two sibling ui-views to be stateful all on their own; changing one shouldn't necessarily effect the other.
Hopefully I'm just missing something so I didn't bother to throw a plunker together or anything, but if it's more complicated and folks are willing to fiddle I'll whip something up.
See this similar question: Independent routing for multiple regions in an AngularJS single page application
I wrote UI-Router Extras - sticky states to accomplish this use case.
View the demo
Check out the demo source code for details.
I wrote UI-Router Extras - sticky states to achieve your goal.
You'll want one named <div ui-view='name'></div> for each region. Then, add sticky: true to the state definition which targets that region's named view.
See this plunk: http://plnkr.co/edit/nc5ebdDonDfxc1PjwEHp?p=preview
<div ui-view="viewA"></div>
<div ui-view="viewB"></div>
...
$stateProvider
.state('home', {
url: '/'
})
.state('shouldOnlyChangeA', {
'url': '',
sticky: true, // Root of independent state tree marked 'sticky'
views: {
'viewA#': {
template: 'Check out my new shoes!<div ui-view></div>'
}
}
})
.state('shouldOnlyChangeA.substate', {
'url': '/substate',
template: 'Lets get some shoes!'
})
.state('shouldOnlyChangeB', {
'url': '/shouldGoToNewUrl',
sticky: true, // Root of independent state tree marked 'sticky'
views: {
'viewB': {
template: "This probably won't work...<div ui-view></div>"
}
}
})
.state('shouldOnlyChangeB.substate', {
'url': '/substate',
template: "Golly, it worked"
}
);

Angular + ui-router implementing tabs

For the past 9 hours I have been trying to implement a page with tabs using Angular and ui-router.
I have the following:
.state('someAbstractParentRoot', {
'abstract': true,
url: '/{id}',
templateUrl: '/admin/templates/rootTemplate',
controller: 'someController1',
data: {
breadcrumbLabel: 'Details'
}
})
.state(someAbstractParentRoot.child, {
url: '',
views: {
details: {
templateUrl: '/admin/templates/details',
controller: 'someController2'
},
videos: {
templateUrl: '/admin/templates/videos',
controller: 'someController3'
},
logs: {
templateUrl: '/admin/templates/logs',
controller: 'someController4',
},
notes: {
templateUrl: '/admin/templates/notes',
controller: 'someController5'
}
}
})
Using bootstrap I simply show the template I need.
so when the videos tab is shown, the url is something like: localhost:8080/users/1#videos.
So, this all works great until I need to send this link to someone, because when I do that, the details tab is automatically opened. I can't just toggle classes on div elements because some of the tabs are using ng-if so jquery doesn't pick up all of the html it should, probably because of angulars digest cycle (it can be solved with setTimeout, but I am trying to avoid that).
Does anyone know how I can use ui-router to define a tab which will support direct linking?
Some example would be very much appreciated.
NB: I need the abstract parent state.
Can urls in ui-router have a '#' symbol?
There is a problem with your states
actually you declare only one state which can be divided in several parts you have to declare a state for each subviews so declare something like. I don't know if you really need an abstract state so i can juste declare:
$stateProvider.state('details', {
templateUrl: '/admin/templates/details',
controller: 'someController2'
})
.state('videos', {
templateUrl: '/admin/templates/details.html',
controller: 'someController3'
})
.state('logs', {
templateUrl: '/admin/templates/logs.html',
controller: 'someController4'
})
.state('notes', {
templateUrl: '/admin/templates/notes.html',
controller: 'someController5'
});
In your rootTemplate juste add this
<div id="tabsContainer">
<a ui-sref="details">Details</a>
<a ui-sref="logs">Logs</a>
....
</div>
<div ui-view> </div> <!-- Where your state template will be inserted -->

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