diff --git a/bin/parser_generator_for_spec.dart b/bin/parser_generator_for_spec.dart index e0dbe26e8..8e9737fc6 100644 --- a/bin/parser_generator_for_spec.dart +++ b/bin/parser_generator_for_spec.dart @@ -175,6 +175,7 @@ main(arguments) { 'add(a,b)', 'notAProperty', "'Foo'|uppercase", + "'f' + ('o'|uppercase) + 'o'", "1|increment:2", "'abcd'|substring:1:offset", "'abcd'|substring:1:3|uppercase", diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index 783769740..9129fe86d 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -109,8 +109,10 @@ class BallPositionDirective { set position(BallModel model) { element.style.backgroundColor = model.color; scope - ..watch('x', (x, _) => element.style.left = '${x + 10}px', context: model, readOnly: true) - ..watch('y', (y, _) => element.style.top = '${y + 10}px', context: model, readOnly: true); + ..watch('x', (x, _) => element.style.left = '${x + 10}px', + context: model, canChangeModel: false) + ..watch('y', (y, _) => element.style.top = '${y + 10}px', + context: model, canChangeModel: false); } } diff --git a/lib/core/filter.dart b/lib/core/filter.dart index b3b33d7ea..2c27715f4 100644 --- a/lib/core/filter.dart +++ b/lib/core/filter.dart @@ -40,9 +40,9 @@ class NgFilter { @NgInjectableService() class FilterMap extends AnnotationMap { Injector _injector; - FilterMap(Injector injector, MetadataExtractor extractMetadata) : - this._injector = injector, - super(injector, extractMetadata); + FilterMap(Injector injector, MetadataExtractor extractMetadata) + : this._injector = injector, + super(injector, extractMetadata); call(String name) { var filter = new NgFilter(name: name); diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index 5071787e5..433d67396 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -1,34 +1,12 @@ part of angular.core; -class Interpolation implements Function { - final String template; - final List separators; - final List expressions; - Function setter = (_) => _; - - Interpolation(this.template, this.separators, this.expressions); - - String call(List parts, [_]) { - if (parts == null) return separators.join(''); - var sb = new StringBuffer(); - for (var i = 0; i < parts.length; i++) { - sb.write(separators[i]); - var value = parts[i]; - sb.write(value == null ? '' : '$value'); - } - sb.write(separators.last); - return setter(sb.toString()); - } -} - /** - * Compiles a string with markup into an interpolation function. This service - * is used by the HTML [Compiler] service for data binding. - * + * Compiles a string with markup into an expression. This service is used by the + * HTML [Compiler] service for data binding. * * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name}}!'); - * expect(exp({name:'Angular'}).toEqual('Hello Angular!'); + * expect(exp).toEqual('"Hello "+(name)+"!"'); */ @NgInjectableService() class Interpolate implements Function { @@ -37,49 +15,54 @@ class Interpolate implements Function { Interpolate(this._parse); /** - * Compiles markup text into interpolation function. + * Compiles markup text into expression. * - * - `text`: The markup text to interpolate in form `foo {{expr}} bar`. + * - `template`: The markup text to interpolate in form `foo {{expr}} bar`. * - `mustHaveExpression`: if set to true then the interpolation string must - * have embedded expression in order to return an interpolation function. - * Strings with no embedded expression will return null for the - * interpolation function. + * have embedded expression in order to return an expression. Strings with + * no embedded expression will return null. * - `startSymbol`: The symbol to start interpolation. '{{' by default. * - `endSymbol`: The symbol to end interpolation. '}}' by default. */ - Interpolation call(String template, [bool mustHaveExpression = false, + + String call(String template, [bool mustHaveExpression = false, String startSymbol = '{{', String endSymbol = '}}']) { - int startSymbolLength = startSymbol.length; - int endSymbolLength = endSymbol.length; - int startIndex; - int endIndex; + if (template == null || template.isEmpty) return ""; + + final startLen = startSymbol.length; + final endLen = endSymbol.length; + final length = template.length; + + int startIdx; + int endIdx; int index = 0; - int length = template.length; + bool hasInterpolation = false; - bool shouldAddSeparator = true; + String exp; - final separators = []; - final expressions = []; + final expParts = []; while (index < length) { - if (((startIndex = template.indexOf(startSymbol, index)) != -1) && - ((endIndex = template.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - separators.add(template.substring(index, startIndex)); - exp = template.substring(startIndex + startSymbolLength, endIndex); - expressions.add(exp); - index = endIndex + endSymbolLength; + startIdx = template.indexOf(startSymbol, index); + endIdx = template.indexOf(endSymbol, startIdx + startLen); + if (startIdx != -1 && endIdx != -1) { + if (index < startIdx) { + // Empty strings could be stripped thanks to the stringify + // filter + expParts.add('"${template.substring(index, startIdx)}"'); + } + expParts.add('(' + template.substring(startIdx + startLen, endIdx) + + '|stringify)'); + + index = endIdx + endLen; hasInterpolation = true; } else { - // we did not find anything, so we have to add the remainder to the - // chunks array - separators.add(template.substring(index)); - shouldAddSeparator = false; + // we did not find any interpolation, so add the remainder + expParts.add('"${template.substring(index)}"'); break; } } - if (shouldAddSeparator) separators.add(''); - return (!mustHaveExpression || hasInterpolation) - ? new Interpolation(template, separators, expressions) - : null; + + return !mustHaveExpression || hasInterpolation ? expParts.join('+') : null; } -} +} \ No newline at end of file diff --git a/lib/core/module.dart b/lib/core/module.dart index 4558a3d5f..bda8ac5c9 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -45,7 +45,6 @@ class NgCoreModule extends Module { value(ScopeStats, new ScopeStats()); value(GetterCache, new GetterCache({})); value(Object, {}); // RootScope context - type(AstParser); type(NgZone); type(Parser, implementedBy: DynamicParser); diff --git a/lib/core/parser/dynamic_parser.dart b/lib/core/parser/dynamic_parser.dart index ef70c5920..fd49a3f7f 100644 --- a/lib/core/parser/dynamic_parser.dart +++ b/lib/core/parser/dynamic_parser.dart @@ -5,7 +5,6 @@ import 'package:angular/core/module.dart' show FilterMap, NgInjectableService; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/parser/lexer.dart'; import 'package:angular/core/parser/dynamic_parser_impl.dart'; -import 'package:angular/core/parser/syntax.dart' show defaultFilterMap; import 'package:angular/core/parser/eval.dart'; import 'package:angular/core/parser/utils.dart' show EvalError; @@ -46,7 +45,7 @@ class DynamicExpression extends Expression { accept(Visitor visitor) => _expression.accept(visitor); toString() => _expression.toString(); - eval(scope, [FilterMap filters = defaultFilterMap]) { + eval(scope, FilterMap filters) { try { return _expression.eval(scope, filters); } on EvalError catch (e, s) { @@ -68,8 +67,7 @@ class DynamicParserBackend extends ParserBackend { final ClosureMap _closures; DynamicParserBackend(this._closures); - bool isAssignable(Expression expression) - => expression.isAssignable; + bool isAssignable(Expression expression) => expression.isAssignable; Expression newFilter(expression, name, arguments) { List allArguments = new List(arguments.length + 1); @@ -78,58 +76,46 @@ class DynamicParserBackend extends ParserBackend { return new Filter(expression, name, arguments, allArguments); } - Expression newChain(expressions) - => new Chain(expressions); - Expression newAssign(target, value) - => new Assign(target, value); + Expression newChain(expressions) => new Chain(expressions); + Expression newAssign(target, value) => new Assign(target, value); Expression newConditional(condition, yes, no) => new Conditional(condition, yes, no); - Expression newAccessKeyed(object, key) - => new AccessKeyed(object, key); + Expression newAccessKeyed(object, key) => new AccessKeyed(object, key); Expression newCallFunction(function, arguments) => new CallFunction(function, arguments); - Expression newPrefixNot(expression) - => new PrefixNot(expression); + Expression newPrefixNot(expression) => new PrefixNot(expression); Expression newBinary(operation, left, right) => new Binary(operation, left, right); - Expression newLiteralPrimitive(value) - => new LiteralPrimitive(value); - Expression newLiteralArray(elements) - => new LiteralArray(elements); - Expression newLiteralObject(keys, values) - => new LiteralObject(keys, values); - Expression newLiteralString(value) - => new LiteralString(value); + Expression newLiteralPrimitive(value) => new LiteralPrimitive(value); + Expression newLiteralArray(elements) => new LiteralArray(elements); + Expression newLiteralObject(keys, values) => new LiteralObject(keys, values); + Expression newLiteralString(value) => new LiteralString(value); Expression newAccessScope(name) { Getter getter = _closures.lookupGetter(name); Setter setter = _closures.lookupSetter(name); - if (getter != null && setter != null) { - return new AccessScopeFast(name, getter, setter); - } else { - return new AccessScope(name); - } + return (getter != null && setter != null) + ? new AccessScopeFast(name, getter, setter) + : new AccessScope(name); } Expression newAccessMember(object, name) { Getter getter = _closures.lookupGetter(name); Setter setter = _closures.lookupSetter(name); - if (getter != null && setter != null) { - return new AccessMemberFast(object, name, getter, setter); - } else { - return new AccessMember(object, name); - } + return (getter != null && setter != null) + ? new AccessMemberFast(object, name, getter, setter) + : new AccessMember(object, name); } Expression newCallScope(name, arguments) { Function constructor = _computeCallConstructor( _callScopeConstructors, name, arguments.length); - return (constructor != null) + return constructor != null ? constructor(name, arguments, _closures) : new CallScope(name, arguments); } diff --git a/lib/core/parser/eval.dart b/lib/core/parser/eval.dart index 3ca42723c..850e873eb 100644 --- a/lib/core/parser/eval.dart +++ b/lib/core/parser/eval.dart @@ -9,7 +9,7 @@ export 'package:angular/core/parser/eval_calls.dart'; class Chain extends syntax.Chain { Chain(List expressions) : super(expressions); - eval(scope, [FilterMap filters]) { + eval(scope, FilterMap filters) { var result; for (int i = 0; i < expressions.length; i++) { var last = expressions[i].eval(scope, filters); @@ -25,13 +25,17 @@ class Filter extends syntax.Filter { this.allArguments) : super(expression, name, arguments); - eval(scope, [FilterMap filters]) => - Function.apply(filters(name), evalList(scope, allArguments, filters)); + eval(scope, FilterMap filters) { + if (filters == null) { + throw 'No NgFilter: $name found!'; + } + return Function.apply(filters(name), evalList(scope, allArguments, filters)); + } } class Assign extends syntax.Assign { Assign(syntax.Expression target, value) : super(target, value); - eval(scope, [FilterMap filters]) => + eval(scope, FilterMap filters) => target.assign(scope, value.eval(scope, filters)); } @@ -39,57 +43,59 @@ class Conditional extends syntax.Conditional { Conditional(syntax.Expression condition, syntax.Expression yes, syntax.Expression no) : super(condition, yes, no); - eval(scope, [FilterMap filters]) => toBool(condition.eval(scope)) - ? yes.eval(scope) - : no.eval(scope); + eval(scope, FilterMap filters) => toBool(condition.eval(scope, filters)) + ? yes.eval(scope, filters) + : no.eval(scope, filters); } class PrefixNot extends syntax.Prefix { PrefixNot(syntax.Expression expression) : super('!', expression); - eval(scope, [FilterMap filters]) => !toBool(expression.eval(scope)); + eval(scope, FilterMap filters) => !toBool(expression.eval(scope, filters)); } class Binary extends syntax.Binary { Binary(String operation, syntax.Expression left, syntax.Expression right): super(operation, left, right); - eval(scope, [FilterMap filters]) { - var left = this.left.eval(scope); + eval(scope, FilterMap filters) { + var lValue = left.eval(scope, filters); switch (operation) { - case '&&': return toBool(left) && toBool(this.right.eval(scope)); - case '||': return toBool(left) || toBool(this.right.eval(scope)); + // evaluates the rValue only if required + case '&&': return toBool(lValue) && toBool(right.eval(scope, filters)); + case '||': return toBool(lValue) || toBool(right.eval(scope, filters)); } - var right = this.right.eval(scope); + + var rValue = right.eval(scope, filters); // Null check for the operations. - if (left == null || right == null) { + if (lValue == null || rValue == null) { switch (operation) { case '+': - if (left != null) return left; - if (right != null) return right; + if (lValue != null) return lValue; + if (rValue != null) return rValue; return 0; case '-': - if (left != null) return left; - if (right != null) return 0 - right; + if (lValue != null) return lValue; + if (rValue != null) return 0 - rValue; return 0; } return null; } switch (operation) { - case '+' : return autoConvertAdd(left, right); - case '-' : return left - right; - case '*' : return left * right; - case '/' : return left / right; - case '~/' : return left ~/ right; - case '%' : return left % right; - case '==' : return left == right; - case '!=' : return left != right; - case '<' : return left < right; - case '>' : return left > right; - case '<=' : return left <= right; - case '>=' : return left >= right; - case '^' : return left ^ right; - case '&' : return left & right; + case '+' : return autoConvertAdd(lValue, rValue); + case '-' : return lValue - rValue; + case '*' : return lValue * rValue; + case '/' : return lValue / rValue; + case '~/' : return lValue ~/ rValue; + case '%' : return lValue % rValue; + case '==' : return lValue == rValue; + case '!=' : return lValue != rValue; + case '<' : return lValue < rValue; + case '>' : return lValue > rValue; + case '<=' : return lValue <= rValue; + case '>=' : return lValue >= rValue; + case '^' : return lValue ^ rValue; + case '&' : return lValue & rValue; } throw new EvalError('Internal error [$operation] not handled'); } @@ -97,22 +103,23 @@ class Binary extends syntax.Binary { class LiteralPrimitive extends syntax.LiteralPrimitive { LiteralPrimitive(dynamic value) : super(value); - eval(scope, [FilterMap filters]) => value; + eval(scope, FilterMap filters) => value; } class LiteralString extends syntax.LiteralString { LiteralString(String value) : super(value); - eval(scope, [FilterMap filters]) => value; + eval(scope, FilterMap filters) => value; } class LiteralArray extends syntax.LiteralArray { LiteralArray(List elements) : super(elements); - eval(scope, [FilterMap filters]) => + eval(scope, FilterMap filters) => elements.map((e) => e.eval(scope, filters)).toList(); } class LiteralObject extends syntax.LiteralObject { - LiteralObject(List keys, Listvalues) : super(keys, values); - eval(scope, [FilterMap filters]) => + LiteralObject(List keys, Listvalues) + : super(keys, values); + eval(scope, FilterMap filters) => new Map.fromIterables(keys, values.map((e) => e.eval(scope, filters))); } diff --git a/lib/core/parser/eval_access.dart b/lib/core/parser/eval_access.dart index 87bf09cd1..8e67cfbe3 100644 --- a/lib/core/parser/eval_access.dart +++ b/lib/core/parser/eval_access.dart @@ -9,7 +9,7 @@ import 'package:angular/core/module.dart'; class AccessScope extends syntax.AccessScope with AccessReflective { final Symbol symbol; AccessScope(String name) : super(name), symbol = newSymbol(name); - eval(scope, [FilterMap filters]) => _eval(scope); + eval(scope, FilterMap filters) => _eval(scope); assign(scope, value) => _assign(scope, scope, value); } @@ -17,7 +17,7 @@ class AccessScopeFast extends syntax.AccessScope with AccessFast { final Getter getter; final Setter setter; AccessScopeFast(String name, this.getter, this.setter) : super(name); - eval(scope, [FilterMap filters]) => _eval(scope); + eval(scope, FilterMap filters) => _eval(scope); assign(scope, value) => _assign(scope, scope, value); } @@ -25,8 +25,8 @@ class AccessMember extends syntax.AccessMember with AccessReflective { final Symbol symbol; AccessMember(object, String name) : super(object, name), symbol = newSymbol(name); - eval(scope, [FilterMap filters]) => _eval(object.eval(scope, filters)); - assign(scope, value) => _assign(scope, object.eval(scope), value); + eval(scope, FilterMap filters) => _eval(object.eval(scope, filters)); + assign(scope, value) => _assign(scope, object.eval(scope, null), value); _assignToNonExisting(scope, value) => object.assign(scope, { name: value }); } @@ -35,16 +35,16 @@ class AccessMemberFast extends syntax.AccessMember with AccessFast { final Setter setter; AccessMemberFast(object, String name, this.getter, this.setter) : super(object, name); - eval(scope, [FilterMap filters]) => _eval(object.eval(scope, filters)); - assign(scope, value) => _assign(scope, object.eval(scope), value); + eval(scope, FilterMap filters) => _eval(object.eval(scope, filters)); + assign(scope, value) => _assign(scope, object.eval(scope, null), value); _assignToNonExisting(scope, value) => object.assign(scope, { name: value }); } class AccessKeyed extends syntax.AccessKeyed { AccessKeyed(object, key) : super(object, key); - eval(scope, [FilterMap filters]) => + eval(scope, FilterMap filters) => getKeyed(object.eval(scope, filters), key.eval(scope, filters)); - assign(scope, value) => setKeyed(object.eval(scope), key.eval(scope), value); + assign(scope, value) => setKeyed(object.eval(scope, null), key.eval(scope, null), value); } diff --git a/lib/core/parser/eval_calls.dart b/lib/core/parser/eval_calls.dart index 0cd58f551..a5b610c03 100644 --- a/lib/core/parser/eval_calls.dart +++ b/lib/core/parser/eval_calls.dart @@ -10,7 +10,7 @@ class CallScope extends syntax.CallScope with CallReflective { CallScope(name, arguments) : super(name, arguments), symbol = newSymbol(name); - eval(scope, [FilterMap filters]) => _eval(scope, scope); + eval(scope, FilterMap filters) => _eval(scope, scope); } class CallMember extends syntax.CallMember with CallReflective { @@ -18,19 +18,19 @@ class CallMember extends syntax.CallMember with CallReflective { CallMember(object, name, arguments) : super(object, name, arguments), symbol = newSymbol(name); - eval(scope, [FilterMap filters]) => _eval(scope, object.eval(scope, filters)); + eval(scope, FilterMap filters) => _eval(scope, object.eval(scope, filters)); } class CallScopeFast0 extends syntax.CallScope with CallFast { final Function function; CallScopeFast0(name, arguments, this.function) : super(name, arguments); - eval(scope, [FilterMap filters]) => _evaluate0(scope); + eval(scope, FilterMap filters) => _evaluate0(scope); } class CallScopeFast1 extends syntax.CallScope with CallFast { final Function function; CallScopeFast1(name, arguments, this.function) : super(name, arguments); - eval(scope, [FilterMap filters]) => + eval(scope, FilterMap filters) => _evaluate1(scope, arguments[0].eval(scope, filters)); } @@ -38,20 +38,20 @@ class CallMemberFast0 extends syntax.CallMember with CallFast { final Function function; CallMemberFast0(object, name, arguments, this.function) : super(object, name, arguments); - eval(scope, [FilterMap filters]) => _evaluate0(object.eval(scope, filters)); + eval(scope, FilterMap filters) => _evaluate0(object.eval(scope, filters)); } class CallMemberFast1 extends syntax.CallMember with CallFast { final Function function; CallMemberFast1(object, name, arguments, this.function) : super(object, name, arguments); - eval(scope, [FilterMap filters]) => _evaluate1(object.eval(scope, filters), + eval(scope, FilterMap filters) => _evaluate1(object.eval(scope, filters), arguments[0].eval(scope, filters)); } class CallFunction extends syntax.CallFunction { CallFunction(function, arguments) : super(function, arguments); - eval(scope, [FilterMap filters]) { + eval(scope, FilterMap filters) { var function = this.function.eval(scope, filters); if (function is! Function) { throw new EvalError('${this.function} is not a function'); @@ -80,7 +80,7 @@ abstract class CallReflective { List get arguments; _eval(scope, holder) { - List arguments = evalList(scope, this.arguments); + List arguments = evalList(scope, this.arguments, null); if (!identical(holder, _cachedHolder)) { return _evaluteUncached(holder, arguments); } diff --git a/lib/core/parser/static_parser.dart b/lib/core/parser/static_parser.dart index b9c59ee5d..44677aaf1 100644 --- a/lib/core/parser/static_parser.dart +++ b/lib/core/parser/static_parser.dart @@ -15,7 +15,7 @@ class StaticParserFunctions { class StaticParser implements Parser { final StaticParserFunctions _functions; final DynamicParser _fallbackParser; - final Map _cache = {}; + final _cache = {}; StaticParser(this._functions, this._fallbackParser); Expression call(String input) { @@ -42,7 +42,7 @@ class StaticExpression extends Expression { accept(Visitor visitor) => throw "Cannot visit static expression $this"; toString() => _input; - eval(scope, [FilterMap filters = defaultFilterMap]) { + eval(scope, FilterMap filters) { try { return _eval(scope, filters); } on EvalError catch (e, s) { @@ -58,4 +58,4 @@ class StaticExpression extends Expression { throw e.unwrap("$this", s); } } -} \ No newline at end of file +} diff --git a/lib/core/parser/syntax.dart b/lib/core/parser/syntax.dart index 3e705c54b..f18d7deb0 100644 --- a/lib/core/parser/syntax.dart +++ b/lib/core/parser/syntax.dart @@ -39,7 +39,7 @@ abstract class Expression { bool get isAssignable => false; bool get isChain => false; - eval(scope, [FilterMap filters = defaultFilterMap]) + eval(scope, FilterMap filters) => throw new EvalError("Cannot evaluate $this"); assign(scope, value) => throw new EvalError("Cannot assign to $this"); @@ -56,7 +56,7 @@ class BoundExpression { final LocalsWrapper _wrapper; BoundExpression(this.expression, this._context, this._wrapper); - call([locals]) => expression.eval(_computeContext(locals)); + call([locals]) => expression.eval(_computeContext(locals), null); assign(value, [locals]) => expression.assign(_computeContext(locals), value); _computeContext(locals) { @@ -183,14 +183,3 @@ class LiteralObject extends Literal { LiteralObject(this.keys, this.values); accept(Visitor visitor) => visitor.visitLiteralObject(this); } - -const defaultFilterMap = const _DefaultFilterMap(); - -class _DefaultFilterMap implements FilterMap { - const _DefaultFilterMap(); - - call(name) => throw 'No NgFilter: $name found!'; - Type operator[](annotation) => null; - forEach(fn) { } - annotationsFor(type) => null; -} diff --git a/lib/core/parser/utils.dart b/lib/core/parser/utils.dart index 7943e7d4f..a761b2eab 100644 --- a/lib/core/parser/utils.dart +++ b/lib/core/parser/utils.dart @@ -20,7 +20,7 @@ class EvalError { } /// Evaluate the [list] in context of the [scope]. -List evalList(scope, List list, [FilterMap filters]) { +List evalList(scope, List list, FilterMap filters) { int length = list.length; for (int cacheLength = _evalListCache.length; cacheLength <= length; cacheLength++) { _evalListCache.add(new List(cacheLength)); diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 331ba153c..0cae1acbd 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -192,40 +192,40 @@ class Scope { /** * Use [watch] to set up a watch in the [apply] cycle. * - * When [readOnly] is [:true:], the watch will be executed in the [flush] - * cycle. It should be used when the [reactionFn] does not change the model - * and allows the [digest] phase to converge faster. + * When [canChangeModel] is [:false:], the watch will be executed in the + * [flush] cycle. It should be used when the [reactionFn] does not change the + * model and allows speeding up the [digest] phase. * - * On the opposite, [readOnly] should be set to [:false:] if the [reactionFn] - * could change the model so that the watch is observed in the [digest] cycle. + * On the opposite, [canChangeModel] should be set to [:true:] if the + * [reactionFn] could change the model so that the watch is evaluated in the + * [digest] cycle. */ - Watch watch(expression, ReactionFn reactionFn, - {context, FilterMap filters, bool readOnly: false}) { + Watch watch(String expression, ReactionFn reactionFn, {Object context, + FilterMap filters, bool canChangeModel: true, bool collection: false}) { assert(isAttached); - assert(expression != null); - AST ast; + assert(expression is String); + assert(canChangeModel is bool); + Watch watch; ReactionFn fn = reactionFn; - if (expression is AST) { - ast = expression; - } else if (expression is String) { - if (expression.startsWith('::')) { - expression = expression.substring(2); - fn = (value, last) { - if (value != null) { - watch.remove(); - return reactionFn(value, last); - } - }; - } else if (expression.startsWith(':')) { - expression = expression.substring(1); - fn = (value, last) => value == null ? null : reactionFn(value, last); - } - ast = rootScope._astParser(expression, context: context, filters: filters); - } else { - throw 'expressions must be String or AST got $expression.'; + + if (expression.startsWith('::')) { + expression = expression.substring(2); + fn = (value, last) { + if (value != null) { + watch.remove(); + return reactionFn(value, last); + } + }; + } else if (expression.startsWith(':')) { + expression = expression.substring(1); + fn = (value, last) => value == null ? null : reactionFn(value, last); } - WatchGroup group = readOnly ? _readOnlyGroup : _readWriteGroup; + + AST ast = rootScope._astParser(expression, context: context, + filters: filters, collection: collection); + + WatchGroup group = canChangeModel ? _readWriteGroup : _readOnlyGroup; return watch = group.watch(ast, fn); } @@ -236,7 +236,7 @@ class Scope { expression is Function); if (expression is String && expression.isNotEmpty) { var obj = locals == null ? context : new ScopeLocals(context, locals); - return rootScope._parser(expression).eval(obj); + return rootScope._parser(expression).eval(obj, null); } assert(locals == null); @@ -256,10 +256,9 @@ class Scope { } catch (e, s) { rootScope._exceptionHandler(e, s); } finally { - rootScope - .._transitionState(RootScope.STATE_APPLY, null) - ..digest() - ..flush(); + rootScope.._transitionState(RootScope.STATE_APPLY, null) + ..digest() + ..flush(); } } @@ -423,7 +422,7 @@ class RootScope extends Scope { static final STATE_FLUSH = 'flush'; final ExceptionHandler _exceptionHandler; - final AstParser _astParser; + final _AstParser _astParser; final Parser _parser; final ScopeDigestTTL _ttl; final NgZone _zone; @@ -436,16 +435,17 @@ class RootScope extends Scope { String _state; - RootScope(Object context, this._astParser, this._parser, - GetterCache cacheGetter, FilterMap filterMap, - this._exceptionHandler, this._ttl, this._zone, - ScopeStats _scopeStats) - : _scopeStats = _scopeStats, + RootScope(Object context, Parser parser, GetterCache cacheGetter, + FilterMap filterMap, this._exceptionHandler, this._ttl, this._zone, + ScopeStats scopeStats) + : _parser = parser, + _astParser = new _AstParser(parser), + _scopeStats = scopeStats, super(context, null, null, new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), '', - _scopeStats) + scopeStats) { _zone.onTurnDone = apply; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); @@ -529,7 +529,7 @@ class RootScope extends Scope { runObservers = false; readOnlyGroup.detectChanges(exceptionHandler:_exceptionHandler); } - if (_domReadHead != null) _stats.domWriteStart(); + if (_domReadHead != null) _stats.domReadStart(); while (_domReadHead != null) { try { _domReadHead.fn(); @@ -848,12 +848,12 @@ class _FunctionChain { } } -class AstParser { +class _AstParser { final Parser _parser; int _id = 0; ExpressionVisitor _visitor = new ExpressionVisitor(); - AstParser(this._parser); + _AstParser(this._parser); AST call(String exp, { FilterMap filters, bool collection: false, diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index ad14c2be5..ae036ab01 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -136,7 +136,7 @@ class ElementBinder { nodeModule.factory(NgTextMustacheDirective, (Injector injector) { return new NgTextMustacheDirective( node, ref.value, injector.get(Interpolate), injector.get(Scope), - injector.get(AstParser), injector.get(FilterMap)); + injector.get(FilterMap)); }); } else if (ref.type == NgAttrMustacheDirective) { if (nodesAttrsDirectives == null) { @@ -146,7 +146,7 @@ class ElementBinder { var interpolate = injector.get(Interpolate); for (var ref in nodesAttrsDirectives) { new NgAttrMustacheDirective(nodeAttrs, ref.value, interpolate, - scope, injector.get(AstParser), injector.get(FilterMap)); + scope, injector.get(FilterMap)); } }); } diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index c6c604b6f..67b9ac720 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -3,20 +3,23 @@ part of angular.core.dom; // This Directive is special and does not go through injection. @NgDirective(selector: r':contains(/{{.*}}/)') class NgTextMustacheDirective { - NgTextMustacheDirective(dom.Node element, - String markup, + final dom.Node _element; + + NgTextMustacheDirective(this._element, + String template, Interpolate interpolate, Scope scope, - AstParser parser, FilterMap filters) { - Interpolation interpolation = interpolate(markup); - interpolation.setter = (text) => element.text = text; + String expression = interpolate(template); + + scope.watch(expression, + _updateMarkup, + canChangeModel: false, + filters: filters); + } - List items = interpolation.expressions - .map((exp) => parser(exp, filters: filters)) - .toList(); - AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); - scope.watch(ast, interpolation.call, readOnly: true); + void _updateMarkup(text, previousText) { + _element.text = text; } } @@ -25,40 +28,32 @@ class NgTextMustacheDirective { class NgAttrMustacheDirective { bool _hasObservers; Watch _watch; + NodeAttrs _attrs; + String _attrName; - // This Directive is special and does not go through injection. - NgAttrMustacheDirective(NodeAttrs attrs, - String markup, + NgAttrMustacheDirective(this._attrs, + String template, Interpolate interpolate, Scope scope, - AstParser parser, FilterMap filters) { + var eqPos = template.indexOf('='); + _attrName = template.substring(0, eqPos); + String expression = interpolate(template.substring(eqPos + 1)); - var eqPos = markup.indexOf('='); - var attrName = markup.substring(0, eqPos); - var attrValue = markup.substring(eqPos + 1); - var lastValue = markup; - Interpolation interpolation = interpolate(attrValue)..setter = (text) { - if (lastValue != text) lastValue = attrs[attrName] = text; - }; - - // TODO(misko): figure out how to remove call to setter. It slows down - // View instantiation - interpolation.setter(''); + _updateMarkup('', null); - List items = interpolation.expressions - .map((exp) => parser(exp, filters: filters)) - .toList(); - - AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); - - attrs.listenObserverChanges(attrName, (hasObservers) { + _attrs.listenObserverChanges(_attrName, (hasObservers) { if (_hasObservers != hasObservers) { - hasObservers = hasObservers; + _hasObservers = hasObservers; if (_watch != null) _watch.remove(); - _watch = scope.watch(ast, interpolation.call, readOnly: !hasObservers); + _watch = scope.watch(expression, _updateMarkup, filters: filters, + canChangeModel: _hasObservers); } }); } + + void _updateMarkup(text, _) { + _attrs[_attrName] = text; + } } diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 7c30282a2..c417490ac 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -68,8 +68,8 @@ part of angular.directive; exportExpressionAttrs: const ['ng-class']) class NgClassDirective extends _NgClassBase { NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs, - AstParser parser, NgAnimate animate) - : super(element, scope, null, attrs, parser, animate); + NgAnimate animate) + : super(element, scope, null, attrs, animate); } /** @@ -104,8 +104,8 @@ class NgClassDirective extends _NgClassBase { exportExpressionAttrs: const ['ng-class-odd']) class NgClassOddDirective extends _NgClassBase { NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs, - AstParser parser, NgAnimate animate) - : super(element, scope, 0, attrs, parser, animate); + NgAnimate animate) + : super(element, scope, 0, attrs, animate); } /** @@ -140,8 +140,8 @@ class NgClassOddDirective extends _NgClassBase { exportExpressionAttrs: const ['ng-class-even']) class NgClassEvenDirective extends _NgClassBase { NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs, - AstParser parser, NgAnimate animate) - : super(element, scope, 1, attrs, parser, animate); + NgAnimate animate) + : super(element, scope, 1, attrs, animate); } abstract class _NgClassBase { @@ -149,14 +149,13 @@ abstract class _NgClassBase { final Scope scope; final int mode; final NodeAttrs nodeAttrs; - final AstParser _parser; final NgAnimate _animate; var previousSet = []; var currentSet = []; Watch _watch; _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs, - this._parser, this._animate) + this._animate) { var prevClass; @@ -170,12 +169,16 @@ abstract class _NgClassBase { set valueExpression(currentExpression) { if (_watch != null) _watch.remove(); - _watch = scope.watch(_parser(currentExpression, collection: true), (current, _) { - currentSet = _flatten(current); - _handleChange(scope.context[r'$index']); - }, readOnly: true); + + _watch = scope.watch(currentExpression, (current, _) { + currentSet = _flatten(current); + _handleChange(scope.context[r'$index']); + }, + canChangeModel: false, + collection: true); + if (mode != null) { - scope.watch(_parser(r'$index'), (index, oldIndex) { + scope.watch(r'$index', (index, oldIndex) { var mod = index % 2; if (oldIndex == null || mod != oldIndex % 2) { if (mod == mode) { @@ -184,7 +187,7 @@ abstract class _NgClassBase { previousSet.forEach((css) => _animate.removeClass(element, css)); } } - }, readOnly: true); + }, canChangeModel: false); } } diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 14d1e98d8..108afd521 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -22,7 +22,6 @@ class _NoopModelConverter extends NgModelConverter { */ @NgDirective(selector: '[ng-model]') class NgModel extends NgControl implements NgAttachAware { - final AstParser _parser; final Scope _scope; BoundSetter setter = (_, [__]) => null; @@ -38,8 +37,8 @@ class NgModel extends NgControl implements NgAttachAware { bool _watchCollection; Function render = (value) => null; - NgModel(this._scope, NgElement element, Injector injector, - this._parser, NodeAttrs attrs, NgAnimate animate) + NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs, + NgAnimate animate) : super(element, injector, animate) { _exp = attrs["ng-model"]; @@ -128,13 +127,12 @@ class NgModel extends NgControl implements NgAttachAware { _watchCollection = value; if (_removeWatch!=null) _removeWatch.remove(); if (_watchCollection) { - _removeWatch = _scope.watch( - _parser(_exp, collection: true), - (changeRecord, _) { - onChange(changeRecord is CollectionChangeRecord - ? changeRecord.iterable - : changeRecord); - }); + _removeWatch = _scope.watch(_exp, (changeRecord, _) { + onChange(changeRecord is CollectionChangeRecord + ? changeRecord.iterable + : changeRecord); + }, + collection: true); } else if (_exp != null) { _removeWatch = _scope.watch(_exp, onChange); } diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index 7b2903a7d..61dee92d6 100644 --- a/lib/directive/ng_pluralize.dart +++ b/lib/directive/ng_pluralize.dart @@ -92,10 +92,11 @@ class NgPluralizeDirective { final dom.Element element; final Scope scope; final Interpolate interpolate; - final AstParser parser; int offset; - var discreteRules = {}; - var categoryRules = {}; + final discreteRules = {}; + final categoryRules = {}; + final expressionCache = {}; + Watch _watch; static final RegExp IS_WHEN = new RegExp(r'^when-(minus-)?.'); static const Map SYMBOLS = const { @@ -108,33 +109,35 @@ class NgPluralizeDirective { }; NgPluralizeDirective(this.scope, this.element, this.interpolate, - NodeAttrs attributes, this.parser) { - Map whens = attributes['when'] == null - ? {} + NodeAttrs attributes) { + final whens = attributes['when'] == null + ? {} : scope.eval(attributes['when']); offset = attributes['offset'] == null ? 0 : int.parse(attributes['offset']); element.attributes.keys.where((k) => IS_WHEN.hasMatch(k)).forEach((k) { - var ruleName = k.replaceFirst('when-', '').replaceFirst('minus-', '-'); + var ruleName = k + .replaceFirst(new RegExp('^when-'), '') + .replaceFirst(new RegExp('^minus-'), '-'); whens[ruleName] = element.attributes[k]; }); if (whens['other'] == null) { throw "ngPluralize error! The 'other' plural category must always be " - "specified"; + "specified"; } whens.forEach((k, v) { Symbol symbol = SYMBOLS[k]; if (symbol != null) { - this.categoryRules[symbol] = v; + categoryRules[symbol] = v; } else { - this.discreteRules[k] = v; + discreteRules[k] = v; } }); } - set count(value) { + void set count(value) { if (value is! num) { try { value = int.parse(value); @@ -163,12 +166,14 @@ class NgPluralizeDirective { } } - _setAndWatch(expression) { - var interpolation = interpolate(expression, false, '\${', '}'); - interpolation.setter = (text) => element.text = text; - interpolation.setter(expression); - var items = interpolation.expressions.map((exp) => parser(exp)).toList(); - AST ast = new PureFunctionAST(expression, new ArrayFn(), items); - scope.watch(ast, interpolation.call); + void _setAndWatch(template) { + if (_watch != null) _watch.remove(); + var expression = expressionCache.putIfAbsent(template, () => + interpolate(template, false, r'${', '}')); + _watch = scope.watch(expression, _updateMarkup); + } + + void _updateMarkup(text, previousText) { + if (text != previousText) element.text = text; } } diff --git a/lib/directive/ng_repeat.dart b/lib/directive/ng_repeat.dart index 087900be9..ef834dca2 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -88,7 +88,6 @@ class NgRepeatDirective { final BoundViewFactory _boundViewFactory; final Scope _scope; final Parser _parser; - final AstParser _astParser; final FilterMap filters; String _expression; @@ -100,9 +99,8 @@ class NgRepeatDirective { Watch _watch = null; Iterable _lastCollection; - NgRepeatDirective(this._viewPort, this._boundViewFactory, - this._scope, this._parser, this._astParser, - this.filters); + NgRepeatDirective(this._viewPort, this._boundViewFactory, this._scope, + this._parser, this.filters); set expression(value) { assert(value != null); @@ -138,12 +136,12 @@ class NgRepeatDirective { if (_valueIdentifier == null) _valueIdentifier = match.group(1); _keyIdentifier = match.group(2); - _watch = _scope.watch( - _astParser(_listExpr, collection: true, filters: filters), - (CollectionChangeRecord collection, _) { + _watch = _scope.watch(_listExpr, (CollectionChangeRecord collection, _) { //TODO(misko): we should take advantage of the CollectionChangeRecord! _onCollectionChange(collection == null ? [] : collection.iterable); - } + }, + collection: true, + filters: filters ); } diff --git a/lib/directive/ng_style.dart b/lib/directive/ng_style.dart index 142081bcd..566ba946b 100644 --- a/lib/directive/ng_style.dart +++ b/lib/directive/ng_style.dart @@ -13,12 +13,11 @@ part of angular.directive; class NgStyleDirective { final dom.Element _element; final Scope _scope; - final AstParser _parser; String _styleExpression; Watch _watch; - NgStyleDirective(this._element, this._scope, this._parser); + NgStyleDirective(this._element, this._scope); /** * ng-style attribute takes an expression which evaluates to an @@ -28,8 +27,8 @@ class NgStyleDirective { set styleExpression(String value) { _styleExpression = value; if (_watch != null) _watch.remove(); - _watch = _scope.watch(_parser(_styleExpression, collection: true), - _onStyleChange, readOnly: true); + _watch = _scope.watch(_styleExpression, _onStyleChange, collection: true, + canChangeModel: false); } _onStyleChange(MapChangeRecord mapChangeRecord, _) { diff --git a/lib/filter/filter.dart b/lib/filter/filter.dart index 1203466dc..61c087d6d 100644 --- a/lib/filter/filter.dart +++ b/lib/filter/filter.dart @@ -175,7 +175,7 @@ class FilterFilter { bool _search(var item, var what) { if (what is Map) { return what.keys.every((key) => _search( - (key == r'$') ? item : _parser(key).eval(item), what[key])); + (key == r'$') ? item : _parser(key).eval(item, null), what[key])); } else if (item is Map) { return item.keys.any((k) => !k.startsWith(r'$') && _search(item[k], what)); } else if (item is List) { diff --git a/lib/filter/module.dart b/lib/filter/module.dart index 5c90c5af4..c720ae528 100644 --- a/lib/filter/module.dart +++ b/lib/filter/module.dart @@ -15,6 +15,7 @@ part 'lowercase.dart'; part 'number.dart'; part 'order_by.dart'; part 'uppercase.dart'; +part 'stringify.dart'; class NgFilterModule extends Module { NgFilterModule() { @@ -27,5 +28,6 @@ class NgFilterModule extends Module { type(NumberFilter); type(OrderByFilter); type(UppercaseFilter); + type(StringifyFilter); } } diff --git a/lib/filter/order_by.dart b/lib/filter/order_by.dart index b31b22651..1762d8e0c 100644 --- a/lib/filter/order_by.dart +++ b/lib/filter/order_by.dart @@ -168,7 +168,7 @@ class OrderByFilter { mappers[i] = _nop; } else { Expression parsed = _parser(strExp); - mappers[i] = (e) => parsed.eval(e); + mappers[i] = (e) => parsed.eval(e, null); } } else if (expression is Mapper) { mappers[i] = (expression as Mapper); diff --git a/lib/filter/stringify.dart b/lib/filter/stringify.dart new file mode 100644 index 000000000..1fde3925d --- /dev/null +++ b/lib/filter/stringify.dart @@ -0,0 +1,16 @@ +part of angular.filter; + +/** + * Allows you to convert an object to a string. + * + * Null object are converted to an empty string. + * + * + * Usage: + * + * {{ expression | stringify }} + */ +@NgFilter(name:'stringify') +class StringifyFilter implements Function { + String call(obj) => obj == null ? "" : obj.toString(); +} diff --git a/perf/parser_perf.dart b/perf/parser_perf.dart index 0a83f3f0e..bab382fab 100644 --- a/perf/parser_perf.dart +++ b/perf/parser_perf.dart @@ -49,9 +49,9 @@ main() { Expression generatedExpr = generatedParser(expr); Expression hybridExpr = hybridParser(expr); var measure = (b) => statMeasure(b).mean_ops_sec; - var gTime = measure(() => generatedExpr.eval(scope)); - var rTime = measure(() => reflectionExpr.eval(scope)); - var hTime = measure(() => hybridExpr.eval(scope)); + var gTime = measure(() => generatedExpr.eval(scope, null)); + var rTime = measure(() => reflectionExpr.eval(scope, null)); + var hTime = measure(() => hybridExpr.eval(scope, null)); var iTime = measure(() => idealFn(scope)); print('$expr => g: ${nf.format(gTime)} ops/sec ' + 'r: ${nf.format(rTime)} ops/sec ' + diff --git a/test/core/interpolate_spec.dart b/test/core/interpolate_spec.dart index 6e8dd92de..6b1c218e8 100644 --- a/test/core/interpolate_spec.dart +++ b/test/core/interpolate_spec.dart @@ -14,100 +14,24 @@ main() { expect($interpolate('some text', true)).toBe(null); }); - it('should suppress falsy objects', (Interpolate $interpolate) { - expect($interpolate('{{undefined}}')([null])).toEqual(''); - expect($interpolate('{{undefined+undefined}}')([null])).toEqual(''); - expect($interpolate('{{null}}')([null])).toEqual(''); - expect($interpolate('{{a.b}}')([null])).toEqual(''); + it('should return an expression', (Interpolate $interpolate) { + expect($interpolate('Hello {{name}}!')) + .toEqual('"Hello "+(name|stringify)+"!"'); + expect($interpolate('a{{b}}C')).toEqual('"a"+(b|stringify)+"C"'); + expect($interpolate('a{{b}}')).toEqual('"a"+(b|stringify)'); + expect($interpolate('{{a}}b')).toEqual('(a|stringify)+"b"'); + expect($interpolate('{{a}}{{b}}')).toEqual('(a|stringify)+(b|stringify)'); + expect($interpolate('{{b}}')).toEqual('(b|stringify)'); + expect($interpolate('{{b}}+{{c}}')) + .toEqual('(b|stringify)+"+"+(c|stringify)'); + expect($interpolate('{{b}}x{{c}}')) + .toEqual('(b|stringify)+"x"+(c|stringify)'); }); - it('should jsonify objects', (Interpolate $interpolate) { - expect($interpolate('{{ {} }}')([{}])).toEqual('{}'); - expect($interpolate('{{ true }}')([true])).toEqual('true'); - expect($interpolate('{{ false }}')([false])).toEqual('false'); + it('should Parse Multiline', (Interpolate $interpolate) { + expect($interpolate("X\nY{{A\n+B}}C\nD")) + .toEqual('"X\nY"+(A\n+B|stringify)+"C\nD"'); }); - - it('should return interpolation function', (Interpolate $interpolate, Scope rootScope) { - rootScope.context['name'] = 'Misko'; - var fn = $interpolate('Hello {{name}}!'); - expect(fn(['Misko'])).toEqual('Hello Misko!'); - }); - - - it('should ignore undefined model', (Interpolate $interpolate) { - expect($interpolate("Hello {{'World' + foo}}")(['World'])).toEqual('Hello World'); - }); - - - it('should use toString to conver objects to string', (Interpolate $interpolate, Scope rootScope) { - expect($interpolate("Hello, {{obj}}!")([new ToStringableObject()])).toEqual('Hello, World!'); - }); - - - describe('parseBindings', () { - it('should Parse Text With No Bindings', (Interpolate $interpolate) { - var parts = $interpolate("a").separators; - expect(parts.length).toEqual(1); - expect(parts[0]).toEqual("a"); - }); - - it('should Parse Empty Text', (Interpolate $interpolate) { - var parts = $interpolate("").separators; - expect(parts.length).toEqual(1); - expect(parts[0]).toEqual(""); - }); - - it('should Parse Inner Binding', (Interpolate $interpolate) { - var parts = $interpolate("a{{b}}C").separators; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual("a"); - expect(parts[1]).toEqual("C"); - }); - - it('should Parse Ending Binding', (Interpolate $interpolate) { - var parts = $interpolate("a{{b}}").separators; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual("a"); - expect(parts[1]).toEqual(""); - }); - - it('should Parse Begging Binding', (Interpolate $interpolate) { - var parts = $interpolate("{{b}}c").separators; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual(""); - expect(parts[1]).toEqual("c"); - }); - - it('should Parse Loan Binding', (Interpolate $interpolate) { - var parts = $interpolate("{{b}}").separators; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual(""); - expect(parts[1]).toEqual(""); - }); - - it('should Parse Two Bindings', (Interpolate $interpolate) { - var parts = $interpolate("{{b}}{{c}}").separators; - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual(""); - expect(parts[1]).toEqual(""); - expect(parts[2]).toEqual(""); - }); - - it('should Parse Two Bindings With Text In Middle', (Interpolate $interpolate) { - var parts = $interpolate("{{b}}x{{c}}").separators; - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual(""); - expect(parts[1]).toEqual("x"); - expect(parts[2]).toEqual(""); - }); - - it('should Parse Multiline', (Interpolate $interpolate) { - var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').separators; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual('"X\nY'); - expect(parts[1]).toEqual('C\nD"'); - }); - }); }); } diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index ff2d78760..05c7eb205 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -43,20 +43,30 @@ class WithPrivateField { toBool(x) => (x is num) ? x != 0 : x == true; +class RelaxedFilterExp { + final exp; + RelaxedFilterExp(this.exp); + eval(o, [FilterMap f = null]) => exp.eval(o, f); + assign(o, v) => exp.assign(o, v); + bind(o, [l]) => exp.bind(o, l); +} + main() { describe('parse', () { Map context; - Parser parser; + Parser _parser; FilterMap filters; beforeEachModule((Module module) { module.type(IncrementFilter); module.type(SubstringFilter); }); beforeEach((Parser injectedParser, FilterMap injectedFilters) { - parser = injectedParser; + _parser = injectedParser; filters = injectedFilters; }); + parser(String exp) => new RelaxedFilterExp(_parser(exp)); + eval(String text, [FilterMap f]) => parser(text).eval(context, f == null ? filters : f); expectEval(String expr) => expect(() => eval(expr)); @@ -152,11 +162,6 @@ main() { }); describe('error handling', () { - Parser parser; - - beforeEach((Parser p) { - parser = p; - }); // We only care about the error strings in the DynamicParser. var errStr = (x) { @@ -980,6 +985,7 @@ main() { describe('filters', () { it('should call a filter', () { expect(eval("'Foo'|uppercase", filters)).toEqual("FOO"); + expect(eval("'f' + ('o'|uppercase) + 'o'", filters)).toEqual("fOo"); expect(eval("'fOo'|uppercase|lowercase", filters)).toEqual("foo"); }); diff --git a/test/core/parser/static_parser_spec.dart b/test/core/parser/static_parser_spec.dart index 6aa999693..1c5465153 100644 --- a/test/core/parser/static_parser_spec.dart +++ b/test/core/parser/static_parser_spec.dart @@ -19,7 +19,7 @@ main() { it('should run a static function', (Parser parser) { - expect(parser('1').eval(null)).toEqual(1); + expect(parser('1').eval(null, null)).toEqual(1); }); diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index d7a9d8766..dc4c78adb 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -163,13 +163,11 @@ void main() { }); it('should support filters', (Logger logger, Map context, - RootScope rootScope, AstParser parser, - FilterMap filters) { + RootScope rootScope, FilterMap filters) { context['a'] = 123; context['b'] = 2; - rootScope.watch( - parser('a | multiply:b', filters: filters), - (value, previous) => logger(value)); + rootScope.watch('a | multiply:b', (value, previous) => logger(value), + filters: filters); rootScope.digest(); expect(logger).toEqual([246]); logger.clear(); @@ -180,12 +178,10 @@ void main() { it('should support arrays in filters', (Logger logger, Map context, RootScope rootScope, - AstParser parser, FilterMap filters) { context['a'] = [1]; - rootScope.watch( - parser('a | sort | listHead:"A" | listTail:"B"', filters: filters), - (value, previous) => logger(value)); + rootScope.watch('a | sort | listHead:"A" | listTail:"B"', + (value, previous) => logger(value), filters: filters); rootScope.digest(); expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 'B']]); logger.clear(); @@ -209,12 +205,10 @@ void main() { it('should support maps in filters', (Logger logger, Map context, RootScope rootScope, - AstParser parser, FilterMap filters) { context['a'] = {'foo': 'bar'}; - rootScope.watch( - parser('a | identity | keys', filters: filters), - (value, previous) => logger(value)); + rootScope.watch('a | identity | keys', + (value, previous) => logger(value), filters: filters); rootScope.digest(); expect(logger).toEqual(['identity', 'keys', ['foo']]); logger.clear(); @@ -868,7 +862,7 @@ void main() { it(r'should apply expression with full lifecycle', (RootScope rootScope) { var log = ''; var child = rootScope.createChild({"parent": rootScope.context}); - rootScope.watch('a', (a, _) { log += '1'; }, readOnly: true); + rootScope.watch('a', (a, _) { log += '1'; }, canChangeModel: false); child.apply('parent.a = 0'); expect(log).toEqual('1'); }); @@ -877,7 +871,7 @@ void main() { it(r'should schedule domWrites and domReads', (RootScope rootScope) { var log = ''; var child = rootScope.createChild({"parent": rootScope.context}); - rootScope.watch('a', (a, _) { log += '1'; }, readOnly: true); + rootScope.watch('a', (a, _) { log += '1'; }, canChangeModel: false); child.apply('parent.a = 0'); expect(log).toEqual('1'); }); @@ -890,7 +884,7 @@ void main() { beforeEach((RootScope rootScope) { rootScope.context['log'] = () { log += 'digest;'; return null; }; log = ''; - rootScope.watch('log()', (v, o) => null, readOnly: true); + rootScope.watch('log()', (v, o) => null, canChangeModel: false); rootScope.digest(); log = ''; }); @@ -899,7 +893,7 @@ void main() { LoggingExceptionHandler exceptionHandler = e; var log = []; var child = rootScope.createChild({}); - rootScope.watch('a', (a, _) => log.add('1'), readOnly: true); + rootScope.watch('a', (a, _) => log.add('1'), canChangeModel: false); rootScope.context['a'] = 0; child.apply(() { throw 'MyError'; }); expect(log.join(',')).toEqual('1'); @@ -935,7 +929,8 @@ void main() { rootScope.context['logger'] = (name) { log(name); return retValue; }; rootScope.watch('logger("watch")', (n, v) => null); - rootScope.watch('logger("flush")', (n, v) => null, readOnly: true); + rootScope.watch('logger("flush")', (n, v) => null, + canChangeModel: false); // clear watches rootScope.digest(); @@ -1419,7 +1414,8 @@ void main() { rootScope.domWrite(() => logger('write3')); throw 'read1'; }); - rootScope.watch('value', (_, __) => logger('observe'), readOnly: true); + rootScope.watch('value', (_, __) => logger('observe'), + canChangeModel: false); rootScope.flush(); expect(logger).toEqual(['write1', 'write2', 'observe', 'read1', 'read2', 'write3']); expect(exceptionHandler.errors.length).toEqual(2); diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 10b22f58c..870c503b0 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -97,7 +97,7 @@ void main() { }); it('should write to input only if the value is different', - (Injector i, AstParser parser, NgAnimate animate) { + (Injector i, NgAnimate animate) { var scope = _.rootScope; var element = new dom.InputElement(); @@ -105,7 +105,8 @@ void main() { NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), parser, nodeAttrs, new NgAnimate()); + var model = new NgModel(scope, ngElement, i.createChild([new Module()]), + nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -143,7 +144,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -162,7 +163,8 @@ void main() { describe('type="number" like', () { - it('should leave input unchanged when text does not represent a valid number', (Injector i) { + it('should leave input unchanged when text does not represent a valid number', + (Injector i) { var modelFieldName = 'modelForNumFromInvalid1'; var element = _.compile(''); dom.querySelector('body').append(element); @@ -190,7 +192,8 @@ void main() { expect(_.rootScope.context[modelFieldName]).toEqual(10); }); - it('should not reformat user input to equivalent numeric representation', (Injector i) { + it('should not reformat user input to equivalent numeric representation', + (Injector i) { var modelFieldName = 'modelForNumFromInvalid2'; var element = _.compile(''); dom.querySelector('body').append(element); @@ -259,7 +262,8 @@ void main() { expect(_.rootScope.context['model']).toEqual(43); }); - it('should update model to a native default value from a blank range input value', () { + it('should update model to a native default value from a blank range input value', + () { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -277,8 +281,9 @@ void main() { _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); }); - - it('should only render the input value upon the next digest', (Scope scope) { + + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -288,7 +293,7 @@ void main() { scope.context['model'] = 123; expect(inputElement.value).not.toEqual('123'); - + scope.apply(); expect(inputElement.value).toEqual('123'); @@ -335,7 +340,7 @@ void main() { }); it('should write to input only if value is different', - (Injector i, AstParser parser, NgAnimate animate) { + (Injector i, NgAnimate animate) { var scope = _.rootScope; var element = new dom.InputElement(); @@ -343,7 +348,8 @@ void main() { NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), parser, nodeAttrs, new NgAnimate()); + var model = new NgModel(scope, ngElement, i.createChild([new Module()]), + nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -369,7 +375,8 @@ void main() { expect(element.selectionEnd).toEqual(3); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -379,7 +386,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -424,7 +431,7 @@ void main() { }); it('should write to input only if value is different', - (Injector i, AstParser parser, NgAnimate animate) { + (Injector i, NgAnimate animate) { var scope = _.rootScope; var element = new dom.InputElement(); @@ -432,7 +439,8 @@ void main() { NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), parser, nodeAttrs, new NgAnimate()); + var model = new NgModel(scope, ngElement, i.createChild([new Module()]), + nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -460,7 +468,8 @@ void main() { expect(element.selectionEnd).toEqual(3); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -470,7 +479,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -481,7 +490,8 @@ void main() { it('should be set "text" as default value for "type" attribute', () { _.compile(''); _.rootScope.apply(); - expect((_.rootElement as dom.InputElement).attributes['type']).toEqual('text'); + expect((_.rootElement as dom.InputElement).attributes['type']) + .toEqual('text'); }); it('should update input value from model', () { @@ -521,7 +531,7 @@ void main() { }); it('should write to input only if value is different', - (Injector i, AstParser parser, NgAnimate animate) { + (Injector i, NgAnimate animate) { var scope = _.rootScope; var element = new dom.InputElement(); @@ -529,7 +539,8 @@ void main() { NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), parser, nodeAttrs, new NgAnimate()); + var model = new NgModel(scope, ngElement, i.createChild([new Module()]), + nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -555,7 +566,8 @@ void main() { expect(element.selectionEnd).toEqual(3); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -565,7 +577,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -588,7 +600,8 @@ void main() { }); it('should render as dirty when checked', (Scope scope) { - var element = _.compile(''); + var element = + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -602,8 +615,10 @@ void main() { }); - it('should update input value from model using ng-true-value/false', (Scope scope) { - var element = _.compile(''); + it('should update input value from model using ng-true-value/false', + (Scope scope) { + var element = _.compile(''); scope.apply(() { scope.context['model'] = 1; @@ -657,7 +672,8 @@ void main() { expect(scope.context['model']).toBe(false); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -667,7 +683,7 @@ void main() { scope.context['model'] = true; expect(inputElement.checked).toBe(false); - + scope.apply(); expect(inputElement.checked).toBe(true); @@ -715,7 +731,7 @@ void main() { // NOTE(deboer): This test passes on Dartium, but fails in the content_shell. // The Dart team is looking into this bug. xit('should write to input only if value is different', - (Injector i, AstParser parser, NgAnimate animate) { + (Injector i, NgAnimate animate) { var scope = _.rootScope; var element = new dom.TextAreaElement(); @@ -723,14 +739,15 @@ void main() { NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), parser, nodeAttrs, new NgAnimate()); + var model = new NgModel(scope, ngElement, i.createChild([new Module()]), + nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); element - ..value = 'abc' - ..selectionStart = 1 - ..selectionEnd = 2; + ..value = 'abc' + ..selectionStart = 1 + ..selectionEnd = 2; model.render('abc'); @@ -747,7 +764,8 @@ void main() { expect(element.selectionEnd).toEqual(0); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -757,7 +775,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -766,9 +784,11 @@ void main() { describe('type="radio"', () { it('should update input value from model', () { - _.compile('' + - '' + - ''); + _.compile(""" + + + + """); _.rootScope.apply(); RadioButtonInputElement redBtn = _.rootScope.context['r'].element; @@ -809,9 +829,11 @@ void main() { }); it('should support ng-value', () { - _.compile('' + - '' + - ''); + _.compile(""" + + + + """); var red = {'name': 'RED'}; var green = {'name': 'GREEN'}; @@ -863,12 +885,12 @@ void main() { }); it('should render as dirty when checked', (Scope scope) { - var element = _.compile( - '
' + - ' ' + - ' ' + - '
' - ); + var element = _.compile(""" +
+ + +
' + """); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -905,12 +927,12 @@ void main() { }); it('should only render the input value upon the next digest', (Scope scope) { - var element = _.compile( - '
' + - ' ' + - ' ' + - '
' - ); + var element = _.compile(""" +
+ + +
+ """); Probe probe1 = _.rootScope.context['i']; var ngModel1 = probe1.directive(NgModel); @@ -925,7 +947,7 @@ void main() { expect(inputElement1.checked).toBe(false); expect(inputElement2.checked).toBe(false); - + scope.apply(); expect(inputElement1.checked).toBe(true); @@ -970,7 +992,8 @@ void main() { expect(_.rootScope.context['model']).toEqual('123'); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile(''); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -980,7 +1003,7 @@ void main() { scope.context['model'] = 'xyz'; expect(inputElement.value).not.toEqual('xyz'); - + scope.apply(); expect(inputElement.value).toEqual('xyz'); @@ -1012,7 +1035,8 @@ void main() { expect(_.rootScope.context['model']).toEqual('def'); }); - it('should only render the input value upon the next digest', (Scope scope) { + it('should only render the input value upon the next digest', + (Scope scope) { _.compile('
'); Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); @@ -1022,7 +1046,7 @@ void main() { scope.context['model'] = 'xyz'; expect(element.innerHtml).not.toEqual('xyz'); - + scope.apply(); expect(element.innerHtml).toEqual('xyz'); @@ -1031,7 +1055,7 @@ void main() { describe('pristine / dirty', () { it('should be set to pristine by default', (Scope scope) { - _.compile(''); + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -1039,8 +1063,9 @@ void main() { expect(model.dirty).toEqual(false); }); - it('should add and remove the correct CSS classes when set to dirty and to pristine', (Scope scope) { - _.compile(''); + it('should add and remove the correct CSS classes when set to dirty and to pristine', + (Scope scope) { + _.compile(''); Probe probe = _.rootScope.context['i']; NgModel model = probe.directive(NgModel); InputElement element = probe.element; @@ -1064,14 +1089,15 @@ void main() { // TODO(matias): figure out why the 2nd apply is optional it('should render the parent form/fieldset as dirty but not the other models', - (Scope scope) { + (Scope scope) { - _.compile('
' + - '
' + - ' ' + - ' ' + - '
' + - '
'); + _.compile(""" +
+
+ + +
+
"""); var formElement = _.rootScope.context['myForm'].element.node; var fieldsetElement = _.rootScope.context['myFieldset'].element.node; @@ -1127,7 +1153,8 @@ void main() { expect(model.valid).toBe(true); }); - it('should happen automatically upon user input via the onInput event', () { + it('should happen automatically upon user input via the onInput event', + () { _.compile(''); _.rootScope.apply(); @@ -1147,8 +1174,9 @@ void main() { }); describe('valid / invalid', () { - it('should add and remove the correct flags when set to valid and to invalid', (Scope scope) { - _.compile(''); + it('should add and remove the correct flags when set to valid and to invalid', + (Scope scope) { + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); InputElement element = probe.element; @@ -1172,8 +1200,9 @@ void main() { // expect(element.classes.contains('ng-valid')).toBe(true); }); - it('should set the validity with respect to all existing validations when setValidity() is used', (Scope scope) { - _.compile(''); + it('should set the validity with respect to all existing validations when setValidity() is used', + (Scope scope) { + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -1194,8 +1223,9 @@ void main() { expect(model.invalid).toEqual(false); }); - it('should register each error only once when invalid', (Scope scope) { - _.compile(''); + it('should register each error only once when invalid', + (Scope scope) { + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -1215,9 +1245,10 @@ void main() { describe('error handling', () { it('should return true or false depending on if an error exists on a form', - (Scope scope, TestBed _) { + (Scope scope, TestBed _) { - var element = $(''); + var element = + $(''); _.compile(element); scope.apply(); @@ -1256,7 +1287,8 @@ void main() { describe('error messages', () { it('should produce a useful error for bad ng-model expressions', () { expect(async(() { - _.compile('
'); + _.compile(''); _.rootScope.apply('myModel = "animal"'); Probe probe = _.rootScope.context['i']; @@ -1295,7 +1327,7 @@ void main() { }); it('should set the model to be untouched when the model is reset', () { - var input = _.compile(''); + var input = _.compile(''); var model = _.rootScope.context['i'].directive(NgModel); expect(model.touched).toBe(false); @@ -1313,20 +1345,20 @@ void main() { }); describe('validators', () { - it('should display the valid and invalid CSS classes on the element for each validation', - (TestBed _, Scope scope) { + it('should display the valid and invalid CSS classes on the element for each validation', + (TestBed _, Scope scope) { - var input = _.compile(''); + var input = _.compile(''); scope.apply(() { - scope.context['myModel'] = 'value'; + scope.context['myModel'] = 'value'; }); expect(input.classes.contains('ng-email-invalid')).toBe(true); expect(input.classes.contains('ng-email-valid')).toBe(false); scope.apply(() { - scope.context['myModel'] = 'value@email.com'; + scope.context['myModel'] = 'value@email.com'; }); expect(input.classes.contains('ng-email-valid')).toBe(true); @@ -1334,9 +1366,10 @@ void main() { }); it('should display the valid and invalid CSS classes on the element for custom validations', - (TestBed _, Scope scope) { + (TestBed _, Scope scope) { - var input = _.compile(''); + var input = _.compile(''); scope.apply(); @@ -1344,7 +1377,7 @@ void main() { expect(input.classes.contains('custom-valid')).toBe(false); scope.apply(() { - scope.context['myModel'] = 'yes'; + scope.context['myModel'] = 'yes'; }); expect(input.classes.contains('custom-valid')).toBe(true); @@ -1383,7 +1416,8 @@ void main() { }); describe('converters', () { - it('should parse the model value according to the given parser', (Scope scope) { + it('should parse the model value according to the given parser', + (Scope scope) { _.compile(''); scope.apply(); @@ -1400,7 +1434,8 @@ void main() { expect(model.modelValue).toEqual('hello'); }); - it('should format the model value according to the given formatter', (Scope scope) { + it('should format the model value according to the given formatter', + (Scope scope) { _.compile(''); scope.apply(); @@ -1418,10 +1453,11 @@ void main() { }); it('should retain the current input value if the parser fails', (Scope scope) { - _.compile('
' + - ' ' + - ' ' + - '
'); + _.compile(""" +
+ + +
"""); scope.apply(); var probe1 = scope.context['i']; @@ -1447,7 +1483,8 @@ void main() { expect(model2.modelValue).toEqual(null); }); - it('should reformat the viewValue when the formatter is changed', (Scope scope) { + it('should reformat the viewValue when the formatter is changed', + (Scope scope) { _.compile(''); scope.apply(); @@ -1524,9 +1561,7 @@ class MyCustomInputValidator extends NgValidator { final String name = 'custom'; - bool isValid(name) { - return name != null && name == 'yes'; - } + bool isValid(name) => name != null && name == 'yes'; } @NgDirective(