aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMisko Hevery2011-03-23 09:33:29 -0700
committerVojta Jina2011-08-02 01:00:03 +0200
commit8f0dcbab804180828d6859b1340c86cf161209fb (patch)
treed13d47d47a1889cb7c96a87cecacd2e25307d51c /src
parent1f4b417184ce53af15474de065400f8a686430c5 (diff)
downloadangular.js-8f0dcbab804180828d6859b1340c86cf161209fb.tar.bz2
feat(scope): new and improved scope implementation
- Speed improvements (about 4x on flush phase) - Memory improvements (uses no function closures) - Break $eval into $apply, $dispatch, $flush - Introduced $watch and $observe Breaks angular.equals() use === instead of == Breaks angular.scope() does not take parent as first argument Breaks scope.$watch() takes scope as first argument Breaks scope.$set(), scope.$get are removed Breaks scope.$config is removed Breaks $route.onChange callback has not "this" bounded
Diffstat (limited to 'src')
-rw-r--r--src/Angular.js41
-rw-r--r--src/Browser.js2
-rw-r--r--src/Compiler.js35
-rw-r--r--src/JSON.js3
-rw-r--r--src/Scope.js1269
-rw-r--r--src/angular-mocks.js2
-rw-r--r--src/apis.js24
-rw-r--r--src/directives.js158
-rw-r--r--src/filters.js13
-rw-r--r--src/parser.js111
-rw-r--r--src/scenario/Runner.js15
-rw-r--r--src/scenario/dsl.js1
-rw-r--r--src/service/cookies.js4
-rw-r--r--src/service/defer.js11
-rw-r--r--src/service/invalidWidgets.js4
-rw-r--r--src/service/location.js47
-rw-r--r--src/service/route.js51
-rw-r--r--src/service/updateView.js6
-rw-r--r--src/service/xhr.bulk.js2
-rw-r--r--src/widgets.js278
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);
}
});