diff options
| author | Misko Hevery | 2010-03-22 13:58:04 -0700 |
|---|---|---|
| committer | Misko Hevery | 2010-03-22 13:58:04 -0700 |
| commit | 84552f7f8ac3f39c4dbd7d946ae2938d63302840 (patch) | |
| tree | d3a4433d12bcbfb9d42f92a7b8ec12762ae7df3a /src | |
| parent | f6664ed7f6f6dd1f4f9756f57611a316089149cb (diff) | |
| download | angular.js-84552f7f8ac3f39c4dbd7d946ae2938d63302840.tar.bz2 | |
got few directives working
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 37 | ||||
| -rw-r--r-- | src/Compiler.js | 149 | ||||
| -rw-r--r-- | src/Parser.js | 107 | ||||
| -rw-r--r-- | src/Scope.js | 52 | ||||
| -rw-r--r-- | src/directives.js | 124 | ||||
| -rw-r--r-- | src/directivesAngularCom.js | 29 |
6 files changed, 290 insertions, 208 deletions
diff --git a/src/Angular.js b/src/Angular.js index 8793274c..cfffab04 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -18,23 +18,32 @@ if (typeof Node == 'undefined') { } function noop() {} +function identity($) {return $;} if (!window['console']) window['console']={'log':noop, 'error':noop}; +function extension(angular, name) { + var extPoint; + return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + if (isDefined(fn)) { + extPoint[name] = extend(fn, prop || {}); + } + return extPoint[name]; + }); +} + var consoleNode, msie, jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy foreach = _.each, extend = _.extend, slice = Array.prototype.slice, - identity = _.identity, angular = window['angular'] || (window['angular'] = {}), - 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'] = {}), - angularFormatter = angular['formatter'] || (angular['formatter'] = {}), - angularCallbacks = angular['callbacks'] || (angular['callbacks'] = {}), + angularDirective = extension(angular, 'directive'), + angularMarkup = extension(angular, 'markup'), + angularWidget = extension(angular, 'widget'), + angularValidator = extension(angular, 'validator'), + angularFilter = extension(angular, 'filter'), + angularFormatter = extension(angular, 'formatter'), + angularCallbacks = extension(angular, 'callbacks'), angularAlert = angular['alert'] || (angular['alert'] = function(){ log(arguments); window.alert.apply(window, arguments); }); @@ -45,7 +54,15 @@ var isVisible = isVisible || function (element) { }; function isDefined(value){ - return typeof value !== 'undefined'; + return typeof value != 'undefined'; +} + +function isObject(value){ + return typeof value == 'object'; +} + +function isFunction(value){ + return typeof value == 'function'; } function log(a, b, c){ diff --git a/src/Compiler.js b/src/Compiler.js index 3c757dd0..5c650204 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -13,7 +13,7 @@ function Template() { Template.prototype = { init: function(element, scope) { foreach(this.inits, function(fn) { - scope.apply(fn, nodeLite(element)); + scope.apply(fn, jqLite(element)); }); var i, @@ -35,8 +35,10 @@ Template.prototype = { addChild: function(index, template) { - this.paths.push(index); - this.children.push(template); + if (template) { + this.paths.push(index); + this.children.push(template); + } }, empty: function() { @@ -45,31 +47,37 @@ Template.prototype = { }; /////////////////////////////////// -//NodeLite +//JQLite ////////////////////////////////// -function NodeLite(element) { +function JQLite(element) { this.element = element; } -function nodeLite(element) { - return element instanceof NodeLite ? element : new NodeLite(element); +function jqLite(element) { + if (typeof element == 'string') { + var div = document.createElement('div'); + div.innerHTML = element; + element = div.childNodes[0]; + } + return element instanceof JQLite ? element : new JQLite(element); } -NodeLite.prototype = { +JQLite.prototype = { eachTextNode: function(fn){ var i, chldNodes = this.element.childNodes || [], size = chldNodes.length, chld; for (i = 0; i < size; i++) { - if((chld = new NodeLite(chldNodes[i])).isText()) { + if((chld = new JQLite(chldNodes[i])).isText()) { fn(chld, i); } } }, + eachNode: function(fn){ var i, chldNodes = this.element.childNodes || [], size = chldNodes.length, chld; for (i = 0; i < size; i++) { - if(!(chld = new NodeLite(chldNodes[i])).isText()) { + if(!(chld = new JQLite(chldNodes[i])).isText()) { fn(chld, i); } } @@ -84,34 +92,51 @@ NodeLite.prototype = { }, replaceWith: function(replaceNode) { - this.element.parentNode.replaceChild(nodeLite(replaceNode).element, this.element); + this.element.parentNode.replaceChild(jqLite(replaceNode).element, this.element); }, - removeAttribute: function(name) { + remove: function() { + this.element.parentNode.removeChild(this.element); + }, + + removeAttr: function(name) { this.element.removeAttribute(name); }, after: function(element) { - this.element.parentNode.insertBefore(nodeLite(element).element, this.element.nextSibling); + this.element.parentNode.insertBefore(jqLite(element).element, this.element.nextSibling); }, attr: function(name, value){ - if (isDefined(value)) { - this.element.setAttribute(name, value); + var e = this.element; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); } else { - return this.element.getAttribute(name); + return e.getAttribute(name); } }, text: function(value) { if (isDefined(value)) { - this.element.nodeValue = value; + this.element.textContent = value; } - return this.element.nodeValue; + return this.element.textContent; }, + html: function(value) { + if (isDefined(value)) { + this.element.innerHTML = value; + } + return this.element.innerHTML; + }, + + parent: function() { return jqLite(this.element.parentNode);}, isText: function() { return this.element.nodeType == Node.TEXT_NODE; }, - clone: function() { return nodeLite(this.element.cloneNode(true)); } + clone: function() { return jqLite(this.element.cloneNode(true)); } }; /////////////////////////////////// @@ -124,14 +149,14 @@ function Compiler(markup, directives, widgets){ this.widgets = widgets; } -DIRECTIVE = /^ng-(.*)$/; - Compiler.prototype = { - compile: function(element) { - var template = this.templetize(nodeLite(element)) || new Template(); - return function(element){ - var scope = new Scope(); + compile: function(rawElement) { + rawElement = jqLite(rawElement); + var template = this.templatize(rawElement) || new Template(); + return function(element, parentScope){ + var scope = new Scope(parentScope); scope.element = element; + // todo return should be a scope with everything already set on it as element return { scope: scope, element:element, @@ -140,57 +165,57 @@ Compiler.prototype = { }; }, - templetize: function(element){ + templatize: function(element){ var self = this, + elementName = element.element.nodeName, + widgets = self.widgets, + widget = widgets[elementName], markup = self.markup, markupSize = markup.length, directives = self.directives, - widgets = self.widgets, - recurse = true, + descend = true, exclusive = false, directiveQueue = [], - template = new Template(); - - // process markup for text nodes only - element.eachTextNode(function(textNode){ - for (var i = 0, text = textNode.text(); i < markupSize; i++) { - markup[i].call(self, text, textNode, element); - } - }); + template = new Template(), + selfApi = { + compile: bind(self, self.compile), + reference:function(name) {return jqLite(document.createComment(name));}, + descend: function(value){ if(isDefined(value)) descend = value; return descend;} + }; + + if (widget) { + template.addInit(widget.call(selfApi, element)); + } else { + // process markup for text nodes only + element.eachTextNode(function(textNode){ + for (var i = 0, text = textNode.text(); i < markupSize; i++) { + markup[i].call(selfApi, text, textNode, element); + } + }); - // Process attributes/directives - element.eachAttribute(function(name, value){ - var match = name.match(DIRECTIVE), - directive; - if (!exclusive && match) { - directive = directives[match[1]]; - if (directive) { + // Process attributes/directives + element.eachAttribute(function(name, value){ + var directive = directives[name]; + if (!exclusive && directive) { if (directive.exclusive) { exclusive = true; directiveQueue = []; } - directiveQueue.push(bind(self, directive, value, element)); - } else { - error("Directive '" + match[0] + "' is not recognized."); + directiveQueue.push(bind(selfApi, directive, value, element)); } - } - }); - - // Execute directives - foreach(directiveQueue, function(directive){ - var init = directive(); - template.addInit(init); - recurse = recurse && init; - }); + }); - // Process non text child nodes - if (recurse) { - element.eachNode(function(child, i){ - var childTemplate = self.templetize(child); - if(childTemplate) { - template.addChild(i, childTemplate); - } + // Execute directives + foreach(directiveQueue, function(directive){ + template.addInit(directive()); }); + + // Process non text child nodes + if (descend) { + element.eachNode(function(child, i){ + template.addChild(i, self.templatize(child)); + }); + } } return template.empty() ? null : template; } diff --git a/src/Parser.js b/src/Parser.js index b59b21a7..c18a6250 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -40,7 +40,7 @@ Lexer.prototype = { return false; } }, - + parse: function() { var tokens = this.tokens; var OPERATORS = Lexer.OPERATORS; @@ -103,22 +103,22 @@ Lexer.prototype = { } return tokens; }, - + isNumber: function(ch) { return '0' <= ch && ch <= '9'; }, - + isWhitespace: function(ch) { return ch == ' ' || ch == '\r' || ch == '\t' || ch == '\n' || ch == '\v'; }, - + isIdent: function(ch) { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' == ch || ch == '$'; }, - + readNumber: function() { var number = ""; var start = this.index; @@ -135,7 +135,7 @@ Lexer.prototype = { this.tokens.push({index:start, text:number, fn:function(){return number;}}); }, - + readIdent: function() { var ident = ""; var start = this.index; @@ -157,15 +157,17 @@ Lexer.prototype = { } this.tokens.push({index:start, text:ident, fn:fn}); }, - + readString: function(quote) { var start = this.index; var dateParseLength = this.dateParseLength; this.index++; var string = ""; + var rawString = quote; var escape = false; while (this.index < this.text.length) { var ch = this.text.charAt(this.index); + rawString += ch; if (escape) { if (ch == 'u') { var hex = this.text.substring(this.index + 1, this.index + 5); @@ -184,7 +186,7 @@ Lexer.prototype = { escape = true; } else if (ch == quote) { this.index++; - this.tokens.push({index:start, text:string, + this.tokens.push({index:start, text:rawString, string:string, fn:function(){ return (string.length == dateParseLength) ? angular['String']['toDate'](string) : string; @@ -199,7 +201,7 @@ Lexer.prototype = { this.text.substring(start) + "] starting at column '" + (start+1) + "' in expression '" + this.text + "'."; }, - + readRegexp: function(quote) { var start = this.index; this.index++; @@ -249,18 +251,18 @@ Parser.ZERO = function(){ Parser.prototype = { error: function(msg, token) { - throw "Token '" + token.text + - "' is " + msg + " at column='" + - (token.index + 1) + "' of expression '" + + throw "Token '" + token.text + + "' is " + msg + " at column='" + + (token.index + 1) + "' of expression '" + this.text + "' starting at '" + this.text.substring(token.index) + "'."; }, - + peekToken: function() { - if (this.tokens.length === 0) + if (this.tokens.length === 0) throw "Unexpected end of expression: " + this.text; return this.tokens[0]; }, - + peek: function(e1, e2, e3, e4) { var tokens = this.tokens; if (tokens.length > 0) { @@ -273,7 +275,7 @@ Parser.prototype = { } return false; }, - + expect: function(e1, e2, e3, e4){ var token = this.peek(e1, e2, e3, e4); if (token) { @@ -283,7 +285,7 @@ Parser.prototype = { } return false; }, - + consume: function(e1){ if (!this.expect(e1)) { var token = this.peek(); @@ -293,30 +295,30 @@ Parser.prototype = { this.text.substring(token.index) + "'."; } }, - + _unary: function(fn, right) { return function(self) { return fn(self, right(self)); }; }, - + _binary: function(left, fn, right) { return function(self) { return fn(self, left(self), right(self)); }; }, - + hasTokens: function () { return this.tokens.length > 0; }, - + assertAllConsumed: function(){ if (this.tokens.length !== 0) { throw "Did not understand '" + this.text.substring(this.tokens[0].index) + "' while evaluating '" + this.text + "'."; } }, - + statements: function(){ var statements = []; while(true) { @@ -335,7 +337,7 @@ Parser.prototype = { } } }, - + filterChain: function(){ var left = this.expression(); var token; @@ -347,15 +349,15 @@ Parser.prototype = { } } }, - + filter: function(){ return this._pipeFunction(angularFilter); }, - + validator: function(){ return this._pipeFunction(angularValidator); }, - + _pipeFunction: function(fnScope){ var fn = this.functionIdent(fnScope); var argsFn = []; @@ -373,7 +375,7 @@ Parser.prototype = { var _this = this; foreach(self, function(v, k) { if (k.charAt(0) == '$') { - _this[k] = v; + _this[k] = v; } }); }; @@ -386,11 +388,11 @@ Parser.prototype = { } } }, - + expression: function(){ return this.throwStmt(); }, - + throwStmt: function(){ if (this.expect('throw')) { var throwExp = this.assignment(); @@ -401,7 +403,7 @@ Parser.prototype = { return this.assignment(); } }, - + assignment: function(){ var left = this.logicalOR(); var token; @@ -417,7 +419,7 @@ Parser.prototype = { return left; } }, - + logicalOR: function(){ var left = this.logicalAND(); var token; @@ -429,7 +431,7 @@ Parser.prototype = { } } }, - + logicalAND: function(){ var left = this.equality(); var token; @@ -438,7 +440,7 @@ Parser.prototype = { } return left; }, - + equality: function(){ var left = this.relational(); var token; @@ -447,7 +449,7 @@ Parser.prototype = { } return left; }, - + relational: function(){ var left = this.additive(); var token; @@ -456,7 +458,7 @@ Parser.prototype = { } return left; }, - + additive: function(){ var left = this.multiplicative(); var token; @@ -465,7 +467,7 @@ Parser.prototype = { } return left; }, - + multiplicative: function(){ var left = this.unary(); var token; @@ -474,7 +476,7 @@ Parser.prototype = { } return left; }, - + unary: function(){ var token; if (this.expect('+')) { @@ -487,7 +489,7 @@ Parser.prototype = { return this.primary(); } }, - + functionIdent: function(fnScope) { var token = this.expect(); var element = token.text.split('.'); @@ -504,7 +506,7 @@ Parser.prototype = { } return instance; }, - + primary: function() { var primary; if (this.expect('(')) { @@ -540,7 +542,7 @@ Parser.prototype = { } return primary; }, - + closure: function(hasArgs) { var args = []; if (hasArgs) { @@ -566,7 +568,7 @@ Parser.prototype = { }; }; }, - + fieldAccess: function(object) { var field = this.expect().text; var fn = function (self){ @@ -575,7 +577,7 @@ Parser.prototype = { fn.isAssignable = field; return fn; }, - + objectIndex: function(obj) { var indexFn = this.expression(); this.consume(']'); @@ -592,7 +594,7 @@ Parser.prototype = { }; } }, - + functionCall: function(fn) { var argsFn = []; if (this.peekToken().text != ')') { @@ -614,7 +616,7 @@ Parser.prototype = { } }; }, - + // This is used with json array declaration arrayDeclaration: function () { var elementFns = []; @@ -632,12 +634,13 @@ Parser.prototype = { return array; }; }, - + object: function () { var keyValues = []; if (this.peekToken().text != '}') { do { - var key = this.expect().text; + var token = this.expect(), + key = token.string || token.text; this.consume(":"); var value = this.expression(); keyValues.push({key:key, value:value}); @@ -654,7 +657,7 @@ Parser.prototype = { return object; }; }, - + entityDeclaration: function () { var decl = []; while(this.hasTokens()) { @@ -671,7 +674,7 @@ Parser.prototype = { return code; }; }, - + entityDecl: function () { var entity = this.expect().text; var instance; @@ -690,16 +693,16 @@ Parser.prototype = { var document = Entity(); document['$$anchor'] = instance; self.scope.set(instance, document); - return "$anchor." + instance + ":{" + + return "$anchor." + instance + ":{" + instance + "=" + entity + ".load($anchor." + instance + ");" + - instance + ".$$anchor=" + angular['String']['quote'](instance) + ";" + + instance + ".$$anchor=" + angular['String']['quote'](instance) + ";" + "};"; } else { return ""; } }; }, - + watch: function () { var decl = []; while(this.hasTokens()) { @@ -716,7 +719,7 @@ Parser.prototype = { } }; }, - + watchDecl: function () { var anchorName = this.expect().text; this.consume(":"); diff --git a/src/Scope.js b/src/Scope.js index 3633f960..d22604fd 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -1,18 +1,23 @@ function Scope(initialState, name) { - this.widgets = []; - this.evals = []; - this.watchListeners = {}; - this.name = name; + var self = this; + self.widgets = []; + self.evals = []; + self.watchListeners = {}; + self.name = name; initialState = initialState || {}; var State = function(){}; State.prototype = initialState; - this.state = new State(); - this.state['$parent'] = initialState; + self.state = new State(); + extend(self.state, { + '$parent': initialState, + '$watch': bind(self, self.addWatchListener), + '$eval': bind(self, self.eval), + // change name to onEval? + '$addEval': bind(self, self.addEval) + }); if (name == "ROOT") { - this.state['$root'] = this.state; + self.state['$root'] = self.state; } - this.set('$watch', bind(this, this.addWatchListener)); - this.set('$eval', bind(this, this.addEval)); }; Scope.expressionCache = {}; @@ -47,6 +52,7 @@ Scope.getter = function(instance, path) { }; Scope.prototype = { + // TODO: rename to update? or eval? updateView: function() { var self = this; this.fireWatchers(); @@ -64,7 +70,13 @@ Scope.prototype = { addEval: function(fn, listener) { // todo: this should take a function/string and a listener - this.evals.push(fn); + // todo: this is a hack, which will need to be cleaned up. + var self = this, + listenFn = listener || noop, + expr = bind(self, self.compile(fn), {scope: self, self: self.state}); + this.evals.push(function(){ + self.apply(listenFn, expr()); + }); }, isProperty: function(exp) { @@ -103,15 +115,21 @@ Scope.prototype = { this.eval(expressionText + "=" + toJson(value)); }, - eval: function(expressionText, context) { -// log('Scope.eval', expressionText); - var expression = Scope.expressionCache[expressionText]; - if (!expression) { - var parser = new Parser(expressionText); - expression = parser.statements(); + compile: function(exp) { + if (isFunction(exp)) return exp; + var expFn = Scope.expressionCache[exp]; + if (!expFn) { + var parser = new Parser(exp); + expFn = parser.statements(); parser.assertAllConsumed(); - Scope.expressionCache[expressionText] = expression; + Scope.expressionCache[exp] = expFn; } + return expFn; + }, + + eval: function(expressionText, context) { +// log('Scope.eval', expressionText); + var expression = this.compile(expressionText); context = context || {}; context.scope = this; context.self = this.state; diff --git a/src/directives.js b/src/directives.js index 26cbfe2c..66a5e864 100644 --- a/src/directives.js +++ b/src/directives.js @@ -1,86 +1,82 @@ - -angular.directive("auth", function(expression, element){ +angularDirective("ng-init", function(expression){ return function(){ - if(expression == "eager") { - this.$users.fetchCurrent(); - } - }; -}); - - -//expression = "book=Book:{year=2000}" -angular.directive("entity", function(expression, element){ - //parse expression, ignore element - var entityName; // "Book"; - var instanceName; // "book"; - var defaults; // {year: 2000}; - - parse(expression); - - return function(){ - this[entityName] = this.$datastore.entity(entityName, defaults); - this[instanceName] = this[entityName](); - this.$watch("$anchor."+instanceName, function(newAnchor){ - this[instanceName] = this[entityName].get(this.$anchor[instanceName]); - }); + this.$eval(expression); }; }); - -angular.directive("init", function(expression, element){ +angularDirective("ng-eval", function(expression){ return function(){ - this.$eval(expresssion); + this.$addEval(expression); }; }); - -//translation of {{ }} to ng-bind is external to this -angular.directive("bind", function(expression, element){ - return function() { +angular.directive("ng-bind", function(expression){ + return function(element) { this.$watch(expression, function(value){ - element.innerText = value; + element.text(value); }); }; }); - -// translation of {{ }} to ng-bind-attr is external to this -// <a href="http://example.com?id={{book.$id}}" alt="{{book.$name}}">link</a> -// becomes -// <a href="" ng-bind-attr="{href:'http://example.com?id={{book.$id}}', alt:'{{book.$name}}'}">link</a> -angular.directive("bind-attr", function(expression, element){ - return function(expression, element){ - var jElement = jQuery(element); - this.$watch(expression, _(jElement.attr).bind(jElement)); +angular.directive("ng-bind-attr", function(expression){ + return function(element){ + this.$watch(expression, bind(element, element.attr)); }; }); -angular.directive("repeat", function(expression, element){ - var anchor = document.createComment(expression); - jQuery(element).replace(anchor); - var template = this.compile(element); - var lhs = "item"; - var rhs = "items"; +angular.directive("ng-non-bindable", function(){ + this.descend(false); +}); + +angular.directive("ng-repeat", function(expression, element){ + var reference = this.reference("ng-repeat: " + expression), + r = element.removeAttr('ng-repeat'), + template = this.compile(element), + path = expression.split(' in '), + lhs = path[0], + rhs = path[1]; + var parent = element.parent(); + element.replaceWith(reference); return function(){ - var children = []; - this.$eval(rhs, function(items){ - foreach(children, function(child){ - child.element.remove(); - }); - foreach(items, function(item){ - var child = template(item); // create scope - element.addChild(child.element, anchor); - children.push(child); + var children = [], + currentScope = this; + this.$addEval(rhs, function(items){ + var index = 0, childCount = children.length, childScope, lastElement = reference; + foreach(items, function(value, key){ + if (index < childCount) { + // reuse existing child + childScope = children[index]; + } else { + // grow children + childScope = template(element.clone(), currentScope); + childScope.init(); + childScope.scope.set('$index', index); + childScope.element.attr('ng-index', index); + lastElement.after(childScope.element); + children.push(childScope); + } + childScope.scope.set(lhs, value); + childScope.scope.updateView(); + lastElement = childScope.element; + index ++; }); + // shrink children + while(children.length > index) { + children.pop().element.remove(); + } }); }; -}); +}, {exclusive: true}); + + +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// + -//ng-non-bindable -angular.directive("non-bindable", function(expression, element){ - return false; -}); //Styling // @@ -99,12 +95,6 @@ angular.directive("action", function(expression, element){ }; }); -//ng-eval -angular.directive("eval", function(expression, element){ - return function(){ - this.$eval(expression); - }; -}); //ng-watch // <div ng-watch="$anchor.book: book=Book.get();"/> angular.directive("watch", function(expression, element){ diff --git a/src/directivesAngularCom.js b/src/directivesAngularCom.js new file mode 100644 index 00000000..84032bdd --- /dev/null +++ b/src/directivesAngularCom.js @@ -0,0 +1,29 @@ + +angular.directive("auth", function(expression, element){ + return function(){ + if(expression == "eager") { + this.$users.fetchCurrent(); + } + }; +}); + + +//expression = "book=Book:{year=2000}" +angular.directive("entity", function(expression, element){ + //parse expression, ignore element + var entityName; // "Book"; + var instanceName; // "book"; + var defaults; // {year: 2000}; + + parse(expression); + + return function(){ + this[entityName] = this.$datastore.entity(entityName, defaults); + this[instanceName] = this[entityName](); + this.$watch("$anchor."+instanceName, function(newAnchor){ + this[instanceName] = this[entityName].get(this.$anchor[instanceName]); + }); + }; +}); + + |
