diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 41 | ||||
| -rw-r--r-- | src/Browser.js | 2 | ||||
| -rw-r--r-- | src/Compiler.js | 35 | ||||
| -rw-r--r-- | src/JSON.js | 3 | ||||
| -rw-r--r-- | src/Scope.js | 1269 | ||||
| -rw-r--r-- | src/angular-mocks.js | 2 | ||||
| -rw-r--r-- | src/apis.js | 24 | ||||
| -rw-r--r-- | src/directives.js | 158 | ||||
| -rw-r--r-- | src/filters.js | 13 | ||||
| -rw-r--r-- | src/parser.js | 111 | ||||
| -rw-r--r-- | src/scenario/Runner.js | 15 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 1 | ||||
| -rw-r--r-- | src/service/cookies.js | 4 | ||||
| -rw-r--r-- | src/service/defer.js | 11 | ||||
| -rw-r--r-- | src/service/invalidWidgets.js | 4 | ||||
| -rw-r--r-- | src/service/location.js | 47 | ||||
| -rw-r--r-- | src/service/route.js | 51 | ||||
| -rw-r--r-- | src/service/updateView.js | 6 | ||||
| -rw-r--r-- | src/service/xhr.bulk.js | 2 | ||||
| -rw-r--r-- | src/widgets.js | 278 |
20 files changed, 1230 insertions, 847 deletions
diff --git a/src/Angular.js b/src/Angular.js index c26b799a..63182ecd 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -56,7 +56,6 @@ function fromCharCode(code) { return String.fromCharCode(code); } var _undefined = undefined, _null = null, - $$element = '$element', $$scope = '$scope', $$validate = '$validate', $angular = 'angular', @@ -65,7 +64,6 @@ var _undefined = undefined, $console = 'console', $date = 'date', $display = 'display', - $element = 'element', $function = 'function', $length = 'length', $name = 'name', @@ -574,6 +572,16 @@ function isLeafNode (node) { } /** + * @workInProgress + * @ngdoc function + * @name angular.copy + * @function + * + * @description + * Alias for {@link angular.Object.copy} + */ + +/** * @ngdoc function * @name angular.Object.copy * @function @@ -657,6 +665,15 @@ function copy(source, destination){ return destination; } +/** + * @workInProgress + * @ngdoc function + * @name angular.equals + * @function + * + * @description + * Alias for {@link angular.Object.equals} + */ /** * @ngdoc function @@ -666,8 +683,8 @@ function copy(source, destination){ * @description * Determines if two objects or value are equivalent. * - * To be equivalent, they must pass `==` comparison or be of the same type and have all their - * properties pass `==` comparison. During property comparision properties of `function` type and + * To be equivalent, they must pass `===` comparison or be of the same type and have all their + * properties pass `===` comparison. During property comparision properties of `function` type and * properties with name starting with `$` are ignored. * * Supports values types, arrays and objects. @@ -707,7 +724,7 @@ function copy(source, destination){ * </doc:example> */ function equals(o1, o2) { - if (o1 == o2) return true; + if (o1 === o2) return true; if (o1 === null || o2 === null) return false; var t1 = typeof o1, t2 = typeof o2, length, key, keySet; if (t1 == t2 && t1 == 'object') { @@ -779,6 +796,10 @@ function concat(array1, array2, index) { return array1.concat(slice.call(array2, index, array2.length)); } +function sliceArgs(args, startIndex) { + return slice.call(args, startIndex || 0, args.length); +} + /** * @workInProgress @@ -797,9 +818,7 @@ function concat(array1, array2, index) { * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ function bind(self, fn) { - var curryArgs = arguments.length > 2 - ? slice.call(arguments, 2, arguments.length) - : []; + var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (typeof fn == $function && !(fn instanceof RegExp)) { return curryArgs.length ? function() { @@ -939,13 +958,14 @@ function angularInit(config, document){ if (autobind) { var element = isString(autobind) ? document.getElementById(autobind) : document, - scope = compile(element)(createScope({'$config':config})), + scope = compile(element)(createScope()), $browser = scope.$service('$browser'); if (config.css) $browser.addCss(config.base_url + config.css); else if(msie<8) $browser.addJs(config.ie_compat, config.ie_compat_id); + scope.$apply(); } } @@ -1001,7 +1021,8 @@ function assertArg(arg, name, reason) { } function assertArgFn(arg, name) { - assertArg(isFunction(arg, name, 'not a function')); + assertArg(isFunction(arg), name, 'not a function, got ' + + (typeof arg == 'object' ? arg.constructor.name : typeof arg)); } diff --git a/src/Browser.js b/src/Browser.js index 815b6b24..562b137d 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -60,7 +60,7 @@ function Browser(window, document, body, XHR, $log) { */ function completeOutstandingRequest(fn) { try { - fn.apply(null, slice.call(arguments, 1)); + fn.apply(null, sliceArgs(arguments, 1)); } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { diff --git a/src/Compiler.js b/src/Compiler.js index 730d175e..8512f0c3 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -29,15 +29,20 @@ Template.prototype = { inits[this.priority] = queue = []; } if (this.newScope) { - childScope = createScope(scope); - scope.$onEval(childScope.$eval); + childScope = isFunction(this.newScope) ? scope.$new(this.newScope(scope)) : scope.$new(); element.data($$scope, childScope); } + // TODO(misko): refactor this!!! + // Why are inits even here? forEach(this.inits, function(fn) { queue.push(function() { - childScope.$tryEval(function(){ - return childScope.$service.invoke(childScope, fn, [element]); - }, element); + childScope.$eval(function(){ + try { + return childScope.$service.invoke(childScope, fn, [element]); + } catch (e) { + childScope.$service('$exceptionHandler')(e); + } + }); }); }); var i, @@ -218,7 +223,6 @@ Compiler.prototype = { scope.$element = element; (cloneConnectFn||noop)(element, scope); template.attach(element, scope); - scope.$eval(); return scope; }; }, @@ -228,6 +232,7 @@ Compiler.prototype = { * @workInProgress * @ngdoc directive * @name angular.directive.ng:eval-order + * @deprecated * * @description * Normally the view is updated from top to bottom. This usually is @@ -244,9 +249,9 @@ Compiler.prototype = { * @example <doc:example> <doc:source> - <div>TOTAL: without ng:eval-order {{ items.$sum('total') | currency }}</div> - <div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}</div> - <table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}]"> + <div>TOTAL: without ng:eval-order {{ total | currency }}</div> + <div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ total | currency }}</div> + <table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}];total=0;"> <tr> <td>QTY</td> <td>Description</td> @@ -258,22 +263,22 @@ Compiler.prototype = { <td><input name="item.qty"/></td> <td><input name="item.desc"/></td> <td><input name="item.cost"/></td> - <td>{{item.total = item.qty * item.cost | currency}}</td> + <td>{{item.qty * item.cost | currency}}</td> <td><a href="" ng:click="items.$remove(item)">X</a></td> </tr> <tr> <td colspan="3"><a href="" ng:click="items.$add()">add</a></td> - <td>{{ items.$sum('total') | currency }}</td> + <td>{{ total = items.$sum('qty*cost') | currency }}</td> </tr> </table> </doc:source> <doc:scenario> it('should check ng:format', function(){ - expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); - expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$9.99'); + expect(using('.doc-example-live div:first').binding("total")).toBe('$0.00'); + expect(using('.doc-example-live div:last').binding("total")).toBe('$9.99'); input('item.qty').enter('2'); - expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99'); - expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$19.98'); + expect(using('.doc-example-live div:first').binding("total")).toBe('$9.99'); + expect(using('.doc-example-live div:last').binding("total")).toBe('$19.98'); }); </doc:scenario> </doc:example> diff --git a/src/JSON.js b/src/JSON.js index 0a826e0e..b0f72a1b 100644 --- a/src/JSON.js +++ b/src/JSON.js @@ -116,6 +116,9 @@ function toJsonArray(buf, obj, pretty, stack) { sep = true; } buf.push("]"); + } else if (isElement(obj)) { + // TODO(misko): maybe in dev mode have a better error reporting? + buf.push('DOM_ELEMENT'); } else if (isDate(obj)) { buf.push(angular.String.quoteUnicode(angular.Date.toString(obj))); } else { diff --git a/src/Scope.js b/src/Scope.js index b9fab638..572e9760 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -1,537 +1,788 @@ 'use strict'; -function getter(instance, path, unboundFn) { - if (!path) return instance; - var element = path.split('.'); - var key; - var lastInstance = instance; - var len = element.length; - for ( var i = 0; i < len; i++) { - key = element[i]; - if (!key.match(/^[\$\w][\$\w\d]*$/)) - throw "Expression '" + path + "' is not a valid expression for accessing variables."; - if (instance) { - lastInstance = instance; - instance = instance[key]; - } - if (isUndefined(instance) && key.charAt(0) == '$') { - var type = angular['Global']['typeOf'](lastInstance); - type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; - var fn = type ? type[[key.substring(1)]] : undefined; - if (fn) { - instance = bind(lastInstance, fn, lastInstance); - return instance; - } - } - } - if (!unboundFn && isFunction(instance)) { - return bind(lastInstance, instance); - } - return instance; -} - -function setter(instance, path, value){ - var element = path.split('.'); - for ( var i = 0; element.length > 1; i++) { - var key = element.shift(); - var newInstance = instance[key]; - if (!newInstance) { - newInstance = {}; - instance[key] = newInstance; - } - instance = newInstance; - } - instance[element.shift()] = value; - return value; -} - -/////////////////////////////////// -var scopeId = 0, - getterFnCache = {}, - compileCache = {}, - JS_KEYWORDS = {}; -forEach( - ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + - "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + - "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," + - "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + - "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), - function(key){ JS_KEYWORDS[key] = true;} -); -function getterFn(path){ - var fn = getterFnCache[path]; - if (fn) return fn; - - var code = 'var l, fn, t;\n'; - forEach(path.split('.'), function(key) { - key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; - code += 'if(!s) return s;\n' + - 'l=s;\n' + - 's=s' + key + ';\n' + - 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l'+key+'.apply(l, arguments); };\n'; - if (key.charAt(1) == '$') { - // special code for super-imposed functions - var name = key.substr(2); - code += 'if(!s) {\n' + - ' t = angular.Global.typeOf(l);\n' + - ' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' + - ' if (fn) s = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n' + - '}\n'; - } - }); - code += 'return s;'; - fn = Function('s', code); - fn["toString"] = function(){ return code; }; - - return getterFnCache[path] = fn; -} +/** + * DESIGN NOTES + * + * The design decisions behind the scope ware heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive from speed as well as memory: + * - no closures, instead ups prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * exposed as $$____ properties + * + * Loop operations are optimized by using while(count--) { ... } + * - this means that in order to keep the same order of execution as addition we have to add + * items to the array at the begging (shift) instead of at the end (push) + * + * Child scopes are created and removed often + * - Using array would be slow since inserts in meddle are expensive so we use linked list + * + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. + */ -/////////////////////////////////// - -function expressionCompile(exp){ - if (typeof exp === $function) return exp; - var fn = compileCache[exp]; - if (!fn) { - var p = parser(exp); - var fnSelf = p.statements(); - fn = compileCache[exp] = extend( - function(){ return fnSelf(this);}, - {fnSelf: fnSelf}); - } - return fn; -} -function errorHandlerFor(element, error) { - elementError(element, NG_EXCEPTION, isDefined(error) ? formatError(error) : error); -} +function createScope(providers, instanceCache) { + var scope = new Scope(); + (scope.$service = createInjector(scope, providers, instanceCache)).eager(); + return scope; +}; /** * @workInProgress - * @ngdoc overview + * @ngdoc function * @name angular.scope * * @description - * Scope is a JavaScript object and the execution context for expressions. You can think about - * scopes as JavaScript objects that have extra APIs for registering watchers. A scope is the - * context in which model (from the model-view-controller design pattern) exists. + * A root scope can be created by calling {@link angular.scope angular.scope()}. Child scopes + * are created using the {@link angular.scope.$new $new()} method. + * (Most scopes are created automatically when compiled HTML template is executed.) + * + * Here is a simple scope snippet to show how you can interact with the scope. + * <pre> + var scope = angular.scope(); + scope.salutation = 'Hello'; + scope.name = 'World'; + + expect(scope.greeting).toEqual(undefined); + + scope.$watch('name', function(){ + this.greeting = this.salutation + ' ' + this.name + '!'; + }); // initialize the watch + + expect(scope.greeting).toEqual(undefined); + scope.name = 'Misko'; + // still old value, since watches have not been called yet + expect(scope.greeting).toEqual(undefined); + + scope.$digest(); // fire all the watches + expect(scope.greeting).toEqual('Hello Misko!'); + * </pre> + * + * # Inheritance + * A scope can inherit from a parent scope, as in this example: + * <pre> + var parent = angular.scope(); + var child = parent.$new(); + + parent.salutation = "Hello"; + child.name = "World"; + expect(child.salutation).toEqual('Hello'); + + child.salutation = "Welcome"; + expect(child.salutation).toEqual('Welcome'); + expect(parent.salutation).toEqual('Hello'); + * </pre> * - * Angular scope objects provide the following methods: + * # Dependency Injection + * See {@link guide/dev_guide.di dependency injection}. + * + * + * @param {Object.<string, function()>=} providers Map of service factory which need to be provided + * for the current scope. Defaults to {@link angular.service}. + * @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy when unit-testing and having + * the need to override a default service. + * @returns {Object} Newly created scope. + * + */ +function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = this.$$observers = + this.$$nextSibling = this.$$childHead = this.$$childTail = null; + this['this'] = this.$root = this; +} + +/** + * @workInProgress + * @ngdoc property + * @name angular.scope.$id + * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for + * debugging. + */ + +/** + * @workInProgress + * @ngdoc property + * @name angular.scope.$service + * @function * - * * {@link angular.scope.$become $become()} - - * * {@link angular.scope.$bind $bind()} - - * * {@link angular.scope.$eval $eval()} - - * * {@link angular.scope.$get $get()} - - * * {@link angular.scope.$new $new()} - - * * {@link angular.scope.$onEval $onEval()} - - * * {@link angular.scope.$service $service()} - - * * {@link angular.scope.$set $set()} - - * * {@link angular.scope.$tryEval $tryEval()} - - * * {@link angular.scope.$watch $watch()} - + * @description + * Provides reference to an instance of {@link angular.injector injector} which can be used to + * retrieve {@link angular.service services}. In general the use of this api is discouraged, + * in favor of proper {@link guide/dev_guide.di dependency injection}. * - * For more information about how angular scope objects work, see {@link guide/dev_guide.scopes - * Angular Scope Objects} in the angular Developer Guide. + * @returns {function} {@link angular.injector injector} + */ + +/** + * @workInProgress + * @ngdoc property + * @name angular.scope.$root + * @returns {Scope} The root scope of the current scope hierarchy. */ -function createScope(parent, providers, instanceCache) { - function Parent(){} - parent = Parent.prototype = (parent || {}); - var instance = new Parent(); - var evalLists = {sorted:[]}; - var $log, $exceptionHandler; - - extend(instance, { - 'this': instance, - $id: (scopeId++), - $parent: parent, - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$bind - * @function - * - * @description - * Binds a function `fn` to the current scope. See: {@link angular.bind}. - - <pre> - var scope = angular.scope(); - var fn = scope.$bind(function(){ - return this; - }); - expect(fn()).toEqual(scope); - </pre> - * - * @param {function()} fn Function to be bound. - */ - $bind: bind(instance, bind, instance), - - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$get - * @function - * - * @description - * Returns the value for `property_chain` on the current scope. Unlike in JavaScript, if there - * are any `undefined` intermediary properties, `undefined` is returned instead of throwing an - * exception. - * - <pre> - var scope = angular.scope(); - expect(scope.$get('person.name')).toEqual(undefined); - scope.person = {}; - expect(scope.$get('person.name')).toEqual(undefined); - scope.person.name = 'misko'; - expect(scope.$get('person.name')).toEqual('misko'); - </pre> - * - * @param {string} property_chain String representing name of a scope property. Optionally - * properties can be chained with `.` (dot), e.g. `'person.name.first'` - * @returns {*} Value for the (nested) property. - */ - $get: bind(instance, getter, instance), - - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$set - * @function - * - * @description - * Assigns a value to a property of the current scope specified via `property_chain`. Unlike in - * JavaScript, if there are any `undefined` intermediary properties, empty objects are created - * and assigned to them instead of throwing an exception. - * - <pre> - var scope = angular.scope(); - expect(scope.person).toEqual(undefined); - scope.$set('person.name', 'misko'); - expect(scope.person).toEqual({name:'misko'}); - expect(scope.person.name).toEqual('misko'); - </pre> - * - * @param {string} property_chain String representing name of a scope property. Optionally - * properties can be chained with `.` (dot), e.g. `'person.name.first'` - * @param {*} value Value to assign to the scope property. - */ - $set: bind(instance, setter, instance), - - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$eval - * @function - * - * @description - * Without the `exp` parameter triggers an eval cycle for this scope and its child scopes. - * - * With the `exp` parameter, compiles the expression to a function and calls it with `this` set - * to the current scope and returns the result. In other words, evaluates `exp` as angular - * expression in the context of the current scope. - * - * # Example - <pre> - var scope = angular.scope(); - scope.a = 1; - scope.b = 2; - - expect(scope.$eval('a+b')).toEqual(3); - expect(scope.$eval(function(){ return this.a + this.b; })).toEqual(3); - - scope.$onEval('sum = a+b'); - expect(scope.sum).toEqual(undefined); - scope.$eval(); - expect(scope.sum).toEqual(3); - </pre> - * - * @param {(string|function())=} exp An angular expression to be compiled to a function or a js - * function. - * - * @returns {*} The result of calling compiled `exp` with `this` set to the current scope. - */ - $eval: function(exp) { - var type = typeof exp; - var i, iSize; - var j, jSize; - var queue; - var fn; - if (type == $undefined) { - for ( i = 0, iSize = evalLists.sorted.length; i < iSize; i++) { - for ( queue = evalLists.sorted[i], - jSize = queue.length, - j= 0; j < jSize; j++) { - instance.$tryEval(queue[j].fn, queue[j].handler); + +/** + * @workInProgress + * @ngdoc property + * @name angular.scope.$parent + * @returns {Scope} The parent scope of the current scope. + */ + + +Scope.prototype = { + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$new + * @function + * + * @description + * Creates a new child {@link angular.scope scope}. The new scope can optionally behave as a + * controller. The parent scope will propagate the {@link angular.scope.$digest $digest()} and + * {@link angular.scope.$flush $flush()} events. The scope can be removed from the scope + * hierarchy using {@link angular.scope.$destroy $destroy()}. + * + * {@link angular.scope.$destroy $destroy()} must be called on a scope when it is desired for + * the scope and its child scopes to be permanently detached from the parent and thus stop + * participating in model change detection and listener notification by invoking. + * + * @param {function()=} constructor Constructor function which the scope should behave as. + * @param {curryArguments=} ... Any additional arguments which are curried into the constructor. + * See {@link guide/dev_guide.di dependency injection}. + * @returns {Object} The newly created child scope. + * + */ + $new: function(Class, curryArguments) { + var Child = function() {}; // should be anonymous; This is so that when the minifier munges + // the name it does not become random set of chars. These will then show up as class + // name in the debugger. + var child; + Child.prototype = this; + child = new Child(); + child['this'] = child; + child.$parent = this; + child.$id = nextUid(); + child.$$phase = child.$$watchers = child.$$observers = + child.$$nextSibling = child.$$childHead = child.$$childTail = null; + if (this.$$childHead) { + this.$$childTail.$$nextSibling = child; + this.$$childTail = child; + } else { + this.$$childHead = this.$$childTail = child; + } + // short circuit if we have no class + if (Class) { + // can't use forEach, we need speed! + var ClassPrototype = Class.prototype; + for(var key in ClassPrototype) { + child[key] = bind(child, ClassPrototype[key]); + } + this.$service.invoke(child, Class, curryArguments); + } + return child; + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$watch + * @function + * + * @description + * Registers a `listener` callback to be executed whenever the `watchExpression` changes. + * + * - The `watchExpression` is called on every call to {@link angular.scope.$digest $digest()} and + * should return the value which will be watched. (Since {@link angular.scope.$digest $digest()} + * reruns when it detects changes the `watchExpression` can execute multiple times per + * {@link angular.scope.$digest $digest()} and should be idempotent.) + * - The `listener` is called only when the value from the current `watchExpression` and the + * previous call to `watchExpression' are not equal. The inequality is determined according to + * {@link angular.equals} function. To save the value of the object for later comparison + * {@link angular.copy} function is used. It also means that watching complex options will + * have adverse memory and performance implications. + * - The watch `listener` may change the model, which may trigger other `listener`s to fire. This + * is achieving my rerunning the watchers until no changes are detected. The rerun iteration + * limit is 100 to prevent infinity loop deadlock. + * + * # When to use `$watch`? + * + * The `$watch` should be used from within controllers to listen on properties *immediately* after + * a stimulus is applied to the system (see {@link angular.scope.$apply $apply()}). This is in + * contrast to {@link angular.scope.$observe $observe()} which is used from within the directives + * and which gets applied at some later point in time. In addition + * {@link angular.scope.$observe $observe()} must not modify the model. + * + * If you want to be notified whenever {@link angular.scope.$digest $digest} is called, + * you can register an `watchExpression` function with no `listener`. (Since `watchExpression`, + * can execute multiple times per {@link angular.scope.$digest $digest} cycle when a change is + * detected, be prepared for multiple calls to your listener.) + * + * # `$watch` vs `$observe` + * + * <table class="table"> + * <tr> + * <th></td> + * <th>{@link angular.scope.$watch $watch()}</th> + * <th>{@link angular.scope.$observe $observe()}</th> + * </tr> + * <tr><th colspan="3" class="section">When to use it?</th></tr> + * <tr> + * <th>Purpose</th> + * <td>Application behavior (including further model mutation) in response to a model + * mutation.</td> + * <td>Update the DOM in response to a model mutation.</td> + * </tr> + * <tr> + * <th>Used from</th> + * <td>{@link angular.directive.ng:controller controller}</td> + * <td>{@link angular.directive directives}</td> + * </tr> + * <tr><th colspan="3" class="section">What fires listeners?</th></tr> + * <tr> + * <th>Directly</th> + * <td>{@link angular.scope.$digest $digest()}</td> + * <td>{@link angular.scope.$flush $flush()}</td> + * </tr> + * <tr> + * <th>Indirectly via {@link angular.scope.$apply $apply()}</th> + * <td>{@link angular.scope.$apply $apply} calls + * {@link angular.scope.$digest $digest()} after apply argument executes.</td> + * <td>{@link angular.scope.$apply $apply} schedules + * {@link angular.scope.$flush $flush()} at some future time via + * {@link angular.service.$updateView $updateView}</td> + * </tr> + * <tr><th colspan="3" class="section">API contract</th></tr> + * <tr> + * <th>Model mutation</th> + * <td>allowed: detecting mutations requires one or mare calls to `watchExpression' per + * {@link angular.scope.$digest $digest()} cycle</td> + * <td>not allowed: called once per {@link angular.scope.$flush $flush()} must be + * {@link http://en.wikipedia.org/wiki/Idempotence idempotent} + * (function without side-effects which can be called multiple times.)</td> + * </tr> + * <tr> + * <th>Initial Value</th> + * <td>uses the current value of `watchExpression` as the initial value. Does not fire on + * initial call to {@link angular.scope.$digest $digest()}, unless `watchExpression` has + * changed form the initial value.</td> + * <td>fires on first run of {@link angular.scope.$flush $flush()} regardless of value of + * `observeExpression`</td> + * </tr> + * </table> + * + * + * + * # Example + <pre> + var scope = angular.scope(); + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$watch('name', function(scope, newValue, oldValue) { counter = counter + 1; }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // no variable change + expect(scope.counter).toEqual(0); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(1); + </pre> + * + * + * + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@link angular.scope.$digest $digest} cycle. A change in the return value triggers a + * call to the `listener`. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {(function()|string)=} listener Callback called whenever the return value of + * the `watchExpression` changes. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope, newValue, oldValue)`: called with current `scope` an previous and + * current values as parameters. + * @returns {function()} a function which will call the `listener` with apprariate arguments. + * Useful for forcing initialization of listener. + */ + $watch: function(watchExp, listener) { + var scope = this; + var get = compileToFn(watchExp, 'watch'); + var listenFn = compileToFn(listener || noop, 'listener'); + var array = scope.$$watchers; + if (!array) { + array = scope.$$watchers = []; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift({ + fn: listenFn, + last: copy(get(scope)), + get: get + }); + // we only return the initialization function for $watch (not for $observe), since creating + // function cost time and memory, and $observe functions do not need it. + return function() { + var value = get(scope); + listenFn(scope, value, value); + }; + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$digest + * @function + * + * @description + * Process all of the {@link angular.scope.$watch watchers} of the current scope and its children. + * Because a {@link angular.scope.$watch watcher}'s listener can change the model, the + * `$digest()` keeps calling the {@link angular.scope.$watch watchers} until no more listeners are + * firing. This means that it is possible to get into an infinite loop. This function will throw + * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 100. + * + * Usually you don't call `$digest()` directly in + * {@link angular.directive.ng:controller controllers} or in {@link angular.directive directives}. + * Instead a call to {@link angular.scope.$apply $apply()} (typically from within a + * {@link angular.directive directive}) will force a `$digest()`. + * + * If you want to be notified whenever `$digest()` is called, + * you can register a `watchExpression` function with {@link angular.scope.$watch $watch()} + * with no `listener`. + * + * You may have a need to call `$digest()` from within unit-tests, to simulate the scope + * life-cycle. + * + * # Example + <pre> + var scope = angular.scope(); + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; }); + expect(scope.counter).toEqual(0); + + scope.$flush(); + // no variable change + expect(scope.counter).toEqual(0); + + scope.name = 'adam'; + scope.$flush(); + expect(scope.counter).toEqual(1); + </pre> + * + * @returns {number} number of {@link angular.scope.$watch listeners} which fired. + * + */ + $digest: function() { + var child, + watch, value, last, + watchers = this.$$watchers, + length, count = 0, + iterationCount, ttl = 100; + + if (this.$$phase) { + throw Error(this.$$phase + ' already in progress'); + } + this.$$phase = '$digest'; + do { + iterationCount = 0; + if (watchers) { + // process our watches + length = watchers.length; + while (length--) { + try { + watch = watchers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if ((value = watch.get(this)) !== (last = watch.last) && !equals(value, last)) { + iterationCount++; + watch.fn(this, watch.last = copy(value), last); + } + } catch (e) { + this.$service('$exceptionHandler')(e); } } - } else if (type === $function) { - return exp.call(instance); - } else if (type === 'string') { - return expressionCompile(exp).call(instance); } - }, - - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$tryEval - * @function - * - * @description - * Evaluates the expression in the context of the current scope just like - * {@link angular.scope.$eval} with expression parameter, but also wraps it in a try/catch - * block. - * - * If an exception is thrown then `exceptionHandler` is used to handle the exception. - * - * # Example - <pre> - var scope = angular.scope(); - scope.error = function(){ throw 'myerror'; }; - scope.$exceptionHandler = function(e) {this.lastException = e; }; - - expect(scope.$eval('error()')); - expect(scope.lastException).toEqual('myerror'); - this.lastException = null; - - expect(scope.$eval('error()'), function(e) {this.lastException = e; }); - expect(scope.lastException).toEqual('myerror'); - - var body = angular.element(window.document.body); - expect(scope.$eval('error()'), body); - expect(body.attr('ng-exception')).toEqual('"myerror"'); - expect(body.hasClass('ng-exception')).toEqual(true); - </pre> - * - * @param {string|function()} expression Angular expression to evaluate. - * @param {(function()|DOMElement)=} exceptionHandler Function to be called or DOMElement to be - * decorated. - * @returns {*} The result of `expression` evaluation. - */ - $tryEval: function (expression, exceptionHandler) { - var type = typeof expression; - try { - if (type == $function) { - return expression.call(instance); - } else if (type == 'string'){ - return expressionCompile(expression).call(instance); - } - } catch (e) { - if ($log) $log.error(e); - if (isFunction(exceptionHandler)) { - exceptionHandler(e); - } else if (exceptionHandler) { - errorHandlerFor(exceptionHandler, e); - } else if (isFunction($exceptionHandler)) { - $exceptionHandler(e); - } + child = this.$$childHead; + while(child) { + iterationCount += child.$digest(); + child = child.$$nextSibling; } - }, - - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$watch - * @function - * - * @description - * Registers `listener` as a callback to be executed every time the `watchExp` changes. Be aware - * that the callback gets, by default, called upon registration, this can be prevented via the - * `initRun` parameter. - * - * # Example - <pre> - var scope = angular.scope(); - scope.name = 'misko'; - scope.counter = 0; - - expect(scope.counter).toEqual(0); - scope.$watch('name', 'counter = counter + 1'); - expect(scope.counter).toEqual(1); - - scope.$eval(); - expect(scope.counter).toEqual(1); - - scope.name = 'adam'; - scope.$eval(); - expect(scope.counter).toEqual(2); - </pre> - * - * @param {function()|string} watchExp Expression that should be evaluated and checked for - * change during each eval cycle. Can be an angular string expression or a function. - * @param {function()|string} listener Function (or angular string expression) that gets called - * every time the value of the `watchExp` changes. The function will be called with two - * parameters, `newValue` and `oldValue`. - * @param {(function()|DOMElement)=} [exceptionHanlder=angular.service.$exceptionHandler] Handler - * that gets called when `watchExp` or `listener` throws an exception. If a DOMElement is - * specified as a handler, the element gets decorated by angular with the information about the - * exception. - * @param {boolean=} [initRun=true] Flag that prevents the first execution of the listener upon - * registration. - * - */ - $watch: function(watchExp, listener, exceptionHandler, initRun) { - var watch = expressionCompile(watchExp), - last = watch.call(instance); - listener = expressionCompile(listener); - function watcher(firstRun){ - var value = watch.call(instance), - // we have to save the value because listener can call ourselves => inf loop - lastValue = last; - if (firstRun || lastValue !== value) { - last = value; - instance.$tryEval(function(){ - return listener.call(instance, value, lastValue); - }, exceptionHandler); - } + count += iterationCount; + if(!(ttl--)) { + throw Error('100 $digest() iterations reached. Aborting!'); } - instance.$onEval(PRIORITY_WATCH, watcher); - if (isUndefined(initRun)) initRun = true; - if (initRun) watcher(true); - }, - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$onEval - * @function - * - * @description - * Evaluates the `expr` expression in the context of the current scope during each - * {@link angular.scope.$eval eval cycle}. - * - * # Example - <pre> - var scope = angular.scope(); - scope.counter = 0; - scope.$onEval('counter = counter + 1'); - expect(scope.counter).toEqual(0); - scope.$eval(); - expect(scope.counter).toEqual(1); - </pre> - * - * @param {number} [priority=0] Execution priority. Lower priority numbers get executed first. - * @param {string|function()} expr Angular expression or function to be executed. - * @param {(function()|DOMElement)=} [exceptionHandler=angular.service.$exceptionHandler] Handler - * function to call or DOM element to decorate when an exception occurs. - * - */ - $onEval: function(priority, expr, exceptionHandler){ - if (!isNumber(priority)) { - exceptionHandler = expr; - expr = priority; - priority = 0; + } while (iterationCount); + this.$$phase = null; + return count; + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$observe + * @function + * + * @description + * Registers a `listener` callback to be executed during the {@link angular.scope.$flush $flush()} + * phase when the `observeExpression` changes.. + * + * - The `observeExpression` is called on every call to {@link angular.scope.$flush $flush()} and + * should return the value which will be observed. + * - The `listener` is called only when the value from the current `observeExpression` and the + * previous call to `observeExpression' are not equal. The inequality is determined according to + * {@link angular.equals} function. To save the value of the object for later comparison + * {@link angular.copy} function is used. It also means that watching complex options will + * have adverse memory and performance implications. + * + * # When to use `$observe`? + * + * {@link angular.scope.$observe $observe()} is used from within directives and gets applied at + * some later point in time. Addition {@link angular.scope.$observe $observe()} must not + * modify the model. This is in contrast to {@link angular.scope.$watch $watch()} which should be + * used from within controllers to trigger a callback *immediately* after a stimulus is applied + * to the system (see {@link angular.scope.$apply $apply()}). + * + * If you want to be notified whenever {@link angular.scope.$flush $flush} is called, + * you can register an `observeExpression` function with no `listener`. + * + * + * # `$watch` vs `$observe` + * + * <table class="table"> + * <tr> + * <th></td> + * <th>{@link angular.scope.$watch $watch()}</th> + * <th>{@link angular.scope.$observe $observe()}</th> + * </tr> + * <tr><th colspan="3" class="section">When to use it?</th></tr> + * <tr> + * <th>Purpose</th> + * <td>Application behavior (including further model mutation) in response to a model + * mutation.</td> + * <td>Update the DOM in response to a model mutation.</td> + * </tr> + * <tr> + * <th>Used from</th> + * <td>{@link angular.directive.ng:controller controller}</td> + * <td>{@link angular.directive directives}</td> + * </tr> + * <tr><th colspan="3" class="section">What fires listeners?</th></tr> + * <tr> + * <th>Directly</th> + * <td>{@link angular.scope.$digest $digest()}</td> + * <td>{@link angular.scope.$flush $flush()}</td> + * </tr> + * <tr> + * <th>Indirectly via {@link angular.scope.$apply $apply()}</th> + * <td>{@link angular.scope.$apply $apply} calls + * {@link angular.scope.$digest $digest()} after apply argument executes.</td> + * <td>{@link angular.scope.$apply $apply} schedules + * {@link angular.scope.$flush $flush()} at some future time via + * {@link angular.service.$updateView $updateView}</td> + * </tr> + * <tr><th colspan="3" class="section">API contract</th></tr> + * <tr> + * <th>Model mutation</th> + * <td>allowed: detecting mutations requires one or mare calls to `watchExpression' per + * {@link angular.scope.$digest $digest()} cycle</td> + * <td>not allowed: called once per {@link angular.scope.$flush $flush()} must be + * {@link http://en.wikipedia.org/wiki/Idempotence idempotent} + * (function without side-effects which can be called multiple times.)</td> + * </tr> + * <tr> + * <th>Initial Value</th> + * <td>uses the current value of `watchExpression` as the initial value. Does not fire on + * initial call to {@link angular.scope.$digest $digest()}, unless `watchExpression` has + * changed form the initial value.</td> + * <td>fires on first run of {@link angular.scope.$flush $flush()} regardless of value of + * `observeExpression`</td> + * </tr> + * </table> + * + * # Example + <pre> + var scope = angular.scope(); + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; }); + expect(scope.counter).toEqual(0); + + scope.$flush(); + // no variable change + expect(scope.counter).toEqual(0); + + scope.name = 'adam'; + scope.$flush(); + expect(scope.counter).toEqual(1); + </pre> + * + * @param {(function()|string)} observeExpression Expression that is evaluated on each + * {@link angular.scope.$flush $flush} cycle. A change in the return value triggers a + * call to the `listener`. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {(function()|string)=} listener Callback called whenever the return value of + * the `observeExpression` changes. + * + * - `string`: Evaluated as {@link guide/dev_guide.expressions expression} + * - `function(scope, newValue, oldValue)`: called with current `scope` an previous and + * current values as parameters. + */ + $observe: function(watchExp, listener) { + var array = this.$$observers; + + if (!array) { + array = this.$$observers = []; + } + // we use unshift since we use a while loop in $flush for speed. + // the while loop reads in reverse order. + array.unshift({ + fn: compileToFn(listener || noop, 'listener'), + last: NaN, + get: compileToFn(watchExp, 'watch') + }); + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$flush + * @function + * + * @description + * Process all of the {@link angular.scope.$observe observers} of the current scope + * and its children. + * + * Usually you don't call `$flush()` directly in + * {@link angular.directive.ng:controller controllers} or in {@link angular.directive directives}. + * Instead a call to {@link angular.scope.$apply $apply()} (typically from within a + * {@link angular.directive directive}) will scheduled a call to `$flush()` (with the + * help of the {@link angular.service.$updateView $updateView} service). + * + * If you want to be notified whenever `$flush()` is called, + * you can register a `observeExpression` function with {@link angular.scope.$observe $observe()} + * with no `listener`. + * + * You may have a need to call `$flush()` from within unit-tests, to simulate the scope + * life-cycle. + * + * # Example + <pre> + var scope = angular.scope(); + scope.name = 'misko'; + scope.counter = 0; + + expect(scope.counter).toEqual(0); + scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; }); + expect(scope.counter).toEqual(0); + + scope.$flush(); + // no variable change + expect(scope.counter).toEqual(0); + + scope.name = 'adam'; + scope.$flush(); + expect(scope.counter).toEqual(1); + </pre> + * + */ + $flush: function() { + var observers = this.$$observers, + child, + length, + observer, value, last; + + if (this.$$phase) { + throw Error(this.$$phase + ' already in progress'); + } + this.$$phase = '$flush'; + if (observers) { + // process our watches + length = observers.length; + while (length--) { + try { + observer = observers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if ((value = observer.get(this)) !== (last = observer.last) && !equals(value, last)) { + observer.fn(this, observer.last = copy(value), last); + } + } catch (e){ + this.$service('$exceptionHandler')(e); + } } - var evalList = evalLists[priority]; - if (!evalList) { - evalList = evalLists[priority] = []; - evalList.priority = priority; - evalLists.sorted.push(evalList); - evalLists.sorted.sort(function(a,b){return a.priority-b.priority;}); + } + // observers can create new children + child = this.$$childHead; + while(child) { + child.$flush(); + child = child.$$nextSibling; + } + this.$$phase = null; + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$destroy + * @function + * + * @description + * Remove the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link angular.scope.$digest $digest()} and + * {@link angular.scope.$flush $flush()} will no longer propagate to the current scope and its + * children. Removal also implies that the current scope is eligible for garbage collection. + * + * The `$destroy()` is usually used by directives such as + * {@link angular.widget.@ng:repeat ng:repeat} for managing the unrolling of the loop. + * + */ + $destroy: function() { + if (this.$root == this) return; // we can't remove the root node; + var parent = this.$parent; + var child = parent.$$childHead; + var lastChild = null; + var nextChild = null; + // We have to do a linear search, since we don't have doubly link list. + // But this is intentional since removals are rare, and doubly link list is not free. + while(child) { + if (child == this) { + nextChild = child.$$nextSibling; + if (parent.$$childHead == child) { + parent.$$childHead = nextChild; + } + if (lastChild) { + lastChild.$$nextSibling = nextChild; + } + if (parent.$$childTail == child) { + parent.$$childTail = lastChild; + } + return; // stop iterating we found it + } else { + lastChild = child; + child = child.$$nextSibling; } - evalList.push({ - fn: expressionCompile(expr), - handler: exceptionHandler - }); - }, - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$become - * @function - * @deprecated This method will be removed before 1.0 - * - * @description - * Modifies the scope to act like an instance of the given class by: - * - * - copying the class's prototype methods - * - applying the class's initialization function to the scope instance (without using the new - * operator) - * - * That makes the scope be a `this` for the given class's methods — effectively an instance of - * the given class with additional (scope) stuff. A scope can later `$become` another class. - * - * `$become` gets used to make the current scope act like an instance of a controller class. - * This allows for use of a controller class in two ways. - * - * - as an ordinary JavaScript class for standalone testing, instantiated using the new - * operator, with no attached view. - * - as a controller for an angular model stored in a scope, "instantiated" by - * `scope.$become(ControllerClass)`. - * - * Either way, the controller's methods refer to the model variables like `this.name`. When - * stored in a scope, the model supports data binding. When bound to a view, {{name}} in the - * HTML template refers to the same variable. - */ - $become: function(Class) { - if (isFunction(Class)) { - instance.constructor = Class; - forEach(Class.prototype, function(fn, name){ - instance[name] = bind(instance, fn); - }); - instance.$service.invoke(instance, Class, slice.call(arguments, 1, arguments.length)); - - //TODO: backwards compatibility hack, remove when we don't depend on init methods - if (isFunction(Class.prototype.init)) { - instance.init(); + } + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$eval + * @function + * + * @description + * Executes the expression on the current scope returning the result. Any exceptions in the + * expression are propagated (uncaught). This is useful when evaluating engular expressions. + * + * # Example + <pre> + var scope = angular.scope(); + scope.a = 1; + scope.b = 2; + + expect(scope.$eval('a+b')).toEqual(3); + expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); + </pre> + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr) { + var fn = isString(expr) + ? parser(expr).statements() + : expr || noop; + return fn(this); + }, + + /** + * @workInProgress + * @ngdoc function + * @name angular.scope.$apply + * @function + * + * @description + * `$apply()` is used to execute an expression in angular from outside of the angular framework. + * (For example from browser DOM events, setTimeout, XHR or third party libraries). + * Because we are calling into the angular framework we need to perform proper scope life-cycle + * of {@link angular.service.$exceptionHandler exception handling}, + * {@link angular.scope.$digest executing watches} and scheduling + * {@link angular.service.$updateView updating of the view} which in turn + * {@link angular.scope.$digest executes observers} to update the DOM. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + $updateView(); } } - }, - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$new - * @function - * - * @description - * Creates a new {@link angular.scope scope}, that: - * - * - is a child of the current scope - * - will {@link angular.scope.$become $become} of type specified via `constructor` - * - * @param {function()} constructor Constructor function of the type the new scope should assume. - * @returns {Object} The newly created child scope. - * - */ - $new: function(constructor) { - var child = createScope(instance); - child.$become.apply(instance, concat([constructor], arguments, 1)); - instance.$onEval(child.$eval); - return child; + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/dev_guide.expressions expression} is executed using the + * {@link angular.scope.$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link angular.service.$exceptionHandler $exceptionHandler} service. + * 3. The {@link angular.scope.$watch watch} listeners are fired immediately after the expression + * was executed using the {@link angular.scope.$digest $digest()} method. + * 4. A DOM update is scheduled using the {@link angular.service.$updateView $updateView} service. + * The `$updateView` may merge multiple update requests into a single update, if the requests + * are issued in close time proximity. + * 6. The {@link angular.service.$updateView $updateView} service then fires DOM + * {@link angular.scope.$observe observers} using the {@link angular.scope.$flush $flush()} + * method. + * + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + return this.$eval(expr); + } catch (e) { + this.$service('$exceptionHandler')(e); + } finally { + this.$root.$digest(); + this.$service('$updateView')(); } - - }); - - if (!parent.$root) { - instance.$root = instance; - instance.$parent = instance; - - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$service - * @function - * - * @description - * Provides access to angular's dependency injector and - * {@link angular.service registered services}. In general the use of this api is discouraged, - * except for tests and components that currently don't support dependency injection (widgets, - * filters, etc). - * - * @param {string} serviceId String ID of the service to return. - * @returns {*} Value, object or function returned by the service factory function if any. - */ - (instance.$service = createInjector(instance, providers, instanceCache)).eager(); } +}; - $log = instance.$service('$log'); - $exceptionHandler = instance.$service('$exceptionHandler'); - - return instance; +function compileToFn(exp, name) { + var fn = isString(exp) + ? parser(exp).statements() + : exp; + assertArgFn(fn, name); + return fn; } diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 5d56ae27..719e87b3 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -374,7 +374,7 @@ angular.service('$browser', function(){ * See {@link angular.mock} for more info on angular mocks. */ angular.service('$exceptionHandler', function() { - return function(e) { throw e;}; + return function(e) { throw e; }; }); diff --git a/src/apis.js b/src/apis.js index 3ccd95d7..8a566a46 100644 --- a/src/apis.js +++ b/src/apis.js @@ -7,7 +7,7 @@ var angularGlobal = { if (type == $object) { if (obj instanceof Array) return $array; if (isDate(obj)) return $date; - if (obj.nodeType == 1) return $element; + if (obj.nodeType == 1) return 'element'; } return type; } @@ -180,7 +180,7 @@ var angularArray = { </doc:example> */ 'sum':function(array, expression) { - var fn = angular['Function']['compile'](expression); + var fn = angularFunction.compile(expression); var sum = 0; for (var i = 0; i < array.length; i++) { var value = 1 * fn(array[i]); @@ -522,21 +522,21 @@ var angularArray = { </doc:source> <doc:scenario> it('should calculate counts', function() { - expect(binding('items.$count(\'points==1\')')).toEqual(2); - expect(binding('items.$count(\'points>1\')')).toEqual(1); + expect(binding('items.$count(\'points==1\')')).toEqual('2'); + expect(binding('items.$count(\'points>1\')')).toEqual('1'); }); it('should recalculate when updated', function() { using('.doc-example-live li:first-child').input('item.points').enter('23'); - expect(binding('items.$count(\'points==1\')')).toEqual(1); - expect(binding('items.$count(\'points>1\')')).toEqual(2); + expect(binding('items.$count(\'points==1\')')).toEqual('1'); + expect(binding('items.$count(\'points>1\')')).toEqual('2'); }); </doc:scenario> </doc:example> */ 'count':function(array, condition) { if (!condition) return array.length; - var fn = angular['Function']['compile'](condition), count = 0; + var fn = angularFunction.compile(condition), count = 0; forEach(array, function(value){ if (fn(value)) { count ++; @@ -635,7 +635,7 @@ var angularArray = { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } - get = expressionCompile(predicate).fnSelf; + get = expressionCompile(predicate); } return reverseComparator(function(a,b){ return compare(get(a),get(b)); @@ -796,14 +796,14 @@ var angularDate = { }; var angularFunction = { - 'compile':function(expression) { + 'compile': function(expression) { if (isFunction(expression)){ return expression; } else if (expression){ - return expressionCompile(expression).fnSelf; + return expressionCompile(expression); } else { - return identity; - } + return identity; + } } }; diff --git a/src/directives.js b/src/directives.js index 9aa0d57e..4712f250 100644 --- a/src/directives.js +++ b/src/directives.js @@ -73,7 +73,7 @@ */ angularDirective("ng:init", function(expression){ return function(element){ - this.$tryEval(expression, element); + this.$eval(expression); }; }); @@ -165,19 +165,19 @@ angularDirective("ng:init", function(expression){ </doc:example> */ angularDirective("ng:controller", function(expression){ - this.scope(true); - return function(element){ - var controller = getter(window, expression, true) || getter(this, expression, true); - if (!controller) - throw "Can not find '"+expression+"' controller."; - if (!isFunction(controller)) - throw "Reference '"+expression+"' is not a class."; - this.$become(controller); - }; + this.scope(function(scope){ + var Controller = + getter(scope, expression, true) || + getter(window, expression, true); + assertArgFn(Controller, expression); + return Controller; + }); + return noop; }); /** * @workInProgress + * @deprecated * @ngdoc directive * @name angular.directive.ng:eval * @@ -208,17 +208,18 @@ angularDirective("ng:controller", function(expression){ <doc:scenario> it('should check eval', function(){ expect(binding('obj.divide')).toBe('3'); - expect(binding('obj.updateCount')).toBe('2'); + expect(binding('obj.updateCount')).toBe('1'); input('obj.a').enter('12'); expect(binding('obj.divide')).toBe('6'); - expect(binding('obj.updateCount')).toBe('3'); + expect(binding('obj.updateCount')).toBe('2'); }); </doc:scenario> </doc:example> */ +// TODO(misko): remove me angularDirective("ng:eval", function(expression){ return function(element){ - this.$onEval(expression, element); + this.$observe(expression); }; }); @@ -257,15 +258,26 @@ angularDirective("ng:bind", function(expression, element){ element.addClass('ng-binding'); return function(element) { var lastValue = noop, lastError = noop; - this.$onEval(function() { + this.$observe(function(scope) { + // TODO(misko): remove error handling https://github.com/angular/angular.js/issues/347 var error, value, html, isHtml, isDomElement, - oldElement = this.hasOwnProperty($$element) ? this.$element : undefined; - this.$element = element; - value = this.$tryEval(expression, function(e){ + hadOwnElement = scope.hasOwnProperty('$element'), + oldElement = scope.$element; + // TODO(misko): get rid of $element https://github.com/angular/angular.js/issues/348 + scope.$element = element; + try { + value = scope.$eval(expression); + } catch (e) { + scope.$service('$exceptionHandler')(e); error = formatError(e); - }); - this.$element = oldElement; - // If we are HTML then save the raw HTML data so that we don't + } finally { + if (hadOwnElement) { + scope.$element = oldElement; + } else { + delete scope.$element; + } + } + // If we are HTML than save the raw HTML data so that we don't // recompute sanitization since it is expensive. // TODO: turn this into a more generic way to compute this if (isHtml = (value instanceof HTML)) @@ -289,7 +301,7 @@ angularDirective("ng:bind", function(expression, element){ element.text(value == undefined ? '' : value); } } - }, element); + }); }; }); @@ -301,10 +313,14 @@ function compileBindTemplate(template){ forEach(parseBindings(template), function(text){ var exp = binding(text); bindings.push(exp - ? function(element){ - var error, value = this.$tryEval(exp, function(e){ + ? function(scope, element) { + var error, value; + try { + value = scope.$eval(exp); + } catch(e) { + scope.$service('$exceptionHandler')(e); error = toJson(e); - }); + } elementError(element, NG_EXCEPTION, error); return error ? error : value; } @@ -312,20 +328,30 @@ function compileBindTemplate(template){ return text; }); }); - bindTemplateCache[template] = fn = function(element, prettyPrintJson){ - var parts = [], self = this, - oldElement = this.hasOwnProperty($$element) ? self.$element : undefined; - self.$element = element; - for ( var i = 0; i < bindings.length; i++) { - var value = bindings[i].call(self, element); - if (isElement(value)) - value = ''; - else if (isObject(value)) - value = toJson(value, prettyPrintJson); - parts.push(value); + bindTemplateCache[template] = fn = function(scope, element, prettyPrintJson) { + var parts = [], + hadOwnElement = scope.hasOwnProperty('$element'), + oldElement = scope.$element; + + // TODO(misko): get rid of $element + scope.$element = element; + try { + for (var i = 0; i < bindings.length; i++) { + var value = bindings[i](scope, element); + if (isElement(value)) + value = ''; + else if (isObject(value)) + value = toJson(value, prettyPrintJson); + parts.push(value); + } + return parts.join(''); + } finally { + if (hadOwnElement) { + scope.$element = oldElement; + } else { + delete scope.$element; + } } - self.$element = oldElement; - return parts.join(''); }; } return fn; @@ -372,13 +398,13 @@ angularDirective("ng:bind-template", function(expression, element){ var templateFn = compileBindTemplate(expression); return function(element) { var lastValue; - this.$onEval(function() { - var value = templateFn.call(this, element, true); + this.$observe(function(scope) { + var value = templateFn(scope, element, true); if (value != lastValue) { element.text(value); lastValue = value; } - }, element); + }); }; }); @@ -446,10 +472,10 @@ var REMOVE_ATTRIBUTES = { angularDirective("ng:bind-attr", function(expression){ return function(element){ var lastValue = {}; - this.$onEval(function(){ - var values = this.$eval(expression); + this.$observe(function(scope){ + var values = scope.$eval(expression); for(var key in values) { - var value = compileBindTemplate(values[key]).call(this, element), + var value = compileBindTemplate(values[key])(scope, element), specialName = REMOVE_ATTRIBUTES[lowercase(key)]; if (lastValue[key] !== value) { lastValue[key] = value; @@ -467,7 +493,7 @@ angularDirective("ng:bind-attr", function(expression){ } } } - }, element); + }); }; }); @@ -510,14 +536,13 @@ angularDirective("ng:bind-attr", function(expression){ * TODO: maybe we should consider allowing users to control event propagation in the future. */ angularDirective("ng:click", function(expression, element){ - return annotate('$updateView', function($updateView, element){ + return function(element){ var self = this; element.bind('click', function(event){ - self.$tryEval(expression, element); - $updateView(); + self.$apply(expression); event.stopPropagation(); }); - }); + }; }); @@ -555,28 +580,27 @@ angularDirective("ng:click", function(expression, element){ </doc:example> */ angularDirective("ng:submit", function(expression, element) { - return annotate('$updateView', function($updateView, element) { + return function(element) { var self = this; element.bind('submit', function(event) { - self.$tryEval(expression, element); - $updateView(); + self.$apply(expression); event.preventDefault(); }); - }); + }; }); function ngClass(selector) { - return function(expression, element){ + return function(expression, element) { var existing = element[0].className + ' '; - return function(element){ - this.$onEval(function(){ - if (selector(this.$index)) { - var value = this.$eval(expression); + return function(element) { + this.$observe(function(scope) { + if (selector(scope.$index)) { + var value = scope.$eval(expression); if (isArray(value)) value = value.join(' '); element[0].className = trim(existing + value); } - }, element); + }); }; }; } @@ -732,9 +756,9 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;})); */ angularDirective("ng:show", function(expression, element){ return function(element){ - this.$onEval(function(){ - toBoolean(this.$eval(expression)) ? element.show() : element.hide(); - }, element); + this.$observe(expression, function(scope, value){ + toBoolean(value) ? element.show() : element.hide(); + }); }; }); @@ -773,9 +797,9 @@ angularDirective("ng:show", function(expression, element){ */ angularDirective("ng:hide", function(expression, element){ return function(element){ - this.$onEval(function(){ - toBoolean(this.$eval(expression)) ? element.hide() : element.show(); - }, element); + this.$observe(expression, function(scope, value){ + toBoolean(value) ? element.hide() : element.show(); + }); }; }); @@ -815,8 +839,8 @@ angularDirective("ng:hide", function(expression, element){ angularDirective("ng:style", function(expression, element){ return function(element){ var resetStyle = getStyle(element); - this.$onEval(function(){ - var style = this.$eval(expression) || {}, key, mergedStyle = {}; + this.$observe(function(scope){ + var style = scope.$eval(expression) || {}, key, mergedStyle = {}; for(key in style) { if (resetStyle[key] === undefined) resetStyle[key] = ''; mergedStyle[key] = style[key]; @@ -825,7 +849,7 @@ angularDirective("ng:style", function(expression, element){ mergedStyle[key] = mergedStyle[key] || resetStyle[key]; } element.css(mergedStyle); - }, element); + }); }; }); diff --git a/src/filters.js b/src/filters.js index bb8426c5..52aafcf3 100644 --- a/src/filters.js +++ b/src/filters.js @@ -645,25 +645,26 @@ angularFilter.html = function(html, option){ </doc:scenario> </doc:example> */ -//TODO: externalize all regexps -angularFilter.linky = function(text){ +var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, + MAILTO_REGEXP = /^mailto:/; + +angularFilter.linky = function(text) { if (!text) return text; - var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/; var match; var raw = text; var html = []; var writer = htmlSanitizeWriter(html); var url; var i; - while (match=raw.match(URL)) { + while (match = raw.match(LINKY_URL_REGEXP)) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; // if we did not match ftp/http/mailto then assume mailto - if (match[2]==match[3]) url = 'mailto:' + url; + if (match[2] == match[3]) url = 'mailto:' + url; i = match.index; writer.chars(raw.substr(0, i)); writer.start('a', {href:url}); - writer.chars(match[0].replace(/^mailto:/, '')); + writer.chars(match[0].replace(MAILTO_REGEXP, '')); writer.end('a'); raw = raw.substring(i + match[0].length); } diff --git a/src/parser.js b/src/parser.js index 73733d48..76f9630e 100644 --- a/src/parser.js +++ b/src/parser.js @@ -659,5 +659,116 @@ function parser(text, json){ } } +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// + +function setter(obj, path, setValue) { + var element = path.split('.'); + for (var i = 0; element.length > 1; i++) { + var key = element.shift(); + var propertyObj = obj[key]; + if (!propertyObj) { + propertyObj = {}; + obj[key] = propertyObj; + } + obj = propertyObj; + } + obj[element.shift()] = setValue; + return setValue; +} + +/** + * Return the value accesible from the object by path. Any undefined traversals are ignored + * @param {Object} obj starting object + * @param {string} path path to traverse + * @param {boolean=true} bindFnToScope + * @returns value as accesbile by path + */ +function getter(obj, path, bindFnToScope) { + if (!path) return obj; + var keys = path.split('.'); + var key; + var lastInstance = obj; + var len = keys.length; + + for (var i = 0; i < len; i++) { + key = keys[i]; + if (obj) { + obj = (lastInstance = obj)[key]; + } + if (isUndefined(obj) && key.charAt(0) == '$') { + var type = angularGlobal.typeOf(lastInstance); + type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; + var fn = type ? type[[key.substring(1)]] : _undefined; + if (fn) { + return obj = bind(lastInstance, fn, lastInstance); + } + } + } + if (!bindFnToScope && isFunction(obj)) { + return bind(lastInstance, obj); + } + return obj; +} + +var getterFnCache = {}, + compileCache = {}, + JS_KEYWORDS = {}; + +forEach( + ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + + "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + + "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," + + "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + + "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), + function(key){ JS_KEYWORDS[key] = true;} +); + +function getterFn(path) { + var fn = getterFnCache[path]; + if (fn) return fn; + + var code = 'var l, fn, t;\n'; + forEach(path.split('.'), function(key) { + key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; + code += 'if(!s) return s;\n' + + 'l=s;\n' + + 's=s' + key + ';\n' + + 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' + + key + '.apply(l, arguments); };\n'; + if (key.charAt(1) == '$') { + // special code for super-imposed functions + var name = key.substr(2); + code += 'if(!s) {\n' + + ' t = angular.Global.typeOf(l);\n' + + ' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' + + ' if (fn) s = function(){ return fn.apply(l, ' + + '[l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n' + + '}\n'; + } + }); + code += 'return s;'; + fn = Function('s', code); + fn["toString"] = function(){ return code; }; + + return getterFnCache[path] = fn; +} +/////////////////////////////////// +// TODO(misko): Should this function be public? +function compileExpr(expr) { + return parser(expr).statements(); +} + +// TODO(misko): Deprecate? Remove! +// I think that compilation should be a service. +function expressionCompile(exp) { + if (typeof exp === $function) return exp; + var fn = compileCache[exp]; + if (!fn) { + fn = compileCache[exp] = parser(exp).statements(); + } + return fn; +} diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index eb9d0320..f3211fd2 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -163,9 +163,13 @@ angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) { */ angular.scenario.Runner.prototype.run = function(application) { var self = this; - var $root = angular.scope(this); + var $root = angular.scope(); + angular.extend($root, this); + angular.forEach(angular.scenario.Runner.prototype, function(fn, name) { + $root[name] = angular.bind(self, fn); + }); $root.application = application; - this.emit('RunnerBegin'); + $root.emit('RunnerBegin'); asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) { var dslCache = {}; var runner = self.createSpecRunner_($root); @@ -175,7 +179,7 @@ angular.scenario.Runner.prototype.run = function(application) { angular.forEach(angular.scenario.dsl, function(fn, key) { self.$window[key] = function() { var line = callerFile(3); - var scope = angular.scope(runner); + var scope = runner.$new(); // Make the dsl accessible on the current chain scope.dsl = {}; @@ -200,7 +204,10 @@ angular.scenario.Runner.prototype.run = function(application) { return scope.dsl[key].apply(scope, arguments); }; }); - runner.run(spec, specDone); + runner.run(spec, function() { + runner.$destroy(); + specDone.apply(this, arguments); + }); }, function(error) { if (error) { diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index b4788ad9..2190f7f7 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -245,7 +245,6 @@ angular.scenario.dsl('repeater', function() { chain.row = function(index) { return this.addFutureAction("repeater '" + this.label + "' row '" + index + "'", function($window, $document, done) { - var values = []; var matches = $document.elements().slice(index, index + 1); if (!matches.length) return done('row ' + index + ' out of bounds'); diff --git a/src/service/cookies.js b/src/service/cookies.js index d6be1364..74e63679 100644 --- a/src/service/cookies.js +++ b/src/service/cookies.js @@ -28,7 +28,7 @@ angularServiceInject('$cookies', function($browser) { lastBrowserCookies = currentCookies; copy(currentCookies, lastCookies); copy(currentCookies, cookies); - if (runEval) rootScope.$eval(); + if (runEval) rootScope.$apply(); } })(); @@ -37,7 +37,7 @@ angularServiceInject('$cookies', function($browser) { //at the end of each eval, push cookies //TODO: this should happen before the "delayed" watches fire, because if some cookies are not // strings or browser refuses to store some cookies, we update the model in the push fn. - this.$onEval(PRIORITY_LAST, push); + this.$observe(push); return cookies; diff --git a/src/service/defer.js b/src/service/defer.js index 551e8bc9..0a69912c 100644 --- a/src/service/defer.js +++ b/src/service/defer.js @@ -18,16 +18,11 @@ * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] of milliseconds to defer the function execution. */ -angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) { +angularServiceInject('$defer', function($browser) { + var scope = this; return function(fn, delay) { $browser.defer(function() { - try { - fn(); - } catch(e) { - $exceptionHandler(e); - } finally { - $updateView(); - } + scope.$apply(fn); }, delay); }; }, ['$browser', '$exceptionHandler', '$updateView']); diff --git a/src/service/invalidWidgets.js b/src/service/invalidWidgets.js index b7ef0b53..7c1b2a9f 100644 --- a/src/service/invalidWidgets.js +++ b/src/service/invalidWidgets.js @@ -42,7 +42,7 @@ angularServiceInject("$invalidWidgets", function(){ /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */ - this.$onEval(PRIORITY_LAST, function() { + this.$watch(function() { for(var i = 0; i < invalidWidgets.length;) { var widget = invalidWidgets[i]; if (isOrphan(widget[0])) { @@ -56,7 +56,7 @@ angularServiceInject("$invalidWidgets", function(){ /** - * Traverses DOM element's (widget's) parents and considers the element to be an orphant if one of + * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of * it's parents isn't the current window.document. */ function isOrphan(widget) { diff --git a/src/service/location.js b/src/service/location.js index 1889266e..23531140 100644 --- a/src/service/location.js +++ b/src/service/location.js @@ -69,18 +69,14 @@ var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+) </doc:example> */ angularServiceInject("$location", function($browser) { - var scope = this, - location = {update:update, updateHash: updateHash}, - lastLocation = {}; + var location = {update: update, updateHash: updateHash}; + var lastLocation = {}; // last state since last update(). - $browser.onHashChange(function() { //register + $browser.onHashChange(bind(this, this.$apply, function() { //register update($browser.getUrl()); - copy(location, lastLocation); - scope.$eval(); - })(); //initialize + }))(); //initialize - this.$onEval(PRIORITY_FIRST, sync); - this.$onEval(PRIORITY_LAST, updateBrowser); + this.$watch(sync); return location; @@ -94,6 +90,8 @@ angularServiceInject("$location", function($browser) { * * @description * Updates the location object. + * Does not immediately update the browser + * Browser is updated at the end of $flush() * * Does not immediately update the browser. Instead the browser is updated at the end of $eval() * cycle. @@ -122,6 +120,8 @@ angularServiceInject("$location", function($browser) { location.href = composeHref(location); } + $browser.setUrl(location.href); + copy(location, lastLocation); } /** @@ -188,34 +188,21 @@ angularServiceInject("$location", function($browser) { if (!equals(location, lastLocation)) { if (location.href != lastLocation.href) { update(location.href); - return; - } - if (location.hash != lastLocation.hash) { - var hash = parseHash(location.hash); - updateHash(hash.hashPath, hash.hashSearch); } else { - location.hash = composeHash(location); - location.href = composeHref(location); + if (location.hash != lastLocation.hash) { + var hash = parseHash(location.hash); + updateHash(hash.hashPath, hash.hashSearch); + } else { + location.hash = composeHash(location); + location.href = composeHref(location); + } + update(location.href); } - update(location.href); } } /** - * If location has changed, update the browser - * This method is called at the end of $eval() phase - */ - function updateBrowser() { - sync(); - - if ($browser.getUrl() != location.href) { - $browser.setUrl(location.href); - copy(location, lastLocation); - } - } - - /** * Compose href string from a location object * * @param {Object} loc The location object with all properties diff --git a/src/service/route.js b/src/service/route.js index 9534968a..e1d0e7be 100644 --- a/src/service/route.js +++ b/src/service/route.js @@ -62,7 +62,7 @@ </doc:scenario> </doc:example> */ -angularServiceInject('$route', function(location, $updateView) { +angularServiceInject('$route', function($location, $updateView) { var routes = {}, onChange = [], matcher = switchRouteMatcher, @@ -207,66 +207,67 @@ angularServiceInject('$route', function(location, $updateView) { function updateRoute(){ - var childScope, routeParams, pathParams, segmentMatch, key, redir; + var selectedRoute, pathParams, segmentMatch, key, redir; + if ($route.current && $route.current.scope) { + $route.current.scope.$destroy(); + } $route.current = null; + // Match a route forEach(routes, function(rParams, rPath) { if (!pathParams) { - if (pathParams = matcher(location.hashPath, rPath)) { - routeParams = rParams; + if (pathParams = matcher($location.hashPath, rPath)) { + selectedRoute = rParams; } } }); - // "otherwise" fallback - routeParams = routeParams || routes[null]; + // No route matched; fallback to "otherwise" route + selectedRoute = selectedRoute || routes[null]; - if(routeParams) { - if (routeParams.redirectTo) { - if (isString(routeParams.redirectTo)) { + if(selectedRoute) { + if (selectedRoute.redirectTo) { + if (isString(selectedRoute.redirectTo)) { // interpolate the redirectTo string redir = {hashPath: '', - hashSearch: extend({}, location.hashSearch, pathParams)}; + hashSearch: extend({}, $location.hashSearch, pathParams)}; - forEach(routeParams.redirectTo.split(':'), function(segment, i) { + forEach(selectedRoute.redirectTo.split(':'), function(segment, i) { if (i==0) { redir.hashPath += segment; } else { segmentMatch = segment.match(/(\w+)(.*)/); key = segmentMatch[1]; - redir.hashPath += pathParams[key] || location.hashSearch[key]; + redir.hashPath += pathParams[key] || $location.hashSearch[key]; redir.hashPath += segmentMatch[2] || ''; delete redir.hashSearch[key]; } }); } else { // call custom redirectTo function - redir = {hash: routeParams.redirectTo(pathParams, location.hash, location.hashPath, - location.hashSearch)}; + redir = {hash: selectedRoute.redirectTo(pathParams, $location.hash, $location.hashPath, + $location.hashSearch)}; } - location.update(redir); - $updateView(); //TODO this is to work around the $location<=>$browser issues + $location.update(redir); return; } - childScope = createScope(parentScope); - $route.current = extend({}, routeParams, { - scope: childScope, - params: extend({}, location.hashSearch, pathParams) - }); + $route.current = extend({}, selectedRoute); + $route.current.params = extend({}, $location.hashSearch, pathParams); } //fire onChange callbacks - forEach(onChange, parentScope.$tryEval); + forEach(onChange, parentScope.$eval, parentScope); - if (childScope) { - childScope.$become($route.current.controller); + // Create the scope if we have mtched a route + if ($route.current) { + $route.current.scope = parentScope.$new($route.current.controller); } } - this.$watch(function(){return dirty + location.hash;}, updateRoute); + this.$watch(function(){return dirty + $location.hash;}, updateRoute)(); return $route; }, ['$location', '$updateView']); diff --git a/src/service/updateView.js b/src/service/updateView.js index 9ac7c1fb..b51e719b 100644 --- a/src/service/updateView.js +++ b/src/service/updateView.js @@ -35,8 +35,8 @@ * without angular knowledge and you may need to call '$updateView()' directly. * * Note: if you wish to update the view immediately (without delay), you can do so by calling - * {@link angular.scope.$eval} at any time from your code: - * <pre>scope.$root.$eval()</pre> + * {@link angular.scope.$apply} at any time from your code: + * <pre>scope.$apply()</pre> * * In unit-test mode the update is instantaneous and synchronous to simplify writing tests. * @@ -47,7 +47,7 @@ function serviceUpdateViewFactory($browser){ var scheduled; function update(){ scheduled = false; - rootScope.$eval(); + rootScope.$flush(); } return $browser.isMock ? update : function(){ if (!scheduled) { diff --git a/src/service/xhr.bulk.js b/src/service/xhr.bulk.js index d7fc7990..816336f8 100644 --- a/src/service/xhr.bulk.js +++ b/src/service/xhr.bulk.js @@ -82,6 +82,6 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ } }); }; - this.$onEval(PRIORITY_LAST, bulkXHR.flush); + this.$observe(bulkXHR.flush); return bulkXHR; }, ['$xhr', '$xhr.error', '$log']); diff --git a/src/widgets.js b/src/widgets.js index 04d64eee..a2a4109b 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -183,9 +183,7 @@ function modelAccessor(scope, element) { }, set: function(value) { if (value !== undefined) { - return scope.$tryEval(function(){ - assignFn(scope, value); - }, element); + assignFn(scope, value); } } }; @@ -332,7 +330,7 @@ function valueAccessor(scope, element) { format = formatter.format; parse = formatter.parse; if (requiredExpr) { - scope.$watch(requiredExpr, function(newValue) { + scope.$watch(requiredExpr, function(scope, newValue) { required = newValue; validate(); }); @@ -529,32 +527,33 @@ function radioInit(model, view, element) { </doc:example> */ function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) { - return annotate('$updateView', '$defer', function($updateView, $defer, element) { + return annotate('$defer', function($defer, element) { var scope = this, model = modelAccessor(scope, element), view = viewAccessor(scope, element), - action = element.attr('ng:change') || '', + action = element.attr('ng:change') || noop, lastValue; if (model) { initFn.call(scope, model, view, element); - this.$eval(element.attr('ng:init')||''); + scope.$eval(element.attr('ng:init') || noop); element.bind(events, function(event){ function handler(){ - var value = view.get(); - if (!textBox || value != lastValue) { - model.set(value); - lastValue = model.get(); - scope.$tryEval(action, element); - $updateView(); - } + scope.$apply(function() { + var value = view.get(); + if (!textBox || value != lastValue) { + model.set(value); + lastValue = model.get(); + scope.$eval(action); + } + }); } event.type == 'keydown' ? $defer(handler) : handler(); }); - scope.$watch(model.get, function(value){ - if (lastValue !== value) { + scope.$watch(model.get, function(scope, value) { + if (!equals(lastValue, value)) { view.set(lastValue = value); } - }); + })(); } }); } @@ -693,7 +692,7 @@ angularWidget('select', function(element){ var isMultiselect = element.attr('multiple'), expression = element.attr('ng:options'), - onChange = expressionCompile(element.attr('ng:change') || "").fnSelf, + onChange = expressionCompile(element.attr('ng:change') || ""), match; if (!expression) { @@ -705,12 +704,12 @@ angularWidget('select', function(element){ " but got '" + expression + "'."); } - var displayFn = expressionCompile(match[2] || match[1]).fnSelf, + var displayFn = expressionCompile(match[2] || match[1]), valueName = match[4] || match[6], keyName = match[5], - groupByFn = expressionCompile(match[3] || '').fnSelf, - valueFn = expressionCompile(match[2] ? match[1] : valueName).fnSelf, - valuesFn = expressionCompile(match[7]).fnSelf, + groupByFn = expressionCompile(match[3] || ''), + valueFn = expressionCompile(match[2] ? match[1] : valueName), + valuesFn = expressionCompile(match[7]), // we can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. optionTemplate = jqLite(document.createElement('option')), @@ -773,17 +772,14 @@ angularWidget('select', function(element){ onChange(scope); model.set(value); } - scope.$tryEval(function(){ - scope.$root.$eval(); - }); + scope.$root.$apply(); } finally { tempScope = null; // TODO(misko): needs to be $destroy } }); - scope.$onEval(function(){ - var scope = this, - optionGroups = {'':[]}, // Temporary location for the option groups before we render them + scope.$observe(function(scope) { + var optionGroups = {'':[]}, // Temporary location for the option groups before we render them optionGroupNames = [''], optionGroupName, optionGroup, @@ -934,7 +930,7 @@ angularWidget('select', function(element){ optionGroupsCache.pop()[0].element.remove(); } } finally { - optionScope = null; // TODO(misko): needs to be $destroy() + optionScope.$destroy(); } }); }; @@ -998,33 +994,36 @@ angularWidget('ng:include', function(element){ } else { element[0]['ng:compiled'] = true; return extend(function(xhr, element){ - var scope = this, childScope; - var changeCounter = 0; - var preventRecursion = false; + var scope = this, + changeCounter = 0, + releaseScopes = [], + childScope, + oldScope; + function incrementChange(){ changeCounter++;} - this.$watch(srcExp, incrementChange); - this.$watch(scopeExp, incrementChange); - - // note that this propagates eval to the current childScope, where childScope is dynamically - // bound (via $route.onChange callback) to the current scope created by $route - scope.$onEval(function(){ - if (childScope && !preventRecursion) { - preventRecursion = true; - try { - childScope.$eval(); - } finally { - preventRecursion = false; - } + this.$observe(srcExp, incrementChange); + this.$observe(function(scope){ + var newScope = scope.$eval(scopeExp); + if (newScope !== oldScope) { + oldScope = newScope; + incrementChange(); } }); - this.$watch(function(){return changeCounter;}, function(){ - var src = this.$eval(srcExp), - useScope = this.$eval(scopeExp); + this.$observe(function(){return changeCounter;}, function(scope) { + var src = scope.$eval(srcExp), + useScope = scope.$eval(scopeExp); + while(releaseScopes.length) { + releaseScopes.pop().$destroy(); + } if (src) { xhr('GET', src, null, function(code, response){ element.html(response); - childScope = useScope || createScope(scope); + if (useScope) { + childScope = useScope; + } else { + releaseScopes.push(childScope = scope.$new()); + } compiler.compile(element)(childScope); scope.$eval(onloadExp); }, false, true); @@ -1091,69 +1090,56 @@ angularWidget('ng:include', function(element){ </doc:scenario> </doc:example> */ -//TODO(im): remove all the code related to using and inline equals -var ngSwitch = angularWidget('ng:switch', function (element){ +angularWidget('ng:switch', function (element) { var compiler = this, watchExpr = element.attr("on"), - usingExpr = (element.attr("using") || 'equals'), - usingExprParams = usingExpr.split(":"), - usingFn = ngSwitch[usingExprParams.shift()], - changeExpr = element.attr('change') || '', - cases = []; - if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; - if (!watchExpr) throw "Missing 'on' attribute."; - eachNode(element, function(caseElement){ - var when = caseElement.attr('ng:switch-when'); - var switchCase = { - change: changeExpr, - element: caseElement, - template: compiler.compile(caseElement) - }; + changeExpr = element.attr('change'), + casesTemplate = {}, + defaultCaseTemplate, + children = element.children(), + length = children.length, + child, + when; + + if (!watchExpr) throw new Error("Missing 'on' attribute."); + while(length--) { + child = jqLite(children[length]); + // this needs to be here for IE + child.remove(); + when = child.attr('ng:switch-when'); if (isString(when)) { - switchCase.when = function(scope, value){ - var args = [value, when]; - forEach(usingExprParams, function(arg){ - args.push(arg); - }); - return usingFn.apply(scope, args); - }; - cases.unshift(switchCase); - } else if (isString(caseElement.attr('ng:switch-default'))) { - switchCase.when = valueFn(true); - cases.push(switchCase); + casesTemplate[when] = compiler.compile(child); + } else if (isString(child.attr('ng:switch-default'))) { + defaultCaseTemplate = compiler.compile(child); } - }); - - // this needs to be here for IE - forEach(cases, function(_case){ - _case.element.remove(); - }); - + } + children = null; // release memory; element.html(''); + return function(element){ - var scope = this, childScope; - this.$watch(watchExpr, function(value){ - var found = false; + var changeCounter = 0; + var childScope; + var selectedTemplate; + + this.$watch(watchExpr, function(scope, value) { element.html(''); - childScope = createScope(scope); - forEach(cases, function(switchCase){ - if (!found && switchCase.when(childScope, value)) { - found = true; - childScope.$tryEval(switchCase.change, element); - switchCase.template(childScope, function(caseElement){ - element.append(caseElement); - }); - } - }); - }); - scope.$onEval(function(){ - if (childScope) childScope.$eval(); + if (selectedTemplate = casesTemplate[value] || defaultCaseTemplate) { + changeCounter++; + if (childScope) childScope.$destroy(); + childScope = scope.$new(); + childScope.$eval(changeExpr); + } + })(); + + this.$observe(function(){return changeCounter;}, function() { + element.html(''); + if (selectedTemplate) { + selectedTemplate(childScope, function(caseElement) { + element.append(caseElement); + }); + } }); }; -}, { - equals: function(on, when) { - return ''+on == when; - } }); @@ -1267,15 +1253,16 @@ angularWidget('@ng:repeat', function(expression, element){ valueIdent = match[3] || match[1]; keyIdent = match[2]; - var children = [], currentScope = this; - this.$onEval(function(){ + var childScopes = []; + var childElements = [iterStartElement]; + var parentScope = this; + this.$observe(function(scope){ var index = 0, - childCount = children.length, - lastIterElement = iterStartElement, - collection = this.$tryEval(rhs, iterStartElement), + childCount = childScopes.length, + collection = scope.$eval(rhs), collectionLength = size(collection, true), - fragment = (element[0].nodeName != 'OPTION') ? document.createDocumentFragment() : null, - addFragment, + fragment = document.createDocumentFragment(), + addFragmentTo = (childCount < collectionLength) ? childElements[childCount] : null, childScope, key; @@ -1283,35 +1270,32 @@ angularWidget('@ng:repeat', function(expression, element){ if (collection.hasOwnProperty(key)) { if (index < childCount) { // reuse existing child - childScope = children[index]; + childScope = childScopes[index]; childScope[valueIdent] = collection[key]; if (keyIdent) childScope[keyIdent] = key; - lastIterElement = childScope.$element; childScope.$position = index == 0 ? 'first' : (index == collectionLength - 1 ? 'last' : 'middle'); childScope.$eval(); } else { // grow children - childScope = createScope(currentScope); + childScope = parentScope.$new(); childScope[valueIdent] = collection[key]; if (keyIdent) childScope[keyIdent] = key; childScope.$index = index; childScope.$position = index == 0 ? 'first' : (index == collectionLength - 1 ? 'last' : 'middle'); - children.push(childScope); + childScopes.push(childScope); linker(childScope, function(clone){ clone.attr('ng:repeat-index', index); - - if (fragment) { - fragment.appendChild(clone[0]); - addFragment = true; - } else { - //temporarily preserve old way for option element - lastIterElement.after(clone); - lastIterElement = clone; - } + fragment.appendChild(clone[0]); + // TODO(misko): Temporary hack - maybe think about it - removed after we add fragment after $flush() + // This causes double $flush for children + // The first flush will couse a lot of DOM access (initial) + // Second flush shuld be noop since nothing has change hence no DOM access. + childScope.$flush(); + childElements[index + 1] = clone; }); } index ++; @@ -1319,15 +1303,19 @@ angularWidget('@ng:repeat', function(expression, element){ } //attach new nodes buffered in doc fragment - if (addFragment) { - lastIterElement.after(jqLite(fragment)); + if (addFragmentTo) { + // TODO(misko): For performance reasons, we should do the addition after all other widgets + // have run. For this should happend after $flush() is done! + addFragmentTo.after(jqLite(fragment)); } // shrink children - while(children.length > index) { - children.pop().$element.remove(); + while(childScopes.length > index) { + // can not use $destroy(true) since there may be multiple iterators on same parent. + childScopes.pop().$destroy(); + childElements.pop().remove(); } - }, iterStartElement); + }); }; }); @@ -1438,39 +1426,29 @@ angularWidget('ng:view', function(element) { if (!element[0]['ng:compiled']) { element[0]['ng:compiled'] = true; return annotate('$xhr.cache', '$route', function($xhr, $route, element){ - var parentScope = this, - childScope; + var template; + var changeCounter = 0; $route.onChange(function(){ - var src; - - if ($route.current) { - src = $route.current.template; - childScope = $route.current.scope; - } + changeCounter++; + })(); //initialize the state forcefully, it's possible that we missed the initial + //$route#onChange already - if (src) { + this.$observe(function(){return changeCounter;}, function() { + var template = $route.current && $route.current.template; + if (template) { //xhr's callback must be async, see commit history for more info - $xhr('GET', src, function(code, response){ + $xhr('GET', template, function(code, response) { element.html(response); - compiler.compile(element)(childScope); + compiler.compile(element)($route.current.scope); }); } else { element.html(''); } - })(); //initialize the state forcefully, it's possible that we missed the initial - //$route#onChange already - - // note that this propagates eval to the current childScope, where childScope is dynamically - // bound (via $route.onChange callback) to the current scope created by $route - parentScope.$onEval(function() { - if (childScope) { - childScope.$eval(); - } }); }); } else { - this.descend(true); - this.directives(true); + compiler.descend(true); + compiler.directives(true); } }); |
