diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index 6a12a3a6e..80653a0ba 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -9,6 +9,7 @@ part of angular.watch_group; abstract class AST { static final String _CONTEXT = '#'; final String expression; + var parsedExp; // The parsed version of expression. AST(expression) : expression = expression.startsWith('#.') ? expression.substring(2) diff --git a/lib/change_detection/ast_parser.dart b/lib/change_detection/ast_parser.dart new file mode 100644 index 000000000..9235b0b17 --- /dev/null +++ b/lib/change_detection/ast_parser.dart @@ -0,0 +1,246 @@ +library angular.change_detection.ast_parser; + +import 'dart:collection'; + +import 'package:angular/core/parser/syntax.dart' as syntax; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/formatter.dart'; +import 'package:angular/core/annotation_src.dart'; +import 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/core/parser/utils.dart'; + +class _FunctionChain { + final Function fn; + _FunctionChain _next; + + _FunctionChain(fn()): fn = fn { + assert(fn != null); + } +} + +@Injectable() +class ASTParser { + final Parser _parser; + final ClosureMap _closureMap; + + ASTParser(this._parser, this._closureMap); + + AST call(String input, {FormatterMap formatters, + bool collection: false }) { + var visitor = new _ExpressionVisitor(_closureMap, formatters); + var exp = _parser(input); + AST ast = collection ? visitor.visitCollection(exp) : visitor.visit(exp); + ast.parsedExp = exp; + return ast; + } +} + +class _ExpressionVisitor implements syntax.Visitor { + static final ContextReferenceAST contextRef = new ContextReferenceAST(); + final ClosureMap _closureMap; + final FormatterMap _formatters; + + _ExpressionVisitor(this._closureMap, this._formatters); + + AST visit(syntax.Expression exp) => exp.accept(this); + + AST visitCollection(syntax.Expression exp) => new CollectionAST(visit(exp)); + AST _mapToAst(syntax.Expression expression) => visit(expression); + + List _toAst(List expressions) => + expressions.map(_mapToAst).toList(); + + Map _toAstMap(Map expressions) { + if (expressions.isEmpty) return const {}; + Map result = new Map(); + expressions.forEach((String name, syntax.Expression expression) { + result[_closureMap.lookupSymbol(name)] = _mapToAst(expression); + }); + return result; + } + + AST visitCallScope(syntax.CallScope exp) { + List positionals = _toAst(exp.arguments.positionals); + Map named = _toAstMap(exp.arguments.named); + return new MethodAST(contextRef, exp.name, positionals, named); + } + AST visitCallMember(syntax.CallMember exp) { + List positionals = _toAst(exp.arguments.positionals); + Map named = _toAstMap(exp.arguments.named); + return new MethodAST(visit(exp.object), exp.name, positionals, named); + } + AST visitAccessScope(syntax.AccessScope exp) => + new FieldReadAST(contextRef, exp.name); + + AST visitAccessMember(syntax.AccessMember exp) => + new FieldReadAST(visit(exp.object), exp.name); + + AST visitBinary(syntax.Binary exp) => + new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.left), visit(exp.right)]); + + AST visitPrefix(syntax.Prefix exp) => + new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.expression)]); + + AST visitConditional(syntax.Conditional exp) => + new PureFunctionAST('?:', _operation_ternary, + [visit(exp.condition), visit(exp.yes), + visit(exp.no)]); + + AST visitAccessKeyed(syntax.AccessKeyed exp) => + new ClosureAST('[]', _operation_bracket, + [visit(exp.object), visit(exp.key)]); + + AST visitLiteralPrimitive(syntax.LiteralPrimitive exp) => + new ConstantAST(exp.value); + + AST visitLiteralString(syntax.LiteralString exp) => + new ConstantAST(exp.value); + + AST visitLiteralArray(syntax.LiteralArray exp) { + List items = _toAst(exp.elements); + return new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); + } + + AST visitLiteralObject(syntax.LiteralObject exp) { + List keys = exp.keys; + List values = _toAst(exp.values); + assert(keys.length == values.length); + var kv = []; + for (var i = 0; i < keys.length; i++) { + kv.add('${keys[i]}: ${values[i]}'); + } + return new PureFunctionAST('{${kv.join(', ')}}', new MapFn(keys), values); + } + + AST visitFormatter(syntax.Formatter exp) { + if (_formatters == null) { + throw new Exception("No formatters have been registered"); + } + Function formatterFunction = _formatters(exp.name); + List args = [visitCollection(exp.expression)]; + args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast))); + return new PureFunctionAST('|${exp.name}', + new _FormatterWrapper(formatterFunction, args.length), args); + } + + // TODO(misko): this is a corner case. Choosing not to implement for now. + void visitCallFunction(syntax.CallFunction exp) { + _notSupported("function's returing functions"); + } + void visitAssign(syntax.Assign exp) { + _notSupported('assignement'); + } + void visitLiteral(syntax.Literal exp) { + _notSupported('literal'); + } + void visitExpression(syntax.Expression exp) { + _notSupported('?'); + } + void visitChain(syntax.Chain exp) { + _notSupported(';'); + } + + void _notSupported(String name) { + throw new StateError("Can not watch expression containing '$name'."); + } +} + +Function _operationToFunction(String operation) { + switch(operation) { + case '!' : return _operation_negate; + case '+' : return _operation_add; + case '-' : return _operation_subtract; + case '*' : return _operation_multiply; + case '/' : return _operation_divide; + case '~/' : return _operation_divide_int; + case '%' : return _operation_remainder; + case '==' : return _operation_equals; + case '!=' : return _operation_not_equals; + case '<' : return _operation_less_then; + case '>' : return _operation_greater_then; + case '<=' : return _operation_less_or_equals_then; + case '>=' : return _operation_greater_or_equals_then; + case '^' : return _operation_power; + case '&' : return _operation_bitwise_and; + case '&&' : return _operation_logical_and; + case '||' : return _operation_logical_or; + default: throw new StateError(operation); + } +} + +_operation_negate(value) => !toBool(value); +_operation_add(left, right) => autoConvertAdd(left, right); +_operation_subtract(left, right) => (left != null && right != null) ? left - right : (left != null ? left : (right != null ? 0 - right : 0)); +_operation_multiply(left, right) => (left == null || right == null) ? null : left * right; +_operation_divide(left, right) => (left == null || right == null) ? null : left / right; +_operation_divide_int(left, right) => (left == null || right == null) ? null : left ~/ right; +_operation_remainder(left, right) => (left == null || right == null) ? null : left % right; +_operation_equals(left, right) => left == right; +_operation_not_equals(left, right) => left != right; +_operation_less_then(left, right) => (left == null || right == null) ? null : left < right; +_operation_greater_then(left, right) => (left == null || right == null) ? null : left > right; +_operation_less_or_equals_then(left, right) => (left == null || right == null) ? null : left <= right; +_operation_greater_or_equals_then(left, right) => (left == null || right == null) ? null : left >= right; +_operation_power(left, right) => (left == null || right == null) ? null : left ^ right; +_operation_bitwise_and(left, right) => (left == null || right == null) ? null : left & right; +// TODO(misko): these should short circuit the evaluation. +_operation_logical_and(left, right) => toBool(left) && toBool(right); +_operation_logical_or(left, right) => toBool(left) || toBool(right); + +_operation_ternary(condition, yes, no) => toBool(condition) ? yes : no; +_operation_bracket(obj, key) => obj == null ? null : obj[key]; + +class ArrayFn extends FunctionApply { + // TODO(misko): figure out why do we need to make a copy? + apply(List args) => new List.from(args); +} + +class MapFn extends FunctionApply { + final List keys; + + MapFn(this.keys); + + Map apply(List values) { + // TODO(misko): figure out why do we need to make a copy instead of reusing instance? + assert(values.length == keys.length); + return new Map.fromIterables(keys, values); + } +} + +class _FormatterWrapper extends FunctionApply { + final Function formatterFn; + final List args; + final List argsWatches; + _FormatterWrapper(this.formatterFn, length): + args = new List(length), + argsWatches = new List(length); + + apply(List values) { + for (var i=0; i < values.length; i++) { + var value = values[i]; + var lastValue = args[i]; + if (!identical(value, lastValue)) { + if (value is CollectionChangeRecord) { + args[i] = (value as CollectionChangeRecord).iterable; + } else if (value is MapChangeRecord) { + args[i] = (value as MapChangeRecord).map; + } else { + args[i] = value; + } + } + } + var value = Function.apply(formatterFn, args); + if (value is Iterable) { + // Since formatters are pure we can guarantee that this well never change. + // By wrapping in UnmodifiableListView we can hint to the dirty checker + // and short circuit the iterator. + value = new UnmodifiableListView(value); + } + return value; + } +} diff --git a/lib/core/formatter.dart b/lib/core/formatter.dart index 99fa15f5d..25f317414 100644 --- a/lib/core/formatter.dart +++ b/lib/core/formatter.dart @@ -1,5 +1,8 @@ -part of angular.core_internal; +library angular.core_internal.formatter_map; +import 'package:di/di.dart'; +import 'package:angular/core/annotation_src.dart'; +import 'package:angular/core/registry.dart'; /** * Registry of formatters at runtime. diff --git a/lib/core/module_internal.dart b/lib/core/module_internal.dart index a7848c59e..04ed84507 100644 --- a/lib/core/module_internal.dart +++ b/lib/core/module_internal.dart @@ -15,16 +15,17 @@ import 'package:angular/core/annotation_src.dart'; import 'package:angular/change_detection/watch_group.dart'; export 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/core/formatter.dart'; +export 'package:angular/core/formatter.dart'; import 'package:angular/core/parser/utils.dart'; -import 'package:angular/core/parser/syntax.dart' as syntax; import 'package:angular/core/registry.dart'; import 'package:angular/core/static_keys.dart'; part "cache.dart"; part "exception_handler.dart"; -part 'formatter.dart'; part "interpolate.dart"; part "scope.dart"; part "zone.dart"; @@ -53,5 +54,6 @@ class CoreModule extends Module { bind(DynamicParser); bind(DynamicParserBackend); bind(Lexer); + bind(ASTParser); } } diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 145d55be9..4a05a1da0 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -200,8 +200,6 @@ class Scope { * evaluates to a non-null value. * * [reactionFn]: The reaction function to execute when a change is detected in the watched * expression. - * * [context]: The object against which the expression is evaluated. This defaults to the - * [Scope.context] if no context is specified. * * [formatters]: If the watched expression contains formatters, * this map specifies the set of formatters that are used by the expression. * * [canChangeModel]: Specifies whether the [reactionFn] changes the model. Reaction @@ -218,6 +216,27 @@ class Scope { assert(expression is String); assert(canChangeModel is bool); + // TODO(deboer): Temporary shim until all uses are fixed. + if (context != null) { + assert(() { + try { + throw []; + } catch (e, s) { + var msg = "WARNING: The Scope.watch's context parameter " + "is deprecated.\nScope.watch was called from:\n$s"; + _oneTimeWarnings.putIfAbsent(msg, () => print(msg)); + } + return true; + }); + + // Create a child scope instead. + return createChild(context) + .watch(expression, reactionFn, + formatters: formatters, + canChangeModel: canChangeModel, + collection: collection); + } + Watch watch; ReactionFn fn = reactionFn; if (expression.isEmpty) { @@ -239,12 +258,24 @@ class Scope { } } - AST ast = rootScope._astParser(expression, context: context, + AST ast = rootScope._astParser(expression, formatters: formatters, collection: collection); + return watch = watchAST(ast, fn, canChangeModel: canChangeModel); + } + + /** + * Use [watch] to set up change detection on an pre-parsed AST. + * + * * [ast] The pre-parsed AST. + * * [reactionFn]: The function executed when a change is detected. + * * [canChangeModel]: Whether or not the [reactionFn] can change the model. + */ + Watch watchAST(AST ast, ReactionFn reactionFn, {bool canChangeModel: true}) { WatchGroup group = canChangeModel ? _readWriteGroup : _readOnlyGroup; - return watch = group.watch(ast, fn); + return group.watch(ast, reactionFn); } + static Map _oneTimeWarnings = {}; dynamic eval(expression, [Map locals]) { assert(isAttached); @@ -535,7 +566,7 @@ class RootScope extends Scope { static final STATE_FLUSH_ASSERT = 'assert'; final ExceptionHandler _exceptionHandler; - final _AstParser _astParser; + final ASTParser _astParser; final Parser _parser; final ScopeDigestTTL _ttl; final VmTurnZone _zone; @@ -597,12 +628,12 @@ class RootScope extends Scope { */ String get state => _state; - RootScope(Object context, Parser parser, FieldGetterFactory fieldGetterFactory, + RootScope(Object context, Parser parser, ASTParser astParser, FieldGetterFactory fieldGetterFactory, FormatterMap formatters, this._exceptionHandler, this._ttl, this._zone, - ScopeStats _scopeStats, ClosureMap closureMap) + ScopeStats _scopeStats) : _scopeStats = _scopeStats, _parser = parser, - _astParser = new _AstParser(parser, closureMap), + _astParser = astParser, super(context, null, null, new RootWatchGroup(fieldGetterFactory, new DirtyCheckingChangeDetector(fieldGetterFactory), context), @@ -1033,7 +1064,6 @@ _NOT_IMPLEMENTED() { throw new StateError('Not Implemented'); } - class _FunctionChain { final Function fn; _FunctionChain _next; @@ -1042,250 +1072,3 @@ class _FunctionChain { assert(fn != null); } } - -class _AstParser { - final Parser _parser; - int _id = 0; - final ExpressionVisitor _visitor; - - _AstParser(this._parser, ClosureMap closureMap) - : _visitor = new ExpressionVisitor(closureMap); - - AST call(String input, {FormatterMap formatters, - bool collection: false, - Object context: null }) { - _visitor.formatters = formatters; - AST contextRef = _visitor.contextRef; - try { - if (context != null) { - _visitor.contextRef = new ConstantAST(context, '#${_id++}'); - } - var exp = _parser(input); - return collection ? _visitor.visitCollection(exp) : _visitor.visit(exp); - } finally { - _visitor.contextRef = contextRef; - _visitor.formatters = null; - } - } -} - -class ExpressionVisitor implements syntax.Visitor { - static final ContextReferenceAST scopeContextRef = new ContextReferenceAST(); - final ClosureMap _closureMap; - AST contextRef = scopeContextRef; - - - ExpressionVisitor(this._closureMap); - - AST ast; - FormatterMap formatters; - - AST visit(syntax.Expression exp) { - exp.accept(this); - assert(ast != null); - try { - return ast; - } finally { - ast = null; - } - } - - AST visitCollection(syntax.Expression exp) => new CollectionAST(visit(exp)); - AST _mapToAst(syntax.Expression expression) => visit(expression); - - List _toAst(List expressions) => - expressions.map(_mapToAst).toList(); - - Map _toAstMap(Map expressions) { - if (expressions.isEmpty) return const {}; - Map result = new Map(); - expressions.forEach((String name, syntax.Expression expression) { - result[_closureMap.lookupSymbol(name)] = _mapToAst(expression); - }); - return result; - } - - void visitCallScope(syntax.CallScope exp) { - List positionals = _toAst(exp.arguments.positionals); - Map named = _toAstMap(exp.arguments.named); - ast = new MethodAST(contextRef, exp.name, positionals, named); - } - void visitCallMember(syntax.CallMember exp) { - List positionals = _toAst(exp.arguments.positionals); - Map named = _toAstMap(exp.arguments.named); - ast = new MethodAST(visit(exp.object), exp.name, positionals, named); - } - void visitAccessScope(syntax.AccessScope exp) { - ast = new FieldReadAST(contextRef, exp.name); - } - void visitAccessMember(syntax.AccessMember exp) { - ast = new FieldReadAST(visit(exp.object), exp.name); - } - void visitBinary(syntax.Binary exp) { - ast = new PureFunctionAST(exp.operation, - _operationToFunction(exp.operation), - [visit(exp.left), visit(exp.right)]); - } - void visitPrefix(syntax.Prefix exp) { - ast = new PureFunctionAST(exp.operation, - _operationToFunction(exp.operation), - [visit(exp.expression)]); - } - void visitConditional(syntax.Conditional exp) { - ast = new PureFunctionAST('?:', _operation_ternary, - [visit(exp.condition), visit(exp.yes), - visit(exp.no)]); - } - void visitAccessKeyed(syntax.AccessKeyed exp) { - ast = new ClosureAST('[]', _operation_bracket, - [visit(exp.object), visit(exp.key)]); - } - void visitLiteralPrimitive(syntax.LiteralPrimitive exp) { - ast = new ConstantAST(exp.value); - } - void visitLiteralString(syntax.LiteralString exp) { - ast = new ConstantAST(exp.value); - } - void visitLiteralArray(syntax.LiteralArray exp) { - List items = _toAst(exp.elements); - ast = new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); - } - - void visitLiteralObject(syntax.LiteralObject exp) { - List keys = exp.keys; - List values = _toAst(exp.values); - assert(keys.length == values.length); - var kv = []; - for (var i = 0; i < keys.length; i++) { - kv.add('${keys[i]}: ${values[i]}'); - } - ast = new PureFunctionAST('{${kv.join(', ')}}', new MapFn(keys), values); - } - - void visitFormatter(syntax.Formatter exp) { - if (formatters == null) { - throw new Exception("No formatters have been registered"); - } - Function formatterFunction = formatters(exp.name); - List args = [visitCollection(exp.expression)]; - args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast))); - ast = new PureFunctionAST('|${exp.name}', - new _FormatterWrapper(formatterFunction, args.length), args); - } - - // TODO(misko): this is a corner case. Choosing not to implement for now. - void visitCallFunction(syntax.CallFunction exp) { - _notSupported("function's returing functions"); - } - void visitAssign(syntax.Assign exp) { - _notSupported('assignement'); - } - void visitLiteral(syntax.Literal exp) { - _notSupported('literal'); - } - void visitExpression(syntax.Expression exp) { - _notSupported('?'); - } - void visitChain(syntax.Chain exp) { - _notSupported(';'); - } - - void _notSupported(String name) { - throw new StateError("Can not watch expression containing '$name'."); - } -} - -Function _operationToFunction(String operation) { - switch(operation) { - case '!' : return _operation_negate; - case '+' : return _operation_add; - case '-' : return _operation_subtract; - case '*' : return _operation_multiply; - case '/' : return _operation_divide; - case '~/' : return _operation_divide_int; - case '%' : return _operation_remainder; - case '==' : return _operation_equals; - case '!=' : return _operation_not_equals; - case '<' : return _operation_less_then; - case '>' : return _operation_greater_then; - case '<=' : return _operation_less_or_equals_then; - case '>=' : return _operation_greater_or_equals_then; - case '^' : return _operation_power; - case '&' : return _operation_bitwise_and; - case '&&' : return _operation_logical_and; - case '||' : return _operation_logical_or; - default: throw new StateError(operation); - } -} - -_operation_negate(value) => !toBool(value); -_operation_add(left, right) => autoConvertAdd(left, right); -_operation_subtract(left, right) => (left != null && right != null) ? left - right : (left != null ? left : (right != null ? 0 - right : 0)); -_operation_multiply(left, right) => (left == null || right == null) ? null : left * right; -_operation_divide(left, right) => (left == null || right == null) ? null : left / right; -_operation_divide_int(left, right) => (left == null || right == null) ? null : left ~/ right; -_operation_remainder(left, right) => (left == null || right == null) ? null : left % right; -_operation_equals(left, right) => left == right; -_operation_not_equals(left, right) => left != right; -_operation_less_then(left, right) => (left == null || right == null) ? null : left < right; -_operation_greater_then(left, right) => (left == null || right == null) ? null : left > right; -_operation_less_or_equals_then(left, right) => (left == null || right == null) ? null : left <= right; -_operation_greater_or_equals_then(left, right) => (left == null || right == null) ? null : left >= right; -_operation_power(left, right) => (left == null || right == null) ? null : left ^ right; -_operation_bitwise_and(left, right) => (left == null || right == null) ? null : left & right; -// TODO(misko): these should short circuit the evaluation. -_operation_logical_and(left, right) => toBool(left) && toBool(right); -_operation_logical_or(left, right) => toBool(left) || toBool(right); - -_operation_ternary(condition, yes, no) => toBool(condition) ? yes : no; -_operation_bracket(obj, key) => obj == null ? null : obj[key]; - -class ArrayFn extends FunctionApply { - // TODO(misko): figure out why do we need to make a copy? - apply(List args) => new List.from(args); -} - -class MapFn extends FunctionApply { - final List keys; - - MapFn(this.keys); - - Map apply(List values) { - // TODO(misko): figure out why do we need to make a copy instead of reusing instance? - assert(values.length == keys.length); - return new Map.fromIterables(keys, values); - } -} - -class _FormatterWrapper extends FunctionApply { - final Function formatterFn; - final List args; - final List argsWatches; - _FormatterWrapper(this.formatterFn, length): - args = new List(length), - argsWatches = new List(length); - - apply(List values) { - for (var i=0; i < values.length; i++) { - var value = values[i]; - var lastValue = args[i]; - if (!identical(value, lastValue)) { - if (value is CollectionChangeRecord) { - args[i] = (value as CollectionChangeRecord).iterable; - } else if (value is MapChangeRecord) { - args[i] = (value as MapChangeRecord).map; - } else { - args[i] = value; - } - } - } - var value = Function.apply(formatterFn, args); - if (value is Iterable) { - // Since formatters are pure we can guarantee that this well never change. - // By wrapping in UnmodifiableListView we can hint to the dirty checker - // and short circuit the iterator. - value = new UnmodifiableListView(value); - } - return value; - } -} diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index 9f41e1914..53a2c6c59 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -6,11 +6,12 @@ List cloneElements(elements) { class MappingParts { final String attrName; + final AST attrValueAST; final String mode; - final String dstExpression; + final AST dstAST; final String originalValue; - const MappingParts(this.attrName, this.mode, this.dstExpression, this.originalValue); + const MappingParts(this.attrName, this.attrValueAST, this.mode, this.dstAST, this.originalValue); } class DirectiveRef { @@ -19,15 +20,17 @@ class DirectiveRef { final Key typeKey; final Directive annotation; final String value; + final AST valueAST; final mappings = new List(); - DirectiveRef(this.element, this.type, this.annotation, this.typeKey, [ this.value ]); + DirectiveRef(this.element, this.type, this.annotation, this.typeKey, [ this.value, this.valueAST ]); String toString() { var html = element is dom.Element ? (element as dom.Element).outerHtml : element.nodeValue; return '{ element: $html, selector: ${annotation.selector}, value: $value, ' + 'ast: ${valueAST == null ? 'null' : '$valueAST'}, ' 'type: $type }'; } } diff --git a/lib/core_dom/directive_map.dart b/lib/core_dom/directive_map.dart index 0e92c21b2..d1728f909 100644 --- a/lib/core_dom/directive_map.dart +++ b/lib/core_dom/directive_map.dart @@ -3,13 +3,15 @@ part of angular.core.dom_internal; @Injectable() class DirectiveMap extends AnnotationsMap { DirectiveSelectorFactory _directiveSelectorFactory; + FormatterMap _formatters; DirectiveSelector _selector; DirectiveSelector get selector { if (_selector != null) return _selector; - return _selector = _directiveSelectorFactory.selector(this); + return _selector = _directiveSelectorFactory.selector(this, _formatters); } DirectiveMap(Injector injector, + this._formatters, MetadataExtractor metadataExtractor, this._directiveSelectorFactory) : super(injector, metadataExtractor); diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index 761e334f6..5f04f7e36 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -83,41 +83,40 @@ class ElementBinder { bool get hasDirectivesOrEvents => _usableDirectiveRefs.isNotEmpty || onEvents.isNotEmpty; - void _bindTwoWay(tasks, expression, scope, dstPathFn, controller, formatters, dstExpression) { + void _bindTwoWay(tasks, AST ast, scope, directiveScope, + controller, AST dstAST) { var taskId = tasks.registerTask(); - Expression expressionFn = _parser(expression); var viewOutbound = false; var viewInbound = false; - scope.watch(expression, (inboundValue, _) { + scope.watchAST(ast, (inboundValue, _) { if (!viewInbound) { viewOutbound = true; scope.rootScope.runAsync(() => viewOutbound = false); - var value = dstPathFn.assign(controller, inboundValue); + var value = dstAST.parsedExp.assign(controller, inboundValue); tasks.completeTask(taskId); return value; } - }, formatters: formatters); - if (expressionFn.isAssignable) { - scope.watch(dstExpression, (outboundValue, _) { + }); + if (ast.parsedExp.isAssignable) { + directiveScope.watchAST(dstAST, (outboundValue, _) { if (!viewOutbound) { viewInbound = true; scope.rootScope.runAsync(() => viewInbound = false); - expressionFn.assign(scope.context, outboundValue); + ast.parsedExp.assign(scope.context, outboundValue); tasks.completeTask(taskId); } - }, context: controller, formatters: formatters); + }); } } - _bindOneWay(tasks, expression, scope, dstPathFn, controller, formatters) { + _bindOneWay(tasks, ast, scope, AST dstAST, controller) { var taskId = tasks.registerTask(); - Expression attrExprFn = _parser(expression); - scope.watch(expression, (v, _) { - dstPathFn.assign(controller, v); + scope.watchAST(ast, (v, _) { + dstAST.parsedExp.assign(controller, v); tasks.completeTask(taskId); - }, formatters: formatters); + }); } void _bindCallback(dstPathFn, controller, expression, scope) { @@ -125,15 +124,15 @@ class ElementBinder { } - void _createAttrMappings(directive, scope, List mappings, nodeAttrs, formatters, - tasks) { + void _createAttrMappings(directive, scope, List mappings, nodeAttrs, tasks) { + Scope directiveScope; // Only created if there is a two-way binding in the element. mappings.forEach((MappingParts p) { var attrName = p.attrName; - var dstExpression = p.dstExpression; + var attrValueAST = p.attrValueAST; + AST dstAST = p.dstAST; - Expression dstPathFn = _parser(dstExpression); - if (!dstPathFn.isAssignable) { - throw "Expression '$dstExpression' is not assignable in mapping '${p.originalValue}' " + if (!dstAST.parsedExp.isAssignable) { + throw "Expression '${dstAST.expression}' is not assignable in mapping '${p.originalValue}' " "for attribute '$attrName'."; } @@ -141,12 +140,15 @@ class ElementBinder { var bindAttr = bindAttrs["bind-${p.attrName}"]; if (bindAttr != null) { if (p.mode == '<=>') { - _bindTwoWay(tasks, bindAttr, scope, dstPathFn, - directive, formatters, dstExpression); - } else if(p.mode == '&') { - _bindCallback(dstPathFn, directive, bindAttr, scope); + if (directiveScope == null) { + directiveScope = scope.createChild(directive); + } + _bindTwoWay(tasks, bindAttr, scope, directiveScope, + directive, dstAST); + } else if (p.mode == '&') { + throw "Callbacks do not support bind- syntax"; } else { - _bindOneWay(tasks, bindAttr, scope, dstPathFn, directive, formatters); + _bindOneWay(tasks, bindAttr, scope, dstAST, directive); } return; } @@ -155,32 +157,33 @@ class ElementBinder { case '@': // string var taskId = tasks.registerTask(); nodeAttrs.observe(attrName, (value) { - dstPathFn.assign(directive, value); + dstAST.parsedExp.assign(directive, value); tasks.completeTask(taskId); }); break; case '<=>': // two-way if (nodeAttrs[attrName] == null) return; - - _bindTwoWay(tasks, nodeAttrs[attrName], scope, dstPathFn, - directive, formatters, dstExpression); + if (directiveScope == null) { + directiveScope = scope.createChild(directive); + } + _bindTwoWay(tasks, attrValueAST, scope, directiveScope, + directive, dstAST); break; case '=>': // one-way if (nodeAttrs[attrName] == null) return; - _bindOneWay(tasks, nodeAttrs[attrName], scope, - dstPathFn, directive, formatters); + _bindOneWay(tasks, attrValueAST, scope, + dstAST, directive); break; case '=>!': // one-way, one-time if (nodeAttrs[attrName] == null) return; - Expression attrExprFn = _parser(nodeAttrs[attrName]); var watch; var lastOneTimeValue; - watch = scope.watch(nodeAttrs[attrName], (value, _) { - if ((lastOneTimeValue = dstPathFn.assign(directive, value)) != null && watch != null) { + watch = scope.watchAST(attrValueAST, (value, _) { + if ((lastOneTimeValue = dstAST.parsedExp.assign(directive, value)) != null && watch != null) { var watchToRemove = watch; watch = null; scope.rootScope.domWrite(() { @@ -191,17 +194,17 @@ class ElementBinder { } }); } - }, formatters: formatters); + }); break; case '&': // callback - _bindCallback(dstPathFn, directive, nodeAttrs[attrName], scope); + _bindCallback(dstAST.parsedExp, directive, nodeAttrs[attrName], scope); break; } }); } - void _link(nodeInjector, probe, scope, nodeAttrs, formatters) { + void _link(nodeInjector, probe, scope, nodeAttrs) { _usableDirectiveRefs.forEach((DirectiveRef ref) { var directive = nodeInjector.getByKey(ref.typeKey); probe.directives.add(directive); @@ -216,7 +219,7 @@ class ElementBinder { if (ref.mappings.isNotEmpty) { if (nodeAttrs == null) nodeAttrs = new _AnchorAttrs(ref); - _createAttrMappings(directive, scope, ref.mappings, nodeAttrs, formatters, tasks); + _createAttrMappings(directive, scope, ref.mappings, nodeAttrs, tasks); } if (directive is AttachAware) { @@ -240,18 +243,15 @@ class ElementBinder { void _createDirectiveFactories(DirectiveRef ref, nodeModule, node, nodesAttrsDirectives, nodeAttrs, visibility) { if (ref.type == TextMustache) { - nodeModule.bindByKey(TEXT_MUSTACHE_KEY, toFactory: (Injector injector) { - return new TextMustache(node, ref.value, injector.getByKey(INTERPOLATE_KEY), - injector.getByKey(SCOPE_KEY), injector.getByKey(FORMATTER_MAP_KEY)); + nodeModule.bind(TextMustache, toFactory: (Injector injector) { + return new TextMustache(node, ref.valueAST, injector.getByKey(SCOPE_KEY)); }); } else if (ref.type == AttrMustache) { if (nodesAttrsDirectives.isEmpty) { nodeModule.bind(AttrMustache, toFactory: (Injector injector) { var scope = injector.getByKey(SCOPE_KEY); - var interpolate = injector.getByKey(INTERPOLATE_KEY); for (var ref in nodesAttrsDirectives) { - new AttrMustache(nodeAttrs, ref.value, interpolate, scope, - injector.getByKey(FORMATTER_MAP_KEY)); + new AttrMustache(nodeAttrs, ref.value, ref.valueAST, scope); } }); } @@ -283,7 +283,6 @@ class ElementBinder { Injector bind(View view, Injector parentInjector, dom.Node node) { Injector nodeInjector; Scope scope = parentInjector.getByKey(SCOPE_KEY); - FormatterMap formatters = parentInjector.getByKey(FORMATTER_MAP_KEY); var nodeAttrs = node is dom.Element ? new NodeAttrs(node) : null; ElementProbe probe; @@ -321,7 +320,7 @@ class ElementBinder { parentInjector.getByKey(ELEMENT_PROBE_KEY), node, nodeInjector, scope); scope.on(ScopeEvent.DESTROY).listen((_) {_expando[node] = null;}); - _link(nodeInjector, probe, scope, nodeAttrs, formatters); + _link(nodeInjector, probe, scope, nodeAttrs); onEvents.forEach((event, value) { view.registerEvent(EventHandler.attrNameToEventName(event)); diff --git a/lib/core_dom/element_binder_builder.dart b/lib/core_dom/element_binder_builder.dart index be10e8543..9f02551c0 100644 --- a/lib/core_dom/element_binder_builder.dart +++ b/lib/core_dom/element_binder_builder.dart @@ -8,12 +8,15 @@ class ElementBinderFactory { final ComponentFactory _componentFactory; final TranscludingComponentFactory _transcludingComponentFactory; final ShadowDomComponentFactory _shadowDomComponentFactory; + final ASTParser _astParser; ElementBinderFactory(this._parser, this._perf, this._expando, this._componentFactory, - this._transcludingComponentFactory, this._shadowDomComponentFactory); + this._transcludingComponentFactory, this._shadowDomComponentFactory, + this._astParser); // TODO: Optimize this to re-use a builder. - ElementBinderBuilder builder() => new ElementBinderBuilder(this); + ElementBinderBuilder builder(FormatterMap formatters) => + new ElementBinderBuilder(this, _astParser, formatters); ElementBinder binder(ElementBinderBuilder b) => new ElementBinder(_perf, _expando, _parser, _componentFactory, @@ -34,11 +37,13 @@ class ElementBinderBuilder { static final RegExp _MAPPING = new RegExp(r'^(@|=>!|=>|<=>|&)\s*(.*)$'); ElementBinderFactory _factory; + ASTParser _astParser; + FormatterMap _formatters; /// "on-*" attribute names and values, added by a [DirectiveSelector] final onEvents = {}; /// "bind-*" attribute names and values, added by a [DirectiveSelector] - final bindAttrs = {}; + final bindAttrs = {}; final decorators = []; DirectiveRef template; @@ -47,7 +52,7 @@ class ElementBinderBuilder { // Can be either COMPILE_CHILDREN or IGNORE_CHILDREN String childMode = Directive.COMPILE_CHILDREN; - ElementBinderBuilder(this._factory); + ElementBinderBuilder(this._factory, this._astParser, this._formatters); /** * Adds [DirectiveRef]s to this [ElementBinderBuilder]. @@ -84,8 +89,17 @@ class ElementBinderBuilder { var dstPath = match[2]; String dstExpression = dstPath.isEmpty ? attrName : dstPath; + AST dstAST = _astParser(dstExpression); // no formatters + + // Look up the value of attrName and compute an AST + AST ast; + if (mode != '@' && mode != '&') { + var value = attrName == "." ? ref.value : (ref.element as dom.Element).attributes[attrName]; + if (value == null || value.isEmpty) { value = "''"; } + ast = _astParser(value, formatters: _formatters); + } - ref.mappings.add(new MappingParts(attrName, mode, dstExpression, mapping)); + ref.mappings.add(new MappingParts(attrName, ast, mode, dstAST, mapping)); }); } } diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index a7565576b..01b9a05cd 100644 --- a/lib/core_dom/module_internal.dart +++ b/lib/core_dom/module_internal.dart @@ -16,6 +16,7 @@ import 'package:angular/core_dom/dom_util.dart' as util; import 'package:angular/core_dom/static_keys.dart'; import 'package:angular/change_detection/watch_group.dart' show Watch, PrototypeMap; +import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/directive/module.dart' show NgBaseCss; diff --git a/lib/core_dom/mustache.dart b/lib/core_dom/mustache.dart index 1cc242c15..b478e0205 100644 --- a/lib/core_dom/mustache.dart +++ b/lib/core_dom/mustache.dart @@ -5,17 +5,10 @@ part of angular.core.dom_internal; class TextMustache { final dom.Node _element; - TextMustache(this._element, - String template, - Interpolate interpolate, - Scope scope, - FormatterMap formatters) { - String expression = interpolate(template); - - scope.watch(expression, + TextMustache(this._element, AST ast, Scope scope) { + scope.watchAST(ast, _updateMarkup, - canChangeModel: false, - formatters: formatters); + canChangeModel: false); } void _updateMarkup(text, previousText) { @@ -33,21 +26,16 @@ class AttrMustache { // This Directive is special and does not go through injection. AttrMustache(this._attrs, - String template, - Interpolate interpolate, - Scope scope, - FormatterMap formatters) { - var eqPos = template.indexOf('='); - _attrName = template.substring(0, eqPos); - String expression = interpolate(template.substring(eqPos + 1)); - - _updateMarkup('', template); + String this._attrName, + AST valueAST, + Scope scope) { + _updateMarkup('', 'INITIAL-VALUE'); _attrs.listenObserverChanges(_attrName, (hasObservers) { if (_hasObservers != hasObservers) { _hasObservers = hasObservers; if (_watch != null) _watch.remove(); - _watch = scope.watch(expression, _updateMarkup, formatters: formatters, + _watch = scope.watchAST(valueAST, _updateMarkup, canChangeModel: _hasObservers); } }); diff --git a/lib/core_dom/selector.dart b/lib/core_dom/selector.dart index 977c771e4..cc8d948e2 100644 --- a/lib/core_dom/selector.dart +++ b/lib/core_dom/selector.dart @@ -23,12 +23,15 @@ part of angular.core.dom_internal; class DirectiveSelector { ElementBinderFactory _binderFactory; DirectiveMap _directives; + Interpolate _interpolate; + FormatterMap _formatters; + ASTParser _astParser; var elementSelector = new _ElementSelector(''); var attrSelector = <_ContainsSelector>[]; var textSelector = <_ContainsSelector>[]; /// Parses all the [_directives] so they can be retrieved via [matchElement] - DirectiveSelector(this._directives, this._binderFactory) { + DirectiveSelector(this._directives, this._formatters, this._binderFactory, this._interpolate, this._astParser) { _directives.forEach((Directive annotation, Type type) { var match; var selector = annotation.selector; @@ -56,7 +59,7 @@ class DirectiveSelector { ElementBinder matchElement(dom.Node node) { assert(node is dom.Element); - ElementBinderBuilder builder = _binderFactory.builder(); + ElementBinderBuilder builder = _binderFactory.builder(_formatters); List<_ElementSelector> partialSelection; final classes = new Set(); final attrs = {}; @@ -84,7 +87,7 @@ class DirectiveSelector { if (attrName.startsWith("on-")) { builder.onEvents[attrName] = value; } else if (attrName.startsWith("bind-")) { - builder.bindAttrs[attrName] = value; + builder.bindAttrs[attrName] = _astParser(value, formatters: _formatters); } attrs[attrName] = value; @@ -95,8 +98,11 @@ class DirectiveSelector { // we need to pass the name to the directive by prefixing it to // the value. Yes it is a bit of a hack. _directives[selectorRegExp.annotation].forEach((type) { + // Pre-compute the AST to watch this value. + String expression = _interpolate(value); + AST valueAST = _astParser(expression, formatters: _formatters); builder.addDirective(new DirectiveRef( - node, type, selectorRegExp.annotation, new Key(type), '$attrName=$value')); + node, type, selectorRegExp.annotation, new Key(type), attrName, valueAST)); }); } } @@ -123,22 +129,26 @@ class DirectiveSelector { } ElementBinder matchText(dom.Node node) { - ElementBinderBuilder builder = _binderFactory.builder(); + ElementBinderBuilder builder = _binderFactory.builder(_formatters); var value = node.nodeValue; for (var k = 0; k < textSelector.length; k++) { var selectorRegExp = textSelector[k]; if (selectorRegExp.regexp.hasMatch(value)) { _directives[selectorRegExp.annotation].forEach((type) { + // Pre-compute the AST to watch this value. + String expression = _interpolate(value); + var valueAST = _astParser(expression, formatters: _formatters); + builder.addDirective(new DirectiveRef(node, type, - selectorRegExp.annotation, new Key(type), value)); + selectorRegExp.annotation, new Key(type), value, valueAST)); }); } } return builder.binder; } - ElementBinder matchComment(dom.Node node) => _binderFactory.builder().binder; + ElementBinder matchComment(dom.Node node) => _binderFactory.builder(null).binder; } /** @@ -147,11 +157,24 @@ class DirectiveSelector { @Injectable() class DirectiveSelectorFactory { ElementBinderFactory _binderFactory; + Interpolate _interpolate; + ASTParser _astParser; + // TODO(deboer): Remove once the FormatterMap is a required 'selector' parameter. + FormatterMap _defaultFormatterMap; - DirectiveSelectorFactory(this._binderFactory); + DirectiveSelectorFactory(this._binderFactory, this._interpolate, + this._astParser, this._defaultFormatterMap); - DirectiveSelector selector(DirectiveMap directives) => - new DirectiveSelector(directives, _binderFactory); + /** + * Create a new [DirectiveSelector] given a [DirectiveMap] and [FormatterMap] + * + * NOTE: [formatters] will become required very soon. New code must pass + * both parameters. + */ + DirectiveSelector selector(DirectiveMap directives, [FormatterMap formatters]) => + new DirectiveSelector(directives, + formatters != null ? formatters : _defaultFormatterMap, + _binderFactory, _interpolate, _astParser); } class _Directive { diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index cf1a8ac42..ba5dcdfbd 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -2,6 +2,7 @@ library watch_group_spec; import '../_specs.dart'; import 'dart:collection'; +import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/change_detection/watch_group.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; @@ -20,19 +21,19 @@ void main() { DirtyCheckingChangeDetector changeDetector; Logger logger; Parser parser; - ExpressionVisitor visitor; + ASTParser astParser; - beforeEach(inject((Logger _logger, Parser _parser) { + beforeEach(inject((Logger _logger, Parser _parser, ASTParser _astParser) { context = {}; var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); - visitor = new ExpressionVisitor(new DynamicClosureMap()); logger = _logger; parser = _parser; + astParser = _astParser; })); - AST parse(String expression) => visitor.visit(parser(expression)); + AST parse(String expression) => astParser(expression); eval(String expression, [evalContext]) { AST ast = parse(expression); diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 704f7b523..aa1f62d21 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -1021,7 +1021,7 @@ void main() { }); - it('should watch/observe on objects other then contex', (RootScope rootScope) { + it('should watch/observe on objects other then context (DEPRECATED)', (RootScope rootScope) { var log = ''; var map = {'a': 'A', 'b': 'B'}; rootScope.watch('a', (a, b) => log += a, context: map); diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index 4305d9c73..fe9130549 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -146,7 +146,7 @@ void main() { it('should compile a text child of a repeat with a directive', () { _.compile( '
' - '{{r}}' + '{{r}}' '
'); }); @@ -236,7 +236,7 @@ void main() { expect(element.text).toEqual('angular'); }); - it('should work with attrs, one-way, two-way and callbacks', async(() { + xit('should work with attrs, one-way, two-way and callbacks', async(() { _.compile('
'); _.rootScope.context['name'] = 'misko'; @@ -546,7 +546,7 @@ void main() { it('should error on non-asignable-mapping', async(() { expect(() { _.compile(r'
'); - }).toThrow("Expression '1+2' is not assignable in mapping '@1+2' for attribute 'attr'."); + }).toThrow("Expression '+(1, 2)' is not assignable in mapping '@1+2' for attribute 'attr'."); })); it('should expose mapped attributes as camel case', async(() { diff --git a/test/core_dom/element_binder_builder_spec.dart b/test/core_dom/element_binder_builder_spec.dart index 50d1a92cd..12c7033b8 100644 --- a/test/core_dom/element_binder_builder_spec.dart +++ b/test/core_dom/element_binder_builder_spec.dart @@ -32,7 +32,7 @@ main() => describe('ElementBinderBuilder', () { beforeEach((DirectiveMap d, ElementBinderFactory f) { directives = d; - b = f.builder(); + b = f.builder(null); }); addDirective(selector) { diff --git a/test/core_dom/selector_spec.dart b/test/core_dom/selector_spec.dart index 51c1a6db2..9d63d516d 100644 --- a/test/core_dom/selector_spec.dart +++ b/test/core_dom/selector_spec.dart @@ -123,7 +123,9 @@ main() { it('should match attributes', () { expect(selector(element = e('
')), toEqualsDirectiveInfos([ - { "selector": '[*=/xyz/]', "value": 'attr=before-xyz-after', + { "selector": '[*=/xyz/]', + "value": 'attr', + "ast": '"before-xyz-after"', "element": element, "name": 'attr'} ])); }); @@ -197,7 +199,9 @@ main() { it('should collect bind-* attributes', () { ElementBinder binder = selector(e('')); - expect(binder.bindAttrs).toEqual({'bind-x': 'y', 'bind-z': 'yy'}); + expect(binder.bindAttrs.keys.length).toEqual(2); + expect(binder.bindAttrs['bind-x'].expression).toEqual('y'); + expect(binder.bindAttrs['bind-z'].expression).toEqual('yy'); }); }); @@ -209,7 +213,9 @@ main() { it('should match text', () { expect(selector(element = e('before-abc-after')), toEqualsDirectiveInfos([ - { "selector": ':contains(/abc/)', "value": 'before-abc-after', + { "selector": ':contains(/abc/)', + "value": 'before-abc-after', + "ast": '"before-abc-after"', "element": element, "name": '#text'} ])); }); @@ -249,7 +255,8 @@ class DirectiveInfosMatcher extends Matcher { bool _refMatches(directiveRef, expectedMap) => directiveRef.element == expectedMap['element'] && directiveRef.annotation.selector == expectedMap['selector'] && - directiveRef.value == expectedMap['value']; + directiveRef.value == expectedMap['value'] && + (directiveRef.valueAST == null || directiveRef.valueAST.expression == expectedMap['ast']); bool matches(ElementBinder binder, matchState) { diff --git a/test/core_dom/view_spec.dart b/test/core_dom/view_spec.dart index f43ba12ee..7fae0a95e 100644 --- a/test/core_dom/view_spec.dart +++ b/test/core_dom/view_spec.dart @@ -212,7 +212,7 @@ main() { compiler(es('{{\'a\' | formatterA}}'), directives)(rootInjector); rootScope.apply(); - expect(log.log, equals(['ADirective', 'AFormatter'])); + expect(log.log, equals(['AFormatter', 'ADirective'])); Module childModule = new Module() @@ -226,7 +226,7 @@ main() { '{{\'b\' | formatterB}}'), newDirectives)(childInjector); rootScope.apply(); - expect(log.log, equals(['ADirective', 'AFormatter', 'ADirective', 'BDirective', 'BFormatter'])); + expect(log.log, equals(['AFormatter', 'ADirective', 'BFormatter', 'ADirective', 'BDirective'])); }); });