diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 1f6eb50a3..4f34a794d 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -187,10 +187,14 @@ class Scope { this._readWriteGroup, this._readOnlyGroup, this.id); /** - * A [watch] sets up a watch in the [digest] phase of the [apply] cycle. + * Use [watch] to set up a watch in the [apply] cycle. * - * Use [watch] if the reaction function can cause updates to model. In your - * controller code you will most likely use [watch]. + * 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. + * + * 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. */ Watch watch(expression, ReactionFn reactionFn, {context, FilterMap filters, bool readOnly: false}) { @@ -831,8 +835,8 @@ class AstParser { AstParser(this._parser); AST call(String exp, { FilterMap filters, - bool collection:false, - Object context:null }) { + bool collection: false, + Object context: null }) { _visitor.filters = filters; AST contextRef = _visitor.contextRef; try { @@ -1009,7 +1013,7 @@ class MapFn extends FunctionApply { MapFn(this.keys); - apply(List values) { + 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); diff --git a/lib/core_dom/directive.dart b/lib/core_dom/directive.dart index 1599105df..720a8701d 100644 --- a/lib/core_dom/directive.dart +++ b/lib/core_dom/directive.dart @@ -1,10 +1,11 @@ part of angular.core.dom; -/** - * Callback function used to notify of attribute changes. - */ +/// Callback function used to notify of attribute changes. typedef AttributeChanged(String newValue); +/// Callback function used to notify of observer changes. +typedef void ObserverChanged(bool hasListeners); + /** * NodeAttrs is a facade for element attributes. The facade is responsible * for normalizing attribute names as well as allowing access to the @@ -15,6 +16,8 @@ class NodeAttrs { Map> _observers; + Map> _observerListeners = {}; + NodeAttrs(this.element); operator [](String attributeName) => element.attributes[attributeName]; @@ -41,6 +44,10 @@ class NodeAttrs { .add(notifyFn); notifyFn(this[attributeName]); + + if (_observerListeners.containsKey(attributeName)) { + _observerListeners[attributeName].forEach((cb) => cb(true)); + } } void forEach(void f(String k, String v)) { @@ -51,6 +58,15 @@ class NodeAttrs { element.attributes.containsKey(attributeName); Iterable get keys => element.attributes.keys; + + void listenObserverChanges(String attributeName, ObserverChanged fn) { + if (_observerListeners == null) { + _observerListeners = >{}; + } + _observerListeners.putIfAbsent(attributeName, () => []) + .add(fn); + fn(false); + } } /** diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index 3aec866ba..f3571527f 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -8,24 +8,21 @@ class ElementBinderFactory { ElementBinderFactory(this._parser, this._perf, this._expando); - binder() { - return new ElementBinder(_parser, _perf, _expando); - } + ElementBinder binder() => new ElementBinder(_parser, _perf, _expando); } /** - * ElementBinder is created by the Selector and is responsible for instantiating individual directives - * and binding element properties. + * ElementBinder is created by the Selector and is responsible for instantiating + * individual directives and binding element properties. */ - class ElementBinder { // DI Services - Parser _parser; - Profiler _perf; - Expando _expando; + final Parser _parser; + final Profiler _perf; + final Expando _expando; // Member fields - List decorators = []; + var decorators = []; DirectiveRef template; ViewFactory templateViewFactory; @@ -34,18 +31,15 @@ class ElementBinder { // Can be either COMPILE_CHILDREN or IGNORE_CHILDREN String childMode = NgAnnotation.COMPILE_CHILDREN; - ElementBinder(this._parser, this._perf, this._expando); - ElementBinder.forTransclusion(ElementBinder other) { - _parser = other._parser; - _perf = other._perf; - _expando = other._expando; - - decorators = other.decorators; - component = other.component; - childMode = other.childMode; - } + ElementBinder.forTransclusion(ElementBinder other) + : _parser = other._parser, + _perf = other._perf, + _expando = other._expando, + decorators = other.decorators, + component = other.component, + childMode = other.childMode; addDirective(DirectiveRef ref) { var annotation = ref.annotation; @@ -66,49 +60,35 @@ class ElementBinder { createMappings(ref); } - bool get hasTemplate { - return template != null; - } + bool get hasTemplate => template != null; - bool get shouldCompileChildren { - return childMode == NgAnnotation.COMPILE_CHILDREN; - } + bool get shouldCompileChildren => + childMode == NgAnnotation.COMPILE_CHILDREN; - ElementBinder get templateBinder { - return new ElementBinder.forTransclusion(this); - } + ElementBinder get templateBinder => new ElementBinder.forTransclusion(this); List get _usableDirectiveRefs { - if (template != null) { - return [template]; - } - if (component != null) { - return new List.from(decorators)..add(component); - } + if (template != null) return [template]; + if (component != null) return new List.from(decorators)..add(component); return decorators; } - bool get hasDirectives { - return (_usableDirectiveRefs != null && _usableDirectiveRefs.length != 0); - } - - // DI visibility callback allowing node-local visibility. + bool get hasDirectives => + _usableDirectiveRefs != null && _usableDirectiveRefs.length != 0; + // DI visibility strategy allowing node-local visibility. static final Function _elementOnly = (Injector requesting, Injector defining) { - if (requesting.name == _SHADOW) { - requesting = requesting.parent; - } + if (requesting.name == _SHADOW) requesting = requesting.parent; return identical(requesting, defining); }; - // DI visibility callback allowing visibility from direct child into parent. - - static final Function _elementDirectChildren = (Injector requesting, Injector defining) { - if (requesting.name == _SHADOW) { - requesting = requesting.parent; - } - return _elementOnly(requesting, defining) || identical(requesting.parent, defining); - }; + // DI visibility strategy allowing visibility from direct child into parent. + static final Function _elementDirectChildren = + (Injector requesting, Injector defining) { + if (requesting.name == _SHADOW) requesting = requesting.parent; + return _elementOnly(requesting, defining) || + identical(requesting.parent, defining); + }; Injector bind(View view, Injector parentInjector, dom.Node node) { var timerId; @@ -122,18 +102,19 @@ class ElementBinder { var directiveRefs = _usableDirectiveRefs; try { - if (directiveRefs == null || directiveRefs.length == 0) return parentInjector; - var nodeModule = new Module(); + if (directiveRefs == null || directiveRefs.length == 0) { + return parentInjector; + } var viewPortFactory = (_) => null; var viewFactory = (_) => null; var boundViewFactory = (_) => null; var nodesAttrsDirectives = null; + var nodeModule = new Module()..type(NgElement) + ..value(View, view) + ..value(dom.Element, node) + ..value(dom.Node, node) + ..value(NodeAttrs, nodeAttrs); - nodeModule.type(NgElement); - nodeModule.value(View, view); - nodeModule.value(dom.Element, node); - nodeModule.value(dom.Node, node); - nodeModule.value(NodeAttrs, nodeAttrs); directiveRefs.forEach((DirectiveRef ref) { NgAnnotation annotation = ref.annotation; var visibility = _elementOnly; @@ -141,11 +122,16 @@ class ElementBinder { scope = scope.createChild(new PrototypeMap(scope.context)); nodeModule.value(Scope, scope); } - if (ref.annotation.visibility == NgDirective.CHILDREN_VISIBILITY) { - visibility = null; - } else if (ref.annotation.visibility == NgDirective.DIRECT_CHILDREN_VISIBILITY) { - visibility = _elementDirectChildren; + + switch (ref.annotation.visibility) { + case NgDirective.CHILDREN_VISIBILITY: + visibility = null; + break; + case NgDirective.DIRECT_CHILDREN_VISIBILITY: + visibility = _elementDirectChildren; + break; } + if (ref.type == NgTextMustacheDirective) { nodeModule.factory(NgTextMustacheDirective, (Injector injector) { return new NgTextMustacheDirective( @@ -187,28 +173,30 @@ class ElementBinder { nodeModule.type(ref.type, visibility: visibility); } for (var publishType in ref.annotation.publishTypes) { - nodeModule.factory(publishType, (Injector injector) => injector.get(ref.type), visibility: visibility); + nodeModule.factory(publishType, (Injector injector) => + injector.get(ref.type), visibility: visibility); } if (annotation.children == NgAnnotation.TRANSCLUDE_CHILDREN) { // Currently, transclude is only supported for NgDirective. assert(annotation is NgDirective); viewPortFactory = (_) => new ViewPort(node, - parentInjector.get(NgAnimate)); + parentInjector.get(NgAnimate)); viewFactory = (_) => templateViewFactory; - boundViewFactory = (Injector injector) => templateViewFactory.bind(injector); + boundViewFactory = (Injector injector) => + templateViewFactory.bind(injector); } }); - nodeModule - ..factory(ViewPort, viewPortFactory) - ..factory(ViewFactory, viewFactory) - ..factory(BoundViewFactory, boundViewFactory) - ..factory(ElementProbe, (_) => probe); + nodeModule..factory(ViewPort, viewPortFactory) + ..factory(ViewFactory, viewFactory) + ..factory(BoundViewFactory, boundViewFactory) + ..factory(ElementProbe, (_) => probe); nodeInjector = parentInjector.createChild([nodeModule]); probe = _expando[node] = new ElementProbe( parentInjector.get(ElementProbe), node, nodeInjector, scope); } finally { assert(_perf.stopTimer(timerId) != false); } + directiveRefs.forEach((DirectiveRef ref) { var linkTimer; try { @@ -311,8 +299,7 @@ class ElementBinder { var viewOutbound = false; var viewInbound = false; scope.watch( - expression, - (inboundValue, _) { + expression, (inboundValue, _) { if (!viewInbound) { viewOutbound = true; scope.rootScope.runAsync(() => viewOutbound = false); @@ -325,8 +312,7 @@ class ElementBinder { ); if (expressionFn.isAssignable) { scope.watch( - dstExpression, - (outboundValue, _) { + dstExpression, (outboundValue, _) { if (!viewOutbound) { viewInbound = true; scope.rootScope.runAsync(() => viewInbound = false); @@ -346,8 +332,7 @@ class ElementBinder { if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); var shadowValue = null; - scope.watch(attrs[attrName], - (v, _) { + scope.watch(attrs[attrName], (v, _) { dstPathFn.assign(controller, shadowValue = v); notify(); }, @@ -360,23 +345,20 @@ class ElementBinder { if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); var watch; - watch = scope.watch( - attrs[attrName], - (value, _) { - if (dstPathFn.assign(controller, value) != null) { - watch.remove(); - } - }, - filters: filters); + watch = scope.watch(attrs[attrName], (value, _) { + if (dstPathFn.assign(controller, value) != null) { + watch.remove(); + } + }, + filters: filters); notify(); }; break; case '&': mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters, notify()) { - dstPathFn - .assign(dst, _parser(attrs[attrName]) - .bind(scope.context, ScopeLocals.wrapper)); + dstPathFn.assign(dst, _parser(attrs[attrName]) + .bind(scope.context, ScopeLocals.wrapper)); notify(); }; break; @@ -386,7 +368,6 @@ class ElementBinder { } } - // Used for walking the DOM class ElementBinderTreeRef { final int offsetIndex; @@ -401,7 +382,6 @@ class ElementBinderTree { ElementBinderTree(this.binder, this.subtrees); } - class TaggedTextBinder { ElementBinder binder; final int offsetIndex; @@ -411,8 +391,8 @@ class TaggedTextBinder { // Used for the tagging compiler class TaggedElementBinder { - ElementBinder binder; - int parentBinderOffset; + final ElementBinder binder; + final int parentBinderOffset; var injector; List textBinders; @@ -424,5 +404,7 @@ class TaggedElementBinder { textBinders.add(tagged); } - toString() => "[TaggedElementBinder binder:$binder parentBinderOffset:$parentBinderOffset textBinders:$textBinders injector:$injector]"; + String toString() => "[TaggedElementBinder binder:$binder parentBinderOffset:" + "$parentBinderOffset textBinders:$textBinders " + "injector:$injector]"; } diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 7a783ecf4..c6c604b6f 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -18,26 +18,30 @@ class NgTextMustacheDirective { AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); scope.watch(ast, interpolation.call, readOnly: true); } - } // This Directive is special and does not go through injection. @NgDirective(selector: r'[*=/{{.*}}/]') class NgAttrMustacheDirective { + bool _hasObservers; + Watch _watch; + + // This Directive is special and does not go through injection. NgAttrMustacheDirective(NodeAttrs attrs, String markup, Interpolate interpolate, Scope scope, AstParser parser, FilterMap filters) { + var eqPos = markup.indexOf('='); var attrName = markup.substring(0, eqPos); var attrValue = markup.substring(eqPos + 1); - Interpolation interpolation = interpolate(attrValue); var lastValue = markup; - interpolation.setter = (text) { + 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(''); @@ -45,17 +49,16 @@ class NgAttrMustacheDirective { List items = interpolation.expressions .map((exp) => parser(exp, filters: filters)) .toList(); + AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); - /* - Attribute bindings are tricky. They need to be resolved on digest - inline with components so that any bindings to component can - be resolved before the component attach method. But once the - component is attached we need to run on the flush cycle rather - then digest cycle. - */ - // TODO(misko): figure out how to get most of these on observe rather then - // watch. - scope.watch(ast, interpolation.call); + + attrs.listenObserverChanges(attrName, (hasObservers) { + if (_hasObservers != hasObservers) { + hasObservers = hasObservers; + if (_watch != null) _watch.remove(); + _watch = scope.watch(ast, interpolation.call, readOnly: !hasObservers); + } + }); } } diff --git a/test/core_dom/ng_mustache_spec.dart b/test/core_dom/ng_mustache_spec.dart index 9bc9ef348..0191ae6f5 100644 --- a/test/core_dom/ng_mustache_spec.dart +++ b/test/core_dom/ng_mustache_spec.dart @@ -110,7 +110,7 @@ main() { } -@NgFilter(name:'hello') +@NgFilter(name: 'hello') class _HelloFilter { call(String str) { return 'Hello, $str!';