angular.js/src/Scope.js

408 lines
11 KiB
JavaScript
Raw Normal View History

2010-01-12 01:32:33 +00:00
function Scope(initialState, name) {
2010-03-22 20:58:04 +00:00
var self = this;
self.widgets = [];
self.evals = [];
self.watchListeners = {};
self.name = name;
2010-01-06 00:36:58 +00:00
initialState = initialState || {};
var State = function(){};
State.prototype = initialState;
2010-03-22 20:58:04 +00:00
self.state = new State();
extend(self.state, {
'$parent': initialState,
'$watch': bind(self, self.addWatchListener),
'$eval': bind(self, self.eval),
2010-03-25 20:01:08 +00:00
'$bind': bind(self, bind, self),
// change name to autoEval?
'$addEval': bind(self, self.addEval),
'$updateView': bind(self, self.updateView)
2010-03-22 20:58:04 +00:00
});
2010-01-06 00:36:58 +00:00
if (name == "ROOT") {
2010-03-22 20:58:04 +00:00
self.state['$root'] = self.state;
2010-01-06 00:36:58 +00:00
}
};
2010-01-09 23:02:43 +00:00
Scope.expressionCache = {};
Scope.getter = function(instance, path) {
2010-01-06 00:36:58 +00:00
if (!path) return instance;
var element = path.split('.');
var key;
var lastInstance = instance;
var len = element.length;
for ( var i = 0; i < len; i++) {
key = element[i];
if (!key.match(/^[\$\w][\$\w\d]*$/))
throw "Expression '" + path + "' is not a valid expression for accesing variables.";
if (instance) {
lastInstance = instance;
instance = instance[key];
}
if (_.isUndefined(instance) && key.charAt(0) == '$') {
var type = angular['Global']['typeOf'](lastInstance);
2010-01-06 00:36:58 +00:00
type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
var fn = type ? type[[key.substring(1)]] : undefined;
if (fn) {
instance = _.bind(fn, lastInstance, lastInstance);
return instance;
}
}
}
if (typeof instance === 'function' && !instance['$$factory']) {
2010-01-09 23:02:43 +00:00
return bind(lastInstance, instance);
2010-01-06 00:36:58 +00:00
}
return instance;
};
2010-03-26 05:03:11 +00:00
Scope.setter = function(instance, path, value){
var element = path.split('.');
for ( var i = 0; element.length > 1; i++) {
var key = element.shift();
var newInstance = instance[key];
if (!newInstance) {
newInstance = {};
instance[key] = newInstance;
}
instance = newInstance;
}
instance[element.shift()] = value;
return value;
};
2010-01-12 01:32:33 +00:00
Scope.prototype = {
2010-03-22 20:58:04 +00:00
// TODO: rename to update? or eval?
2010-01-12 01:32:33 +00:00
updateView: function() {
var self = this;
this.fireWatchers();
2010-03-18 21:43:49 +00:00
foreach(this.widgets, function(widget){
2010-01-12 01:32:33 +00:00
self.evalWidget(widget, "", {}, function(){
this.updateView(self);
});
});
2010-03-18 21:43:49 +00:00
foreach(this.evals, bind(this, this.apply));
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
addWidget: function(controller) {
if (controller) this.widgets.push(controller);
},
2010-03-15 21:36:50 +00:00
2010-03-18 21:43:49 +00:00
addEval: function(fn, listener) {
// todo: this should take a function/string and a listener
2010-03-22 20:58:04 +00:00
// todo: this is a hack, which will need to be cleaned up.
var self = this,
listenFn = listener || noop,
expr = self.compile(fn);
2010-03-22 20:58:04 +00:00
this.evals.push(function(){
self.apply(listenFn, expr());
});
2010-03-18 21:43:49 +00:00
},
2010-01-12 01:32:33 +00:00
isProperty: function(exp) {
for ( var i = 0; i < exp.length; i++) {
var ch = exp.charAt(i);
if (ch!='.' && !Lexer.prototype.isIdent(ch)) {
return false;
}
2010-01-06 00:36:58 +00:00
}
2010-01-12 01:32:33 +00:00
return true;
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
get: function(path) {
// log('SCOPE.get', path, Scope.getter(this.state, path));
2010-01-12 01:32:33 +00:00
return Scope.getter(this.state, path);
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
set: function(path, value) {
// log('SCOPE.set', path, value);
2010-01-12 01:32:33 +00:00
var instance = this.state;
2010-03-26 05:03:11 +00:00
return Scope.setter(instance, path, value);
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
setEval: function(expressionText, value) {
this.eval(expressionText + "=" + toJson(value));
},
2010-03-15 21:36:50 +00:00
2010-03-22 20:58:04 +00:00
compile: function(exp) {
2010-03-23 21:57:11 +00:00
if (isFunction(exp)) return bind(this.state, exp);
var expFn = Scope.expressionCache[exp], self = this;
2010-03-22 20:58:04 +00:00
if (!expFn) {
var parser = new Parser(exp);
expFn = parser.statements();
2010-01-12 01:32:33 +00:00
parser.assertAllConsumed();
2010-03-22 20:58:04 +00:00
Scope.expressionCache[exp] = expFn;
2010-01-06 00:36:58 +00:00
}
return function(context){
context = context || {};
context.self = self.state;
context.scope = self;
return expFn.call(self, context);
};
2010-03-22 20:58:04 +00:00
},
2010-03-26 05:03:11 +00:00
eval: function(exp, context) {
2010-03-22 20:58:04 +00:00
// log('Scope.eval', expressionText);
2010-03-26 05:03:11 +00:00
return this.compile(exp)(context);
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
//TODO: Refactor. This function needs to be an execution closure for widgets
// move to widgets
// remove expression, just have inner closure.
evalWidget: function(widget, expression, context, onSuccess, onFailure) {
try {
var value = this.eval(expression, context);
if (widget.hasError) {
widget.hasError = false;
jQuery(widget.view).
removeClass('ng-exception').
removeAttr('ng-error');
}
if (onSuccess) {
value = onSuccess.apply(widget, [value]);
}
return true;
} catch (e){
var jsonError = toJson(e, true);
2010-03-15 21:36:50 +00:00
error('Eval Widget Error:', jsonError);
2010-01-12 01:32:33 +00:00
widget.hasError = true;
jQuery(widget.view).
addClass('ng-exception').
attr('ng-error', jsonError);
if (onFailure) {
onFailure.apply(widget, [e, jsonError]);
}
return false;
2010-01-06 00:36:58 +00:00
}
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
validate: function(expressionText, value, element) {
2010-01-12 01:32:33 +00:00
var expression = Scope.expressionCache[expressionText];
if (!expression) {
expression = new Parser(expressionText).validator();
Scope.expressionCache[expressionText] = expression;
2010-01-06 00:36:58 +00:00
}
var self = {scope:this, self:this.state, '$element':element};
2010-01-12 01:32:33 +00:00
return expression(self)(self, value);
},
2010-03-15 21:36:50 +00:00
entity: function(entityDeclaration, datastore) {
2010-01-12 01:32:33 +00:00
var expression = new Parser(entityDeclaration).entityDeclaration();
return expression({scope:this, datastore:datastore});
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
2010-02-04 22:02:20 +00:00
clearInvalid: function() {
var invalid = this.state['$invalidWidgets'];
while(invalid.length > 0) {invalid.pop();}
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
markInvalid: function(widget) {
2010-01-25 03:33:04 +00:00
this.state['$invalidWidgets'].push(widget);
2010-01-12 01:32:33 +00:00
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
watch: function(declaration) {
var self = this;
new Parser(declaration).watch()({
scope:this,
addListener:function(watch, exp){
self.addWatchListener(watch, function(n,o){
try {
return exp({scope:self}, n, o);
} catch(e) {
alert(e);
}
});
}
});
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
addWatchListener: function(watchExpression, listener) {
2010-03-22 23:07:42 +00:00
// TODO: clean me up!
if (!isFunction(listener)) {
listener = this.compile(listener);
2010-03-22 23:07:42 +00:00
}
2010-01-12 01:32:33 +00:00
var watcher = this.watchListeners[watchExpression];
if (!watcher) {
watcher = {listeners:[], expression:watchExpression};
this.watchListeners[watchExpression] = watcher;
2010-01-06 00:36:58 +00:00
}
2010-01-12 01:32:33 +00:00
watcher.listeners.push(listener);
},
2010-03-15 21:36:50 +00:00
2010-01-12 01:32:33 +00:00
fireWatchers: function() {
2010-03-18 21:43:49 +00:00
var self = this, fired = false;
2010-01-12 01:32:33 +00:00
foreach(this.watchListeners, function(watcher) {
var value = self.eval(watcher.expression);
if (value !== watcher.lastValue) {
foreach(watcher.listeners, function(listener){
listener(value, watcher.lastValue);
fired = true;
});
watcher.lastValue = value;
}
});
return fired;
},
apply: function(fn) {
2010-03-18 21:43:49 +00:00
fn.apply(this.state, slice.call(arguments, 1, arguments.length));
2010-01-12 01:32:33 +00:00
}
2010-03-15 21:36:50 +00:00
};
2010-03-26 05:03:11 +00:00
//////////////////////////////
function getter(instance, path) {
if (!path) return instance;
var element = path.split('.');
var key;
var lastInstance = instance;
var len = element.length;
for ( var i = 0; i < len; i++) {
key = element[i];
if (!key.match(/^[\$\w][\$\w\d]*$/))
throw "Expression '" + path + "' is not a valid expression for accesing variables.";
if (instance) {
lastInstance = instance;
instance = instance[key];
}
if (_.isUndefined(instance) && key.charAt(0) == '$') {
var type = angular['Global']['typeOf'](lastInstance);
type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
var fn = type ? type[[key.substring(1)]] : undefined;
if (fn) {
instance = _.bind(fn, lastInstance, lastInstance);
return instance;
}
}
}
if (typeof instance === 'function' && !instance['$$factory']) {
return bind(lastInstance, instance);
}
return instance;
};
function setter(instance, path, value){
var element = path.split('.');
for ( var i = 0; element.length > 1; i++) {
var key = element.shift();
var newInstance = instance[key];
if (!newInstance) {
newInstance = {};
instance[key] = newInstance;
}
instance = newInstance;
}
instance[element.shift()] = value;
return value;
};
var compileCache = {};
function expressionCompile(exp){
if (isFunction(exp)) return exp;
var expFn = compileCache[exp];
if (!expFn) {
var parser = new Parser(exp);
expFn = parser.statements();
parser.assertAllConsumed();
compileCache[exp] = expFn;
}
// return expFn
// TODO(remove this hack)
return function(){
return expFn({
scope: {
set: this.$set,
get: this.$get
}
});
};
};
var NON_RENDERABLE_ELEMENTS = {
'#text': 1, '#comment':1, 'TR':1, 'TH':1
};
function isRenderableElement(element){
return element && element[0] && !NON_RENDERABLE_ELEMENTS[element[0].nodeName];
}
function rethrow(e) { throw e; }
function errorHandlerFor(element) {
while (!isRenderableElement(element)) {
element = element.parent() || jqLite(document.body);
}
return function(error) {
element.attr('ng-error', angular.toJson(error));
element.addClass('ng-exception');
};
}
function scope(parent, Class) {
function Parent(){}
function API(){}
function Behavior(){}
var instance, behavior, api, watchList = [], evalList = [];
Class = Class || noop;
parent = Parent.prototype = parent || {};
api = API.prototype = new Parent();
behavior = Behavior.prototype = extend(new API(), Class.prototype);
instance = new Behavior();
extend(api, {
$parent: parent,
$bind: bind(instance, bind, instance),
$get: bind(instance, getter, instance),
$set: bind(instance, setter, instance),
$eval: function(exp) {
if (isDefined(exp)) {
return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));
} else {
foreach(evalList, function(eval) {
instance.$tryEval(eval.fn, eval.handler);
});
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;
}
});
}
},
$tryEval: function (expression, exceptionHandler) {
try {
return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length));
} catch (e) {
error(e);
if (isFunction(exceptionHandler)) {
exceptionHandler(e);
} else if (exceptionHandler) {
errorHandlerFor(exceptionHandler)(e);
}
}
},
$watch: function(watchExp, listener, exceptionHandler) {
var watch = expressionCompile(watchExp);
watchList.push({
watch: watch,
last: watch.call(instance),
handler: exceptionHandler,
listener:expressionCompile(listener)
});
},
$onEval: function(expr, exceptionHandler){
evalList.push({
fn: expressionCompile(expr),
handler: exceptionHandler
});
}
});
Class.apply(instance, slice.call(arguments, 2, arguments.length));
return instance;
}