initial revision of new plugable compiler

This commit is contained in:
Misko Hevery 2010-03-18 12:20:06 -07:00
parent f1b50b92ac
commit 7634a3ed52
6 changed files with 224 additions and 27 deletions

View file

@ -24,9 +24,14 @@ var consoleNode, msie,
jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy
foreach = _.each, foreach = _.each,
extend = _.extend, extend = _.extend,
slice = Array.prototype.slice,
identity = _.identity, identity = _.identity,
angular = window['angular'] || (window['angular'] = {}), angular = window['angular'] || (window['angular'] = {}),
angularValidator = angular['validator'] || (angular['validator'] = {}), angularValidator = angular['validator'] || (angular['validator'] = {}),
angularDirective = angular['directive'] || (angular['directive'] = function(name, fn){
if (fn) {angularDirective[name] = fn;};
return angularDirective[name];
}),
angularFilter = angular['filter'] || (angular['filter'] = {}), angularFilter = angular['filter'] || (angular['filter'] = {}),
angularFormatter = angular['formatter'] || (angular['formatter'] = {}), angularFormatter = angular['formatter'] || (angular['formatter'] = {}),
angularCallbacks = angular['callbacks'] || (angular['callbacks'] = {}), angularCallbacks = angular['callbacks'] || (angular['callbacks'] = {}),
@ -37,7 +42,7 @@ angular['copy'] = copy;
var isVisible = isVisible || function (element) { var isVisible = isVisible || function (element) {
return jQuery(element).is(":visible"); return jQuery(element).is(":visible");
} };
function log(a, b, c){ function log(a, b, c){
var console = window['console']; var console = window['console'];
@ -154,12 +159,13 @@ function escapeAttr(html) {
} }
function bind(_this, _function) { function bind(_this, _function) {
var curryArgs = slice.call(arguments, 2, arguments.length);
if (!_this) if (!_this)
throw "Missing this"; throw "Missing this";
if (!_.isFunction(_function)) if (!_.isFunction(_function))
throw "Missing function"; throw "Missing function";
return function() { return function() {
return _function.apply(_this, arguments); return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length)));
}; };
} }

View file

@ -29,7 +29,7 @@ DataStore.prototype = {
} }
return cachedDocument; return cachedDocument;
}, },
load: function(instance, id, callback, failure) { load: function(instance, id, callback, failure) {
if (id && id !== '*') { if (id && id !== '*') {
var self = this; var self = this;
@ -43,7 +43,7 @@ DataStore.prototype = {
} }
return instance; return instance;
}, },
loadMany: function(entity, ids, callback) { loadMany: function(entity, ids, callback) {
var self=this; var self=this;
var list = []; var list = [];
@ -58,7 +58,7 @@ DataStore.prototype = {
}); });
return list; return list;
}, },
loadOrCreate: function(instance, id, callback) { loadOrCreate: function(instance, id, callback) {
var self=this; var self=this;
return this.load(instance, id, callback, function(response){ return this.load(instance, id, callback, function(response){
@ -70,7 +70,7 @@ DataStore.prototype = {
} }
}); });
}, },
loadAll: function(entity, callback) { loadAll: function(entity, callback) {
var self = this; var self = this;
var list = []; var list = [];
@ -89,7 +89,7 @@ DataStore.prototype = {
}); });
return list; return list;
}, },
save: function(document, callback) { save: function(document, callback) {
var self = this; var self = this;
var data = {}; var data = {};
@ -109,7 +109,7 @@ DataStore.prototype = {
callback(document); callback(document);
}); });
}, },
remove: function(document, callback) { remove: function(document, callback) {
var self = this; var self = this;
var data = {}; var data = {};
@ -127,7 +127,7 @@ DataStore.prototype = {
(callback||noop)(response); (callback||noop)(response);
}); });
}, },
_jsonRequest: function(request, callback, failure) { _jsonRequest: function(request, callback, failure) {
request['$$callback'] = callback; request['$$callback'] = callback;
request['$$failure'] = failure||function(response){ request['$$failure'] = failure||function(response){
@ -135,7 +135,7 @@ DataStore.prototype = {
}; };
this.bulkRequest.push(request); this.bulkRequest.push(request);
}, },
flush: function() { flush: function() {
if (this.bulkRequest.length === 0) return; if (this.bulkRequest.length === 0) return;
var self = this; var self = this;
@ -169,7 +169,7 @@ DataStore.prototype = {
} }
this.post(bulkRequest, callback); this.post(bulkRequest, callback);
}, },
saveScope: function(scope, callback) { saveScope: function(scope, callback) {
var saveCounter = 1; var saveCounter = 1;
function onSaveDone() { function onSaveDone() {
@ -186,7 +186,7 @@ DataStore.prototype = {
} }
onSaveDone(); onSaveDone();
}, },
query: function(type, query, arg, callback){ query: function(type, query, arg, callback){
var self = this; var self = this;
var queryList = []; var queryList = [];
@ -205,7 +205,7 @@ DataStore.prototype = {
}); });
return queryList; return queryList;
}, },
entities: function(callback) { entities: function(callback) {
var entities = []; var entities = [];
var self = this; var self = this;
@ -218,7 +218,7 @@ DataStore.prototype = {
}); });
return entities; return entities;
}, },
documentCountsByUser: function(){ documentCountsByUser: function(){
var counts = {}; var counts = {};
var self = this; var self = this;
@ -227,7 +227,7 @@ DataStore.prototype = {
}); });
return counts; return counts;
}, },
userDocumentIdsByEntity: function(user){ userDocumentIdsByEntity: function(user){
var ids = {}; var ids = {};
var self = this; var self = this;
@ -236,7 +236,7 @@ DataStore.prototype = {
}); });
return ids; return ids;
}, },
entity: function(name, defaults){ entity: function(name, defaults){
if (!name) { if (!name) {
return DataStore.NullEntity; return DataStore.NullEntity;
@ -271,7 +271,7 @@ DataStore.prototype = {
}); });
return entity; return entity;
}, },
join: function(join){ join: function(join){
function fn(){ function fn(){
throw "Joined entities can not be instantiated into a document."; throw "Joined entities can not be instantiated into a document.";
@ -327,4 +327,4 @@ DataStore.prototype = {
}; };
return fn; return fn;
} }
}; };

View file

@ -79,7 +79,7 @@ ResourceFactory.prototype = {
case 1: if (isPost) data = a1; else params = a1; break; case 1: if (isPost) data = a1; else params = a1; break;
case 0: break; case 0: break;
default: default:
throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments." throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments.";
} }
var value = action.isArray ? [] : new Resource(data); var value = action.isArray ? [] : new Resource(data);
@ -109,7 +109,7 @@ ResourceFactory.prototype = {
case 1: if (typeof a1 == 'function') callback = a1; else params = a1; case 1: if (typeof a1 == 'function') callback = a1; else params = a1;
case 0: break; case 0: break;
default: default:
throw "Expected between 1-3 arguments [params, data, callback], got " + arguments.length + " arguments." throw "Expected between 1-3 arguments [params, data, callback], got " + arguments.length + " arguments.";
} }
var self = this; var self = this;
Resource[name](params, this, function(response){ Resource[name](params, this, function(response){

View file

@ -10,6 +10,7 @@ function Scope(initialState, name) {
if (name == "ROOT") { if (name == "ROOT") {
this.state['$root'] = this.state; this.state['$root'] = this.state;
} }
this.set('$watch', bind(this, this.addWatchListener));
}; };
Scope.expressionCache = {}; Scope.expressionCache = {};
@ -202,5 +203,9 @@ Scope.prototype = {
} }
}); });
return fired; return fired;
},
apply: function(fn) {
fn.apply(this.state, slice(arguments, 0, arguments.length));
} }
}; };

View file

@ -4,7 +4,7 @@ angular.directive("auth", function(expression, element){
if(expression == "eager") { if(expression == "eager") {
this.$users.fetchCurrent(); this.$users.fetchCurrent();
} }
} };
}); });
@ -30,7 +30,7 @@ angular.directive("entity", function(expression, element){
angular.directive("init", function(expression, element){ angular.directive("init", function(expression, element){
return function(){ return function(){
this.$eval(expresssion); this.$eval(expresssion);
} };
}); });
@ -49,8 +49,8 @@ angular.directive("bind", function(expression, element){
// becomes // becomes
// <a href="" ng-bind-attr="{href:'http://example.com?id={{book.$id}}', alt:'{{book.$name}}'}">link</a> // <a href="" ng-bind-attr="{href:'http://example.com?id={{book.$id}}', alt:'{{book.$name}}'}">link</a>
angular.directive("bind-attr", function(expression, element){ angular.directive("bind-attr", function(expression, element){
var jElement = jQuery(element); return function(expression, element){
return function(){ var jElement = jQuery(element);
this.$watch(expression, _(jElement.attr).bind(jElement)); this.$watch(expression, _(jElement.attr).bind(jElement));
}; };
}); });
@ -58,7 +58,7 @@ angular.directive("bind-attr", function(expression, element){
angular.directive("repeat", function(expression, element){ angular.directive("repeat", function(expression, element){
var anchor = document.createComment(expression); var anchor = document.createComment(expression);
jQuery(element).replace(anchor); jQuery(element).replace(anchor);
var template = this.compile(element); var template = this.templetize(element);
var lhs = "item"; var lhs = "item";
var rhs = "items"; var rhs = "items";
var children = []; var children = [];
@ -103,7 +103,7 @@ angular.directive("action", function(expression, element){
angular.directive("eval", function(expression, element){ angular.directive("eval", function(expression, element){
return function(){ return function(){
this.$onUpdate( expression); this.$onUpdate( expression);
} };
}); });
//ng-watch //ng-watch
// <div ng-watch="$anchor.book: book=Book.get();"/> // <div ng-watch="$anchor.book: book=Book.get();"/>
@ -113,7 +113,7 @@ angular.directive("watch", function(expression, element){
}; // parse }; // parse
return function(){ return function(){
this.$watch(watches); this.$watch(watches);
} };
}); });
//widget related //widget related

186
test/CompilerSpec.js Normal file
View file

@ -0,0 +1,186 @@
function Template() {
this.paths = [];
this.children = [];
this.inits = [];
}
Template.prototype = {
init: function(element, scope) {
foreach(this.inits, function(fn) {
scope.apply(fn, element);
});
var i,
childNodes = element.childNodes,
children = this.children,
paths = this.paths,
length = paths.length;
for (i = 0; i < length; i++) {
children[i].init(childNodes[paths[i]], scope);
}
},
addInit:function(init) {
if (init) {
this.inits.push(init);
}
},
addChild: function(index, template) {
this.paths.push(index);
this.children.push(template);
}
};
function Compiler(directives){
this.directives = directives;
}
DIRECTIVE = /^ng-(.*)$/;
/**
* return {
* element:
* init: function(element){...}
* }
*
* internal data structure: {
* paths: [4, 5, 6],
* directive: name,
* init: function(expression, element){}
* }
*
* template : {
* inits: [fn(), fn()}
* paths: [1, 5],
* templates: [
* inits: []
* paths: []
* templates:
* ]
* }
*/
Compiler.prototype = {
compile: function(element) {
var template = this.templetize(element);
return function(){
var scope = new Scope();
return {
scope: scope,
element:element,
init: bind(template, template.init, element, scope)
};
};
},
templetize: function(element){
var items, item, length, i, directive, init, template,
childTemplate, recurse = true;
// Process attributes/directives
for (i = 0, items = element.attributes, length = items.length;
i < length; i++) {
item = items[i];
var match = item.name.match(DIRECTIVE);
if (match) {
directive = this.directives[match[1]];
if (directive) {
init = directive.call({}, item.value, element);
template = template || new Template();
template.addInit(init);
recurse = recurse && init;
}
}
}
// Process children
if (recurse) {
for (i = 0, items = element.childNodes, length = items.length;
i < length; i++) {
if(childTemplate = this.templetize(items[i])) {
template = template || new Template();
template.addChild(i, childTemplate);
}
}
}
return template;
}
};
describe('compiler', function(){
function element(html) {
return jQuery(html)[0];
}
var compiler, directives, compile, log;
beforeEach(function(){
log = "";
directives = {
hello: function(expression, element){
log += "hello ";
return function() {
log += expression;
};
},
watch: function(expression, element){
return function() {
this.$watch(expression, function(val){
log += ":" + val;
});
};
}
};
compiler = new Compiler(directives);
compile = function(html){
var e = element(html);
var view = compiler.compile(e)(e);
view.init();
return view.scope;
};
});
it('should recognize a directive', function(){
var e = element('<div ng-directive="expr" ignore="me"></div>');
directives.directive = function(expression, element){
log += "found";
expect(expression).toEqual("expr");
expect(element).toEqual(e);
return function initFn() {
log += ":init";
};
};
var template = compiler.compile(e);
var init = template(e).init;
expect(log).toEqual("found");
init();
expect(log).toEqual("found:init");
});
it('should recurse to children', function(){
var scope = compile('<div><span ng-hello="misko"/></div>');
expect(log).toEqual("hello misko");
});
it('should watch scope', function(){
var scope = compile('<span ng-watch="name"/>');
expect(log).toEqual("");
scope.updateView();
scope.set('name', 'misko');
scope.updateView();
scope.updateView();
scope.set('name', 'adam');
scope.updateView();
scope.updateView();
expect(log).toEqual(":misko:adam");
});
it('should prevent recursion', function(){
directives.stop = function(){ return false; };
var scope = compile('<span ng-hello="misko" ng-stop="true"><span ng-hello="adam"/></span>');
expect(log).toEqual("hello misko");
});
});