diff options
| -rw-r--r-- | src/Angular.js | 71 | ||||
| -rw-r--r-- | src/Compiler.js | 1 | ||||
| -rw-r--r-- | src/Parser.js | 4 | ||||
| -rw-r--r-- | src/Scope.js | 55 | ||||
| -rw-r--r-- | src/UrlWatcher.js | 40 | ||||
| -rw-r--r-- | src/services.js | 34 | ||||
| -rw-r--r-- | test/AngularTest.js | 44 | ||||
| -rw-r--r-- | test/BinderTest.js | 38 | ||||
| -rw-r--r-- | test/ScopeSpec.js | 44 | ||||
| -rw-r--r-- | test/UrlWatcherTest.js | 25 | ||||
| -rw-r--r-- | test/servicesSpec.js | 27 |
11 files changed, 251 insertions, 132 deletions
diff --git a/src/Angular.js b/src/Angular.js index db177082..8eef6ac0 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -4,6 +4,9 @@ if (typeof document.getAttribute == 'undefined') if (!window['console']) window['console']={'log':noop, 'error':noop}; var consoleNode, + PRIORITY_FIRST = -99999; + PRIORITY_WATCH = -1000; + PRIORITY_LAST = 99999; NOOP = 'noop', NG_ERROR = 'ng-error', NG_EXCEPTION = 'ng-exception', @@ -21,11 +24,30 @@ var consoleNode, angularValidator = extensionMap(angular, 'validator'), angularFilter = extensionMap(angular, 'filter'), angularFormatter = extensionMap(angular, 'formatter'), + angularService = extensionMap(angular, 'service'), angularCallbacks = extensionMap(angular, 'callbacks'), - angularAlert = angular['alert'] || (angular['alert'] = function(){ - log(arguments); window.alert.apply(window, arguments); - }); -angular['copy'] = copy; + urlWatcher = new UrlWatcher(window.location); + +function angularAlert(){ + log(arguments); window.alert.apply(window, arguments); +}; + +extend(angular, { + 'compile': compile, + 'startUrlWatch': bind(urlWatcher, urlWatcher.start), + 'copy': copy, + 'extend': extend, + 'foreach': foreach, + 'noop':noop, + 'identity':identity, + 'isUndefined': isUndefined, + 'isDefined': isDefined, + 'isString': isString, + 'isFunction': isFunction, + 'isNumber': isNumber, + 'isArray': isArray, + 'alert': angularAlert +}); function foreach(obj, iterator, context) { var key; @@ -43,6 +65,17 @@ function foreach(obj, iterator, context) { return obj; } +function foreachSorted(obj, iterator, context) { + var keys = []; + for (var key in obj) keys.push(key); + keys.sort(); + for ( var i = 0; i < keys.length; i++) { + iterator.call(context, obj[keys[i]], keys[i]); + } + return keys; +} + + function extend(dst) { foreach(arguments, function(obj){ if (obj !== dst) { @@ -285,19 +318,21 @@ function merge(src, dst) { } } -///////////////////////////////////////////////// - -angular['compile'] = function(element, config) { - config = extend({ - 'onUpdateView': noop, - 'server': "", - 'location': {'get':noop, 'set':noop, 'listen':noop} - }, config||{}); - +function compile(element, config) { var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); $element = jqLite(element), - rootScope = { - '$window': window - }; - return rootScope['$root'] = compiler.compile($element)($element, rootScope); -}; + rootScope = createScope({ + $element: $element, + $config: extend({ + 'onUpdateView': noop, + 'server': "", + 'location': { + 'get':bind(urlWatcher, urlWatcher.get), + 'set':bind(urlWatcher, urlWatcher.set), + 'watch':bind(urlWatcher, urlWatcher.watch) + } + }, config || {}) + }, serviceAdapter(angularService)); + return compiler.compile($element)($element, rootScope); +} +///////////////////////////////////////////////// diff --git a/src/Compiler.js b/src/Compiler.js index 361d6946..c9039928 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -65,7 +65,6 @@ Compiler.prototype = { element = jqLite(element); parentScope = parentScope || {}; var scope = createScope(parentScope); - parentScope.$root = parentScope.$root || scope; return extend(scope, { $element:element, $init: function() { diff --git a/src/Parser.js b/src/Parser.js index ef1465a0..ec58295a 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -11,8 +11,8 @@ Lexer.OPERATORS = { 'true':function(self){return true;}, 'false':function(self){return false;}, 'undefined':noop, - '+':function(self, a,b){return (a||0)+(b||0);}, - '-':function(self, a,b){return (a||0)-(b||0);}, + '+':function(self, a,b){return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, + '-':function(self, a,b){return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, '*':function(self, a,b){return a*b;}, '/':function(self, a,b){return a/b;}, '%':function(self, a,b){return a%b;}, diff --git a/src/Scope.js b/src/Scope.js index 4144d456..ba86e24f 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -89,7 +89,11 @@ function createScope(parent, Class) { function API(){} function Behavior(){} - var instance, behavior, api, watchList = [], evalList = []; + var instance, behavior, api, evalLists = {}; + if (isFunction(parent)) { + Class = parent; + parent = {}; + } Class = Class || noop; parent = Parent.prototype = parent || {}; @@ -107,15 +111,10 @@ function createScope(parent, Class) { if (isDefined(exp)) { return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length)); } else { - foreach(watchList, function(watch) { - var value = instance.$tryEval(watch.watch, watch.handler); - if (watch.last !== value) { - instance.$tryEval(watch.listener, watch.handler, value, watch.last); - watch.last = value; - } - }); - foreach(evalList, function(eval) { - instance.$tryEval(eval.fn, eval.handler); + foreachSorted(evalLists, function(list) { + foreach(list, function(eval) { + instance.$tryEval(eval.fn, eval.handler); + }); }); } }, @@ -134,16 +133,24 @@ function createScope(parent, Class) { }, $watch: function(watchExp, listener, exceptionHandler) { - var watch = expressionCompile(watchExp); - watchList.push({ - watch: watch, - last: watch.call(instance), - handler: exceptionHandler, - listener:expressionCompile(listener) + var watch = expressionCompile(watchExp), + last = watch.call(instance); + instance.$onEval(PRIORITY_WATCH, function(){ + var value = watch.call(instance); + if (last !== value) { + instance.$tryEval(listener, exceptionHandler, value, last); + last = value; + } }); }, - $onEval: function(expr, exceptionHandler){ + $onEval: function(priority, expr, exceptionHandler){ + if (!isNumber(priority)) { + exceptionHandler = expr; + expr = priority; + priority = 0; + } + var evalList = evalLists[priority] || (evalLists[priority] = []); evalList.push({ fn: expressionCompile(expr), handler: exceptionHandler @@ -151,7 +158,21 @@ function createScope(parent, Class) { } }); + if (isUndefined(instance.$root)) { + behavior.$root = instance; + behavior.$parent = instance; + } + Class.apply(instance, slice.call(arguments, 2, arguments.length)); return instance; } + +function serviceAdapter(services) { + return function(){ + var self = this; + foreach(services, function(service, name){ + self[name] = service.call(self); + }); + }; +}; diff --git a/src/UrlWatcher.js b/src/UrlWatcher.js index 0892eb1a..1b2a9cf0 100644 --- a/src/UrlWatcher.js +++ b/src/UrlWatcher.js @@ -9,42 +9,26 @@ function UrlWatcher(location) { this.setTimeout = function(fn, delay) { window.setTimeout(fn, delay); }; - this.listener = function(url) { - return url; - }; this.expectedUrl = location.href; + this.listeners = []; } UrlWatcher.prototype = { - listen: function(fn){ - this.listener = fn; + watch: function(fn){ + this.listeners.push(fn); }, - watch: function() { + + start: function() { var self = this; - var pull = function() { + (function pull () { if (self.expectedUrl !== self.location.href) { - var notify = self.location.hash.match(/^#\$iframe_notify=(.*)$/); - if (notify) { - if (!self.expectedUrl.match(/#/)) { - self.expectedUrl += "#"; - } - self.location.href = self.expectedUrl; - var id = '_iframe_notify_' + notify[1]; - var notifyFn = angularCallbacks[id]; - delete angularCallbacks[id]; - try { - (notifyFn||noop)(); - } catch (e) { - alert(e); - } - } else { - self.listener(self.location.href); - self.expectedUrl = self.location.href; - } + foreach(self.listeners, function(listener){ + listener(self.location.href); + }); + self.expectedUrl = self.location.href; } self.setTimeout(pull, self.delay); - }; - pull(); + })(); }, set: function(url) { @@ -57,6 +41,6 @@ UrlWatcher.prototype = { }, get: function() { - return window.location.href; + return this.location.href; } }; diff --git a/src/services.js b/src/services.js new file mode 100644 index 00000000..14c71363 --- /dev/null +++ b/src/services.js @@ -0,0 +1,34 @@ +angularService("$window", bind(window, identity, window)); + +angularService("$anchor", function(){ + var scope = this; + function anchor(url){ + if (isDefined(url)) { + if (url.charAt(0) == '#') url = url.substr(1); + var pathQuery = url.split('?'); + anchor.path = decodeURIComponent(pathQuery[0]); + anchor.param = {}; + foreach((pathQuery[1] || "").split('&'), function(keyValue){ + if (keyValue) { + var parts = keyValue.split('='); + var key = decodeURIComponent(parts[0]); + var value = parts[1]; + if (!value) value = true; + anchor.param[key] = decodeURIComponent(value); + } + }); + } + var params = []; + foreach(anchor.param, function(value, key){ + params.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return (anchor.path ? anchor.path : '') + (params.length ? '?' + params.join('&') : ''); + }; + this.$config.location.watch(function(url){ + anchor(url); + }); + this.$onEval(PRIORITY_LAST, function(){ + scope.$config.location.set(anchor()); + }); + return anchor; +}); diff --git a/test/AngularTest.js b/test/AngularTest.js deleted file mode 100644 index 8db723e5..00000000 --- a/test/AngularTest.js +++ /dev/null @@ -1,44 +0,0 @@ -AngularTest = TestCase('AngularTest'); - -UrlWatcherTest = TestCase('UrlWatcherTest'); - -UrlWatcherTest.prototype.testUrlWatcher = function () { - expectAsserts(2); - var location = {href:"http://server", hash:""}; - var watcher = new UrlWatcher(location); - watcher.delay = 1; - watcher.listener = function(url){ - assertEquals('http://getangular.test', url); - }; - watcher.setTimeout = function(fn, delay){ - assertEquals(1, delay); - location.href = "http://getangular.test"; - watcher.setTimeout = function(fn, delay) { - }; - fn(); - }; - watcher.watch(); -}; - -UrlWatcherTest.prototype.testItShouldFireOnUpdateEventWhenSpecialURLSet = function(){ - expectAsserts(2); - var location = {href:"http://server", hash:"#$iframe_notify=1234"}; - var watcher = new UrlWatcher(location); - angular.callbacks._iframe_notify_1234 = function () { - assertEquals("undefined", typeof angularCallbacks._iframe_notify_1234); - assertEquals("http://server2#", location.href); - }; - watcher.delay = 1; - watcher.expectedUrl = "http://server2"; - watcher.setTimeout = function(fn, delay){ - watcher.setTimeout = function(fn, delay) {}; - fn(); - }; - watcher.watch(); -}; - -FunctionTest = TestCase("FunctionTest"); - -FunctionTest.prototype.testEscapeHtml = function () { - assertEquals("<div>&amp;</div>", escapeHtml('<div>&</div>')); -}; diff --git a/test/BinderTest.js b/test/BinderTest.js index 4d996a8e..67800e62 100644 --- a/test/BinderTest.js +++ b/test/BinderTest.js @@ -201,24 +201,6 @@ BinderTest.prototype.XtestParseAnchor = function(){ assertTrue(!binder.anchor.x); }; -BinderTest.prototype.XtestWriteAnchor = function(){ - var binder = this.compile("<div/>").binder; - binder.location.set('a'); - binder.anchor.a = 'b'; - binder.anchor.c = ' '; - binder.anchor.d = true; - binder.updateAnchor(); - assertEquals(binder.location.get(), "a#a=b&c=%20&d"); -}; - -BinderTest.prototype.XtestWriteAnchorAsPartOfTheUpdateView = function(){ - var binder = this.compile("<div/>").binder; - binder.location.set('a'); - binder.anchor.a = 'b'; - binder.updateView(); - assertEquals(binder.location.get(), "a#a=b"); -}; - BinderTest.prototype.testRepeaterUpdateBindings = function(){ var a = this.compile('<ul><LI ng-repeat="item in model.items" ng-bind="item.a"/></ul>'); var form = a.node; @@ -821,3 +803,23 @@ BinderTest.prototype.testItShouldUseFormaterForText = function() { x.scope.$eval(); assertEquals('1, 2, 3', input[0].value); }; + +BinderTest.prototype.XtestWriteAnchor = function(){ + var binder = this.compile("<div/>").binder; + binder.location.set('a'); + binder.anchor.a = 'b'; + binder.anchor.c = ' '; + binder.anchor.d = true; + binder.updateAnchor(); + assertEquals(binder.location.get(), "a#a=b&c=%20&d"); +}; + +BinderTest.prototype.XtestWriteAnchorAsPartOfTheUpdateView = function(){ + var binder = this.compile("<div/>").binder; + binder.location.set('a'); + binder.anchor.a = 'b'; + binder.updateView(); + assertEquals(binder.location.get(), "a#a=b"); +}; + + diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index 7e1a899f..8d2a0ed4 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -11,7 +11,8 @@ describe('scope/model', function(){ model.$set('name', 'adam'); expect(model.name).toEqual('adam'); expect(model.$get('name')).toEqual('adam'); - expect(model.$parent).toEqual(parent); + expect(model.$parent).toEqual(model); + expect(model.$root).toEqual(model); }); //$eval @@ -78,15 +79,50 @@ describe('scope/model', function(){ expect(model.printed).toEqual(true); }); - - //$tryEval it('should report error on element', function(){ - + var scope = createScope(); + scope.$tryEval('throw "myerror";', function(error){ + scope.error = error; + }); + expect(scope.error).toEqual('myerror'); }); it('should report error on visible element', function(){ + var element = jqLite('<div></div>'); + var scope = createScope(); + scope.$tryEval('throw "myError"', element); + expect(element.attr('ng-error')).toEqual('"myError"'); // errors are jsonified + expect(element.hasClass('ng-exception')).toBeTruthy(); + }); + + // $onEval + + it("should eval using priority", function(){ + var scope = createScope(); + scope.log = ""; + scope.$onEval('log = log + "middle;"'); + scope.$onEval(-1, 'log = log + "first;"'); + scope.$onEval(1, 'log = log + "last;"'); + scope.$eval(); + expect(scope.log).toEqual('first;middle;last;'); + }); + + // Services are initialized + it("should inject services", function(){ + var scope = createScope(serviceAdapter({ + $window: function(){ + return window; + } + })); + expect(scope.$window).toEqual(window); + }); + it("should have $root and $parent", function(){ + var parent = createScope(); + var scope = createScope(parent); + expect(scope.$root).toEqual(parent); + expect(scope.$parent).toEqual(parent); }); }); diff --git a/test/UrlWatcherTest.js b/test/UrlWatcherTest.js new file mode 100644 index 00000000..6080ca62 --- /dev/null +++ b/test/UrlWatcherTest.js @@ -0,0 +1,25 @@ +UrlWatcherTest = TestCase('UrlWatcherTest'); + +UrlWatcherTest.prototype.testUrlWatcher = function () { + expectAsserts(2); + var location = {href:"http://server", hash:""}; + var watcher = new UrlWatcher(location); + watcher.delay = 1; + watcher.watch(function(url){ + assertEquals('http://getangular.test', url); + }); + watcher.setTimeout = function(fn, delay){ + assertEquals(1, delay); + location.href = "http://getangular.test"; + watcher.setTimeout = function(fn, delay) { + }; + fn(); + }; + watcher.start(); +}; + +FunctionTest = TestCase("FunctionTest"); + +FunctionTest.prototype.testEscapeHtml = function () { + assertEquals("<div>&amp;</div>", escapeHtml('<div>&</div>')); +}; diff --git a/test/servicesSpec.js b/test/servicesSpec.js new file mode 100644 index 00000000..5a6bcedc --- /dev/null +++ b/test/servicesSpec.js @@ -0,0 +1,27 @@ +describe("services", function(){ + var scope; + + beforeEach(function(){ + scope = createScope({ + $config: { + 'location': {'get':noop, 'set':noop, 'watch':noop} + } + }, serviceAdapter(angularService)); + }); + + it("should inject $window", function(){ + expect(scope.$window).toEqual(window); + }); + + it("should inject $anchor", function(){ + scope.$anchor('#path?key=value'); + expect(scope.$anchor.path).toEqual("path"); + expect(scope.$anchor.param).toEqual({key:'value'}); + + scope.$anchor.path = 'page=http://path'; + scope.$anchor.param = {k:'a=b'}; + + expect(scope.$anchor()).toEqual('page=http://path?k=a%3Db'); + + }); +}); |
