50 | {{myAsyncValue}}
51 | No result
52 |
53 | ```
54 |
55 | This would yield:
56 |
57 | 1. "Loading" would initially appear
58 | 2. Two seconds later, "No result" would show
59 |
60 | Any nested directives (if any) in `wait-done`, would not be processed until the `until` condition is met (see technical notes).
61 |
62 | # Requirements
63 |
64 | Tested in IE8+, latest Chrome, and latest Safari. Angular 1.2+.
65 |
66 | # Installing
67 |
68 | ```shell
69 | bower install -S angular-wait
70 | ```
71 |
72 | Include the script and then Integrate into your app:
73 |
74 | ```js
75 | var myapp = angular.module('myapp', ['michiKono']);
76 | ```
77 |
78 | # Usage
79 |
80 | Simply define the `wait` directive with `wait-loading` (mandatory) and `wait-done` nodes inside it as shown below:
81 |
82 | ```html
83 |
84 | shown while waiting
85 | shown when finished
86 |
87 |
88 |
89 | shown while waiting
90 | shown when finished
91 |
92 |
93 |
94 | shown while waiting
95 | shown when finished
96 |
97 | ```
98 |
99 | ## Wait Until _____
100 |
101 | All three available attributes to the directive watch the passed condition or variable until its value matches
102 | the asked state. For example the following uses all immediately render the `wait-done` nodes:
103 |
104 | ```html
105 |
106 | not shown
107 | SHOWS IMMEDIATELY
108 |
109 |
110 |
111 | not shown
112 | SHOWS IMMEDIATELY
113 |
114 |
115 |
116 | not shown
117 | SHOWS IMMEDIATELY
118 |
119 | ```
120 |
121 | Note that the matching is using triple equals (`===`). This means that falsey values for the `until-not-null` do not
122 | necessarily trigger it. The following example illustrates this:
123 |
124 | ```html
125 |
126 | SHOWN
127 | not shown
128 |
129 |
130 |
131 | SHOWN
132 | not shown
133 |
134 |
135 |
136 | SHOWN
137 | not shown
138 |
139 | ```
140 |
141 | ## Technical notes ##
142 |
143 | The inner contents are using transclusion can handle child directives. Inner directives are not
144 | processed at all (compile or controller methods) until the `wait-done` node renders.
145 |
146 | ## Releases ##
147 |
148 | * 1.0.2 Aug 23, 2014 - Inner directives not processed until done node shows
149 | * 1.0.1 Aug 23, 2014 - Support for minification by using ngAnnotate
150 | * 1.0.0 Aug 23, 2014 - Initial Release
151 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-wait",
3 | "version": "1.0.1",
4 | "authors": [
5 | "Michi Kono "
6 | ],
7 | "description": "An Angular directive for showing different content while an asynchronous request is pending",
8 | "main": "dist/angular-wait.js",
9 | "keywords": [
10 | "angular",
11 | "wait",
12 | "waiting",
13 | "directive",
14 | "load",
15 | "loading"
16 | ],
17 | "license": "MIT",
18 | "homepage": "http://www.michikono.com",
19 | "ignore": [
20 | "**/.*",
21 | "node_modules",
22 | "bower_components",
23 | "test",
24 | "tests"
25 | ],
26 | "dependencies": {
27 | },
28 | "devDependencies": {
29 | "angular": "~1.2",
30 | "angular-mocks": "~1.2",
31 | "jasmine": "~2.0.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/dist/angular-wait.js:
--------------------------------------------------------------------------------
1 | /**
2 | * angular-wait version 1.0.2
3 | * License: MIT.
4 | * Copyright (C) 2014 Michi Kono
5 | * https://github.com/michikono/angular-wait
6 | */
7 |
8 | 'use strict';
9 |
10 | (function (window) {
11 | var angular = window.angular;
12 | angular
13 | .module('michiKono', [])
14 | .controller('waitDirectiveCtrl', ['$scope', '$element', function WaitDirectiveCtrl($scope, $element) {
15 | $scope.show = false;
16 | var childScopes = [];
17 |
18 | var updateChildren = function () {
19 | for (var i = 0; i < childScopes.length; i++) {
20 | childScopes[i].show = $scope.show;
21 | }
22 | };
23 | this.registerChild = function (scope) {
24 | childScopes.push(scope);
25 | updateChildren();
26 | };
27 |
28 | $scope.$watch('untilNotNull', function (newValues, oldValues, scope) {
29 | if (typeof newValues !== 'undefined' && newValues !== null) {
30 | $scope.show = true;
31 | updateChildren();
32 | }
33 | });
34 | $scope.$watch('untilNotFalse', function (newValues, oldValues, scope) {
35 | if (typeof newValues !== 'undefined' && newValues !== false) {
36 | $scope.show = true;
37 | updateChildren();
38 | }
39 | });
40 | $scope.$watch('untilNotUndefined', function (newValues, oldValues, scope) {
41 | if (typeof newValues !== 'undefined') {
42 | $scope.show = true;
43 | updateChildren();
44 | }
45 | });
46 | }])
47 | .directive('wait', function () {
48 | return {
49 | controller: 'waitDirectiveCtrl',
50 | restrict: 'AE',
51 | scope: {
52 | // these assume "when no longer undefined and..."
53 | untilNotNull: '=',
54 | untilNotFalse: '=',
55 | untilNotUndefined: '='
56 | },
57 | link: function postLink(scope, element, attrs) {
58 | }
59 | };
60 | })
61 | .directive('waitLoading', function () {
62 | return {
63 | restrict: 'EA',
64 | replace: true,
65 | template: '',
66 | transclude: true,
67 | require: '^wait',
68 | link: function link(scope, $element, attrs, controller, transclude) {
69 | scope.$watch('show', function (contents, old) {
70 | $element.html('');
71 | if (!contents) {
72 | transclude(scope, function (clone) {
73 | $element.html('');
74 | $element.append(clone);
75 | });
76 | }
77 | });
78 | // why we need something like this: http://stackoverflow.com/questions/16866749/access-parent-scope-in-transcluded-directive
79 | controller.registerChild(scope);
80 | }
81 | };
82 | })
83 | .directive('waitDone', function () {
84 | return {
85 | priority: 1,
86 | terminal: true,
87 | restrict: 'EA',
88 | replace: true,
89 | template: '',
90 | transclude: true,
91 | scope: false,
92 | require: '^wait',
93 | link: function link(scope, $element, attrs, controller, transclude) {
94 | scope.$watch('show', function (contents, old) {
95 | $element.html('');
96 | if (contents) {
97 | transclude(scope, function (clone) {
98 | $element.html('');
99 | $element.append(clone);
100 | });
101 | }
102 | });
103 | controller.registerChild(scope);
104 | }
105 | };
106 | });
107 | }(window));
108 |
--------------------------------------------------------------------------------
/grunt/jshint.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | jshintrc: '.jshintrc'
4 | },
5 | all: [
6 | 'Gruntfile.js',
7 | 'grunt/*.js',
8 | 'src/*.js',
9 | 'test/*.js',
10 | 'karma.conf.js'
11 | ]
12 | };
--------------------------------------------------------------------------------
/grunt/karma.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | unit: {
3 | configFile: 'karma.conf.js',
4 | singleRun: true
5 | }
6 | };
--------------------------------------------------------------------------------
/grunt/ngAnnotate.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | // Tells if ngAnnotate should add annotations (true by default).
4 | add: true,
5 |
6 | // Tells if ngAnnotate should remove annotations (false by default).
7 | remove: false,
8 |
9 | // If provided, only strings matched by the regexp are interpreted as module
10 | // names. You can provide both a regular expression and a string representing
11 | // one. See README of ng-annotate for further details:
12 | // https://npmjs.org/package/ng-annotate
13 | // regexp: regexp,
14 |
15 | // Switches the quote type for strings in the annotations array to single
16 | // ones; e.g. '$scope' instead of "$scope" (false by default).
17 | singleQuotes: true
18 |
19 | // If ngAnnotate supports a new option that is not directly supported via
20 | // this grunt task yet, you can pass it here. These options gets merged
21 | // with the above specific to ngAnnotate. Options passed here have lower
22 | // precedence to the direct ones described above.
23 | // ngAnnotateOptions: {}
24 | },
25 | dist: {
26 | cwd: '.',
27 | src: 'src/angular-wait.js',
28 | dest: 'dist/angular-wait.js'
29 | }
30 | };
--------------------------------------------------------------------------------
/grunt/uglify.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | dist: {
3 | files: {
4 | 'dist/angular-wait.min.js': 'dist/angular-wait.js'
5 | }
6 | }
7 | };
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* License: MIT.
2 | * Copyright (C) 2013, 2014, Uri Shaked and contributors.
3 | */
4 |
5 | 'use strict';
6 |
7 | module.exports = function (config) {
8 | config.set({
9 | basePath: '',
10 | frameworks: ['jasmine'],
11 | logLevel: config.LOG_INFO,
12 | browsers: ['PhantomJS'],
13 | autoWatch: true,
14 | reporters: ['dots', 'coverage'],
15 | files: [
16 | 'bower_components/angular/angular.js',
17 | 'src/*.js',
18 | 'bower_components/angular-mocks/angular-mocks.js',
19 | 'test/mocks/*.js',
20 | 'test/*.js'
21 | ],
22 | preprocessors: {
23 | 'src/*.js': 'coverage'
24 | },
25 | coverageReporter: {
26 | type: 'lcov',
27 | dir: 'coverage/'
28 | }
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-wait",
3 | "version": "1.0.1",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/michikono/angular-wait.git"
7 | },
8 | "main": "dist/angular-wait.js",
9 | "dependencies": {
10 | "grunt-ng-annotate": "^0.3.2"
11 | },
12 | "devDependencies": {
13 | "grunt-cli": "*",
14 | "grunt-contrib-jshint": "~0.10.0",
15 | "grunt-contrib-uglify": "~0.2.7",
16 | "grunt-karma": "~0.8.0",
17 | "jshint-stylish": "~0.1.3",
18 | "karma": "~0.12.0",
19 | "karma-jasmine": "~0.2.2",
20 | "karma-phantomjs-launcher": "~0.1.1",
21 | "load-grunt-config": "^0.12.0",
22 | "load-grunt-tasks": "~0.4.0"
23 | },
24 | "engines": {
25 | "node": ">=0.10.0"
26 | },
27 | "scripts": {
28 | "test": "grunt test"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/angular-wait.js:
--------------------------------------------------------------------------------
1 | /**
2 | * angular-wait version 1.0.2
3 | * License: MIT.
4 | * Copyright (C) 2014 Michi Kono
5 | * https://github.com/michikono/angular-wait
6 | */
7 |
8 | 'use strict';
9 |
10 | (function (window) {
11 | var angular = window.angular;
12 | angular
13 | .module('michiKono', [])
14 | .controller('waitDirectiveCtrl', function WaitDirectiveCtrl($scope, $element) {
15 | $scope.show = false;
16 | var childScopes = [];
17 |
18 | var updateChildren = function () {
19 | for (var i = 0; i < childScopes.length; i++) {
20 | childScopes[i].show = $scope.show;
21 | }
22 | };
23 | this.registerChild = function (scope) {
24 | childScopes.push(scope);
25 | updateChildren();
26 | };
27 |
28 | $scope.$watch('untilNotNull', function (newValues, oldValues, scope) {
29 | if (typeof newValues !== 'undefined' && newValues !== null) {
30 | $scope.show = true;
31 | updateChildren();
32 | }
33 | });
34 | $scope.$watch('untilNotFalse', function (newValues, oldValues, scope) {
35 | if (typeof newValues !== 'undefined' && newValues !== false) {
36 | $scope.show = true;
37 | updateChildren();
38 | }
39 | });
40 | $scope.$watch('untilNotUndefined', function (newValues, oldValues, scope) {
41 | if (typeof newValues !== 'undefined') {
42 | $scope.show = true;
43 | updateChildren();
44 | }
45 | });
46 | })
47 | .directive('wait', function () {
48 | return {
49 | controller: 'waitDirectiveCtrl',
50 | restrict: 'AE',
51 | scope: {
52 | // these assume "when no longer undefined and..."
53 | untilNotNull: '=',
54 | untilNotFalse: '=',
55 | untilNotUndefined: '='
56 | },
57 | link: function postLink(scope, element, attrs) {
58 | }
59 | };
60 | })
61 | .directive('waitLoading', function () {
62 | return {
63 | restrict: 'EA',
64 | replace: true,
65 | template: '',
66 | transclude: true,
67 | require: '^wait',
68 | link: function link(scope, $element, attrs, controller, transclude) {
69 | scope.$watch('show', function (contents, old) {
70 | $element.html('');
71 | if (!contents) {
72 | transclude(scope, function (clone) {
73 | $element.html('');
74 | $element.append(clone);
75 | });
76 | }
77 | });
78 | // why we need something like this: http://stackoverflow.com/questions/16866749/access-parent-scope-in-transcluded-directive
79 | controller.registerChild(scope);
80 | }
81 | };
82 | })
83 | .directive('waitDone', function () {
84 | return {
85 | priority: 1,
86 | terminal: true,
87 | restrict: 'EA',
88 | replace: true,
89 | template: '',
90 | transclude: true,
91 | scope: false,
92 | require: '^wait',
93 | link: function link(scope, $element, attrs, controller, transclude) {
94 | scope.$watch('show', function (contents, old) {
95 | $element.html('');
96 | if (contents) {
97 | transclude(scope, function (clone) {
98 | $element.html('');
99 | $element.append(clone);
100 | });
101 | }
102 | });
103 | controller.registerChild(scope);
104 | }
105 | };
106 | });
107 | }(window));
108 |
--------------------------------------------------------------------------------
/test/angular-wait.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Directive: wait', function () {
4 | // load the directive's module
5 | beforeEach(module('michiKono'));
6 |
7 | var element,
8 | $scope,
9 | $rootScope,
10 | $compile,
11 | $templateCache;
12 |
13 | var doDirective = function (html, $compile) {
14 | element = angular.element(html);
15 | var compiled = $compile(element)($scope);
16 | compiled.scope().$digest();
17 | return element;
18 | };
19 |
20 | beforeEach(inject(function (_$compile_, _$rootScope_, _$templateCache_) {
21 | $rootScope = _$rootScope_;
22 | $compile = _$compile_;
23 | $templateCache = _$templateCache_;
24 | $scope = $rootScope.$new();
25 | }));
26 |
27 | describe('attribute until-not-false', function () {
28 | it('should show "noiseLOADING" while initially waiting and hide wait-done contents', inject(function ($compile) {
29 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
30 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
31 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
32 | }));
33 |
34 | it('should show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
35 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
36 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
37 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
38 | }));
39 |
40 | it('should show nothing if no done node provided', inject(function ($compile) {
41 | element = doDirective('noiseLOADING', $compile);
42 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
43 | }));
44 |
45 | it('should respect changes to $scope and show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
46 | $scope.testVal = false;
47 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
48 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
49 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
50 |
51 | $scope.testVal = null; // using a falsey value to ensure === tests are being used
52 | $scope.$digest();
53 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
54 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
55 | }));
56 | });
57 |
58 | describe('attribute until-not-null', function () {
59 | it('should show "noiseLOADING" while initially waiting and hide wait-done contents', inject(function ($compile) {
60 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
61 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
62 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
63 | }));
64 |
65 | it('should show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
66 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
67 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
68 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
69 | }));
70 |
71 | it('should show nothing if no done node provided', inject(function ($compile) {
72 | element = doDirective('noise<\/span>LOADING<\/span>', $compile);
73 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
74 | }));
75 |
76 | it('should respect changes to $scope and show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
77 | $scope.testVal = null;
78 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
79 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
80 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
81 |
82 | $scope.testVal = false; // using a falsey value to ensure === tests are being used
83 | $scope.$digest();
84 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
85 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
86 | }));
87 | });
88 |
89 | describe('attribute until-not-undefined', function () {
90 | it('should show "noiseLOADING" while initially waiting and hide wait-done contents', inject(function ($compile) {
91 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
92 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
93 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
94 | }));
95 |
96 | it('should show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
97 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
98 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
99 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
100 | }));
101 |
102 | it('should show nothing if no done node provided', inject(function ($compile) {
103 | element = doDirective('noiseLOADING', $compile);
104 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
105 | }));
106 |
107 | it('should respect changes to $scope and show wait-done contents when until conditional met AND hide wait-loading', inject(function ($compile) {
108 | element = doDirective('noiseLOADINGnoiseDONE', $compile);
109 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>LOADING<\/span>);
110 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>DONE<\/span>);
111 |
112 | $scope.testVal = false; // using a falsey value to ensure === tests are being used
113 | $scope.$digest();
114 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/>noise<\/span>LOADING<\/span>);
115 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/>noise<\/span>DONE<\/span>);
116 | }));
117 | });
118 |
119 | describe('render order', function() {
120 | it('should not process inner scope changes or directives until done condition met', inject(function ($compile) {
121 | $scope.doNotRender = null;
122 | element = doDirective('', $compile);
123 | $scope.$digest();
124 | expect($scope.doNotRender).toBeNull();
125 | }));
126 |
127 | it('should process inner scope changes when done condition met', inject(function ($compile) {
128 | $scope.doNotRender = null;
129 | element = doDirective('', $compile);
130 | $scope.$digest();
131 | expect($scope.doNotRender).toEqual('value');
132 | }));
133 |
134 | it('should run controller and compile methods of inner directives when done condition met', inject(function ($compile) {
135 | $scope.exampleDirective = {};
136 | element = doDirective('', $compile);
137 | $scope.$digest();
138 | expect($scope.exampleDirective.controller).toBe(true);
139 | expect($scope.exampleDirective.pre).toBe(true);
140 | expect($scope.exampleDirective.post).toBe(true);
141 | expect(element.html().replace(/ class="ng-scope"/g, '')).toMatch(/<\/example-stub>/);
142 | }));
143 |
144 | it('should not run controller and compile methods of inner directives until done condition met', inject(function ($compile) {
145 | $scope.exampleDirective = {};
146 | element = doDirective('', $compile);
147 | $scope.$digest();
148 | expect($scope.exampleDirective.compile).not.toBeDefined();
149 | expect($scope.exampleDirective.controller).not.toBeDefined();
150 | expect($scope.exampleDirective.pre).not.toBeDefined();
151 | expect($scope.exampleDirective.post).not.toBeDefined();
152 | expect(element.html().replace(/ class="ng-scope"/g, '')).not.toMatch(/<\/example-directive>/);
153 | }));
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/test/mocks/example-directive.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | (function (window) {
4 | var angular = window.angular;
5 | angular
6 | .module('michiKono')
7 | .directive('exampleDirective', function () {
8 | return {
9 | restrict: 'E',
10 | replace: true,
11 | template: '',
12 | controller: function ($scope) {
13 | $scope.exampleDirective.controller = true;
14 | },
15 | compile: function () {
16 | return {
17 | pre: function ($scope) {
18 | $scope.exampleDirective.pre = true;
19 | },
20 | post: function ($scope) {
21 | $scope.exampleDirective.post = true;
22 | }
23 | };
24 | }
25 | };
26 | });
27 | }(window));
28 |
--------------------------------------------------------------------------------