AngularJs: stateful client-side routing - javascript

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

Related

changing the view of the div using angular ui-router

Here is the code which i have written so as to change my view (without any change in the url)
<a ui-sref="chat.menu" ng-click="click1();">Latest</a>
<a ui-sref="chat.menu" ng-click="click2();">Mail</a>
<div ui-view="{{item}}"></div>
var app = angular.module('myApp',['ui.router'])
.run(['$rootScope','$state','$stateParams', function($rootScope,$state,$stateParams){
$rootScope.$state = $state;
$state.$stateParams = $stateParams;
}]);
app.config(function($stateProvider,$urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home',{
url:'/',
templateUrl:'templates/main.html',
})
.state('chat',{
url:'/chat',
templateUrl:'templates/chatPage.html',
controller: 'chatController',
})
.state('chat.menu',{
views: {
'':{
template: '<h1>Blank</h1>',
},
'latest': {
template: '<h1>Latest</h1>',
},
'mail': {
template: '<h1>Mail</h1>',
}
}
});
});
app.controller('Controller', function($scope,$state, authentication){
$scope.item='';
$scope.click1 = function(){
$scope.item = "latest";
}
$scope.click2 = function(){
$scope.item = "mail";
}
});
Its working fine, but the problem is that I change the the view only once. Once a view is loaded in the ui-view, then another view is not getting loaded in it on the click of the button.
All i want is that, when the user click the button the view should be changed according to the click of the button without any change in the url.
So I'd trying something like this: having two states instead of one. If you want to keep only one state, you'd need to pass parameters to that state, but for a simple example like this, two states will work just fine:
myApp.config(function ($stateProvider){
$stateProvider
.state("latest", {
url: "#",
template: "<h1>Latest</h1>",
controller: "Ctrl1"
})
.state("mail", {
url: "#",
template: "<h1>Mail</h1>",
controller: "Ctrl2"
});
});
What we are doing here is either setting the URL to the same in both states, or setting no URL at all. Also, if you want to assign controllers to your views, here is were we do it. You can assign no controllers, two different controllers, or the same controller if you would like.
Then setup your view like this:
<nav>
<a ui-sref="latest">Latest</a>
<a ui-sref="mail">Mail</a>
</nav>
<div ui-view></div>
Here is a working JSFiddle

Nested (and default) views in AngularJS + ui-router

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.

AngularJS append html to dom element

Im working on a small web app, and there is a side menu that has nav links in it. Each link when clicked pulls out a hidden panel and should display a list of items specific to that link.
I have most of the functionality working except Im stuck on how to append either a templateURL or just html to the panel.
Any guidance would be great.
heres what I have so far:
html
<!-- Pullout menu -->
<nav id="sidebar-pullout">
<div id="menu-list"></div>
</nav>
app.js
var configApp = angular.module("configApp", ['ngRoute','ui.bootstrap'])
.config(function($routeProvider){
$routeProvider..when('/organizations', {
templateUrl: 'templates/dashboard/organizations/organizations-title.html',
controller: 'OrganizationController',
activetab: 'organizations'
})
.otherwise( {redirectTo: '/dashboard'} );
});
// Side Nav Link Controllers
configApp.controller('OrganizationController', function($scope) {});
configApp.controller('SideNavCtrl', function($scope, $location) {
$scope.isActive = function(route) {
return route === $location.path();
}
});
// adding html to the menu-list
configApp.directive('menu-list', function(){
return {
template: '<span ng-transclude >append som html here</span>',
replace: true,
transclude: true,
controller: 'OrganizationController'
};
});
Here is another way you might be able to go about it. By keeping a reference to menu items and contents. You could keep the side panel content in separate HTML files.
configApp.directive('menuList', function() {
return {
restrict: 'EA',
link: function(scope, el, attr) {
var activeId = null;
scope.showContent = function(id) {
activeId = id;
};
scope.isActive = function(id) {
return activeId === id;
}
scope.menuItems = [{
id: 'item1',
name: 'Menu Item 1',
content: 'path/to/menuItem1content.html'
}, {
id: 'item2',
name: 'Menu Item 2',
content: 'path/to/menuItem2content.html'
}]
}
};
});
Then in you HTML maybe something like this.
<div menuList>
<nav id="sidebar-menu">
<ul>
<li ng-repeat="item in menuItems">
<a ng-click="showContent(item.id)">{{ item.name }}</a>
</li>
</ul>
</nav>
<div id="sidebar-content">
<div class="content"
ng-repeat="item in menuItems"
ng-include="item.content"
ng-show="isActive(item.id)"></div>
</div>
</div>
This is just an idea and you could use angular animation to animate the sidebar sliding and stuff.
You are specifying your ng-transclude directive on the wrong element. You are placing it on your span tag. Try something like this instead:
<div>
<span>/*My template html here*/</span>
<div ng-transclude></div>
</div>
It also looks like you're specifying your directive incorrectly. Try this:
configApp.directive('menuList', function () {
return {
restrict: 'A',
replace: true, // note: this syntax will soon be deprecated
template: '<see above snippet>'
};
});
In particular, notice restrict, which specifies how this directive will be used (A: attribute on html element, E: as an element itself, C: as a class). Here we are saying we want to use our directive as an element, E. Also, note that I used the name menuList instead of menu-list. AngularJS uses camel-case in directive definition, and maps the directive names found in the html into their camel case counterparts. So, in the html we will still use this directive like this: menu-list, but we will declare it using camel-case.
Hope this helps!

What makes these two Controllers related?

I'm reading a book on AngularJS and There's something that confuses me
There are two Controllers
EditCtrl
app.controller('EditCtrl', ['$scope', '$location', 'recipe',
function($scope, $location, recipe){
$scope.recipe = recipe;
$scope.save = function(){
$scope.recipe.$save(function(recipe){
$location.path('/view/', + recipe.id);
});
};
$scope.remove = function(){
delete $scope.recipe;
$location.path("/");
};
}]);
IngredientsCtrl
app.controller('IngredientsCtrl', ['$scope',
function($scope){
$scope.addIngredients = function(){
var ingredients = $scope.recipe.ingredients;
ingredients[ingredients.length] = {};
};
$scope.removeIngredient = function(index) {
$scope.recipe.ingredients.slice(index, 1);
};
}]);
What I don't understand is how the IngredientsCtrl is a child of EditCtrl. I can't see the relation. The book clearly states this case, and I'm sure it's the case because the example app works fine, but I need help understanding what it is that makes IngredientsCtrl a child of EditCtrl. Doesn't makes sense to me.
Edit: With relevent HTML
<div class="control-group">
<label class="control-label" for="ingredients">Ingredients:</label>
<div class="controls">
<ul id="ingredients" class="unstyled" ng-controller="IngredientsCtrl">
<li ng-repeat="ingredient in recipe.ingredients">
<input ng-model="ingredient.amount" class="input-mini">
<input ng-model="ingredient.amountUnits" class="input-small">
<input ng-model="ingredient.ingredientName">
<button type="button" class="btn btn-mini" ng-click="removeIngredient($index)"><i class="icon-minus-sign"></i> Delete </button>
</li>
<button type="button" class="btn btn-mini" ng-click="addIngredient()"><i class="icon-plus-sign"></i> Add </button>
</ul>
</div>
Edit: Snippet from book
All the other controllers that we saw so far are linked to particular views on the UI. But
the Ingredients Controller is special. It’s a child controller that is used on the edit pages
to encapsulate certain functionality that is not needed at the higher level. The interesting thing to note is that since it is a child controller, it inherits the scope from the parent
controller (the Edit/New controllers in this case). Thus, it has access to the
$scope.recipe from the parent.
Edit: with routing
var app = angular.module('guthub',
['guthub.directives', 'guthub.services']);
app.config(['$routeProvider',
function($routeProvider){
$routeProvider.
when('/', {
controller: 'ListCtrl',
resolve: {
recipes: function(MultipleRecipeLoader){
return MultipleRecipeLoader();
}
},
templateUrl: '/view/list.html'
}).
when('/edit/:recipeId', {
controller: 'EditCtrl',
resolve: {
recipe: function(RecipeLoader) {
return RecipeLoader();
}
},
templateUrl: '/view/recipeForm.html'
}).
when('/view/:recipeId', {
controller: 'ViewCtrl',
resolve: {
recipe: function(RecipeLoader) {
return RecipeLoader();
}
},
templateUrl: '/view/viewRecipe.html'
}).
when('/new', {
controller: 'NewCtrl',
templateUrl: '/view/recipeForm.html'
}).
otherwise({redirectTo: '/'});
}]);
Two controllers share a Parent-Child relationship if you place the ng-controller directive on two nested html elements.
If you take a look at your HTML template, you should see something like this:
<!-- parent controller -->
<div ng-controller="EditCtrl">
<!-- child controller -->
<div ng-controller="IngredientsCtrl"></div>
</div>
There is no such thing in angular as a child controller. However, you can place a controller inside of another in the dom.
<div ng-controller="EditCtrl">
<div ng-controller="IngredientsCtrl">
// Here you have access to the scope of both controllers
</div>
</div>
The answer to your question "What makes these two controllers related?" is "nothing". They can be nested as I described, but so could any two controllers be.
Both controllers in your example read from the scope. This is bad practice as stated by Miško Hevery himself (http://www.youtube.com/watch?v=ZhfUv0spHCY).
Paraphrasing:
Inside of a controller you shoul only do write-operations to the scope and in the templates you should do read only
Based on these code snippets. I would not recommend the book you read for learning angularjs.

Toggle visibility of a ng-include depending on route

I have the following configuration:
$routeProvider
.when('/cars', { templateUrl: 'app/cars/index.html', controller: 'CarsCtrl', reloadOnSearch: false })
.when('/bikes', { templateUrl: 'app/bikes/index.html', controller: 'BikesCtrl', reloadOnSearch: false });
and somewhere in my root index.html there is a:
Cars
Bikes
<div ng-view></div>
Now, I want both views loaded and generated in the DOM at the same time, and show one of them depending on the route/URL.
Something like the following (not actual working code, just to give you an idea).
app.js:
$routeProvider
.when('/cars', { controller: 'CarsCtrl', reloadOnSearch: false })
.when('/bikes', { controller: 'BikesCtrl', reloadOnSearch: false });
root index.html:
Cars
Bikes
<div ng-include="'app/cars/index.html'" ng-show="carsVisible"></div>
<div ng-include="'app/bikes/index.html'" ng-show="bikesVisible"></div>
UPDATE: I know that ng-view kind of does this, but the difference, if subtle, exists. I want the html of each view to be generated once and stay in the DOM at all times.
I created a single RouteCtrl to load all of your views via ng-include. ng-view is not used. I inlined the templates. The templates could contain their own ng-controller directives to pull in specific controllers.
<body ng-controller="RouteCtrl">
Cars
Bikes
<div ng-controller="RouteCtrl">
<div ng-include="'/cars.html'" ng-show="carsVisible"></div>
<div ng-include="'/bikes.html'" ng-show="bikesVisible"></div>
</div>
<script type="text/ng-template" id="/cars.html">
Cars template.
</script>
<script type="text/ng-template" id="/bikes.html">
Bikes template.
</script>
$routeProvider is still configured, but no template or controller is specified, causing the RouteCtrl to always be active. That controller listens for the $routeChangeSuccess event and manipulates the ng-show properties accordingly.
app.config(function($routeProvider) {
$routeProvider
.when('/cars', {} )
.when('/bikes', {})
});
app.controller('RouteCtrl', function($scope, $route, $location) {
$scope.$on('$routeChangeSuccess', function() {
var path = $location.path();
console.log(path);
$scope.carsVisible = false;
$scope.bikesVisible = false;
if(path === '/cars') {
$scope.carsVisible = true;
} else if(path === '/bikes') {
$scope.bikesVisible = true;
}
});
});
Plunker
The idea for this solution is from #Andy.
I found another way, which I think is the simplest, quickest and most manageable:
How to set bootstrap navbar active class with Angular JS?
Which is:
Use ng-controller to run a single controller outside of the ng-view:
<div class="collapse navbar-collapse" ng-controller="HeaderController">
<ul class="nav navbar-nav">
<li ng-class="{ active: isActive('/')}">Home</li>
<li ng-class="{ active: isActive('/dogs')}">Dogs</li>
<li ng-class="{ active: isActive('/cats')}">Cats</li>
</ul>
</div>
<div ng-view></div>
and include in controllers.js:
function HeaderController($scope, $location)
{
$scope.isActive = function (viewLocation) {
return viewLocation === $location.path();
};
}
Instead of using ng-include, you should use ng-view;
This will display the content of either app/cars/index.html or app/bikes/index.html
Cars
Bikes
<div ng-view></div>
See the Template section from http://docs.angularjs.org/tutorial/step_07
Use a service with a show directive:
<div ng-show="myService.isActive('/')">Show this when at default route</div>
<div ng-show="myService.isActive('/cars')">Show this when at cars</div>
Service:
myApp.factory('MyService',
function ($location) {
return {
isActive: function (path) {
return (($location.path() == path));
}
}
}
);
App.js:
// this is run after angular is instantiated and bootstrapped
myApp.run(function ($rootScope, myService) {
$rootScope.breadcrumbService = MyService;
});

Categories

Resources