started to add services

This commit is contained in:
Misko Hevery 2010-03-31 17:56:16 -07:00
parent 35a9108500
commit 11a6431f89
11 changed files with 290 additions and 167 deletions

View file

@ -1,9 +1,61 @@
//////////////////////////////
//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;
}
};
if (typeof document.getAttribute == 'undefined')
document.getAttribute = function() {};
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',
@ -13,7 +65,7 @@ var consoleNode,
msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)),
jqLite = jQuery || jqLiteWrap,
slice = Array.prototype.slice,
angular = window['angular'] || (window['angular'] = {}),
angular = window['angular'] || (window['angular'] = {}),
angularTextMarkup = extensionMap(angular, 'textMarkup'),
angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
angularDirective = extensionMap(angular, 'directive'),
@ -21,11 +73,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 +114,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 +367,22 @@ 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);
}
/////////////////////////////////////////////////

View file

@ -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() {

View file

@ -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;},

View file

@ -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);
});
};
};

View file

@ -1,62 +0,0 @@
// ////////////////////////////
// UrlWatcher
// ////////////////////////////
function UrlWatcher(location) {
this.location = location;
this.delay = 25;
this.setTimeout = function(fn, delay) {
window.setTimeout(fn, delay);
};
this.listener = function(url) {
return url;
};
this.expectedUrl = location.href;
}
UrlWatcher.prototype = {
listen: function(fn){
this.listener = fn;
},
watch: function() {
var self = this;
var pull = function() {
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;
}
}
self.setTimeout(pull, self.delay);
};
pull();
},
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 window.location.href;
}
};

34
src/services.js Normal file
View file

@ -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;
});

View file

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

View file

@ -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");
};

View file

@ -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);
});
});

25
test/UrlWatcherTest.js Normal file
View file

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

27
test/servicesSpec.js Normal file
View file

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