diff options
| -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'); }); }); |
