diff --git a/lib/core/annotation_src.dart b/lib/core/annotation_src.dart index f7920a6da..9aca03cdf 100644 --- a/lib/core/annotation_src.dart +++ b/lib/core/annotation_src.dart @@ -203,6 +203,19 @@ abstract class Directive { */ final List exportExpressions; + /** + * Event names to listen to during Web Component two-way binding. + * + * To support web components efficiently, Angular only reads element + * bindings when specific events are fired. By default, Angular listens + * to 'change'. Adding events names to this listen will cause Angular + * to listen to those events instead. + * + * The name is intentionally long: this should be rarely used and therefore + * it is important that it is self-documenting. + */ + final List updateBoundElementPropertiesOnEvents; + const Directive({ this.selector, this.children, @@ -210,7 +223,8 @@ abstract class Directive { this.module, this.map: const {}, this.exportExpressions: const [], - this.exportExpressionAttrs: const [] + this.exportExpressionAttrs: const [], + this.updateBoundElementPropertiesOnEvents }); toString() => selector; @@ -282,15 +296,17 @@ class Component extends Directive { exportExpressions, exportExpressionAttrs, this.useShadowDom, - this.useNgBaseCss: true}) - : _cssUrls = cssUrl, + this.useNgBaseCss: true, + updateBoundElementPropertiesOnEvents + }) : _cssUrls = cssUrl, super(selector: selector, children: Directive.COMPILE_CHILDREN, visibility: visibility, map: map, module: module, exportExpressions: exportExpressions, - exportExpressionAttrs: exportExpressionAttrs); + exportExpressionAttrs: exportExpressionAttrs, + updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents); List get cssUrls => _cssUrls == null ? const [] : @@ -309,7 +325,8 @@ class Component extends Directive { exportExpressions: exportExpressions, exportExpressionAttrs: exportExpressionAttrs, useShadowDom: useShadowDom, - useNgBaseCss: useNgBaseCss); + useNgBaseCss: useNgBaseCss, + updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents); } /** @@ -332,14 +349,16 @@ class Decorator extends Directive { DirectiveBinderFn module, visibility, exportExpressions, - exportExpressionAttrs}) + exportExpressionAttrs, + updateBoundElementPropertiesOnEvents}) : super(selector: selector, children: children, visibility: visibility, map: map, module: module, exportExpressions: exportExpressions, - exportExpressionAttrs: exportExpressionAttrs); + exportExpressionAttrs: exportExpressionAttrs, + updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents); Directive _cloneWithNewMap(newMap) => new Decorator( @@ -349,7 +368,8 @@ class Decorator extends Directive { selector: selector, visibility: visibility, exportExpressions: exportExpressions, - exportExpressionAttrs: exportExpressionAttrs); + exportExpressionAttrs: exportExpressionAttrs, + updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents); } /** diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index 7edfd9d3b..756ff889d 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -59,6 +59,20 @@ class ElementBinder { bool get shouldCompileChildren => childMode == Directive.COMPILE_CHILDREN; + List _bindAssignablePropsOnCache; + List get _bindAssignablePropsOn { + if (_bindAssignablePropsOnCache != null) return _bindAssignablePropsOnCache; + _bindAssignablePropsOnCache = []; + _usableDirectiveRefs.forEach((DirectiveRef ref) { + var eventNames = ref.annotation.updateBoundElementPropertiesOnEvents; + if (eventNames != null) { + _bindAssignablePropsOnCache.addAll(eventNames); + } + }); + if (_bindAssignablePropsOnCache.isEmpty) _bindAssignablePropsOnCache.add('change'); + return _bindAssignablePropsOnCache; + } + var _directiveCache; List get _usableDirectiveRefs { if (_directiveCache != null) return _directiveCache; @@ -308,13 +322,11 @@ class ElementBinder { // due to https://code.google.com/p/dart/issues/detail?id=17406 // we have to manually run the zone. var zone = Zone.current; - node.addEventListener('change', (_) => - zone.run(() => - bindAssignableProps.forEach((propAndExp) => - propAndExp[1].assign(scope.context, jsNode[propAndExp[0]]) - ) - ) - ); + _bindAssignablePropsOn.forEach((String eventName) => + node.addEventListener(eventName, (_) => + zone.run(() => + bindAssignableProps.forEach((propAndExp) => + propAndExp[1].assign(scope.context, jsNode[propAndExp[0]]))))); } if (onEvents.isNotEmpty) { diff --git a/test/core/annotation_src_spec.dart b/test/core/annotation_src_spec.dart index 8c5380bb2..28aa3e44c 100644 --- a/test/core/annotation_src_spec.dart +++ b/test/core/annotation_src_spec.dart @@ -44,7 +44,8 @@ void main() => describe('annotations', () { visibility: Directive.LOCAL_VISIBILITY, exportExpressions: [], exportExpressionAttrs: [], - useShadowDom: true + useShadowDom: true, + updateBoundElementPropertiesOnEvents: [] ); // Check that no fields are null @@ -64,7 +65,8 @@ void main() => describe('annotations', () { module: (i){}, visibility: Directive.LOCAL_VISIBILITY, exportExpressions: [], - exportExpressionAttrs: [] + exportExpressionAttrs: [], + updateBoundElementPropertiesOnEvents: [] ); // Check that no fields are null diff --git a/test/core_dom/web_components_spec.dart b/test/core_dom/web_components_spec.dart index 6cad2a403..bdb68278a 100644 --- a/test/core_dom/web_components_spec.dart +++ b/test/core_dom/web_components_spec.dart @@ -43,6 +43,10 @@ main() { beforeEach((TestBed tb) { _ = tb; }); + + beforeEachModule((Module m) { + m.bind(TestsTwoWayCustom); + }); it('should create custom elements', () { registerElement('tests-basic', {'prop-x': 6}); @@ -89,5 +93,26 @@ main() { expect(_.rootScope.context['x']).toEqual(6); }); + + it('should support two-way bindings for components that trigger a defined event', () { + registerElement('tests-twoway-custom', {}); + compileAndUpgrade(''); + + setCustomProp('prop', 6); + _.rootElement.dispatchEvent(new Event.eventType('CustomEvent', 'x-change')); + + expect(_.rootScope.context['x']).toEqual(6); + + // The change event should not cause an update + setCustomProp('prop', 7); + _.rootElement.dispatchEvent(new Event.eventType('CustomEvent', 'change')); + expect(_.rootScope.context['x']).toEqual(6); + }); }); } + +@Decorator( + selector: 'tests-twoway-custom', + updateBoundElementPropertiesOnEvents: const ['x-change'] +) +class TestsTwoWayCustom {}