├── .gitignore ├── .travis.yml ├── Gulpfile.js ├── LICENSE ├── README.md ├── angular-fng.js ├── angular-fng.min.js ├── bower.json ├── karma.conf.js ├── package.json └── test ├── angular-fng.spec.js ├── demo.html └── test-main.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamCraven/angular-fng/52d28d89cdcc4054514bb1b66d96cf2dcbec0702/.travis.yml -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var karma = require('karma').server; 2 | var gulp = require('gulp'); 3 | var uglify = require('gulp-uglify'); 4 | var rename = require('gulp-rename'); 5 | 6 | gulp.task('default', function() { 7 | karma.start({ 8 | configFile: __dirname + '/karma.conf.js', 9 | action: 'watch' 10 | }); 11 | }); 12 | 13 | gulp.task('build', function() { 14 | return gulp.src('./angular-fng.js') 15 | .pipe(rename('./angular-fng.min.js')) 16 | .pipe(uglify()) 17 | .pipe(gulp.dest('.')) 18 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2015 Google, Inc. http://angularjs.org 4 | Copyright (c) 2015 Adam Craven 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # angular-fng (**F**aster a**NG**ular) 3 | 4 | Performance focused event directives, that act the same as the ng-event directives (ng-click, ng-mousedown, etc.). But instead of triggering a global root scope digest, it can trigger a partial scope digest, increasing performance and responsiveness. 5 | 6 | Example: Simulated large app (Greater than 1000 watchers) 7 | 8 | ng-event 9 | fng-event 10 | 11 | LEFT: Using ng-event. RIGHT: Using fng-event, not refreshing all the watchers in an app. 12 | 13 | New directives defined, which can be used as a replacement or in addition to the default directives: 14 | 15 | * fng-click 16 | * fng-dblclick 17 | * fng-mousedown 18 | * fng-mouseup 19 | * fng-mouseover 20 | * fng-mouseout 21 | * fng-mousemove 22 | * fng-mouseenter 23 | * fng-mouseleave 24 | * fng-keydown 25 | * fng-keyup 26 | * fng-keypress 27 | * fng-submit 28 | * fng-focus 29 | * fng-blur 30 | * fng-copy 31 | * fng-cut 32 | * fng-paste 33 | 34 | 35 | ## Why 36 | 37 | Not sure what's it all about? Have a read of: [angular-fng - Improve the performance of large angular 1.x apps, by using faster event directives](https://code.adamcrvn.com/increasing-performance-on-large-angular-apps/) 38 | 39 | ## Requirements 40 | 41 | * Angular 1.2.0 or greater - May work on older versions. 42 | 43 | ## Installation 44 | 45 | * bower: `bower install angular-fng --save` 46 | * npm: `npm install angular-fng --save` 47 | * Or download from github: [angular-fng.zip](https://github.com/AdamCraven/angular-fng/archive/master.zip) 48 | 49 | Include angular-fng after angular.js has been loaded. 50 | 51 | ```html 52 | 53 | ``` 54 | 55 | Or can be required in via require.js or other module loaders which support CommonJS or AMD module definition, just be sure that angular is loaded first 56 | 57 | ## Usage 58 | 59 | To enable add fng to your main module: 60 | 61 | ```js 62 | angular.module('myApp', ['fng']) 63 | ``` 64 | 65 | Enable partial digesting by setting $stopDigestPropagation on your chosen scope: 66 | 67 | ```js 68 | $chosenScope.$stopDigestPropagation = true 69 | ``` 70 | 71 | Then replace all uses of the ng-event directives with fng: 72 | ```html 73 | Click Me 74 | ``` 75 | 76 | When clicked, the digest will occur from the $chosenScope. 77 | 78 | ### How it works 79 | 80 | The fng events are opt-in directives, which behave *the same* as an ng event directive. However, it differs in one important way. When triggered (e.g. fng-click) it bubbles up the scope tree and searches for a defined $stopDigestPropagation property. 81 | 82 | When found it will call a $digest in the scope where $stopDigestPropagation is set and checks all the child scopes as shown below: 83 | 84 |
85 | Scope tree local 86 | Scope tree 87 |
88 | 89 |
90 | 91 | If $stopDigestPropagation property isn't found, it will fallback to the default behaviour and act **the same** as the ng-event directives, calling a root scope digest: 92 | 93 |
94 | Scope tree local 95 | Scope tree 96 |
97 | 98 | Because they work the same as the existing ng-event directives, they can be dropped in and used as a replacement. 99 | That means all ng-keydowns can be converted to fng-keydowns, and so forth. 100 | 101 | 102 | ### How to chose where to digest 103 | 104 | It is not recommended that these are used at low levels, such as in individual components. The live search component, mentioned in [angular-fng - Improve the performance of large angular 1.x apps, by using faster event directives](https://code.adamcrvn.com/increasing-performance-on-large-angular-apps/), would not implement $stopDigestPropagation property. It should be implemented at the module level, or higher. Such as a group of modules that relate to a major aspect of functionality on a page. 105 | -------------------------------------------------------------------------------- /angular-fng.js: -------------------------------------------------------------------------------- 1 | // Angular fng / MIT License 2 | (function() { 3 | 'use strict'; 4 | 5 | // Refer to https://github.com/angular/angular.js/blob/master/src/ng/directive/ngEventDirs.js 6 | // and angular code base for much of the originating source code. 7 | // There are many private methods in angular, duplication has been necessary. 8 | 9 | var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; 10 | var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; 11 | 12 | /** 13 | * Converts all accepted directives format into proper directive name. 14 | * @param name Name to normalize 15 | */ 16 | function directiveNormalize(name) { 17 | return camelCase(name.replace(PREFIX_REGEXP, '')); 18 | } 19 | 20 | /** 21 | * Converts snake_case to camelCase. 22 | * Also there is special case for Moz prefix starting with upper case letter. 23 | * @param name Name to normalize 24 | */ 25 | function camelCase(name) { 26 | return name. 27 | replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { 28 | return offset ? letter.toUpperCase() : letter; 29 | }); 30 | } 31 | 32 | /** 33 | * Bubbles up the scope tree recursively to check if stopDigestPropagation is set on scope 34 | * returns that scope if defined otherwise undefined 35 | * @param {object} scope Where scope originated 36 | * @return {object} Scope to partial digest in or undefined 37 | */ 38 | function findPartialScope(scope) { 39 | if (scope.hasOwnProperty('$stopDigestPropagation')) { 40 | return scope; 41 | } else if (scope.$parent) { 42 | return findPartialScope(scope.$parent); 43 | } 44 | return; 45 | } 46 | 47 | function partialDigest(scope, callback) { 48 | callback(); 49 | scope.$digest(); 50 | } 51 | 52 | function fullDigest (scope, callback) { 53 | scope.$apply(callback); 54 | } 55 | 56 | // For events that might fire synchronously during DOM manipulation 57 | // we need to execute their event handlers asynchronously using $evalAsync, 58 | // so that they are not executed in an inconsistent state. 59 | var forceAsyncEvents = { 60 | 'blur': true, 61 | 'focus': true 62 | }; 63 | var events = 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '); 64 | 65 | function fng(angular) { 66 | var fngModule = angular.module('fng', []); 67 | 68 | function assignDirectives(eventName) { 69 | var directiveName = directiveNormalize('fng-' + eventName); 70 | fngModule.directive(directiveName, ['$parse', '$rootScope', function($parse, $rootScope) { 71 | return { 72 | restrict: 'A', 73 | compile: function($element, attr) { 74 | // We expose the powerful $event object on the scope that provides access to the Window, 75 | // etc. that isn't protected by the fast paths in $parse. We explicitly request better 76 | // checks at the cost of speed since event handler expressions are not executed as 77 | // frequently as regular change detection. 78 | var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); 79 | return function ngEventHandler(scope, element) { 80 | element.on(eventName, function fngEventHandler(event) { 81 | var partialDigestScope; 82 | var callback = function() { 83 | fn(scope, { 84 | $event: event 85 | }); 86 | }; 87 | 88 | if (forceAsyncEvents[eventName] && $rootScope.$$phase) { 89 | scope.$evalAsync(callback); 90 | } else if ((partialDigestScope = findPartialScope(scope))) { 91 | partialDigest(partialDigestScope, callback); 92 | } else { 93 | fullDigest(scope, callback); 94 | } 95 | }); 96 | }; 97 | } 98 | }; 99 | }]); 100 | } 101 | events.forEach(assignDirectives); 102 | } 103 | 104 | if (typeof define === 'function' && define.amd) { 105 | define(['angular'], fng); 106 | } else if (typeof module !== 'undefined' && module && module.exports) { 107 | fng(angular); 108 | module.exports = 'fng'; 109 | } else { 110 | fng(angular); 111 | } 112 | 113 | })(); -------------------------------------------------------------------------------- /angular-fng.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function e(e){return n(e.replace(i,""))}function n(e){return e.replace(c,function(e,n,o,u){return u?o.toUpperCase():o})}function o(e){return e.hasOwnProperty("$stopDigestPropagation")?e:e.$parent?o(e.$parent):void 0}function u(e,n){n(),e.$digest()}function t(e,n){e.$apply(n)}function r(n){function r(n){var r=e("fng-"+n);i.directive(r,["$parse","$rootScope",function(e,i){return{restrict:"A",compile:function(c,f){var s=e(f[r],null,!0);return function(e,r){r.on(n,function(r){var c,f=function(){s(e,{$event:r})};a[n]&&i.$$phase?e.$evalAsync(f):(c=o(e))?u(c,f):t(e,f)})}}}}])}var i=n.module("fng",[]);f.forEach(r)}var i=/^((?:x|data)[\:\-_])/i,c=/([\:\-\_]+(.))/g,a={blur:!0,focus:!0},f="click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" ");"function"==typeof define&&define.amd?define(["angular"],r):"undefined"!=typeof module&&module&&module.exports?(r(angular),module.exports="fng"):r(angular)}(); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-fng", 3 | "description": "Performance focused event directives, that act the same as the ng-event directives (ng-click, ng-mousedown, etc.). But instead of triggering a global root scope digest, it can trigger a partial scope digest, increasing performance and responsiveness.", 4 | "author": "Adam Craven", 5 | "license": "MIT", 6 | "homepage": "https://github.com/AdamCraven/angular-fng", 7 | "main": "./angular-fng.js", 8 | "ignore": [ 9 | ], 10 | "dependencies": { 11 | "angular": ">=1.2.0 <2.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/AdamCraven/angular-fng.git" 16 | } 17 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jun 10 2015 18:47:59 GMT+0100 (BST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine', 'requirejs'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | {pattern: 'node_modules/angular/angular.js', included: false}, 19 | {pattern: 'node_modules/angular-mocks/angular-mocks.js', included: false}, 20 | {pattern: 'node_modules/jquery/dist/jquery.min.js', included: false}, 21 | {pattern: 'test/*.spec.js', included: false}, 22 | {pattern: 'angular-fng.js', included: false}, 23 | 'test/test-main.js', 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | }, 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ['progress'], 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | 52 | // level of logging 53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: true, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['Chrome'], 64 | 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the tests and exits 68 | singleRun: false 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-fng", 3 | "version": "1.0.4", 4 | "description": "Performance focused event directives, that act the same as the ng-event directives (ng-click, ng-mousedown, etc.). But instead of triggering a global root scope digest, it can trigger a partial scope digest, increasing performance and responsiveness.", 5 | "main": "angular-fng.js", 6 | "scripts": { 7 | "test": "gulp" 8 | }, 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/AdamCraven/angular-fng" 13 | }, 14 | "devDependencies": { 15 | "angular": "1.4.0", 16 | "angular-mocks": "1.4.0", 17 | "gulp": "^3.9.0", 18 | "gulp-karma": "0.0.4", 19 | "gulp-rename": "^1.2.2", 20 | "gulp-uglify": "^1.2.0", 21 | "gulp-umd": "^0.2.0", 22 | "jquery": "^2.1.4", 23 | "karma": "^0.12.36", 24 | "karma-chrome-launcher": "^0.1.12", 25 | "karma-jasmine": "0.1.5", 26 | "karma-requirejs": "^0.2.2", 27 | "requirejs": "^2.1.18" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/angular-fng.spec.js: -------------------------------------------------------------------------------- 1 | define(['angular', 'angular-mocks', '../angular-fng.js'], function(angular, angularMocks, angularFng) { 2 | 'use strict'; 3 | 4 | beforeEach(function() { 5 | module('fng'); 6 | }); 7 | 8 | describe('standard angular tests for event directives', function() { 9 | var element; 10 | 11 | describe('fngSubmit', function() { 12 | 13 | it('should get called on form submit', inject(function($rootScope, $compile) { 14 | element = $compile('
' + 15 | '' + 16 | '
')($rootScope); 17 | $rootScope.$digest(); 18 | 19 | // prevent submit within the test harness 20 | element.on('submit', function(e) { 21 | e.preventDefault(); 22 | }); 23 | 24 | expect($rootScope.submitted).not.toBeDefined(); 25 | 26 | element.children().eq(0).submit(); 27 | expect($rootScope.submitted).toEqual(true); 28 | })); 29 | 30 | it('should expose event on form submit', inject(function($rootScope, $compile) { 31 | $rootScope.formSubmission = function(e) { 32 | if (e) { 33 | $rootScope.formSubmitted = 'foo'; 34 | } 35 | }; 36 | 37 | element = $compile('
' + 38 | '' + 39 | '
')($rootScope); 40 | $rootScope.$digest(); 41 | 42 | // prevent submit within the test harness 43 | element.on('submit', function(e) { 44 | e.preventDefault(); 45 | }); 46 | 47 | expect($rootScope.formSubmitted).not.toBeDefined(); 48 | 49 | element.children().eq(0).submit(); 50 | expect($rootScope.formSubmitted).toEqual('foo'); 51 | })); 52 | }); 53 | 54 | describe('focus', function() { 55 | 56 | describe('call the listener asynchronously during $apply', function() { 57 | function run(scope) { 58 | inject(function($compile) { 59 | element = $compile('')(scope); 60 | scope.focus = jasmine.createSpy('focus'); 61 | 62 | scope.$apply(function() { 63 | element.triggerHandler('focus'); 64 | expect(scope.focus).not.toHaveBeenCalled(); 65 | }); 66 | 67 | expect(scope.focus).toHaveBeenCalled(); 68 | }); 69 | } 70 | 71 | it('should call the listener with non isolate scopes', inject(function($rootScope) { 72 | run($rootScope.$new()); 73 | })); 74 | 75 | it('should call the listener with isolate scopes', inject(function($rootScope) { 76 | run($rootScope.$new(true)); 77 | })); 78 | 79 | }); 80 | 81 | it('should call the listener synchronously inside of $apply if outside of $apply', 82 | inject(function($rootScope, $compile) { 83 | element = $compile('')($rootScope); 84 | $rootScope.focus = jasmine.createSpy('focus').andCallFake(function() { 85 | $rootScope.value = 'newValue'; 86 | }); 87 | 88 | element.triggerHandler('focus'); 89 | 90 | expect($rootScope.focus).toHaveBeenCalled(); 91 | expect(element.val()).toBe('newValue'); 92 | })); 93 | 94 | }); 95 | 96 | describe('security', function() { 97 | it('should allow access to the $event object', inject(function($rootScope, $compile) { 98 | var scope = $rootScope.$new(); 99 | element = $compile('')(scope); 100 | element.triggerHandler('click'); 101 | expect(scope.e.target).toBe(element[0]); 102 | })); 103 | 104 | it('should block access to DOM nodes (e.g. exposed via $event)', inject(function($rootScope, $compile) { 105 | var scope = $rootScope.$new(); 106 | element = $compile('')(scope); 107 | expect(function() { 108 | element.triggerHandler('click'); 109 | }).toThrow(); 110 | })); 111 | }); 112 | 113 | describe('blur', function() { 114 | 115 | describe('call the listener asynchronously during $apply', function() { 116 | function run(scope) { 117 | inject(function($compile) { 118 | element = $compile('')(scope); 119 | scope.blur = jasmine.createSpy('blur'); 120 | 121 | scope.$apply(function() { 122 | element.triggerHandler('blur'); 123 | expect(scope.blur).not.toHaveBeenCalled(); 124 | }); 125 | 126 | expect(scope.blur).toHaveBeenCalled(); 127 | }); 128 | } 129 | 130 | it('should call the listener with non isolate scopes', inject(function($rootScope) { 131 | run($rootScope.$new()); 132 | })); 133 | 134 | it('should call the listener with isolate scopes', inject(function($rootScope) { 135 | run($rootScope.$new(true)); 136 | })); 137 | 138 | }); 139 | 140 | it('should call the listener synchronously inside of $apply if outside of $apply', 141 | inject(function($rootScope, $compile) { 142 | element = $compile('')($rootScope); 143 | $rootScope.blur = jasmine.createSpy('blur').andCallFake(function() { 144 | $rootScope.value = 'newValue'; 145 | }); 146 | 147 | element.triggerHandler('blur'); 148 | 149 | expect($rootScope.blur).toHaveBeenCalled(); 150 | expect(element.val()).toBe('newValue'); 151 | })); 152 | 153 | }); 154 | }); 155 | 156 | describe('fng scope propagation tests', function() { 157 | var element; 158 | 159 | describe('click', function() { 160 | 161 | var root, child, grandChild, greatGrandChild; 162 | var childCalled, rootCalled, grandChildCalled, greatGrandChildCalled; 163 | var rootScopeApplySpy; 164 | 165 | beforeEach(inject(function($rootScope) { 166 | root = $rootScope; 167 | child = $rootScope.$new(); 168 | grandChild = child.$new(); 169 | greatGrandChild = grandChild.$new(); 170 | 171 | grandChildCalled = jasmine.createSpy('grandChildCalled'); 172 | greatGrandChildCalled = jasmine.createSpy('greatGrandChildCalled'); 173 | childCalled = jasmine.createSpy('childCalled'); 174 | rootCalled = jasmine.createSpy('rootCalled'); 175 | 176 | rootScopeApplySpy = spyOn($rootScope, '$apply').andCallThrough(); 177 | 178 | root.$watch(rootCalled); 179 | child.$watch(childCalled); 180 | grandChild.$watch(grandChildCalled); 181 | greatGrandChild.$watch(greatGrandChildCalled); 182 | })); 183 | 184 | it('should act regularly on a click', inject(function($compile) { 185 | element = $compile('')(child); 186 | element.click(); 187 | 188 | expect(rootCalled).toHaveBeenCalled(); 189 | expect(childCalled).toHaveBeenCalled(); 190 | expect(grandChildCalled).toHaveBeenCalled(); 191 | expect(greatGrandChildCalled).toHaveBeenCalled(); 192 | expect(rootScopeApplySpy).toHaveBeenCalled(); 193 | })); 194 | 195 | it('when $stopDigestPropagation is set to child, should call there and children', inject(function($compile) { 196 | element = $compile('')(grandChild); 197 | child.$stopDigestPropagation = true; 198 | element.triggerHandler('click'); 199 | 200 | expect(rootCalled).not.toHaveBeenCalled(); 201 | expect(childCalled).toHaveBeenCalled(); 202 | expect(grandChildCalled).toHaveBeenCalled(); 203 | expect(greatGrandChildCalled).toHaveBeenCalled(); 204 | expect(rootScopeApplySpy).not.toHaveBeenCalled(); 205 | })); 206 | it('when $stopDigestPropagation is set to grandChild, should call there and children', inject(function($compile) { 207 | element = $compile('')(grandChild); 208 | grandChild.$stopDigestPropagation = true; 209 | element.triggerHandler('click'); 210 | 211 | expect(rootCalled).not.toHaveBeenCalled(); 212 | expect(childCalled).not.toHaveBeenCalled(); 213 | expect(grandChildCalled).toHaveBeenCalled(); 214 | expect(greatGrandChildCalled).toHaveBeenCalled(); 215 | expect(rootScopeApplySpy).not.toHaveBeenCalled(); 216 | })); 217 | 218 | it('when $stopDigestPropagation is set to greatGrandChild, should call there', inject(function($compile) { 219 | element = $compile('')(greatGrandChild); 220 | greatGrandChild.$stopDigestPropagation = true; 221 | element.triggerHandler('click'); 222 | 223 | expect(rootCalled).not.toHaveBeenCalled(); 224 | expect(childCalled).not.toHaveBeenCalled(); 225 | expect(grandChildCalled).not.toHaveBeenCalled(); 226 | expect(greatGrandChildCalled).toHaveBeenCalled(); 227 | expect(rootScopeApplySpy).not.toHaveBeenCalled(); 228 | })); 229 | 230 | describe('call the listener asynchronously during $apply, fallback to standard digest', function() { 231 | function run(scope) { 232 | inject(function($compile) { 233 | element = $compile('')(scope); 234 | scope.$stopDigestPropagation = true; 235 | scope.blur = jasmine.createSpy('blur'); 236 | 237 | scope.$apply(function() { 238 | element.triggerHandler('blur'); 239 | expect(scope.blur).not.toHaveBeenCalled(); 240 | }); 241 | 242 | expect(scope.blur).toHaveBeenCalled(); 243 | }); 244 | } 245 | 246 | it('should call the listener with non isolate scopes', inject(function($rootScope) { 247 | run($rootScope.$new()); 248 | })); 249 | 250 | it('should call the listener with isolate scopes', inject(function($rootScope) { 251 | run($rootScope.$new(true)); 252 | })); 253 | 254 | }); 255 | 256 | 257 | }); 258 | }); 259 | }); -------------------------------------------------------------------------------- /test/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | angular fng demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 69 | 70 | -------------------------------------------------------------------------------- /test/test-main.js: -------------------------------------------------------------------------------- 1 | var tests = []; 2 | for (var file in window.__karma__.files) { 3 | if (window.__karma__.files.hasOwnProperty(file)) { 4 | if (/spec\.js$/.test(file)) { 5 | tests.push(file); 6 | } 7 | } 8 | } 9 | 10 | requirejs.config({ 11 | // Karma serves files from '/base' 12 | baseUrl: '/base/', 13 | 14 | paths: { 15 | 'angular': 'node_modules/angular/angular', 16 | 'angular-mocks': 'node_modules/angular-mocks/angular-mocks', 17 | 'jquery': 'node_modules/jquery/dist/jquery.min', 18 | }, 19 | 20 | shim: { 21 | 'angular': { 22 | deps: ['jquery'], 23 | exports: 'angular' 24 | }, 25 | 'angular-mocks': { 26 | deps: ['angular'] 27 | } 28 | }, 29 | 30 | // ask Require.js to load these files (all our tests) 31 | deps: tests, 32 | 33 | // start test run, once Require.js is done 34 | callback: window.__karma__.start 35 | }); --------------------------------------------------------------------------------