AngularJS Cookbook
70 Recipes for AngularJS 1.2
Sascha Brink
This book is for sale athttp://leanpub.com/angularjs-cookbook This version was published on 2014-03-12
This is aLeanpubbook. Leanpub empowers authors and publishers with the Lean Publishing process.Lean Publishingis the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do.
Tweet This Book!
Please help Sascha Brink by spreading the word about this book onTwitter! The suggested hashtag for this book is#angularjs.
Find out what other people are saying about the book by clicking on this link to search for this hashtag on Twitter:
Contents
Introduction . . . . i
I Directive / View Recipes
. . .1
1 Create an analog clock with SVG . . . . 2
2 Build a date select . . . . 4
3 How to use AngularJS with a curly braced template engine . . . . 7
4 Use the $compile function . . . . 9
5 Show a confirm box before ng-click is executed . . . 10
6 Create a digital clock . . . 13
7 Enable the save button only on valid form data . . . 15
8 Disable trimming in input fields . . . 17
9 Dynamic service loading with the $injector . . . 18
10 Create a dynamic templateUrl . . . 21
11 Show a box on outstanding http requests (simple) . . . 23
12 Show only initialized data on startup . . . 25
13 Create a markdown live preview . . . 27
14 Table double rows with ng-repeat . . . 29
15 Prevent duplicate warnings in ng-repeat . . . 31
16 Slide right/left with animations . . . 32
CONTENTS
18 Select box with multiple option . . . 37
19 Select and ng-options . . . 39
20 Make a sortable table . . . 44
21 Make a stacked bar chart . . . 47
22 Prevent event propagation from ng-click . . . 49
23 Submit form on enter key . . . 50
24 Make a syntax highlighter . . . 52
25 Textarea char limit with remaining counter . . . 54
26 Theme support . . . 56
27 Use the inner html of a directive in your template . . . 59
28 Write a blacklist validator . . . 60
29 General purpose uniqueness validator . . . 62
30 Forms with view / edit mode . . . 65
II Controller Recipes
. . .67
31 All / none / invert for a list of checkboxes . . . 68
32 Controller to controller communication . . . 70
33 Use your view filters in your controller (quick) . . . 72
34 Reset a form . . . 73
35 How to use the same function for multiple watchers . . . 75
III Service Recipes
. . .76
36 Get current app name (quick) . . . 77
37 Prevent heavy computing operations from making your app sluggish . . . 78
38 How to structure your services . . . 81
CONTENTS
40 Notification service delayed / sticky . . . 87
41 Why is there a Provider at the end of some services ($route and $routeProvider)? . . 90
42 Replace history path . . . 91
IV Filter Recipes
. . .92
43 Filter an exact match (quick) . . . 93
44 Get last element(s) in a collection (quick) . . . 95
45 How to highlight a search . . . 97
46 Easy filtering with the filter filter . . . 99
V Promise Recipes
. . . .101
47 How to cache data with promises . . . 102
48 Convert a 3rd party promise with $q.when . . . 104
49 TODO: How to wait for several async events . . . 106
50 How to transform every callback into a promise . . . 108
VI Testing Recipes
. . . .110
51 Testing focus directive . . . 111
52 Mocking http requests . . . 112
53 Testing only a subset of tests . . . 114
VII Big Picture Recipes
. . . .115
54 Redirect to an error page . . . 116
55 Spreading route definitions among modules . . . 117
56 Stop timers before a scope is removed . . . 118
57 What all the extra .js files are doing? . . . 119
CONTENTS
59 EcmaScript 5 array functions you should know and use . . . 125
60 Execute code at startup . . . 129
61 Finding Bottlenecks with Batarang . . . 130
62 How to use regular urls without the hash . . . 135
63 Report backend errors . . . 137
64 Optional params and wildcards in Router . . . 139
65 Deregister an event listener . . . 141
66 How to use the dot correctly . . . 142
Introduction
About the author
Sascha Brink is a web technology consultant, freelance writer and author. He’s developing web applications since 12 years. For him AngularJS is the most promising framework on the client side today. On the server side he favors Ruby on Rails. Sascha has published several articles about AngularJS and is a part of the german chapter http://angularjs.de.
1 Create an analog clock with SVG
Problem
You want to create a simple animation without using the canvas element.
Solution
This is more an inspirational than a complex example.SVG¹is a vector image format which can be embedded in HTML. So if you’re a little creative, you can do otherwise complex animations with very little code.
Here we use an analog clock as an example for AngularJS in combination with SVG.
It consists of only a circle and 3 lines. The 3 lines are the hands for hour, minute and second. We rotate them with angular and the$intervalservice.
In the HTML, you see an example of how easy it is to embed a SVG. For more information, seehere². 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body ng-controller="MainController">
7 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
8 <g>
9 <circle style="stroke: #ccc; fill: #fff;" cx="100" cy="100" r="100"/>
10 <line x1="100" y1="100" x2="100" y2="50"
11 style="stroke-width: 5px; stroke: #333;"
12 ng-attr-transform="rotate({{hourRotation}} 100 100)" />
13 <line x1="100" y1="100" x2="100" y2="20"
14 style="stroke-width: 3px; stroke: #888;"
15 ng-attr-transform="rotate({{minuteRotation}} 100 100)" />
16 <line x1="100" y1="100" x2="100" y2="5"
17 style="stroke-width: 2px; stroke: #bb0000;"
18 ng-attr-transform="rotate({{secondRotation}} 100 100)" />
¹http://en.wikipedia.org/wiki/Scalable_Vector_Graphics
Create an analog clock with SVG 3 19 </g>
20 </svg>
21 </body>
22 </html>
The application code is easy, too. We inject and use the$intervalservice which runs every second. We then calculate the angle of rotation for each hand.
1 angular.module('cookbookApp', [])
2 .controller('MainController', function($scope, $interval) { 3
4 function calculateRotation() {
5 var now = new Date();
6 $scope.hourRotation = 360 * now.getHours() / 12; 7 $scope.minuteRotation = 360 * now.getMinutes() / 60; 8 $scope.secondRotation = 360 * now.getSeconds() / 60; 9 } 10 $interval(calculateRotation, 1000); 11 calculateRotation(); 12 });
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-svg-clock
2 Build a date select
Problem
You want to give the user a date selection with month, day and year.
Solution
For this we build a custom directive which takes and date object. The directive uses two-way-databinding to sync the select fields with the attributemodel.
We will call our directive dateselect and will use the following html code for it. We use two directive to show the synchronization between them:
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body ng-controller="MainController">
7 <dateselect model="current"></dateselect><br/>
8 <dateselect model="current"></dateselect><br/>
9
10 {{current | date:'yyyy-MM-dd'}} 11 </body>
12 </html>
Build a date select 5 1 angular.module('cookbookApp', [])
2 .directive('dateselect', function() {
3 return {
4 restrict: 'E',
5 template:
6 '<select ng-model="date.month" ' +
7 'ng-options="month for month in months"></select>' + 8 '<select ng-model="date.day" ' +
9 'ng-options="day for day in days"></select>' + 10 '<select ng-model="date.year" ' +
11 'ng-options="year for year in years"></select>',
12 scope : {
13 model: '='
14 },
15 controller: function($scope) {
16 var i; 17 $scope.date = {}; 18 19 $scope.days = []; 20 for (i = 1; i <= 31; i++) { $scope.days.push(i); } 21 22 $scope.months = []; 23 for (i = 1; i <= 12; i++) { $scope.months.push(i); } 24 25 $scope.years = [];
26 for (i = 1980; i <= (new Date().getFullYear()); i++) {
27 $scope.years.push(i);
28 }
29
30 $scope.$watch('model', function(newDate) { 31 $scope.date.month = newDate.getMonth()+1; 32 $scope.date.day = newDate.getDate(); 33 $scope.date.year = newDate.getFullYear();
34 }, true);
35
36 $scope.$watch('date', function(newDate) { 37 $scope.model.setDate(newDate.day); 38 $scope.model.setMonth(newDate.month-1); 39 $scope.model.setFullYear(newDate.year); 40 }, true); 41 } 42 };
Build a date select 6
43 })
44 .controller('MainController', function($scope) { 45 $scope.current = new Date();
46 });
The directive works as follows:
We setrestrictthe E (for element), to allow dateselect as tag. We use an isolated scope withmodel as our communicator to the outside world. In the template we simply draw three select fields for month, day and year. We generate the necessary options in the controller.
Because we can fully rely on the template, we don’t need the link function here and just use the controller.
The controller is the part where the whole work is done. Here we have three loops for days, months and years for the select boxes. Furthermore we initialize an objectdate, to hold the selected month, day and year.
Additionally we have to watches. One to reflect a model change from the outside and one to reflect a change from the select boxes.
Be careful to set thetrueas second parameter for$scope.$watch('...', function() {}, true). This signals to do a deep watch. Otherwise the updating won’t work all of the time.
The two watches are very similar. If themodelattribute changes, we update the select fields. If we change one of the select boxes, we update the model. Just just have to be careful with the month. The month is inconsistent. While days begin with 1, months begin with 0. This means you have to add or subtract 1.
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-birthdate-select
3 How to use AngularJS with a curly
braced template engine
Problem
You want to change the double curly braces {{ from AngularJS because they’re conflicting with your template engine on the backend.
Solution
You can indeed change the curly braces to e.g.[[. In AngularJS, the$interpolateprovider does the template interpretation and has an option for changing the symbols.
To do so, you can inject the$interpolateProviderin the config block of your module. There you can setstartSymbolandendSymbolas demonstrated below:
1 angular.module('cookbookApp', [])
2 .config(function($interpolateProvider){ 3 $interpolateProvider.startSymbol('[[');
4 $interpolateProvider.endSymbol(']]');
5 });
If you split your application into different modules, you have to configure the the alternative syntax for every module!
Tip 1: $interpolate stand alone
You can also use the interpolate provider for your own templating purposes. Just inject $interpolate and use:
1 $interpolate('Hello {{name}}!')({name: 'World'}); // => 'Hello World!'
Tip 2: Skipping interpolation
How to use AngularJS with a curly braced template engine 8 1 <div ng-non-bindable>
2 ...
4 Use the $compile function
Problem
You created an html string with some AngularJS directives inside your link function. As you attach it to the DOM, you just get exactly what you wrote as output but you expected AngularJS to compile it for you.
Solution
If you you use AngularJS directive in your normal html, AngularJS will compile it for you when the site is rendered. If you create a string inside a link function of a directive, you have to do this manually. You can do this by injecting the$compileservice. You can use $compilewith either a stringor an angular.element. This is returning a linking function which is called with a scope. This again returns an element which the can be inserted into the DOM.
With a string:
1 newElement = $compile(myString)(scope) With an element:
1 newElement = $compile(angular.element)(scope) Returns anangular.element, not a string!
What if don’t have a scope to compile it against?
Usually you would use$compileinside a link function of a directive and there you would have a scope. If for some reason you use it somewhere else, e.g. inside a service, you could of pass the scope to the function from outside. If you just need a scope, you can also just use the root scope. Inject $rootScopeand you’re ready to go.
5 Show a confirm box before ng-click
is executed
Problem
For an delete button you don’t want to execute a ng-click immediately but first show a confirm box
Solution
We create a new directive which is called confirmed-click instead of ng-click. This directive works the same (you can also use$eventin it) but with a confirm box before the click is executed. Here is an example:
1 <button confirmed-click="removeTask($index)">
The full view we use is a simple list with tasks you can delete. Before the deletion comes a confirm box:
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 <link rel="stylesheet" ng-href="style.css"/>
6 </head>
7 <body ng-controller="MainController">
8 <table>
9 <tr ng-repeat="task in tasks">
10 <td>{{task}}</td>
11 <td>
12 <button confirmed-click="removeTask($index)">Delete</button>
13 </td>
14 </tr>
15 </table>
16 </body>
Show a confirm box before ng-click is executed 11 For this to works, we bind the element to the click event and create a new box and disable the butotn when its clicked. The box itself has a new scope with two functionsok()andcancel(). If we click ok()we trigger the original click event. Oncancel()we close the box an reenable the button.
1 angular.module('cookbookApp', [])
2 .directive('confirmedClick', function($parse, $q, $compile, $rootScope) { 3 var box = '<div class="box"><div>Really?</div> ' +
4 '<button ng-click="close($event, true)">OK</button>' + 5 '<button ng-click="close($event)">Cancel</button>' + 6 '</div>';
7 return {
8 link: function(scope, element, attrs) {
9 var fn = $parse(attrs.confirmedClick); 10 element.on('click', function() { 11 var boxScope = $rootScope.$new();
12 var boxElement = $compile(box)(boxScope); 13
14 element.attr('disabled', 'disabled'); 15 element.append(boxElement);
16
17 boxScope.close = function(event, execute) {
18 event.stopPropagation();
19 element.removeAttr('disabled');
20 boxElement.remove();
21 if (execute) {
22 fn(scope, {$event: event});
23 } 24 }; 25 }); 26 } 27 }; 28 })
29 .controller('MainController', function($scope) { 30 $scope.tasks = ['Tidy up', 'Wash the dishes']; 31 $scope.removeTask = function(index){
32 $scope.tasks.splice(index, 1);
33 };
34 });
For the style we give the directive element a relative position, so that we can place the box we show absolute to it:
Show a confirm box before ng-click is executed 12 1 button[confirmed-click] {
2 position: relative;
3 } 4
5 button[confirmed-click] .box {
6 position: absolute; 7 border: 1px solid #ccc; 8 width: 100px; 9 background-color: #f9f9f9; 10 top: 0; 11 right: -100px; 12 }
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-confirm-box
6 Create a digital clock
Problem
You want to create a directive which shows the current time.
Solution
The solution is a little directive with a timer. For displaying the date, we use the date filter which does the formatting for us (<div ng-bind="now | date:\'HH:mm:ss\'"></div>). The updating is done through a timer with$interval. The important thing is to remove the timer when the directive is removed from the DOM. This is done by listening to the$destroyevent which is called when the removal of the directive happens.
$interval is first available since Angular 1.2.
1 angular.module('cookbookApp', [])
2 .directive('digitalClock', function($interval) {
3 return {
4 restrict: 'E', 5 scope: {},
6 template: '<div ng-bind="now | date:\'HH:mm:ss\'"></div>',
7 link: function (scope) {
8 scope.now = new Date();
9 var clockTimer = $interval(function() { 10 scope.now = new Date();
11 }, 1000);
12
13 scope.$on('$destroy', function(){ 14 $interval.cancel(clockTimer);
15 });
16 }
17 };
Create a digital clock 14 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head> 6 <body> 7 <digital-clock /> 8 </body> 9 </html>
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-digital-clock
7 Enable the save button only on valid
form data
Problem
You have a form with a save button and you want to only enable it when the form data use typed is valid.
Solution
Not only single form fields but the whole form has its own states to test its validity. You can test for $dirtyand$valid.
For example: If we a use a form with the nameform, we could only enable the submit button if the user changes one of the fields (dirty) and if then all fields are valid.
1 <button ng-disabled="!(form.$dirty && form.$valid)">Save</button>
Be sure to give the form a name. Additionally, setnovalidateas attribute and disable the browser’s own validations.
Here’s the full working example: 1 <html ng-app>
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <link rel="stylesheet" ng-href="style.css"/>
5 </head>
6 <body>
7 <form name="form" novalidate>
8 <label>Login:</label> <input type="text" ng-model="login" required/>
9 <button ng-disabled="!(form.$dirty && form.$valid)">Save</button>
10 </form>
11 </body>
Enable the save button only on valid form data 16 1 input.ng-invalid.ng-dirty { 2 background-color: #ffc4d0; 3 } 4 5 input.ng-valid.ng-dirty { 6 background-color: #d8ffd0; 7 }
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-disable-save-button
8 Disable trimming in input fields
Problem
You need the data from a textarea as it is on saving without the automatic trimming.
Solution
AngularJS makes good assumptions as default. For example, automatic trimming. If you don’t want it, you can disable it with:
9 Dynamic service loading with the
$injector
Problem
You have a lot of select boxes whose items are loaded from the server. You don’t want to repeat yourself here. Instead of injecting the needed services into the controller, you just want the directive to handle it automatically.
Solution
Instead of letting the injector automatically inject the services by name in the directive definition, you trigger the injection process manually. To do this, you use the$injectorinstance.
In this example, we create a directive with the following attributes: • model: Translates to ng-model in the select field
• resource: The service name and the function of the service you want to use separated by a dot. • resource-id: The key in the returning object you want to use as options value.
• resource-label: The key in the returning object you want to use as options label. The directive with looks like this:
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body ng-controller="MainController">
7 <dynamic-select model="personId" resource="People.getList"
8 resource-id="id" resource-label="name" />
9 </body>
Dynamic service loading with the $injector 19 The directive dynamicSelect itself is not that complicated. We have an isolated scope where we model, resourceId and resourceLabel. resource is directly read through the attrs function parameters because we don’t allow to dynamically change the service and we’re not needing it in the template. (See recipe use the scope right TODO).
In the link function, we split the string from resource into two parts and write them into an object for better readability. Then we just use$injector.getto return the service as object. We then use the function as read intoparams.fn. We use.thenhere directly because we assume that our service function returns a promise.
1 angular.module('cookbookApp', []) 2 .factory('People', function($http) {
3 return {
4 getList: function() { return $http.get('person.json'); }
5 };
6 })
7 .directive('dynamicSelect', function($injector) {
8 return { 9 restrict : 'E', 10 scope: { 11 model: '=', 12 resourceId: '@', 13 resourceLabel: '@' 14 },
15 template: '<select ng-model="model" ng-options="item[resourceId] ' + 16 'as item[resourceLabel] for item in items" />',
17 link: function(scope, element, attrs) {
18 var temp = attrs.resource.split('.');
19 var params = { name: temp[0], fn: temp[1] }; 20
21 var service = $injector.get(params.name); 22
23 service[params.fn]().then(function(serviceResponse) { 24 scope.items = serviceResponse.data;
25 });
26 }
27 };
28 })
29 .controller('MainController', function($scope) { 30 $scope.personId = 2;
Dynamic service loading with the $injector 20 This is example is greatly simplified. You would of course need error checking if you’re consuming services other than $http.
Just for a complete example, the used JSON file. 1 [
2 { "id": 1, "name": "John" }, 3 { "id": 2, "name": "Bill" }, 4 { "id": 3, "name": "Phil" } 5 ]
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-dynamic-service-loading
10 Create a dynamic templateUrl
Problem
You have several html templates which you want to use dynamically inside your directive.
Solution
Inside a directive,templateUrlcannot only take a string but also a function. The function has the element and the attributes as parameters.
In the following example, we dynamically fill a textarea with two different templates which are loaded dynamically. We write a directive which is named ‘prefill’. This takes the name of the template as argument.
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body>
7 <textarea prefill="filler1"></textarea>
8 <textarea prefill="filler2"></textarea>
9 </body>
10 </html>
The directive itself consists only of thetemplateUrlwith a function. Here we just take the argument from theprefillattribute and append.htmlto it to create the url.
Create a dynamic templateUrl 22 1 angular.module('cookbookApp', [])
2 .directive('prefill', function() {
3 return {
4 templateUrl: function(element, attrs) {
5 return attrs.prefill + '.html';
6 }
7 };
8 });
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-dynamic-template-url
11 Show a box on outstanding http
requests (simple)
Problem
While you sending http request, you want to show a box or a loading spinner
Solution
As the solution here, we write a directive which shows an element if a condition is true. This could be a simple div with a string or a loading spinner. It doesn’t matter where you place it in your DOM because the scope is isolated.
The way it works is, that it injects the $httpbecause we need to know the count of the pending requests. After a request is started, the count changes and so does$http.pendingRequests.length. When it does, we get the result of http.pendingRequests.length > 0in the variable value. This is either true or false. We write the result in $scope.waitingand our div is visible or hidden depending of the state.
1 .directive('waitingForRequest', function($http) { 2 var pendingRequests = function() {
3 return $http.pendingRequests.length > 0;
4 };
5 return {
6 restrict: 'E', 7 scope: {},
8 template: '<div ng-show="waiting">Waiting for request to finish...</div>', 9 controller: function($scope) {
10 $scope.$watch(pendingRequests, function(value) {
11 console.log('Pending requests: '+ $http.pendingRequests.length); 12 $scope.waiting = value;
13 });
14 }
15 };
Show a box on outstanding http requests (simple) 24
Discussion
This is a really simple but not the best solution. But for most applications it should be good enough. There are to issues here:
The watcher is executed every time an apply() is called. This means$http.pendingRequests.length > 0is called a lot of times. But for most applications it’s does not mean a real performance hit.
12 Show only initialized data on
startup
Problem
You start your not so small app and while AngularJS is booting up, your whole screen is crowded with curly braces.
Solution
First, the why. This situation can appear when open yourindex.html. The browser has to download all the javascript and css files and during this time, AngularJS hasn’t parsed the page yet. So all over the page, you see curly braces for a short moment which are replaced when AngularJS is initialized. To prevent this, you can use 2 directives:ng-cloakandng-bind.
ng-clock
ng-cloak is a directive which removes itself when the application is initialized. If used with the following CSS, all elements are hidden during the initialization process.
1 [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
2 display: none !important;
3 }
ng-bind
If you don’t want to hide some elements completely because then the layout is broken, you can also useng-bind.ng-bindis similar to{{}}but replaces everything which is inside the tag where it is defined. If you would use curly braces and define a counter like this:
1 <span>{{counter}}</span>
Show only initialized data on startup 26 1 <span ng-bind="counter">?</span>
This has the advantage that a question mark is displayed as long the app isn’t fully initialized. When the app is ready, the question mark is replaced with the real data.
ng-bind-template
If you want to useng-bindbut have more than one expression, you can useng-bind-template. It’s likeng-bindbut can contain multiple expressions.
13 Create a markdown live preview
Problem
You’re using markdown somewhere in your application. Users who edit the markdown templates should see a live preview of the resulting html.
Solution
In this example, we implement the directive with the Showdown¹ library which is a popular markdown interpreter.
We create a directive ‘markdownPreview’ which has an isolated scope and takes a model as input attribute. As we create a new tag<markdown-preview>, we have to restrict it to element (E). In the link function, we’re watching for a change of the model attribute and convert the markdown input to an html output. The result is pasted into the elements’ inner html.
1 angular.module('cookbookApp', [])
2 .directive('markdownPreview', function() { 3 var converter = new Showdown.converter();
4 return {
5 restrict: 'E',
6 scope : {
7 model: '='
8 },
9 link: function(scope, element) {
10 scope.$watch('model', function(markdownText){
11 var resultHtml = converter.makeHtml(markdownText || ''); 12 element.html(resultHtml); 13 }); 14 } 15 }; 16 }); ¹https://github.com/coreyti/showdown
Create a markdown live preview 28 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="showdown/showdown.js"></script>
5 <script src="application.js"></script>
6 </head>
7 <body>
8 <textarea ng-model="markdown" cols="60" rows="10"></textarea>
9 <markdown-preview model="markdown"></markdown-preview>
10 </body>
11 </html>
Discussion
Oftentimes, there are more solutions to a problem. If you want, you could also implement the markdown as a filter instead of directive. But be aware that you have to include the ngSanitize module and useng-bind-html. Otherwise, your resulting html will be escaped.
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-markdown-live-preview
14 Table double rows with ng-repeat
Problem
You want to to repeat multiple elements which are siblings. Usually ng-repeat only repeat the current element and its children. See what’s new in AngularJS 1.2.
Solution
Since AngularJS 1.2 there’s a new optional syntax forng-repeat. With it you can define a start and an end element for the repeater. The only restriction is that the element have to be siblings.
1 <table border="1">
2 <tr ng-repeat-start="user in users">
3 <td colspan="2">{{user.name}}</td> 4 </tr> 5 <tr ng-repeat-end> 6 <td>{{user.age}}</td> 7 <td>{{user.gender}}</td> 8 </tr> 9 </table> The source: 1 <html ng-app="cookbookApp"> 2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body ng-controller="MainController">
7 <table border="1">
8 <tr ng-repeat-start="user in users">
9 <td colspan="2">{{user.name}}</td>
10 </tr>
11 <tr ng-repeat-end>
12 <td>{{user.age}}</td>
Table double rows with ng-repeat 30 14 </tr> 15 </table> 16 </body> 17 </html> <<(code/directives-ng-repeat-double-rows/style.css) 1 [
2 { "name": "Bill", "age": 42, "gender": "male" }, 3 { "name": "John", "age": 52, "gender": "male" }, 4 { "name": "Anne", "age": 19, "gender": "female" }, 5 { "name": "Phil", "age": 21, "gender": "male" }, 6 { "name": "Mary", "age": 23, "gender": "female" } 7 ]
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-ng-repeat-double-rows
15 Prevent duplicate warnings in
ng-repeat
Problem
If you useng-repeatwith duplicates in an array, you get the error message “Duplicates in a repeater are not allowed”.
Solution
This happens because AngularJS exspects every element to have an unique identifier. This is for tracking the insertion, deletion and moving of element. To change the identifier for the element you can use thetrack bysyntax.
If the the following snippet throws an error: 1 <li ng-repeat="item in [4,4,4]">
You can enforce artifical uniqueness by using the index of the current element in the repeater: 1 <li ng-repeat="item in [4,4,4] track by $index">
16 Slide right/left with animations
Problem
You want to add slide effects for left and right to your ng-view div.
Solution
With a little trick, ng-animate will do all the heavy lifting for us. The idea is to set a variable for the direction before the url is changed. The animation direction depends on a css class which is altered when the direction changes.
Step 1: The styling
The first thing we need is some styling for the slide effects and a container to wrap them: 1 .container {
2 position:relative; overflow:hidden;
3 border: 1px solid black; height:100px;
4 } 5
6 .slide-left, .slide-right {
7 position: absolute; top: 0; left: 0; right: 0; bottom: 0;
8 width: 100%; padding: 10px;
9 } 10
11 .slide-left.ng-enter, .slide-left.ng-leave, 12 .slide-right.ng-enter, .slide-right.ng-leave {
13 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s; 14 -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s; 15 -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s; 16 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s; 17 }
18
19 .slide-left.ng-enter { left: 100%; }
20 .slide-left.ng-enter.ng-enter-active { left: 0; } 21 .slide-left.ng-leave { left: 0; }
Slide right/left with animations 33 22 .slide-left.ng-leave.ng-leave-active { left: -100%; } 23 24 .slide-right.ng-enter { left: -100%; } 25 .slide-right.ng-enter.ng-enter-active { left: 0; } 26 .slide-right.ng-leave { left: 0; } 27 .slide-right.ng-leave.ng-leave-active { left: 100%; }
The container must haveposition: absolute or relative. The slide classes must haveposition: absolute. For left and right sliding, we defineslide-leftandslide-rightwith additional classes forng-animate¹.
Step 2: The code
For demo purposes, we define two sample routes with direct template declaration with the $routeProvider. For the url change, we define two functions, moveLeft and moveRight, in a controller for the left and right slide. With $location service, we can change the url from a controller.
1 angular.module('cookbookApp', ['ngRoute', 'ngAnimate']) 2 .config(function($routeProvider) {
3 $routeProvider
4 .when('/1', { template: '<h1>first page</h1>' }) 5 .when('/2', { template: '<h1>second page</h1>' }) 6 .otherwise({redirectTo: '/1' });
7 })
8 .controller('MainController', function($scope, $location) { 9 $scope.moveLeft = function(href) {
10 $scope.direction = 'left'; 11 $location.path(href);
12 };
13
14 $scope.moveRight = function(href) { 15 $scope.direction = 'right';
16 $location.path(href);
17 };
18 });
Since AngularJS 1.2, ngRoute is a module on its own. Be sure to include ngRoute and ngAnimate!
Slide right/left with animations 34
Step 3: The markup
In the app code itself we use the defined methods for the links withng-click="moveRight('/1')".
Depending on the direction variable,ng-viewgets the right animation withng-class="'slide-'+direction". 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="../vendor/angular-route.js"></script>
5 <script src="../vendor/angular-animate.js"></script>
6 <script src="application.js"></script>
7 <link rel="stylesheet" href="style.css"/>
8 </head>
9 <body ng-controller="MainController">
10 <a href ng-click="moveRight('/1')">Page 1</a>
11 <a href ng-click="moveLeft('/2')">Page 2</a>
12
13 <div class="container">
14 <div ng-view ng-class="'slide-'+direction"></div>
15 </div>
16 </body>
17 </html>
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-ng-view-slide-animation
17 Pass a function to a directive with
an isolated scope (&)
Problem
You have a directive with an isolated scope want to call a function in the outside scope. Or you to understand what the&in the isolated scope definition is doing.
Solution
While isolated scopes with@and=are easy understood by most people, a lot of them have a problem to understand what exactly the&is doing.
This is a basic concept but we repeat it here, because it is so often misunderstood.
As a general information. Every declaration of the scope isolation refers to an attribute of the element of the directive.
An declaration like
1 directive('colorpicker', function() {
2 ... 3 scope: { 4 a: '@', 5 b: '=', 6 c: '&' 7 }
means the that you can use a, b and c inside the directive. The connection to the outer world is declared through attributes on the directive element. So the element would look like<colorpicker a="4" b="myVar" c="myCallback()">. This is important. You don’t connect to the parent scope directly, but only through attribute declaration.
So, what is the
&
doing?
If you have the following situation:Pass a function to a directive with an isolated scope (&) 36 1 +- controller scope
2 +--- directive scope
The directive is isolated and nested under the controller scope, you can execute a function on the controller scope from the directive scope.
Why is this important
This is important because you’re free to choose your function name and which parameters you pass. This means you could use<colorpicker c="myCallback(paramA, paramB)">in one controller and <colorpicker c="mySuperCallback (paramB, paramA)">in another.
How do I use it?
So, you want to call a function like this in the controller 1 $scope.myCallback = function(first, second) { ... }
The directive could have a execution of the function like this: 1 scope.c({ paramA: 123, paramB: 'xzy');
The you would have to use the directive in the template like this 1 `<colorpicker c="myCallback(paramA, paramB)">`
This means, every time the function is trigger inside the directive, it is translated to: 1 myCallback(123, 'xzy')
18 Select box with multiple option
Problem
You want to use a select box with the optionmultiple.
Solution
Theng-options directive of AngularJS does work well with themultipleoption. You don’t need an extra configuration for this. If multipleis present as an attribute,ng-modelcontains an array instead of a simple type.
The binding here is also bidirectional. Below is an example of how to use it. As you can see, you can set initial values just by declaring an array.
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body ng-controller="MainController">
7 <select multiple ng-model="selected"
8 ng-options="person.id as person.name for person in people">
9 </select>
10 {{ selected }} 11 </body>
Select box with multiple option 38 1 angular.module('cookbookApp', [])
2 .controller('MainController', function($scope) { 3 $scope.people = [ 4 { id:1, name:'John' }, 5 { id:2, name:'Bill' }, 6 { id:3, name:'Phil' } 7 ]; 8 9 $scope.selected = [1,2]; 10 });
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-select-multiple
19 Select and ng-options
Problem
You struggle with the instructions forng-optionsgiven in the api.
Solution
Here we give examples for every way you can useng-options. Source code ((((here)))))
Array data sources
For all the examples, we use the following array: 1 users = [
2 { id: '800', name: 'Bill Mayer', role: 'Admin' }, 3 { id: '801', name: 'Anne Black', role: 'User' } 4 ];
What you’ll see in all examples is thatng-optionssets the values for arrays on it’s own. You can’t change the value of the options.ng-optionstranslates them automatically.
label for value in array Select tag withng-options:
1 <select ng-model="arr1" ng-options="user.name for user in users"> The resulting options:
1 <option value="0" selected="selected">Bill Mayer</option>
2 <option value="1">Anne Black</option>
Select and ng-options 40 1 {"id":"800","name":"Bill Mayer","role":"Admin"}
select as label for value in array Select tag withng-options:
1 <select ng-model="arr2" ng-options="user.id as user.name for user in users"> The resulting options:
1 <option value="0" selected="selected">Bill Mayer</option>
2 <option value="1">Anne Black</option>
The contents of ng-model: 1 800
label group by group for value in array Select tag withng-options:
1 <select ng-model="arr3"
2 ng-options="user.name group by user.role for user in users"> The resulting options:
1 <optgroup label="Admin">
2 <option value="0">Bill Mayer</option>
3 </optgroup>
4 <optgroup label="User">
5 <option value="1">Anne Black</option>
6 </optgroup>
The contents of ng-model:
1 {"id":"800","name":"Bill Mayer","role":"Admin"}
select as label group by group for value in array Select tag withng-options:
Select and ng-options 41 1 <select ng-model="arr4"
2 ng-options="user.id as user.name group by user.role for user in
3 users">
The resulting options: 1 <optgroup label="Admin">
2 <option value="0">Bill Mayer</option>
3 </optgroup>
4 <optgroup label="User">
5 <option value="1">Anne Black</option>
6 </optgroup>
The contents of ng-model: 1 800
Object data sources
For all the examples we use the following object: 1 roles = {
2 150: { name: 'Admin', rights: 'Read+Write' }, 3 151: { name: 'User', rights: 'Read' }
4 };
label for (key, value) in object Select tag withng-options:
1 <select ng-model="obj1" ng-options="obj.name for (roleId, obj) in roles"> The resulting options:
1 <option value="150" selected="selected">Admin</option>
2 <option value="151">User</option>
Select and ng-options 42 1 {"name":"Admin","rights":"Read+Write"}
select as label for (key , value) in object Select tag withng-options:
1 <select ng-model="obj2" ng-options="id as obj.name for (id, obj) in roles"> The resulting options:
1 <option value="150" selected="selected">Admin</option>
2 <option value="151">User</option>
The contents of ng-model: 1 150
label group by group for (key, value) in object Select tag withng-options:
1 <select ng-model="obj3"
2 ng-options="obj.name group by obj.rights for (id, obj) in roles"> The resulting options:
1 <optgroup label="Read+Write">
2 <option value="150">Admin</option>
3 </optgroup>
4 <optgroup label="Read">
5 <option value="151">User</option>
6 </optgroup>
The contents of ng-model:
1 {"name":"Admin","rights":"Read+Write"}
select as label group by group for (key, value) in object Select tag withng-options:
Select and ng-options 43 1 <select ng-model="obj4"
2 ng-options="id as obj.name group by obj.rights for (id, obj) in roles"> The resulting options:
1 <optgroup label="Read+Write">
2 <option value="150">Admin</option>
3 </optgroup>
4 <optgroup label="Read">
5 <option value="151">User</option>
6 </optgroup>
The contents of ng-model: 1 150
Custom empty option
If you don’t want to show a blank field but your own custom label, you can just insert an option tag with a blank value.
1 <select ng-model="choose" ng-options="user.name for user in users">
2 <option value="">- Choose user -</option>
3 </select><br>
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-select-ng-options
20 Make a sortable table
Problem
You want to make your table sortable. The element in the header which the table is ordered by is highlighted. If you click again on the active element the sort order is reversed.
Solution
For this we write a directivethSortablewhich you can later use like this: 1 <th th-sortable="sort" th-column="name" th-default>Name</th>
The directive wraps the contents of an th tag into a link. The link gets a classactive if it is the current selected sort order. If the element is already active and you click the link again the order is reversed.
We isolate the scope of the directive and set two variables: 1 angular.module('cookbookApp', [])
2 .directive('thSortable', function() {
3 return {
4 transclude: true,
5 template: '<a href ng-click="changeColumn()" ng-class="{' + 6 'active: sort.column == column, ' +
7 'asc: !sort.reverse, ' + 8 'desc: sort.reverse' + 9 '}">' + 10 '<span ng-transclude></span></a>', 11 scope : { 12 sort: '=thSortable', 13 column: '@thColumn' 14 },
15 controller: function($scope, $attrs) { 16 $scope.sort = $scope.sort || {}; 17
Make a sortable table 45 19 $scope.sort.column = 'name'; 20 } 21 22 $scope.changeColumn = function() { 23 if ($scope.sort.column === $scope.column) { 24 $scope.sort.reverse = !$scope.sort.reverse; 25 } else {
26 $scope.sort = { column: $scope.column, reverse: false };
27 }
28 };
29 }
30 };
31 })
32 .controller('MainController', function($scope, $http) { 33 $http.get('users.json').then(function(usersResponse) { 34 $scope.users = usersResponse.data;
35 });
36 });
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 <link rel="stylesheet" ng-href="style.css"/>
6 </head>
7 <body ng-controller="MainController">
8 <table>
9 <tr>
10 <th th-sortable="sort" th-column="name">Name</th>
11 <th th-sortable="sort" th-column="age" >Age</th>
12 <th th-sortable="sort" th-column="gender" th-default>Gender</th>
13 </tr>
14 <tr ng-repeat="user in users | orderBy:sort.column:sort.reverse">
15 <td ng-bind="user.name"></td> 16 <td ng-bind="user.age"></td> 17 <td ng-bind="user.gender"></td> 18 </tr> 19 </table> 20 </body> 21 </html>
We use stylesheets to highlight the current column and show the order. Because we set .asc or .desc as class for the direction, we can use the the css content attribute and append and arrow.
Make a sortable table 46 1 th a { 2 display: block; 3 text-decoration: none; 4 } 5 6 th a.active { 7 background-color: #ccc; 8 } 9 10 th a.active.asc:after { 11 content: "\2193"; 12 } 13 14 th a.active.desc:after { 15 content: "\2191"; 16 }
Just for demo purposes the json file with the demo data. 1 [
2 { "name": "Bill", "age": 42, "gender": "male" }, 3 { "name": "John", "age": 52, "gender": "male" }, 4 { "name": "Anne", "age": 19, "gender": "female" }, 5 { "name": "Phil", "age": 21, "gender": "male" }, 6 { "name": "Mary", "age": 23, "gender": "female" } 7 ]
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-sortable-table
21 Make a stacked bar chart
Problem
You want to create a chart with stack bars.
Solution
We solve it through a directive which takes an array of percentages and draws the chart.
For the bars, we define an array of integers in the controller. In the directive, it’s enough to just use a template with anng-repeat. There’s one trick here.ng-repeatdoesn’t allow duplicates. If we would just useng-repeat="bar in bars"and use an array like[20,20,40], AngularJS would throw an error likeDuplicates in a repeater are not allowed. We solve this by usingtrack by $index" and force uniqueness.
1 .stacked-bar-chart { 2 overflow: hidden; 3 margin: 10px 0; 4 height: 20px; 5 border: 1px solid #ccc; 6 background-color: #f9f9f9; 7 } 8
9 .stacked-bar { float: left; height: 100%; } 10 .stacked-bar-0 { background-color: #bb0000; } 11 .stacked-bar-1 { background-color: #00bb00; } 12 .stacked-bar-2 { background-color: #0000bb; }
Make a stacked bar chart 48 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 <link rel="stylesheet" ng-href="style.css"/>
6 </head>
7 <body ng-controller="MainController">
8 <stacked-bar bars="bars"></stacked-bar>
9
10 <input type="number" ng-model="bars[0]" />
11 <input type="number" ng-model="bars[1]" />
12 <input type="number" ng-model="bars[2]" />
13 </body>
14 </html>
1 angular.module('cookbookApp', []) 2 .directive('stackedBar', function() {
3 return {
4 restrict: 'E',
5 template: '<div class="stacked-bar-chart">' + 6 '<div ng-repeat="bar in bars track by $index" ' + 7 'class="stacked-bar stacked-bar-{{$index}}" ' + 8 'ng-style="{width: bar+\'%\'}">' + 9 '</div>' + 10 '</div>', 11 scope : { 12 bars: '=' 13 } 14 }; 15 })
16 .controller('MainController', function($scope) { 17 $scope.bars = [30,30,40];
18 });
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-stacked-bar-chart
22 Prevent event propagation from
ng-click
Problem
You have several nested elements withng-click. If you click the front element, you don’t want the click function of the back element to be triggered.
Solution
The trick here is to use $event which is injected into ng-click.$event is the normal JavaScript event object and thus you can usestopPropagation()to prevent the bubbling of events.
You pass $eventas parameter of the topmostng-clickand callevent.stopPropagation()in the function declaration inside of the controller.
1 <div ng-click="back()">
2 ...
3 <a ng-click="front($event)">Click me!</a>
4 ...
5 </div>
Alternative solution
You can of course use$eventdirectly inside theng-clickdefinition. 1 <div ng-click="back()">
2 ...
3 <a ng-click="front(); $event.stopPropagation()">Click me!</a>
4 ...
23 Submit form on enter key
Problem
Usually, if you press enter in a form field, the form is submitted to the action defined in the ´<form> ´ tag but you want to call a function in your controller.
Solution
The solution is to leave out the actionattribute in <form>and useng-submit. The expression in ng-submitis what gets executed when you press return inside a form field.
An complete example:
1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 <link rel="stylesheet" ng-href="style.css"/>
6 </head>
7 <body ng-controller="MainController">
8 <form name="form" ng-submit="addTask(taskName)" novalidate>
9 <input type="text" ng-model="taskName" />
10 </form>
11
12 <ul>
13 <li ng-repeat="task in tasks track by $index" ng-bind="task"></li>
14 </ul>
15 </body>
Submit form on enter key 51 1 angular.module('cookbookApp', [])
2 .controller('MainController', function($scope) { 3 $scope.tasks = [];
4
5 $scope.addTask = function(taskName) { 6 $scope.tasks.push(taskName);
7 };
8 });
We usetrack by $indexinng-repeat here. This is a hack to allow duplicate entries in your list. Try to see what happens if you leave it out.
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-submit-form-on-enter
24 Make a syntax highlighter
Problem
You want to make a syntax highlighter but your code inside the code block is interpreted by AngularJS.
Solution
As an example, we take the following code: 1 <html ng-app="cookbookApp">
2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="prism/prism.js"></script>
5 <script src="application.js"></script>
6 <link rel="stylesheet" ng-href="prism/prism.css"/>
7 </head>
8 <body>
9 <pre><code class="language-markup">
10 <ul>
11 <li ng-repeat="task in tasks">{{ task }}</li>
12 </ul>
13 <button ng-click="add()">Add task</button>
14 </code></pre>
15 </body>
16 </html>
If we look at the code block, there are several problems we have to tackle.
• Start: From a html perspective, the code snippet starts right behindcode class="language-markup"> but we don’t want to insert an empty line before the code snippet.
• Indention: For reasons of readability, we want the indention in the html file to be 4 spaces. But when we present it to the user, we want it to start at 0.
• Angle brackets: We don’t want to paste our code like ‘>pre<. We have to convert it.
• AngularJS: We don’t want AngularJS to interpret the code inside the code block because if we would use an expression, it would be interpreted by AngularJS.
Make a syntax highlighter 53 As a syntax highlighter, we usePrism¹by Lea Verou. It’s used by a lot of well-known companies for their sites.
1 angular.module('cookbookApp', []) 2 .directive('code', function() {
3 function escapeAngleBrackets(text) {
4 return text.replace(/</gi, '<').replace(/>/gi, '>');
5 }
6 function trimSurroundingEmptyLines(text) {
7 return text.replace(/^(\n)*/, '').replace(/(\n)*(\s)*$/, '');
8 }
9 function fixIndention(text) {
10 return text.replace(
11 new RegExp('^ {' + text.search(/[^\s-]/) + '}', 'gm'), '');
12 }
13 return {
14 restrict: 'E', 15 terminal: true,
16 link: function(scope, element) {
17 var content = element.html();
18 content = escapeAngleBrackets(content); 19 content = trimSurroundingEmptyLines(content); 20 content = fixIndention(content); 21 element.html(content); 22 Prism.highlightElement(element.get(0)); 23 } 24 }; 25 });
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-syntax-highlighter
Online demo:http://sbrink.github.io/angularjs-cookbook-code/directives-syntax-highlighter/
25 Textarea char limit with remaining
counter
Problem
You want a counter for remaining chars for your textarea
Solution
We create a directive which appends an counter element after the textarea. To get the value of ng-model, we require thengModelcontroller which is the preferred way if there’s ang-model. For the counter of the remaining characters we create a template in a variablecounterTemplate. for this template we create a new fresh scopecounterScopeand compile the template against this scope. We then append it after the textarea element withelement.after(...).
Everytime there’s a change in the model, the parser chain of thengModelcontroller is called which we use. There we just calculate the current length, truncate it when it’s to long and refresh the variableremainingon thecounterScope.
1 angular.module('cookbookApp', [])
2 .directive('maxlengthCounter', function ($compile) {
3 var counterTemplate = '<p>Remaining charaters: {{remaining}}</p>';
4 return {
5 require:'ngModel',
6 link:function (scope, element, attrs, ngModelCtrl) {
7 var maxLength = parseInt(attrs.maxlengthCounter, 10); 8
9 var counterScope = scope.$new(); 10 counterScope.remaining = maxLength;
11 element.after($compile(counterTemplate)(counterScope)); 12
13 ngModelCtrl.$parsers.push(function (value) { 14 var currentLength = parseInt(value.length, 10); 15
16 if (currentLength > maxLength) {
Textarea char limit with remaining counter 55
18 currentLength = maxLength;
19 }
20 counterScope.remaining = maxLength - currentLength;
21 }); 22 } 23 }; 24 }); 1 <html ng-app="cookbookApp"> 2 <head>
3 <script src="../vendor/angular.js"></script>
4 <script src="application.js"></script>
5 </head>
6 <body>
7 <textarea ng-model="result" maxlength-counter="16"></textarea>
8 <p>{{result}}</p>
9 </body>
10 </html>
Code
Complete source: https://github.com/sbrink/angularjs-cookbook-code/tree/gh-pages/directives-textarea-maxlength-counter
26 Theme support
Problem
You want to support different themes which you can change on the fly.
Solution
Overview:
• Create a controller for the<head>tag which changes the href to your stylesheet on the fly. • Create a service which contains the active theme and supports the changing of it.
• Inject the service where you want to change the theme. The code:
First we create a ThemeService service , a HeadController controller and a MainController controller.
• ThemeServiceholds the theme state
• HeadControllerexposes getTheme() to change the stylesheet link in the<head>tag • MainControlleracts as an example for a controller which can change the theme.
1 angular.module('cookbookApp', []) 2 .factory('ThemeService', function(){
3 var themes = { available: ['default', 'greenish'], active: 'default' }; 4
5 function getTheme() { return themes.active; }
6 7 function setTheme(name) { 8 if (themes.available.indexOf(name) === -1) { return; } 9 themes.active = name; 10 } 11 12 return { 13 getTheme: getTheme,