diff options
| author | Misko Hevery | 2010-04-16 14:01:29 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2010-04-16 14:01:29 -0700 | 
| commit | deb86fe357a901889bc4289087f0b9e69cb8a302 (patch) | |
| tree | fce4db8501a6c24430d611c95a4aa001119c7b89 | |
| parent | 70e401ef100614295fc808e32f0142f07c315461 (diff) | |
| download | angular.js-deb86fe357a901889bc4289087f0b9e69cb8a302.tar.bz2 | |
lots of small fixes
| -rw-r--r-- | angular-debug.js | 217 | ||||
| -rw-r--r-- | example/memoryLeak.html | 4 | ||||
| -rw-r--r-- | example/temp.html | 2 | ||||
| -rw-r--r-- | example/tweeter/tweeter_addressbook.html | 16 | ||||
| -rw-r--r-- | example/tweeter/tweeter_demo.html | 2 | ||||
| -rw-r--r-- | src/Angular.js | 8 | ||||
| -rw-r--r-- | src/Browser.js | 9 | ||||
| -rw-r--r-- | src/Scope.js | 12 | ||||
| -rw-r--r-- | src/directives.js | 9 | ||||
| -rw-r--r-- | src/jqLite.js | 2 | ||||
| -rw-r--r-- | src/services.js | 50 | ||||
| -rw-r--r-- | src/validators.js | 57 | ||||
| -rw-r--r-- | src/widgets.js | 35 | ||||
| -rw-r--r-- | test/ValidatorsTest.js | 34 | ||||
| -rw-r--r-- | test/directivesSpec.js | 16 | ||||
| -rw-r--r-- | test/servicesSpec.js | 113 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 22 | 
17 files changed, 424 insertions, 184 deletions
| diff --git a/angular-debug.js b/angular-debug.js index fd9b9902..c3971b35 100644 --- a/angular-debug.js +++ b/angular-debug.js @@ -62,6 +62,12 @@ function foreach(obj, iterator, context) {    if (obj) {      if (obj.forEach) {        obj.forEach(iterator, context); +    } else if (isFunction(obj)){ +      for (key in obj) { +        if (key != 'prototype' && key != 'length' && key != 'name') { +          iterator.call(context, obj[key], key); +        } +      }      } else if (isObject(obj) && isNumber(obj.length)) {        for (key = 0; key < obj.length; key++)          iterator.call(context, obj[key], key); @@ -137,7 +143,7 @@ function isElement(node) {  function isVisible(element) {    var rect = element[0].getBoundingClientRect(); -  return rect.width !=0 && rect.height !=0; +  return rect.width && rect.height;  }  function map(obj, iterator, context) { @@ -771,7 +777,7 @@ function createScope(parent, services, existing) {    function API(){}    function Behavior(){} -  var instance, behavior, api, evalLists = {}, servicesCache = extend({}, existing); +  var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing);    parent = Parent.prototype = (parent || {});    api = API.prototype = new Parent(); @@ -790,7 +796,7 @@ function createScope(parent, services, existing) {        if (isDefined(exp)) {          return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));        } else { -        foreachSorted(evalLists, function(list) { +        foreach(evalLists.sorted, function(list) {            foreach(list, function(eval) {              instance.$tryEval(eval.fn, eval.handler);            }); @@ -833,7 +839,13 @@ function createScope(parent, services, existing) {          expr = priority;          priority = 0;        } -      var evalList = evalLists[priority] || (evalLists[priority] = []); +      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;}); +      }        evalList.push({          fn: expressionCompile(expr),          handler: exceptionHandler @@ -1820,10 +1832,11 @@ Browser.prototype = {    setUrl: function(url) {     var existingURL = this.location.href; -   if (!existingURL.match(/#/)) -     existingURL += '#'; -   if (existingURL != url) -     this.location.href = url; +   if (!existingURL.match(/#/)) existingURL += '#'; +   if (!url.match(/#/)) url += '#'; +   if (existingURL != url) { +     this.location.href = this.expectedUrl = url; +   }    },    getUrl: function() { @@ -2017,7 +2030,7 @@ JQLite.prototype = {      } else if (isDefined(value)) {        e.setAttribute(name, value);      } else { -      return e.getAttribute(name); +      return e.getAttribute ? e.getAttribute(name) : undefined;      }    }, @@ -2790,33 +2803,52 @@ foreach({      }    }, -  'asynchronous': function(text, asynchronousFn) { -    var element = this['$element']; -    var cache = element.data('$validateState'); +  /* +   * cache is attached to the element +   * cache: { +   *   inputs : { +   *     'user input': { +   *        response: server response, +   *        error: validation error +   *     }, +   *   current: 'current input' +   * } +   * +   */ +  'asynchronous': function(input, asynchronousFn, updateFn) { +    if (!input) return; +    var scope = this; +    var element = scope.$element; +    var cache = element.data('$asyncValidator');      if (!cache) { -      cache = { state: {}}; -      element.data('$validateState', cache); +      element.data('$asyncValidator', cache = {inputs:{}});      } -    var state = cache.state[text]; -    cache.lastKey = text; -    if (state === undefined) { -      // we have never seen this before, Request it + +    cache.current = input; + +    var inputState = cache.inputs[input]; +    if (!inputState) { +      cache.inputs[input] = inputState = { inFlight: true }; +      scope.$invalidWidgets.markInvalid(scope.$element);        element.addClass('ng-input-indicator-wait'); -      state = cache.state[text] = null; -      (asynchronousFn || noop)(text, function(error){ -        state = cache.state[text] = error ? error : false; -        if (cache.state[cache.lastKey] !== null) { +      asynchronousFn(input, function(error, data) { +        inputState.response = data; +        inputState.error = error; +        inputState.inFlight = false; +        if (cache.current == input) {            element.removeClass('ng-input-indicator-wait'); +          scope.$invalidWidgets.markValid(element);          } -        elementError(element, NG_VALIDATION_ERROR, error); +        element.data('$validate')(input); +        scope.$root.$eval();        }); -    } - -    if (state === null && this['$invalidWidgets']){ +    } else if (inputState.inFlight) {        // request in flight, mark widget invalid, but don't show it to user -      this['$invalidWidgets'].markInvalid(this.$element); +      scope.$invalidWidgets.markInvalid(scope.$element); +    } else { +      (updateFn||noop)(inputState.response);      } -    return state; +    return inputState.error;    }  }, function(v,k) {angularValidator[k] = v;}); @@ -2924,8 +2956,13 @@ angularDirective("ng-bind-attr", function(expression){      this.$onEval(function(){        foreach(this.$eval(expression), function(bindExp, key) {          var value = compileBindTemplate(bindExp).call(this, element); -        if (REMOVE_ATTRIBUTES[lowercase(key)] && !toBoolean(value)) { -          element.removeAttr('disabled'); +        if (REMOVE_ATTRIBUTES[lowercase(key)]) { +          if (!toBoolean(value)) { +            element.removeAttr('disabled'); +          } else { +            element.attr(key, value); +          } +          (element.data('$validate')||noop)();          } else {            element.attr(key, value);          } @@ -3165,6 +3202,11 @@ function valueAccessor(scope, element) {    required = required || required === '';    if (!validator) throw "Validator named '" + validatorName + "' not found.";    function validate(value) { +    if (element[0].disabled || isString(element.attr('readonly'))) { +      elementError(element, NG_VALIDATION_ERROR, null); +      invalidWidgets.markValid(element); +      return value; +    }      var error,          validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element});      error = required && !trim(value) ? @@ -3180,6 +3222,7 @@ function valueAccessor(scope, element) {      }      return value;    } +  element.data('$validate', validate);    return {      get: function(){ return validate(element.val()); },      set: function(value){ element.val(validate(value)); } @@ -3305,20 +3348,31 @@ angularWidget('SELECT', function(element){  angularWidget('NG:INCLUDE', function(element){    var compiler = this, -      src = element.attr("src"); -  if (element.attr('switch-instance')) { +      srcExp = element.attr("src"), +      scopeExp = element.attr("scope") || ''; +  if (element[0]['ng-compiled']) {      this.descend(true);      this.directives(true);    } else { +    element[0]['ng-compiled'] = true;      return function(element){        var scope = this, childScope; -      element.attr('switch-instance', 'compiled'); -      scope.$browser.xhr('GET', src, function(code, response){ -        element.html(response); -        childScope = createScope(scope); -        compiler.compile(element)(element, childScope); -        childScope.$init(); -        scope.$root.$eval(); +      var changeCounter = 0; +      function incrementChange(){ changeCounter++;} +      this.$watch(srcExp, incrementChange); +      this.$watch(scopeExp, incrementChange); +      this.$watch(function(){return changeCounter;}, function(){ +        var src = this.$eval(srcExp), +        useScope = this.$eval(scopeExp); +        if (src) { +          scope.$browser.xhr('GET', src, function(code, response){ +            element.html(response); +            childScope = useScope || createScope(scope); +            compiler.compile(element)(element, childScope); +            childScope.$init(); +            scope.$root.$eval(); +          }); +        }        });        scope.$onEval(function(){          if (childScope) childScope.$eval(); @@ -3405,12 +3459,13 @@ angularService("$document", function(window){    return jqLite(window.document);  }, {inject:['$window']}); -var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?((#([^\?]*))?(\?([^\?]*))?)$/; +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/; +var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/;  var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};  angularService("$location", function(browser){ -  var scope = this, location = {parse:parse, toString:toString}; -  var lastHash; -  function parse(url){ +  var scope = this, location = {parse:parseUrl, toString:toString}; +  var lastHash, lastUrl; +  function parseUrl(url){      if (isDefined(url)) {        var match = URL_MATCH.exec(url);        if (match) { @@ -3420,38 +3475,46 @@ angularService("$location", function(browser){          location.port = match[5] || DEFAULT_PORTS[location.href] || null;          location.path = match[6];          location.search = parseKeyValue(match[8]); -        location.hash = match[9]; +        location.hash = match[9] || '';          if (location.hash)            location.hash = location.hash.substr(1); -        lastHash = location.hash; -        location.hashPath = match[11] || ''; -        location.hashSearch = parseKeyValue(match[13]); +        parseHash(location.hash);        }      }    } +  function parseHash(hash) { +    var match = HASH_MATCH.exec(hash); +    location.hashPath = match[1] || ''; +    location.hashSearch = parseKeyValue(match[3]); +    lastHash = hash; +  }    function toString() {      if (lastHash === location.hash) {        var hashKeyValue = toKeyValue(location.hashSearch),            hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''),            url = location.href.split('#')[0] + '#' + (hash ? hash : ''); -      if (url !== location.href) parse(url); +      if (url !== location.href) parseUrl(url);        return url;      } else { -      parse(location.href.split('#')[0] + '#' + location.hash); +      parseUrl(location.href.split('#')[0] + '#' + location.hash);        return toString();      }    }    browser.watchUrl(function(url){ -    parse(url); +    parseUrl(url);      scope.$root.$eval();    }); -  parse(browser.getUrl()); -  var lastURL; +  parseUrl(browser.getUrl()); +  this.$onEval(PRIORITY_FIRST, function(){ +    if (location.hash != lastHash) { +      parseHash(location.hash); +    } +  });    this.$onEval(PRIORITY_LAST, function(){      var url = toString(); -    if (lastURL != url) { +    if (lastUrl != url) {        browser.setUrl(url); -      lastURL = url; +      lastUrl = url;      }    });    return location; @@ -3539,6 +3602,51 @@ angularService("$invalidWidgets", function(){    }    return invalidWidgets;  }); + +angularService('$route', function(location, params){ +  var routes = {}, +      onChange = [], +      matcher = angularWidget('NG:SWITCH').route, +      parentScope = this, +      $route = { +        routes: routes, +        onChange: bind(onChange, onChange.push), +        when:function (path, params){ +          if (angular.isUndefined(path)) return routes; +          var route = routes[path]; +          if (!route) route = routes[path] = {}; +          if (params) angular.extend(route, params); +          if (matcher(location.hashPath, path)) updateRoute(); +          return route; +        } +      }; +  function updateRoute(){ +    console.log('updating route'); +    var childScope; +    $route.current = null; +    angular.foreach(routes, function(routeParams, route) { +      if (!childScope) { +        var pathParams = matcher(location.hashPath, route); +        if (pathParams) { +          console.log('new route', routeParams.template, location.hashPath, location.hash); +          childScope = angular.scope(parentScope); +          $route.current = angular.extend({}, routeParams, { +            scope: childScope, +            params: angular.extend({}, location.hashSearch, pathParams) +          }); +        } +      } +    }); +    angular.foreach(onChange, parentScope.$tryEval); +    if (childScope) { +      childScope.$become($route.current.controller); +      parentScope.$tryEval(childScope.init); +    } +  } +  this.$watch(function(){return location.hash;}, updateRoute); +  return $route; +}, {inject: ['$location']}); +  var browserSingleton;  angularService('$browser', function browserFactory(){    if (!browserSingleton) { @@ -3557,6 +3665,7 @@ extend(angular, {    'extend': extend,    'foreach': foreach,    'noop':noop, +  'bind':bind,    'identity':identity,    'isUndefined': isUndefined,    'isDefined': isDefined, diff --git a/example/memoryLeak.html b/example/memoryLeak.html index bdfe3faf..9e5f512d 100644 --- a/example/memoryLeak.html +++ b/example/memoryLeak.html @@ -48,8 +48,8 @@      <link rel="StyleSheet" type="text/css" href="../css/angular.css"/>    </head>    <body> -  <input type="button" value="add" ng-action="add()"/> -  <input type="button" value="remove" ng-action="remove()"/> +  <input type="button" value="add" ng-click="add()"/> +  <input type="button" value="remove" ng-click="remove()"/>    <div id="partial"></div>    </body>  </html> diff --git a/example/temp.html b/example/temp.html index 3580249d..d6414417 100644 --- a/example/temp.html +++ b/example/temp.html @@ -4,6 +4,6 @@      <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script>    </head>    <body> -    <a href="#"> {{'first'}}<br/>{{'second'}}</a> +    <a href="#" ng-click="$window.location.hash='123'"> {{'first'}}<br/>{{'second'}}</a>    </body>  </html> diff --git a/example/tweeter/tweeter_addressbook.html b/example/tweeter/tweeter_addressbook.html index a2c0f52d..4844c035 100644 --- a/example/tweeter/tweeter_addressbook.html +++ b/example/tweeter/tweeter_addressbook.html @@ -14,12 +14,12 @@        [ Filter: <input type="text" name="userFilter"/>]        <ul>          <li ng-repeat="user in users.$filter(userFilter).$orderBy('screen_name')" ng-class-even="'even'" ng-class-odd="'odd'"> -          <a href="" ng-action="$anchor.user=user.screen_name"><img src="{{user.profile_image_url}}"/></a> -          <a href="" ng-action="$anchor.user=user.screen_name">{{user.screen_name}}</a> +          <a href="" ng-click="$anchor.user=user.screen_name"><img src="{{user.profile_image_url}}"/></a> +          <a href="" ng-click="$anchor.user=user.screen_name">{{user.screen_name}}</a>            as <span class="nickname">{{user.name}}</span> -          [ <a href="#" ng-action="$anchor.edituser=user.screen_name">edit</a> -          | <a href="#" ng-action="users.$remove(user)">X</a> -          | <a href="#" ng-action="mute[user.screen_name] = ! mute[user.screen_name]">mute</a> +          [ <a href="#" ng-click="$anchor.edituser=user.screen_name">edit</a> +          | <a href="#" ng-click="users.$remove(user)">X</a> +          | <a href="#" ng-click="mute[user.screen_name] = ! mute[user.screen_name]">mute</a>            ]            <div class="notes">{{user.notes|linky}}</div>            <div class="clrleft"></div> @@ -37,7 +37,7 @@            <label>Notes:</label>            <textarea type="text" name="user.notes"></textarea> -          <input type="button" ng-action="$anchor.edituser=undefined" value="Close"/> +          <input type="button" ng-click="$anchor.edituser=undefined" value="Close"/>          </div>        </div>        <hr/> @@ -66,8 +66,8 @@ tweets={{tweets}}             ng-class-even="'even'" ng-class-odd="'odd'"             ng-eval="user = users.$find({: $.screen_name == tweet.user.screen_name}) || tweet.user">           <img src="{{user.profile_image_url}}"/> -         [ <a href="" ng-action="$anchor.user=user.screen_name">{{user.nickname || user.name || user.screen_name }}</a> -         | <a href="" ng-action="users.$includeIf(user, true)">+</a> +         [ <a href="" ng-click="$anchor.user=user.screen_name">{{user.nickname || user.name || user.screen_name }}</a> +         | <a href="" ng-click="users.$includeIf(user, true)">+</a>           ]:           {{tweet.text | linky}}           <span class="notes">{{tweet.created_at}}</span> diff --git a/example/tweeter/tweeter_demo.html b/example/tweeter/tweeter_demo.html index e3c4f739..138d4e2b 100644 --- a/example/tweeter/tweeter_demo.html +++ b/example/tweeter/tweeter_demo.html @@ -20,7 +20,7 @@         <li Xng-repeat="tweet in tweets"             ng-class-even="'even'" ng-class-odd="'odd'">           <img src="{{tweet.user.profile_image_url}}"/> -         [ <a href="" Xng-action="$anchor.user=tweet.user.screen_name">{{tweet.user.nickname || tweet.user.name || tweet.user.screen_name }}</a> +         [ <a href="" Xng-click="$anchor.user=tweet.user.screen_name">{{tweet.user.nickname || tweet.user.name || tweet.user.screen_name }}</a>           ]:           {{tweet.text}} (TODO: I want urls as links)           <span class="notes">{{tweet.created_at}}</span> diff --git a/src/Angular.js b/src/Angular.js index 88aab8e7..87a2f3d6 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -38,6 +38,12 @@ function foreach(obj, iterator, context) {    if (obj) {      if (obj.forEach) {        obj.forEach(iterator, context); +    } else if (isFunction(obj)){ +      for (key in obj) { +        if (key != 'prototype' && key != 'length' && key != 'name') { +          iterator.call(context, obj[key], key); +        } +      }      } else if (isObject(obj) && isNumber(obj.length)) {        for (key = 0; key < obj.length; key++)          iterator.call(context, obj[key], key); @@ -113,7 +119,7 @@ function isElement(node) {  function isVisible(element) {    var rect = element[0].getBoundingClientRect(); -  return rect.width !=0 && rect.height !=0; +  return rect.width && rect.height;  }  function map(obj, iterator, context) { diff --git a/src/Browser.js b/src/Browser.js index 69f3eb9a..ff8d9775 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -83,10 +83,11 @@ Browser.prototype = {    setUrl: function(url) {     var existingURL = this.location.href; -   if (!existingURL.match(/#/)) -     existingURL += '#'; -   if (existingURL != url) -     this.location.href = url; +   if (!existingURL.match(/#/)) existingURL += '#'; +   if (!url.match(/#/)) url += '#'; +   if (existingURL != url) { +     this.location.href = this.expectedUrl = url; +   }    },    getUrl: function() { diff --git a/src/Scope.js b/src/Scope.js index 8d44f4ef..54e75dbd 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -81,7 +81,7 @@ function createScope(parent, services, existing) {    function API(){}    function Behavior(){} -  var instance, behavior, api, evalLists = {}, servicesCache = extend({}, existing); +  var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing);    parent = Parent.prototype = (parent || {});    api = API.prototype = new Parent(); @@ -100,7 +100,7 @@ function createScope(parent, services, existing) {        if (isDefined(exp)) {          return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));        } else { -        foreachSorted(evalLists, function(list) { +        foreach(evalLists.sorted, function(list) {            foreach(list, function(eval) {              instance.$tryEval(eval.fn, eval.handler);            }); @@ -143,7 +143,13 @@ function createScope(parent, services, existing) {          expr = priority;          priority = 0;        } -      var evalList = evalLists[priority] || (evalLists[priority] = []); +      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;}); +      }        evalList.push({          fn: expressionCompile(expr),          handler: exceptionHandler diff --git a/src/directives.js b/src/directives.js index 2ead4979..a37076d4 100644 --- a/src/directives.js +++ b/src/directives.js @@ -102,8 +102,13 @@ angularDirective("ng-bind-attr", function(expression){      this.$onEval(function(){        foreach(this.$eval(expression), function(bindExp, key) {          var value = compileBindTemplate(bindExp).call(this, element); -        if (REMOVE_ATTRIBUTES[lowercase(key)] && !toBoolean(value)) { -          element.removeAttr('disabled'); +        if (REMOVE_ATTRIBUTES[lowercase(key)]) { +          if (!toBoolean(value)) { +            element.removeAttr('disabled'); +          } else { +            element.attr(key, value); +          } +          (element.data('$validate')||noop)();          } else {            element.attr(key, value);          } diff --git a/src/jqLite.js b/src/jqLite.js index d4c5492c..92bc22a7 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -185,7 +185,7 @@ JQLite.prototype = {      } else if (isDefined(value)) {        e.setAttribute(name, value);      } else { -      return e.getAttribute(name); +      return e.getAttribute ? e.getAttribute(name) : undefined;      }    }, diff --git a/src/services.js b/src/services.js index 90a5bb85..1d3ba006 100644 --- a/src/services.js +++ b/src/services.js @@ -3,12 +3,13 @@ angularService("$document", function(window){    return jqLite(window.document);  }, {inject:['$window']}); -var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?((#([^\?]*))?(\?([^\?]*))?)$/; +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/; +var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/;  var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};  angularService("$location", function(browser){ -  var scope = this, location = {parse:parse, toString:toString}; -  var lastHash; -  function parse(url){ +  var scope = this, location = {parse:parseUrl, toString:toString}; +  var lastHash, lastUrl; +  function parseUrl(url){      if (isDefined(url)) {        var match = URL_MATCH.exec(url);        if (match) { @@ -18,38 +19,46 @@ angularService("$location", function(browser){          location.port = match[5] || DEFAULT_PORTS[location.href] || null;          location.path = match[6];          location.search = parseKeyValue(match[8]); -        location.hash = match[9]; +        location.hash = match[9] || '';          if (location.hash)            location.hash = location.hash.substr(1); -        lastHash = location.hash; -        location.hashPath = match[11] || ''; -        location.hashSearch = parseKeyValue(match[13]); +        parseHash(location.hash);        }      }    } +  function parseHash(hash) { +    var match = HASH_MATCH.exec(hash); +    location.hashPath = match[1] || ''; +    location.hashSearch = parseKeyValue(match[3]); +    lastHash = hash; +  }    function toString() {      if (lastHash === location.hash) {        var hashKeyValue = toKeyValue(location.hashSearch),            hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''),            url = location.href.split('#')[0] + '#' + (hash ? hash : ''); -      if (url !== location.href) parse(url); +      if (url !== location.href) parseUrl(url);        return url;      } else { -      parse(location.href.split('#')[0] + '#' + location.hash); +      parseUrl(location.href.split('#')[0] + '#' + location.hash);        return toString();      }    }    browser.watchUrl(function(url){ -    parse(url); +    parseUrl(url);      scope.$root.$eval();    }); -  parse(browser.getUrl()); -  var lastURL; +  parseUrl(browser.getUrl()); +  this.$onEval(PRIORITY_FIRST, function(){ +    if (location.hash != lastHash) { +      parseHash(location.hash); +    } +  });    this.$onEval(PRIORITY_LAST, function(){      var url = toString(); -    if (lastURL != url) { +    if (lastUrl != url) {        browser.setUrl(url); -      lastURL = url; +      lastUrl = url;      }    });    return location; @@ -142,6 +151,7 @@ angularService('$route', function(location, params){    var routes = {},        onChange = [],        matcher = angularWidget('NG:SWITCH').route, +      parentScope = this,        $route = {          routes: routes,          onChange: bind(onChange, onChange.push), @@ -150,16 +160,19 @@ angularService('$route', function(location, params){            var route = routes[path];            if (!route) route = routes[path] = {};            if (params) angular.extend(route, params); +          if (matcher(location.hashPath, path)) updateRoute();            return route;          }        }; -  this.$watch(function(){return location.hash;}, function(hash){ -    var parentScope = this, childScope; +  function updateRoute(){ +    console.log('updating route'); +    var childScope;      $route.current = null;      angular.foreach(routes, function(routeParams, route) {        if (!childScope) {          var pathParams = matcher(location.hashPath, route);          if (pathParams) { +          console.log('new route', routeParams.template, location.hashPath, location.hash);            childScope = angular.scope(parentScope);            $route.current = angular.extend({}, routeParams, {              scope: childScope, @@ -173,7 +186,8 @@ angularService('$route', function(location, params){        childScope.$become($route.current.controller);        parentScope.$tryEval(childScope.init);      } -  }); +  } +  this.$watch(function(){return location.hash;}, updateRoute);    return $route;  }, {inject: ['$location']}); diff --git a/src/validators.js b/src/validators.js index 4544b96c..27b4b404 100644 --- a/src/validators.js +++ b/src/validators.js @@ -81,33 +81,52 @@ foreach({      }    }, -  'asynchronous': function(text, asynchronousFn) { -    var element = this['$element']; -    var cache = element.data('$validateState'); +  /* +   * cache is attached to the element +   * cache: { +   *   inputs : { +   *     'user input': { +   *        response: server response, +   *        error: validation error +   *     }, +   *   current: 'current input' +   * } +   * +   */ +  'asynchronous': function(input, asynchronousFn, updateFn) { +    if (!input) return; +    var scope = this; +    var element = scope.$element; +    var cache = element.data('$asyncValidator');      if (!cache) { -      cache = { state: {}}; -      element.data('$validateState', cache); +      element.data('$asyncValidator', cache = {inputs:{}});      } -    var state = cache.state[text]; -    cache.lastKey = text; -    if (state === undefined) { -      // we have never seen this before, Request it + +    cache.current = input; + +    var inputState = cache.inputs[input]; +    if (!inputState) { +      cache.inputs[input] = inputState = { inFlight: true }; +      scope.$invalidWidgets.markInvalid(scope.$element);        element.addClass('ng-input-indicator-wait'); -      state = cache.state[text] = null; -      (asynchronousFn || noop)(text, function(error){ -        state = cache.state[text] = error ? error : false; -        if (cache.state[cache.lastKey] !== null) { +      asynchronousFn(input, function(error, data) { +        inputState.response = data; +        inputState.error = error; +        inputState.inFlight = false; +        if (cache.current == input) {            element.removeClass('ng-input-indicator-wait'); +          scope.$invalidWidgets.markValid(element);          } -        elementError(element, NG_VALIDATION_ERROR, error); +        element.data('$validate')(input); +        scope.$root.$eval();        }); -    } - -    if (state === null && this['$invalidWidgets']){ +    } else if (inputState.inFlight) {        // request in flight, mark widget invalid, but don't show it to user -      this['$invalidWidgets'].markInvalid(this.$element); +      scope.$invalidWidgets.markInvalid(scope.$element); +    } else { +      (updateFn||noop)(inputState.response);      } -    return state; +    return inputState.error;    }  }, function(v,k) {angularValidator[k] = v;}); diff --git a/src/widgets.js b/src/widgets.js index 09b602af..2909aed1 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -27,6 +27,11 @@ function valueAccessor(scope, element) {    required = required || required === '';    if (!validator) throw "Validator named '" + validatorName + "' not found.";    function validate(value) { +    if (element[0].disabled || isString(element.attr('readonly'))) { +      elementError(element, NG_VALIDATION_ERROR, null); +      invalidWidgets.markValid(element); +      return value; +    }      var error,          validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element});      error = required && !trim(value) ? @@ -42,6 +47,7 @@ function valueAccessor(scope, element) {      }      return value;    } +  element.data('$validate', validate);    return {      get: function(){ return validate(element.val()); },      set: function(value){ element.val(validate(value)); } @@ -167,20 +173,31 @@ angularWidget('SELECT', function(element){  angularWidget('NG:INCLUDE', function(element){    var compiler = this, -      src = element.attr("src"); -  if (element.attr('switch-instance')) { +      srcExp = element.attr("src"), +      scopeExp = element.attr("scope") || ''; +  if (element[0]['ng-compiled']) {      this.descend(true);      this.directives(true);    } else { +    element[0]['ng-compiled'] = true;      return function(element){        var scope = this, childScope; -      element.attr('switch-instance', 'compiled'); -      scope.$browser.xhr('GET', src, function(code, response){ -        element.html(response); -        childScope = createScope(scope); -        compiler.compile(element)(element, childScope); -        childScope.$init(); -        scope.$root.$eval(); +      var changeCounter = 0; +      function incrementChange(){ changeCounter++;} +      this.$watch(srcExp, incrementChange); +      this.$watch(scopeExp, incrementChange); +      this.$watch(function(){return changeCounter;}, function(){ +        var src = this.$eval(srcExp), +        useScope = this.$eval(scopeExp); +        if (src) { +          scope.$browser.xhr('GET', src, function(code, response){ +            element.html(response); +            childScope = useScope || createScope(scope); +            compiler.compile(element)(element, childScope); +            childScope.$init(); +            scope.$root.$eval(); +          }); +        }        });        scope.$onEval(function(){          if (childScope) childScope.$eval(); diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js index 49416ae4..b2403eab 100644 --- a/test/ValidatorsTest.js +++ b/test/ValidatorsTest.js @@ -88,17 +88,16 @@ describe('Validator:asynchronous', function(){    var value, fn;    beforeEach(function(){ -    var invalidWidgets = []; -    invalidWidgets.markInvalid = function(element){ -      invalidWidgets.push(element); -    }; +    var invalidWidgets = angularService('$invalidWidgets')();      value = null;      fn = null;      self = {          $element:jqLite('<input />'),          $invalidWidgets:invalidWidgets, -        $updateView: noop +        $eval: noop      }; +    self.$element.data('$validate', noop); +    self.$root = self;    });    afterEach(function(){ @@ -122,14 +121,14 @@ describe('Validator:asynchronous', function(){      expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy();      fn("myError");      expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy(); -    expect(input.attr('ng-validation-error')).toEqual("myError"); +    expect(input.attr(NG_VALIDATION_ERROR)).toEqual("myError");      scope.$element.remove();    });    it("should not make second request to same value", function(){      asynchronous.call(self, "kai", function(v,f){value=v; fn=f;});      expect(value).toEqual('kai'); -    expect(self.$invalidWidgets[0][0]).toEqual(self.$element[0]); +    expect(self.$invalidWidgets[0]).toEqual(self.$element);      var spy = jasmine.createSpy();      asynchronous.call(self, "kai", spy); @@ -145,9 +144,26 @@ describe('Validator:asynchronous', function(){      asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;});      firstCb(); -    expect(jqLite(self.$element).hasClass('ng-input-indicator-wait')).toBeTruthy(); +    expect(self.$element.hasClass('ng-input-indicator-wait')).toBeTruthy();      secondCb(); -    expect(jqLite(self.$element).hasClass('ng-input-indicator-wait')).toBeFalsy(); +    expect(self.$element.hasClass('ng-input-indicator-wait')).toBeFalsy();    }); + +  it("should handle update function", function(){ +    var scope = angular.compile('<input name="name" ng-validate="asynchronous:asyncFn:updateFn"/>'); +    scope.asyncFn = jasmine.createSpy(); +    scope.updateFn = jasmine.createSpy(); +    scope.name = 'misko'; +    scope.$init(); +    scope.$eval(); +    expect(scope.asyncFn).wasCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]); +    assertTrue(scope.$element.hasClass('ng-input-indicator-wait')); +    scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'}); +    assertFalse(scope.$element.hasClass('ng-input-indicator-wait')); +    assertEquals('myError', scope.$element.attr('ng-validation-error')); +    expect(scope.updateFn.mostRecentCall.args[0]).toEqual({id: 1234, data:'data'}); +    scope.$element.remove(); +  }); +  }); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index 76a12616..1ddd7477 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -57,6 +57,22 @@ describe("directives", function(){      expect(element.attr('alt')).toEqual('myalt');    }); +  it('should remove special attributes on false', function(){ +    var scope = compile('<div disabled="{{disabled}}"  readonly="{{readonly}}" checked="{{checked}}"/>'); +    expect(scope.$element.attr('disabled')).toEqual(null); +    expect(scope.$element.attr('readonly')).toEqual(null); +    expect(scope.$element.attr('checked')).toEqual(null); + +    scope.disabled = true; +    scope.readonly = true; +    scope.checked = true; +    scope.$eval(); + +    expect(scope.$element.attr('disabled')).not.toEqual(null); +    expect(scope.$element.attr('readonly')).not.toEqual(null); +    expect(scope.$element.attr('checked')).not.toEqual(null); +  }); +    it('should ng-non-bindable', function(){      var scope = compile('<div ng-non-bindable><span ng-bind="name"></span></div>');      scope.$set('name', 'misko'); diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 715a232e..f917f968 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -9,53 +9,6 @@ describe("services", function(){      expect(scope.$window).toEqual(window);    }); -  it("should inject $location", function(){ -    scope.$location.parse('http://host:123/p/a/t/h.html?query=value#path?key=value'); -    expect(scope.$location.href).toEqual("http://host:123/p/a/t/h.html?query=value#path?key=value"); -    expect(scope.$location.protocol).toEqual("http"); -    expect(scope.$location.host).toEqual("host"); -    expect(scope.$location.port).toEqual("123"); -    expect(scope.$location.path).toEqual("/p/a/t/h.html"); -    expect(scope.$location.search).toEqual({query:'value'}); -    expect(scope.$location.hash).toEqual('path?key=value'); -    expect(scope.$location.hashPath).toEqual('path'); -    expect(scope.$location.hashSearch).toEqual({key:'value'}); - -    scope.$location.hashPath = 'page=http://path'; -    scope.$location.hashSearch = {k:'a=b'}; - -    expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db'); -  }); - -  it('should parse file://', function(){ -    scope.$location.parse('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); -    expect(scope.$location.href).toEqual("file:///Users/Shared/misko/work/angular.js/scenario/widgets.html"); -    expect(scope.$location.protocol).toEqual("file"); -    expect(scope.$location.host).toEqual(""); -    expect(scope.$location.port).toEqual(null); -    expect(scope.$location.path).toEqual("/Users/Shared/misko/work/angular.js/scenario/widgets.html"); -    expect(scope.$location.search).toEqual({}); -    expect(scope.$location.hash).toEqual(''); -    expect(scope.$location.hashPath).toEqual(''); -    expect(scope.$location.hashSearch).toEqual({}); - -    expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#'); -  }); - -  it('should update url on hash change', function(){ -    scope.$location.parse('http://server/#path?a=b'); -    scope.$location.hash = ''; -    expect(scope.$location.toString()).toEqual('http://server/#'); -    expect(scope.$location.hashPath).toEqual(''); -  }); - -  it('should update url on hashPath change', function(){ -    scope.$location.parse('http://server/#path?a=b'); -    scope.$location.hashPath = ''; -    expect(scope.$location.toString()).toEqual('http://server/#?a=b'); -    expect(scope.$location.hash).toEqual('?a=b'); -  }); -    xit('should add stylesheets', function(){      scope.$document = {        getElementsByTagName: function(name){ @@ -64,9 +17,71 @@ describe("services", function(){        }      };      scope.$document.addStyleSheet('css/angular.css'); -    }); +  describe("$location", function(){ +    it("should inject $location", function(){ +      scope.$location.parse('http://host:123/p/a/t/h.html?query=value#path?key=value'); +      expect(scope.$location.href).toEqual("http://host:123/p/a/t/h.html?query=value#path?key=value"); +      expect(scope.$location.protocol).toEqual("http"); +      expect(scope.$location.host).toEqual("host"); +      expect(scope.$location.port).toEqual("123"); +      expect(scope.$location.path).toEqual("/p/a/t/h.html"); +      expect(scope.$location.search).toEqual({query:'value'}); +      expect(scope.$location.hash).toEqual('path?key=value'); +      expect(scope.$location.hashPath).toEqual('path'); +      expect(scope.$location.hashSearch).toEqual({key:'value'}); + +      scope.$location.hashPath = 'page=http://path'; +      scope.$location.hashSearch = {k:'a=b'}; + +      expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db'); +    }); + +    it('should parse file://', function(){ +      scope.$location.parse('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); +      expect(scope.$location.href).toEqual("file:///Users/Shared/misko/work/angular.js/scenario/widgets.html"); +      expect(scope.$location.protocol).toEqual("file"); +      expect(scope.$location.host).toEqual(""); +      expect(scope.$location.port).toEqual(null); +      expect(scope.$location.path).toEqual("/Users/Shared/misko/work/angular.js/scenario/widgets.html"); +      expect(scope.$location.search).toEqual({}); +      expect(scope.$location.hash).toEqual(''); +      expect(scope.$location.hashPath).toEqual(''); +      expect(scope.$location.hashSearch).toEqual({}); + +      expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#'); +    }); + +    it('should update url on hash change', function(){ +      scope.$location.parse('http://server/#path?a=b'); +      scope.$location.hash = ''; +      expect(scope.$location.toString()).toEqual('http://server/#'); +      expect(scope.$location.hashPath).toEqual(''); +    }); + +    it('should update url on hashPath change', function(){ +      scope.$location.parse('http://server/#path?a=b'); +      scope.$location.hashPath = ''; +      expect(scope.$location.toString()).toEqual('http://server/#?a=b'); +      expect(scope.$location.hash).toEqual('?a=b'); +    }); + +    it('should update hash before any processing', function(){ +      var scope = compile('<div>'); +      var log = ''; +      scope.$watch('$location.hash', function(){ +        log += this.$location.hashPath + ';'; +      }); +      expect(log).toEqual(';'); + +      log = ''; +      scope.$location.hash = '/abc'; +      scope.$eval(); +      expect(log).toEqual('/abc;'); +    }); + +  });  });  describe("service $invalidWidgets", function(){ @@ -135,5 +150,7 @@ describe("service $route", function(){      expect(log).toEqual('onChange();');      expect(scope.$route.current).toEqual(null); +    scope.$route.when('/NONE', {template:'instant update'}); +    expect(scope.$route.current.template).toEqual('instant update');    });  }); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 04b8b1ec..ae6a17df 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -76,6 +76,18 @@ describe("input widget", function(){      expect(element.attr('ng-validation-error')).toEqual('Not a number');    }); +  it("should ignore disabled widgets", function(){ +    compile('<input type="text" name="price" ng-required disabled/>'); +    expect(element.hasClass('ng-validation-error')).toBeFalsy(); +    expect(element.attr('ng-validation-error')).toBeFalsy(); +  }); + +  it("should ignore readonly widgets", function(){ +    compile('<input type="text" name="price" ng-required readonly/>'); +    expect(element.hasClass('ng-validation-error')).toBeFalsy(); +    expect(element.attr('ng-validation-error')).toBeFalsy(); +  }); +    it("should process ng-required", function(){      compile('<input type="text" name="price" ng-required/>');      expect(element.hasClass('ng-validation-error')).toBeTruthy(); @@ -244,13 +256,15 @@ describe('ng:switch', function(){  describe('ng:include', function(){    it('should include on external file', function() { -    var element = jqLite('<ng:include src="myUrl"></ng:include>'); +    var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');      var scope = compile(element); -    scope.$browser.xhr.expect('GET', 'myUrl').respond('{{1+2}}'); +    scope.childScope = createScope(); +    scope.childScope.name = 'misko'; +    scope.url = 'myUrl'; +    scope.$browser.xhr.expect('GET', 'myUrl').respond('{{name}}');      scope.$init(); -    expect(sortedHtml(element)).toEqual('<ng:include src="myUrl" switch-instance="compiled"></ng:include>');      scope.$browser.xhr.flush(); -    expect(element.text()).toEqual('3'); +    expect(element.text()).toEqual('misko');    });  }); | 
