I'm experimenting with AngularJS but I'm failing to achieve a combination of different, independent modules and nested views.
What I'm trying to do:
On top, one module for the navigation (completly separate from the other modules and always present by default)
Then, a module changing based on the URL, for the content (the easiest part)
But, inside the above mentioned module, another independent module for the user interface, like a search/paging toolbar. This module should be able to interact with the (dynamic) 'content' module. I'm not sure if it's better to have the toolbar in a separate template or in a nested template.
I can get parts of this to work, but not a combination of all of them. Currently, I managed to get the navigation to work (as default $state), but when I do that the two other templates just seem to fail (the controllers aren't even called)
index.html
<html data-ng-app="myApp">
<head>
<meta charset="utf-8">
<title>Test</title>
<link rel="stylesheet" href="./css/style.min.css">
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-dark">
<div class="container">
<!-- this should replace the list below! -->
<div class="container" data-ui-view="navigation"></div>
<!-- this should be replaced / made dynamic -->
<div class="navbar-container navbar-primary">
<ul class="nav navbar-nav">
<li class="nav-item active" data-ng-repeat="entry in navigation">
<a class="nav-link" href="#">Dashboard <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Costumers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">...</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- this can either be here, or inside data-ui-view="content" -->
<div class="container" data-ui-view="toolbar"></div>
<div class="container" data-ui-view="content"></div>
<script src="./js/script.min.js"></script>
</body>
</html>
customer.html
<!-- the toolbar data-ui-view="toolbar" could be nested here too! better or worse? -->
<div class="card-columns card-columns-4">
<div class="card" data-ng-repeat="customer in customers">
<img class="card-img-top" data-src="..." alt="Card image cap">
<div class="card-block">
<h4 class="card-title">{{customer.firstname}} {{customer.lastname}}</h4>
<p class="card-text"><small class="text-muted">{{customer.city}}, {{customer.country}}</small></p>
</div>
</div>
</div>
myApp.js
var routes = angular.module( 'myApp', [
'ui.router',
'toolbarControllers',
'customerControllers',
'navigationControllers'
] )
routes.config( [ '$stateProvider', '$urlRouterProvider', function( $stateProvider, $urlRouterProvider ) {
// trying to set 'navigation' as default view for ALL states
// other states should just inherit view:navigation and leave it as it is!
$stateProvider
.state( 'default', {
abstract: true,
views: {
// also tried: 'navigation#'
'navigation': {
templateUrl: 'view/navigation/navigation.html',
controller: 'NavigationController'
}
// no other default views, since we don't know the rest! It should be independent if possible
}
} )
} ] )
customer.js
var customerControllers = angular.module( 'customerControllers', [ 'ui.router' ] )
customerControllers.config( [ '$stateProvider', '$urlRouterProvider', function( $stateProvider, $urlRouterProvider ) {
$stateProvider
.state( 'customers', {
parent: 'default', // this doesn't seem to happen!
url: '/customers',
views: {
/* 'navigation': {
templateUrl: 'view/navigation/navigation.html',
controller: 'NavigationController'
},*/
'toolbar': {
templateUrl: 'view/ui/toolbar.html',
controller: 'ToolbarController'
},
'content': {
templateUrl: 'view/customers/list.html',
controller: 'CustomerListController'
}
}
} )
} ] )
customerControllers.controller( 'CustomerListController', [ '$scope', '$http', function( $scope, $http ) {
console.log('Customer controller called!') // only works if navigation doesn't work
// get the data
} ] )
toolbar.js
var toolbar = angular.module( 'toolbarControllers', [] )
toolbar.controller( 'ToolbarController', [ '$scope', '$http', '$state', function( $scope, $http, $state ) {
console.log( 'Toolbar controller' ) // this works if navigation doesn't works
// how can I tell customerControllers what to do now on certain events, like filtering of data?
} ] )
navigation.js
var navigation = angular.module( 'navigationControllers', [] )
navigation.controller( 'NavigationController', [ '$scope', '$http', '$state', function( $scope, $http, $state ) {
console.log('Navigation controller called!') // this works
// get the data
} ] )
Everything is still quite barebones, because I'm just trying to set up the basic interaction between the views before proceeding.
Currently, with the code above, only the navigation works while the other two views are ignored. If I remove the code in routes.config() and parent: "default", the other two views seem to work instead. Something doesn't quite work with the inheritance here.
How can I get all three views to work at the same time? (with navigation always being there regardless of the content)
What's the best way to get the toolbar to interact with the content (in this case, customerControllers) while still being an independent module? (less important, my main issue is the first problem)
Edit: After further testing I came to the conclusion that somehow, the parent state overwrites the child states' settings. It's very weird: If I remove parent: 'default' and just configure everything in one place, it works. AS soon as I add parent: 'default' it starts failing, since the parent's configuration takes precedence over the child configuration... but shouldn't it be the exact opposite? Also, it's not really a solution, since I'm trying to write my modules independent from each other.
Related
I have a question regarding Angular UI-Router and its ui-views. I declare three ui-views inside another one, and the only one that shows up is the one with the name "languages". I don't understand why this happens, and if anybody could help that would be great.
index.html:
<div ui-view="languages">
<div ui-view="dashboard"></div>
<div ui-view="partners"></div>
<div ui-view="footer"></div>
</div>
routes.js:
angular.module('TMHM')
.config(routeConfig);
routeConfig.$inject = ['$stateProvider', '$urlRouterProvider'];
function routeConfig ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('home', {
url: '/',
views: {
'languages': {
templateUrl: 'views/languages/languages.html'
},
'dashboard': {
templateUrl: 'views/dashboard/dashboard.html'
},
'partners': {
templateUrl: 'views/partners/partners.html'
},
'footer': {
templateUrl: 'views/footer/footer.html'
}
}
});
$urlRouterProvider.otherwise('/');
};
Here's the Plunker code, although I couldn't get that to work:
https://plnkr.co/edit/z8cFGHKVQNN623QbBUqi
There is updated and working plunker https://plnkr.co/edit/vKOr2yLUfaAfwoGyK0ws?p=preview
I created new routes.html, with this content
<h1>Routes</h1>
<hr />
<div ui-view="languages"></div>
<div ui-view="dashboard"></div>
<div ui-view="partners"></div>
<div ui-view="footer"></div>
And changed index.html to contain
<div ui-view=""></div>
And then state adjustment is:
.state('home', {
url: '/',
views: {
'': {
templateUrl: 'routes.html'
},
'languages#home': {
templateUrl: 'languages.html'
},
'dashboard#home': {
templateUrl: 'dashboard.html'
},
'partners#home': {
templateUrl: 'partners.html'
},
'footer#home': {
templateUrl: 'footer.html'
}
}
});
Also, essential was move the ng-app from <head> to <html> element
<html ng-app="TMHM">
<head >
check it here
More details and examples about named and multi views:
Nested states or views for layout with leftbar in ui-router?
Angular UI Router - Nested States with multiple layouts
I've never seen that done this way before (a routed view in a routed view). That may be because it doesn't work, or I've just never run into it, I don't know. I tend to think of views as being top level, and then includes as being the nested content.
If that's the idea, I've done something very similar to this, but I used ng-include (I currently have this in production in an app serving a lot of users):
<div ng-include="mycontroller.pathToFileIWantToShow"></div>
// alternatively, although hardcoding can be evil...
<div ng-include="path/to/some/file.html"></div>
This allows me to change the content dynamically, use binding etc. etc, and each included template can reference its own controller, or just use the controller that wraps it. It doesn't seem to matter how many I nest.
I'm trying to use ui-route do control my app states but I want the states to be URL oriented.
I want to click on a link and automagically goes to the state with the associated URL.
I followed the In-Depth Guide and my code looks like this (please see plnkr https://plnkr.co/edit/wk1RphKq6G3t4GqfppYm):
JS:
angularModule.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
//$urlRouterProvider.otherwise("/management");
$stateProvider
.state('management', {
url: '/',
template: '<p>:)</p>'
})
.state('management.users', {
url: '/users',
templateUrl: './users.html'
});
}]);
HTML:
<div class="container-fluid" ng-controller="PageController">
<aside class="sidebar col-md-3">
<nav>
<ul>
<li>Management</li>
<li>Users</li>
</ul>
</nav>
</aside>
<section class="content col-md-9" ui-view>
</section>
</div>
What am I missing?
ui-router has it's own html properties , so instead of
href="management/users"
you should use
ui-sref="management.users"
the path name you use with ui-sref is the state name not the url path itself.
take a look at this https://github.com/angular-ui/ui-router/wiki/Quick-Reference#ui-sref
hope this helps
When you define the states, set the root state url to '/':
$stateProvider
.state('management', {
url: '/', // INSTEAD OF "index.html"
template: '<p>:)</p>'
})
.state('management.users', {
url: '/users',
templateUrl: 'users.html'
});
Next. You're trying to inject 'dev.api' into the root module:
angularModule = angular.module('app', [
'ui.router',
'ui.bootstrap',
'dev.api' // HERE
]);
BUT you never creating the module. You need to change the service.js to this:
angular
.module('dev.api', []); // THIS IS HOW YOU DEFINE A NEW MODULE (Without external dependencies)
angular
.module('dev.api').service('usersApiService', ['$http', function ($http) { .....
Now you'll see that the ui-sref are working (https://plnkr.co/edit/XbDx2EonQvZYXsyvWhUW?p=preview). Note that you still have some JS errors, but those are related to the logic of your app, and not relevant the the question.
I am using ui-router and I try to create several views inside one of my nested states.
In my logic, all of the views should be visible whenever the parent state is active.
I have read the wiki page on multiple named views multiple times, but could not get anything working yet.
My state template would show up, but my views never do.
Here is a working plunker
(You have to click on "Followers" in the navbar for the view to show up. Haven't figured why yet).
Important parts are the config
app.config([
'$stateProvider',
'$urlRouterProvider',
function ($stateProvider, $urlRouterProvider){
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard.html',
controller: 'DashboardCtrl',
authenticate: true
}).state('dashboard.followers', {
url: '/followers',
templateUrl: 'dashboard.followers.html',
controller: 'DFollowersCtrl',
authenticate: true
}).state('dashboard.followers.add', {
views: {
'add': {
templateUrl: 'dashboard.followers.add.html',
controller: 'DFollowersAddCtrl',
authenticate: true
}
},
});
$urlRouterProvider.otherwise('dashboard');
}
]);
The main dashboard template (level 2, using a generic ui-view)
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<div class="flash-area">
<flash-message duration="2000" show-close="true"></flash-message>
</div>
<ui-view></ui-view>
</div>
and the dashobard.followers specific level 3 view, that has a specific name
<div>
<h1 class="page-header">Followers</h1>
<div ui-view="add"></div>
</div>
The trick is coming from, I think, a combination of :
I want to use level 3 nesting
Level 2 uses a generic ui-view, because it may contains my dashboard or something else.
Level 3 contains specific views, as it is where I want to use "views" and not "states" (as far as I understood, at least).
The final aim is to have more than one view in my template, but for now I reduced the attempts to only show the 'add' view.
I have seen several similar questions on SO already, such as this one or this other one but so far my attempts have not been fruitful.
I can access my "add" view directly if I reach its URL (when I try setting one)
But the dashboard.followers state does not get populated by the views.
I think there are several mistakes here you made:
if you want the url of dashboard.followers state and dashboard.followers.add state to be same, the child state dashboard.followers.add does not need the url option
probably can be a mistake(I am not sure because no code is provided), if you don't use the
views: { ... }
named views, but just directly use
url: '/followers',
templateUrl: '/partials/dashboard.followers.html'
angular just assume you want to insert the template in an unnamed <div ui-view></div> in the parents state's template not the root template. for example, in my example code, for state dashboard.followers, since it is a child state of dashboard, if I want to insert the template in root html template, I have to use
views: {
'#': {
template: '<div><h1 class="page-header">Followers</h1><a ui-sref="dashboard.followers.add">add</a><div ui-view="add"></div></div>'
}
}
/* myApp module */
var myApp = angular.module('myApp', ['ui.router'])
.config(['$stateProvider', function($stateProvider) {
$stateProvider
.state('landing', {
url: '/',
template: '<p>landing</p><a ui-sref="dashboard">dashboard</a>'
})
.state('dashboard', {
url: '/dashboard',
template: '<p>landing</p><a ui-sref="dashboard.followers">followers</a>'
})
.state('dashboard.followers', {
url: '/followers',
views: {
'#': {
template: '<div><h1 class="page-header">Followers</h1><a ui-sref="dashboard.followers.add">add</a><div ui-view="add"></div></div>'
}
}
})
.state('dashboard.followers.add', {
views: {
'add': {
template: '<p>followers</p>'
}
}
});
}])
.controller('MyAppCtrl', function($scope, $state /*, $stateParams*/ ) {
$state.go("landing");
});
<body ng-app="myApp" ng-controller="MyAppCtrl">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.js"></script>
<div ui-view></div>
</body>
update
I made two plunkers to fit two different situation:
if you want to dynamically load add state to ui-view="add" by a
link, check
this out.
if you just want a sub template to be loaded always on dashboard.followers state, simply remove add states, and use views: {...} to load the add template in. here is the plunker.
I have implemented UI-Router in my application but I'm having a problem with a nested route. My index.html page looks like;
<body>
<!-- Navigation code -->
<!-- Content from the template -->
<div ui-view></div>
<!-- Footer -->
<!-- Application Scripts -->
</body>
My Angular ui-route code is;
(function () {
'use strict';
var app = angular.module('rbApp', ['ngAnimate', 'ui.router', 'ui.bootstrap']);
app.config(["$stateProvider", "$urlRouterProvider", function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/");
$stateProvider
.state("home", {
url: "/",
templateUrl: "/App/WebSite/home.html",
controller: "homeController as homeVm"
})
.state("programs", {
url: "/programs",
templateUrl: "/App/WebSite/programs.html",
controller: "programsController as programVm"
})
.state("programs.complete", {
url: "/complete",
templateUrl: "/App/WebSite/programsComplete.html"
})
.state("articles", {
url: "/articles",
templateUrl: "/App/WebSite/articles.html",
controller: "articleController as articleVm"
})
}]);
app.run(['$rootScope', function ($rootScope) {
$rootScope.$on('$locationChangeStart', function (event, newUrl, oldUrl) {
console.log('Location change: %s --> %s', oldUrl, newUrl);
});
}]);
})();
When I select the "programs" route the page is displayed correctly and the "app.run" code above updates the console log with;
Location change: http://localhost:50321/#/ --> http://localhost:50321/#/programs
On the programs page I have an image within an anchor tag as follows;
<div class="col-md-3 hidden-sm hidden-xs">
<a ui-sref="programs.complete"><img class="img-thumbnail img-responsive" src="/Images/Programs/Program01Small.jpg" /></a>
</div>
When the image is clicked the console log displays;
Location change: http://localhost:50321/#/programs --> http://localhost:50321/#/programs/complete
So it appears the routes are behaving as expected. The only problem is the "programsComplete.html" template is not displayed, the browser continues to display the "programs.html" template.
I have checked the console log but no errors are displayed and currently the "programsComplete.html" only contains a <h1> tag and text so I can confirm the template is being loaded.
Can anyone advise why this would not be loading the template?
It seems that the parent template programs.html is just missing for its child. Be sure that this tempalte programs.html contains unnamed view target
<div ui-view="">
Each child is inserted into its parent. In case we want child to replace parent, we have to use absolute name, and in this case target the index.html like this:
.state("programs.complete", {
url: "/complete",
views : {
'#' : {
templateUrl: "/App/WebSite/programsComplete.html"
}
}
})
while this naming convention could be weird: views : { '#' : ..., it is just absolute name:
View Names - Relative vs. Absolute Names
Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname#statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.
so '#' means unnamed view in the unnamed state === root state
Here is the demo I have created for asking this question and the inline code as requested: http://jsfiddle.net/Gncja/1/
<script type='text/ng-template' id='root.html'>
<list template-id='sidebar.templateId' selected-item-id='sidebar.selectedItemId'></list>
</script>
<script type='text/ng-template' id='sidebar.html'>
<ul style='width:100%;' class='nav nav-list bs-docs-sidenav'>
<li ng-repeat='item in data' ng-class="{active:item.id==selectedItemId}">
<a ng-href='#/{{item.id}}'>
<i class=icon-chevron-right></i>
<span ng-bind='item.text'></span>
</a>
</li>
</ul>
</script>
<body ng-app='main'>
<div ng-view></div>
</body>
function RootCtrl($scope, $routeParams){
$scope.sidebar = {
templateId: 'sidebar',
selectedItemId: $routeParams.navItemId
};
}
RootCtrl.$inject = ['$scope','$routeParams'];
angular.module('main', []).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/:navItemId', {
templateUrl: 'root.html',
controller: RootCtrl
}).
otherwise({redirectTo: '/1'});
}]).
directive('list', function(){
return {
restrict: 'E',
replace: true,
scope:{
'templateId': '=',
'selectedItemId':'='
},
template:'<ng-include src="templateUrl"></ng-include>',
controller: function($scope, $element, $attrs){
$scope.templateUrl = $scope.templateId + '.html';
$scope.data = [
{'id':'1', text:'lorem ipsum'},
{'id':'2', text:'dolor sit amet'},
];
}
};
});
This is a small piece of the application I have been working on, but it clearly shows what it is doing. There is the navigation menu in the page, the menu item is a link to the hash that is handled by angular.js routing that initializes the root controller, etc., it's quite tricky to describe, but the code sample clearly shows it.
The problem the entire page content is re-rendered each time when I click the navigation menu item - the routing is stateless and it does know nothing about the previous state. I would like to avoid this by re-using the navigation menu data/template rendering result when a user just navigates between the menu items(or browser history). Is it possible? I am sure that it is, just want to check out whether someone has good ideas. Thanks!
UPDATE:
I have found something that might help me:
http://www.bennadel.com/blog/2420-Mapping-AngularJS-Routes-Onto-URL-Parameters-And-Client-Side-Events.htm
I would put the navigation menu outside ng-view (so it doesn't re-render), but use ng-class in conjunction with location.path() to differentiate the currently selected item. E.g.,
<div ng-controller="navCtrl">
<ul ...>
<li ng-repeat='item in navData' ng-class="{active:isActiveRoute(item.id)}">
...
</ul>
</div>
<div ng-view></div>
Then in navCtrl:
$scope.navData = [
{'id':'1', text:'lorem ipsum'},
{'id':'2', text:'dolor sit amet'},
];
$scope.isActiveRoute = function(route) {
return '/' + route === $location.path();
};
Fiddle