diff options
| -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 | ||||
| -rwxr-xr-x | test.sh | 2 | ||||
| -rw-r--r-- | test/CompilerSpec.js | 33 | ||||
| -rw-r--r-- | test/ParserTest.js | 19 | ||||
| -rw-r--r-- | test/directivesSpec.js | 71 |
10 files changed, 391 insertions, 232 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]); + }); + }; +}); + + @@ -1,2 +1,2 @@ -java -jar lib/jstestdriver/JsTestDriver.jar --tests all | grep -v lib/jasmine +java -jar lib/jstestdriver/JsTestDriver.jar --tests all | grep -v lib/jasmine diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js index 9f02262d..3ea2e473 100644 --- a/test/CompilerSpec.js +++ b/test/CompilerSpec.js @@ -36,7 +36,7 @@ describe('compiler', function(){ }); it('should recognize a directive', function(){ - var e = element('<div ng-directive="expr" ignore="me"></div>'); + var e = element('<div directive="expr" ignore="me"></div>'); directives.directive = function(expression, element){ log += "found"; expect(expression).toEqual("expr"); @@ -53,12 +53,12 @@ describe('compiler', function(){ }); it('should recurse to children', function(){ - var scope = compile('<div><span ng-hello="misko"/></div>'); + var scope = compile('<div><span hello="misko"/></div>'); expect(log).toEqual("hello misko"); }); it('should watch scope', function(){ - var scope = compile('<span ng-watch="name"/>'); + var scope = compile('<span watch="name"/>'); expect(log).toEqual(""); scope.updateView(); scope.set('name', 'misko'); @@ -70,24 +70,24 @@ describe('compiler', function(){ 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>'); + it('should prevent descend', function(){ + directives.stop = function(){ this.descend(false); }; + var scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>'); expect(log).toEqual("hello misko"); }); it('should allow creation of templates', function(){ directives.duplicate = function(expr, element){ element.replaceWith(document.createComment("marker")); - element.removeAttribute("ng-duplicate"); + element.removeAttr("duplicate"); var template = this.compile(element); return function(marker) { - this.$eval(function() { + this.$addEval(function() { marker.after(template(element.clone()).element); }); }; }; - var scope = compile('before<span ng-duplicate="expr">x</span>after'); + var scope = compile('before<span duplicate="expr">x</span>after'); expect($(scope.element).html()).toEqual('before<!--marker-->after'); scope.updateView(); expect($(scope.element).html()).toEqual('before<!--marker--><span>x</span>after'); @@ -103,7 +103,7 @@ describe('compiler', function(){ }; directives.exclusive.exclusive = true; - compile('<span ng-hello="misko", ng-exclusive/>'); + compile('<span hello="misko", exclusive/>'); expect(log).toEqual('exclusive'); }); @@ -111,24 +111,25 @@ describe('compiler', function(){ markup.push(function(text, textNode, parentNode) { if (text == 'middle') { expect(textNode.text()).toEqual(text); - parentNode.attr('ng-hello', text); + parentNode.attr('hello', text); textNode.text('replaced'); } }); var scope = compile('before<span>middle</span>after'); - expect(scope.element.innerHTML).toEqual('before<span ng-hello="middle">replaced</span>after'); + expect(scope.element.innerHTML).toEqual('before<span hello="middle">replaced</span>after'); expect(log).toEqual("hello middle"); }); - xit('should replace widgets', function(){ - widgets.button = function(element) { - element.parentNode.replaceChild(button, element); + it('should replace widgets', function(){ + widgets['NG:BUTTON'] = function(element) { + element.replaceWith('<div>button</div>', element); return function(element) { log += 'init'; }; }; var scope = compile('<ng:button>push me</ng:button>'); - expect(scope.element.innerHTML).toEqual('before<span ng-hello="middle">replaced</span>after'); + expect(scope.element.innerHTML).toEqual('<div>button</div>'); + expect(log).toEqual('init'); }); }); diff --git a/test/ParserTest.js b/test/ParserTest.js index 09c3b8de..53ca9eda 100644 --- a/test/ParserTest.js +++ b/test/ParserTest.js @@ -41,7 +41,7 @@ LexerTest.prototype.testTokenizeAString = function(){ i++; assertEquals(tokens[i].index, 15); - assertEquals(tokens[i].text, "a'c"); + assertEquals(tokens[i].string, "a'c"); i++; assertEquals(tokens[i].index, 21); @@ -49,7 +49,7 @@ LexerTest.prototype.testTokenizeAString = function(){ i++; assertEquals(tokens[i].index, 22); - assertEquals(tokens[i].text, 'd"e'); + assertEquals(tokens[i].string, 'd"e'); }; @@ -68,10 +68,10 @@ LexerTest.prototype.testQuotedString = function(){ var tokens = lexer.parse(); assertEquals(1, tokens[1].index); - assertEquals("'", tokens[1].text); + assertEquals("'", tokens[1].string); assertEquals(7, tokens[3].index); - assertEquals('"', tokens[3].text); + assertEquals('"', tokens[3].string); }; @@ -80,14 +80,14 @@ LexerTest.prototype.testQuotedStringEscape = function(){ var lexer = new Lexer(str); var tokens = lexer.parse(); - assertEquals('"\n\f\r\t\v\u00A0', tokens[0].text); + assertEquals('"\n\f\r\t\v\u00A0', tokens[0].string); }; LexerTest.prototype.testTokenizeUnicode = function(){ var lexer = new Lexer('"\\u00A0"'); var tokens = lexer.parse(); assertEquals(1, tokens.length); - assertEquals('\u00a0', tokens[0].text); + assertEquals('\u00a0', tokens[0].string); }; LexerTest.prototype.testTokenizeRegExpWithOptions = function(){ @@ -408,7 +408,7 @@ ParserTest.prototype.testItShouldParseOnChangeIntoHashSet = function () { ParserTest.prototype.testItShouldParseOnChangeBlockIntoHashSet = function () { var scope = new Scope({count:0}); var listeners = {a:[], b:[]}; - scope.watch("a:{count=count+1;count=count+20;};b:count=count+300", + scope.watch("a:{count=count+1;count=count+20;};b:count=count+300", function(n, fn){listeners[n].push(fn);}); assertEquals(1, scope.watchListeners.a.listeners.length); @@ -477,3 +477,8 @@ ParserTest.prototype.testNegationBug = function () { assertEquals(12/6/2, scope.eval("12/6/2")); }; +ParserTest.prototype.testBugStringConfusesParser = function() { + var scope = new Scope(); + assertEquals('!', scope.eval('suffix = "!"')); +}; + diff --git a/test/directivesSpec.js b/test/directivesSpec.js new file mode 100644 index 00000000..176f9e70 --- /dev/null +++ b/test/directivesSpec.js @@ -0,0 +1,71 @@ +describe("directives", function(){ + + var compile, element; + + beforeEach(function() { + var compiler = new Compiler(angularMarkup, angularDirective, angularWidget); + compile = function(html) { + element = jqLite(html); + var view = compiler.compile(element.element)(element.element); + view.init(); + return view.scope; + }; + }); + + it("should ng-init", function() { + var scope = compile('<div ng-init="a=123"></div>'); + expect(scope.get('a')).toEqual(123); + }); + + it("should ng-eval", function() { + var scope = compile('<div ng-init="a=0" ng-eval="a = a + 1"></div>'); + expect(scope.get('a')).toEqual(0); + scope.updateView(); + expect(scope.get('a')).toEqual(1); + scope.updateView(); + expect(scope.get('a')).toEqual(2); + }); + + it('should ng-bind', function() { + var scope = compile('<div ng-bind="a"></div>'); + expect(element.text()).toEqual(''); + scope.set('a', 'misko'); + scope.updateView(); + expect(element.text()).toEqual('misko'); + }); + + it('should ng-bind-attr', function(){ + var scope = compile('<img ng-bind-attr="{src:\'mysrc\', alt:\'myalt\'}"/>'); + expect(element.attr('src')).toEqual(null); + expect(element.attr('alt')).toEqual(null); + scope.updateView(); + expect(element.attr('src')).toEqual('mysrc'); + expect(element.attr('alt')).toEqual('myalt'); + }); + + it('should ng-non-bindable', function(){ + var scope = compile('<div ng-non-bindable><span ng-bind="name"></span></div>'); + scope.set('name', 'misko'); + scope.updateView(); + expect(element.text()).toEqual(''); + }); + + it('should ng-repeat over array', function(){ + var scope = compile('<ul><li ng-repeat="item in items" ng-init="suffix = \';\'" ng-bind="item + suffix"></li></ul>'); + + scope.set('items', ['misko', 'shyam']); + scope.updateView(); + expect(element.text()).toEqual('misko;shyam;'); + + scope.set('items', ['adam', 'kai', 'brad']); + scope.updateView(); + expect(element.text()).toEqual('adam;kai;brad;'); + + scope.set('items', ['brad']); + scope.updateView(); + expect(element.text()).toEqual('brad;'); + }); + + it('should ng-repeat over object', function(){}); + it('should error on wrong parsing of ng-repeat', function(){}); +}); |
