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