diff options
| -rw-r--r-- | jsTestDriver.conf | 2 | ||||
| -rw-r--r-- | scenario/application-account.html | 4 | ||||
| -rw-r--r-- | scenario/application.html | 19 | ||||
| -rw-r--r-- | src/Angular.js | 9 | ||||
| -rw-r--r-- | src/AngularPublic.js | 35 | ||||
| -rw-r--r-- | src/Browser.js | 20 | ||||
| -rw-r--r-- | src/Compiler.js | 1 | ||||
| -rw-r--r-- | src/Scope.js | 17 | ||||
| -rw-r--r-- | src/UrlWatcher.js | 46 | ||||
| -rw-r--r-- | src/Widgets.js | 59 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 2 | ||||
| -rw-r--r-- | src/jqLite.js | 7 | ||||
| -rw-r--r-- | src/services.js | 29 | ||||
| -rw-r--r-- | src/~AngularPublic.js | 17 | ||||
| -rw-r--r-- | test/BinderTest.js | 15 | ||||
| -rw-r--r-- | test/ScenarioSpec.js | 12 | ||||
| -rw-r--r-- | test/ScopeSpec.js | 2 | ||||
| -rw-r--r-- | test/ValidatorsTest.js | 2 | ||||
| -rw-r--r-- | test/angular-mocks.js | 26 | ||||
| -rw-r--r-- | test/servicesSpec.js | 8 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 40 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 16 |
22 files changed, 198 insertions, 190 deletions
diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 21851c99..a0cae9a4 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -11,6 +11,7 @@ load: - src/scenario/_namespace.js - src/scenario/*.js - test/testabilityPatch.js + - test/angular-mocks.js - test/scenario/*.js - test/*.js @@ -18,3 +19,4 @@ exclude: - src/angular.prefix - src/angular.suffix - src/angular-bootstrap.js + - src/AngularPublic.js diff --git a/scenario/application-account.html b/scenario/application-account.html new file mode 100644 index 00000000..8520d07c --- /dev/null +++ b/scenario/application-account.html @@ -0,0 +1,4 @@ +<div> +account page goes here! +</div> + diff --git a/scenario/application.html b/scenario/application.html new file mode 100644 index 00000000..be6390f6 --- /dev/null +++ b/scenario/application.html @@ -0,0 +1,19 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <link rel="stylesheet" type="text/css" href="style.css"></link> + <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script> + </head> + <body ng-init="$window.$scope = this"> + [ <a href="#login">login</a> + | <a href="#account">account</a> + ] + + <ng:switch on="$location.hashPath"> + <div ng-switch-when="login">login screen</div> + <ng:include ng-switch-when="account" src="application-account.html"></ng:include> + </ng:switch> + + <pre>$location={{$location}}</pre> + </body> + </html> diff --git a/src/Angular.js b/src/Angular.js index d00a9bf6..0952a352 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -247,7 +247,16 @@ function escapeHtml(html) { replace(/>/g, '>'); } + +function isRenderableElement(element) { + var name = element && element[0] && element[0].nodeName; + return name && name.charAt(0) != '#' && + !includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name); +} function elementError(element, type, error) { + while (!isRenderableElement(element)) { + element = element.parent() || jqLite(document.body); + } if (error) { element.addClass(type); element.attr(NG_ERROR, error); diff --git a/src/AngularPublic.js b/src/AngularPublic.js new file mode 100644 index 00000000..470eb258 --- /dev/null +++ b/src/AngularPublic.js @@ -0,0 +1,35 @@ +var browserSingleton; +angularService('$browser', function browserFactory(){ + if (!browserSingleton) { + var XHR = XMLHttpRequest; + if (isUndefined(XHR)) { + XHR = function () { + try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} + try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} + throw new Error("This browser does not support XMLHttpRequest."); + }; + } + browserSingleton = new Browser(window.location, XHR); + browserSingleton.startUrlWatcher(); + } + return browserSingleton; +}); + +extend(angular, { + 'element': jqLite, + 'compile': compile, + 'scope': createScope, + 'copy': copy, + 'extend': extend, + 'foreach': foreach, + 'noop':noop, + 'identity':identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isNumber': isNumber, + 'isArray': isArray +}); + diff --git a/src/Browser.js b/src/Browser.js index 893459ae..6036884f 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -3,9 +3,10 @@ // Browser ////////////////////////////// -function Browser(location) { +function Browser(location, XHR) { this.location = location; this.delay = 25; + this.XHR = XHR; this.setTimeout = function(fn, delay) { window.setTimeout(fn, delay); }; @@ -14,6 +15,17 @@ function Browser(location) { } Browser.prototype = { + xhr: function(method, url, callback){ + var xhr = new this.XHR(); + xhr.open(method, url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + callback(xhr.status, xhr.responseText); + } + }; + xhr.send(''); + }, + watchUrl: function(fn){ this.listeners.push(fn); }, @@ -23,7 +35,11 @@ Browser.prototype = { (function pull () { if (self.expectedUrl !== self.location.href) { foreach(self.listeners, function(listener){ - listener(self.location.href); + try { + listener(self.location.href); + } catch (e) { + error(e); + } }); self.expectedUrl = self.location.href; } diff --git a/src/Compiler.js b/src/Compiler.js index dac4931f..67c22461 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -71,6 +71,7 @@ Compiler.prototype = { $init: function() { template.init(element, scope); scope.$eval(); + delete scope.$init; return scope; } }); diff --git a/src/Scope.js b/src/Scope.js index 52ab3ed7..562dfbd8 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -70,17 +70,8 @@ function parserNewScopeAdapter(fn) { }; } -function isRenderableElement(element) { - var name = element && element[0] && element[0].nodeName; - return name && name.charAt(0) != '#' && - !includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name); -} - function rethrow(e) { throw e; } function errorHandlerFor(element, error) { - while (!isRenderableElement(element)) { - element = element.parent() || jqLite(document.body); - } elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error); } @@ -132,14 +123,16 @@ function createScope(parent, services, existing) { $watch: function(watchExp, listener, exceptionHandler) { var watch = expressionCompile(watchExp), - last = watch.call(instance); - instance.$onEval(PRIORITY_WATCH, function(){ + last; + function watcher(){ var value = watch.call(instance); if (last !== value) { instance.$tryEval(listener, exceptionHandler, value, last); last = value; } - }); + } + instance.$onEval(PRIORITY_WATCH, watcher); + watcher(); }, $onEval: function(priority, expr, exceptionHandler){ diff --git a/src/UrlWatcher.js b/src/UrlWatcher.js deleted file mode 100644 index 1b2a9cf0..00000000 --- a/src/UrlWatcher.js +++ /dev/null @@ -1,46 +0,0 @@ - -// //////////////////////////// -// UrlWatcher -// //////////////////////////// - -function UrlWatcher(location) { - this.location = location; - this.delay = 25; - this.setTimeout = function(fn, delay) { - window.setTimeout(fn, delay); - }; - this.expectedUrl = location.href; - this.listeners = []; -} - -UrlWatcher.prototype = { - watch: function(fn){ - this.listeners.push(fn); - }, - - start: function() { - var self = this; - (function pull () { - if (self.expectedUrl !== self.location.href) { - foreach(self.listeners, function(listener){ - listener(self.location.href); - }); - self.expectedUrl = self.location.href; - } - self.setTimeout(pull, self.delay); - })(); - }, - - set: function(url) { - var existingURL = this.location.href; - if (!existingURL.match(/#/)) - existingURL += '#'; - if (existingURL != url) - this.location.href = url; - this.existingURL = url; - }, - - get: function() { - return this.location.href; - } -}; diff --git a/src/Widgets.js b/src/Widgets.js index 1e703a56..8e668c8f 100644 --- a/src/Widgets.js +++ b/src/Widgets.js @@ -156,40 +156,47 @@ angularWidget('SELECT', function(element){ }); -angularWidget('INLINE', function(element){ - element.replaceWith(this.element("div")); +angularWidget('NG:INCLUDE', function(element){ var compiler = this, - behavior = element.attr("behavior"), - template = element.attr("template"), - initExpr = element.attr("init"); - return function(boundElement){ - var scope = this; - boundElement.load(template, function(){ - var templateScope = compiler.compile(boundElement)(boundElement, scope); - templateScope.$tryEval(initExpr, boundElement); - templateScope.$init(); + src = element.attr("src"); + return element.attr('switch-instance') ? null : 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.$onEval(function(){ if (childScope) childScope.$eval(); }); }; }); -angularWidget('INCLUDE', function(element){ - element.replaceWith(this.element("div")); - var matches = []; - element.find("INLINE").each(function(){ - matches.push({match: jQuery(this).attr("match"), element: jQuery(this)}); - }); +angularWidget('NG:SWITCH', function(element){ var compiler = this, - watchExpr = element.attr("watch"); - return function(boundElement){ + watchExpr = element.attr("on"), + cases = []; + eachNode(element, function(caseElement){ + var when = caseElement.attr('ng-switch-when'); + if (when) { + cases.push({ + when: function(value){ return value == when; }, + element: caseElement, + template: compiler.compile(caseElement) + }); + } + }); + element.html(''); + return function(element){ var scope = this; this.$watch(watchExpr, function(value){ - foreach(matches, function(inline){ - if(inline.match == value) { - var template = inline.element.attr("template"); - boundElement.load(template, function(){ - var templateScope = compiler.compile(boundElement)(boundElement, scope); - templateScope.$init(); - }); + element.html(''); + foreach(cases, function(switchCase){ + if (switchCase.when(value)) { + element.append(switchCase.element); + var childScope = createScope(scope); + switchCase.template(switchCase.element, childScope); + childScope.$init(); } }); }); diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index d9633854..ce7849d8 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -47,7 +47,7 @@ addScript("/Parser.js"); addScript("/Resource.js"); addScript("/Browser.js"); - addScript("/AngularPublic.js"); + addScript("/~AngularPublic.js"); // Extension points addScript("/apis.js"); diff --git a/src/jqLite.js b/src/jqLite.js index ec77a6fb..f8ed4d7d 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -109,6 +109,10 @@ JQLite.prototype = { this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); }, + append: function(node) { + this[0].appendChild(jqLite(node)[0]); + }, + remove: function() { this.dealoc(); this[0].parentNode.removeChild(this[0]); @@ -182,6 +186,9 @@ JQLite.prototype = { html: function(value) { if (isDefined(value)) { + for ( var i = 0, children = this[0].childNodes; i < children.length; i++) { + jqLite(children[i]).dealoc(); + } this[0].innerHTML = value; } return this[0].innerHTML; diff --git a/src/services.js b/src/services.js index 16b48031..2532d3d3 100644 --- a/src/services.js +++ b/src/services.js @@ -6,8 +6,8 @@ angularService("$document", function(window){ var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?((#([^\?]*))?(\?([^\?]*))?)$/; var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; angularService("$location", function(browser){ - var scope = this; - function location(url){ + var scope = this, location = {parse:parse, toString:toString}; + function parse(url){ if (isDefined(url)) { var match = URL_MATCH.exec(url); if (match) { @@ -23,17 +23,19 @@ angularService("$location", function(browser){ location.hashSearch = parseKeyValue(match[13]); } } - var hashKeyValue = toKeyValue(location.hashSearch); - var hash = (location.hashPath ? location.hashPath : '') + - (hashKeyValue ? '?' + hashKeyValue : ''); + } + function toString() { + var hashKeyValue = toKeyValue(location.hashSearch), + hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''); return location.href.split('#')[0] + '#' + (hash ? hash : ''); } browser.watchUrl(function(url){ - location(url); + parse(url); + scope.$root.$eval(); }); - location(browser.getUrl()); + parse(browser.getUrl()); this.$onEval(PRIORITY_LAST, function(){ - var href = location(); + var href = toString(); if (href != location.href) { browser.setUrl(href); location.href = href; @@ -42,14 +44,3 @@ angularService("$location", function(browser){ return location; }, {inject: ['$browser']}); -if (!angularService['$browser']) { - var browserSingleton; - angularService('$browser', function browserFactory(){ - if (!browserSingleton) { - browserSingleton = new Browser(window.location); - browserSingleton.startUrlWatcher(); - } - return browserSingleton; - }); -} - diff --git a/src/~AngularPublic.js b/src/~AngularPublic.js deleted file mode 100644 index b9d0f9d7..00000000 --- a/src/~AngularPublic.js +++ /dev/null @@ -1,17 +0,0 @@ -extend(angular, { - 'element': jqLite, - 'compile': compile, - 'scope': createScope, - 'copy': copy, - 'extend': extend, - 'foreach': foreach, - 'noop':noop, - 'identity':identity, - 'isUndefined': isUndefined, - 'isDefined': isDefined, - 'isString': isString, - 'isFunction': isFunction, - 'isNumber': isNumber, - 'isArray': isArray -}); - diff --git a/test/BinderTest.js b/test/BinderTest.js index fa3127d7..660ad78c 100644 --- a/test/BinderTest.js +++ b/test/BinderTest.js @@ -629,21 +629,6 @@ BinderTest.prototype.testDeleteAttributeIfEvaluatesFalse = function() { assertChild(5, false); }; -BinderTest.prototype.testRepeaterErrorShouldBePlacedOnInstanceNotOnTemplateComment = function () { - var c = this.compile( - '<input name="person.{{name}}" ng-repeat="name in [\'a\', \'b\']" />'); - c.scope.$eval(); - assertTrue(c.node.hasClass("ng-exception")); -}; - -BinderTest.prototype.testItShouldApplyAttributesBeforeTheWidgetsAreMaterialized = function() { - var c = this.compile( - '<input name="person.{{name}}" ng-repeat="name in [\'a\', \'b\']" />'); - c.scope.$set('person', {a:'misko', b:'adam'}); - c.scope.$eval(); - assertEquals("", c.node.html()); -}; - BinderTest.prototype.XtestItShouldCallListenersWhenAnchorChanges = function() { var log = ""; var c = this.compile('<div ng-watch="$anchor.counter:count = count+1">'); diff --git a/test/ScenarioSpec.js b/test/ScenarioSpec.js index 5b88a175..ff3a55b5 100644 --- a/test/ScenarioSpec.js +++ b/test/ScenarioSpec.js @@ -1,26 +1,26 @@ describe("ScenarioSpec: Compilation", function(){ it("should compile dom node and return scope", function(){ var node = jqLite('<div ng-init="a=1">{{b=a+1}}</div>')[0]; - var scope = angular.compile(node); + var scope = compile(node); scope.$init(); expect(scope.a).toEqual(1); expect(scope.b).toEqual(2); }); it("should compile jQuery node and return scope", function(){ - var scope = angular.compile(jqLite('<div>{{a=123}}</div>')).$init(); + var scope = compile(jqLite('<div>{{a=123}}</div>')).$init(); expect(jqLite(scope.$element).text()).toEqual('123'); }); it("should compile text node and return scope", function(){ - var scope = angular.compile('<div>{{a=123}}</div>').$init(); + var scope = compile('<div>{{a=123}}</div>').$init(); expect(jqLite(scope.$element).text()).toEqual('123'); }); }); describe("ScenarioSpec: Scope", function(){ xit("should have set, get, eval, $init, updateView methods", function(){ - var scope = angular.compile('<div>{{a}}</div>').$init(); + var scope = compile('<div>{{a}}</div>').$init(); scope.$eval("$invalidWidgets.push({})"); expect(scope.$set("a", 2)).toEqual(2); expect(scope.$get("a")).toEqual(2); @@ -31,7 +31,7 @@ describe("ScenarioSpec: Scope", function(){ }); xit("should have $ objects", function(){ - var scope = angular.compile('<div></div>', {a:"b"}); + var scope = compile('<div></div>', {a:"b"}); expect(scope.$get('$anchor')).toBeDefined(); expect(scope.$get('$eval')).toBeDefined(); expect(scope.$get('$config')).toBeDefined(); @@ -49,7 +49,7 @@ xdescribe("ScenarioSpec: configuration", function(){ set:function(u){url = u;}, get:function(){return url;} }; - var scope = angular.compile("<div>{{$anchor}}</div>", {location:location}); + var scope = compile("<div>{{$anchor}}</div>", {location:location}); var $anchor = scope.$get('$anchor'); expect($anchor.book).toBeUndefined(); expect(onUrlChange).toBeUndefined(); diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index a7322cae..09f4d875 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -44,7 +44,7 @@ describe('scope/model', function(){ model.$onEval(function(){evalCount ++;}); model.name = 'misko'; model.$eval(); - expect(nameCount).toEqual(1); + expect(nameCount).toEqual(2); expect(evalCount).toEqual(1); expect(model.newValue).toEqual('misko'); expect(model.oldValue).toEqual('adam'); diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js index 37be526d..d7da28cd 100644 --- a/test/ValidatorsTest.js +++ b/test/ValidatorsTest.js @@ -106,7 +106,7 @@ describe('Validator:asynchronous', function(){ it('should make a request and show spinner', function(){ var value, fn; - var scope = angular.compile('<input type="text" name="name" ng-validate="asynchronous:asyncFn"/>'); + var scope = compile('<input type="text" name="name" ng-validate="asynchronous:asyncFn"/>'); scope.$init(); var input = scope.$element; scope.asyncFn = function(v,f){ diff --git a/test/angular-mocks.js b/test/angular-mocks.js new file mode 100644 index 00000000..ab3638b1 --- /dev/null +++ b/test/angular-mocks.js @@ -0,0 +1,26 @@ + +function MockBrowser() { + this.url = "http://server"; + this.watches = []; +} +MockBrowser.prototype = { + xhr: function(method, url, callback) { + + }, + + getUrl: function(){ + return this.url; + }, + + setUrl: function(url){ + this.url = url; + }, + + watchUrl: function(fn) { + this.watches.push(fn); + } +}; + +angular.service('$browser', function(){ + return new MockBrowser(); +}); diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 193351d1..43511853 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -10,7 +10,7 @@ describe("services", function(){ }); it("should inject $location", function(){ - scope.$location('http://host:123/p/a/t/h.html?query=value#path?key=value'); + 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"); @@ -24,11 +24,11 @@ describe("services", function(){ scope.$location.hashPath = 'page=http://path'; scope.$location.hashSearch = {k:'a=b'}; - expect(scope.$location()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db'); + 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('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); + 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(""); @@ -39,7 +39,7 @@ describe("services", function(){ expect(scope.$location.hashPath).toEqual(''); expect(scope.$location.hashSearch).toEqual({}); - expect(scope.$location()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#'); + expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#'); }); xit('should add stylesheets', function(){ diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 752f8ef2..b2ee5526 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -5,46 +5,6 @@ function nakedExpect(obj) { return expect(angular.fromJson(angular.toJson(obj))); } -swfobject = { - createSwf:function() { - fail("must mock out swfobject.createSwf in test."); - } -}; - -function html(content) { - return jQuery("<div></div>").html(content); -} - -function report(reportTest){ - $("#tests").children().each(function(i){ - var success = this.className == "pass"; - var strong = this.firstChild; - var msg = strong.firstChild.nodeValue; - var parts = msg.split(" module: "); - var module = parts[0]; - var name = parts[1].replace(/ *$/, ""); - reportTest(success, module, name, this.nodeValue); - }); -} - -function MockBrowser() { - this.url = "http://server"; - this.watches = []; -} -MockBrowser.prototype = { - getUrl: function(){ - return this.url; - }, - - setUrl: function(url){ - this.url = url; - }, - - watchUrl: function(fn) { - this.watches.push(fn); - } -}; - angularService('$browser', function(){ return new MockBrowser(); }); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index a4afa21c..63c18700 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -188,5 +188,21 @@ describe("input widget", function(){ expect(element.hasClass('ng-exception')).toBeTruthy(); }); + it('should switch on value change', function(){ + compile('<ng:switch on="select"><div ng-switch-when="1">first</div><div ng-switch-when="2">second</div></ng:switch>'); + expect(element.html()).toEqual(''); + scope.select = 1; + scope.$eval(); + expect(element.text()).toEqual('first'); + scope.select = 2; + scope.$eval(); + expect(element.text()).toEqual('second'); + }); +}); +describe('ng:include', function(){ + it('should include on external file', function() { + var element = jqLite('<ng:include src="myUrl"></ng:include>'); + var scope = compile(element).$init(); + }); }); |
