aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/Angular.js71
-rw-r--r--src/Compiler.js1
-rw-r--r--src/Parser.js4
-rw-r--r--src/Scope.js55
-rw-r--r--src/UrlWatcher.js40
-rw-r--r--src/services.js34
-rw-r--r--test/AngularTest.js44
-rw-r--r--test/BinderTest.js38
-rw-r--r--test/ScopeSpec.js44
-rw-r--r--test/UrlWatcherTest.js25
-rw-r--r--test/servicesSpec.js27
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("&lt;div&gt;&amp;amp;&lt;/div&gt;", escapeHtml('<div>&amp;</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("&lt;div&gt;&amp;amp;&lt;/div&gt;", escapeHtml('<div>&amp;</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');
+
+ });
+});