Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

feat(web components): Support custom events for element property binding #1453

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions lib/core/annotation_src.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,28 @@ abstract class Directive {
*/
final List<String> 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<String> updateBoundElementPropertiesOnEvents;

const Directive({
this.selector,
this.children,
this.visibility,
this.module,
this.map: const {},
this.exportExpressions: const [],
this.exportExpressionAttrs: const []
this.exportExpressionAttrs: const [],
this.updateBoundElementPropertiesOnEvents
});

toString() => selector;
Expand Down Expand Up @@ -315,8 +329,9 @@ class Component extends Directive {
exportExpressions,
exportExpressionAttrs,
this.useShadowDom,
this.useNgBaseCss: true})
: _cssUrls = cssUrl,
this.useNgBaseCss: true,
updateBoundElementPropertiesOnEvents
}) : _cssUrls = cssUrl,
_applyAuthorStyles = applyAuthorStyles,
_resetStyleInheritance = resetStyleInheritance,
super(selector: selector,
Expand All @@ -325,7 +340,8 @@ class Component extends Directive {
map: map,
module: module,
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs);
exportExpressionAttrs: exportExpressionAttrs,
updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents);

List<String> get cssUrls => _cssUrls == null ?
const [] :
Expand All @@ -346,7 +362,8 @@ class Component extends Directive {
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs,
useShadowDom: useShadowDom,
useNgBaseCss: useNgBaseCss);
useNgBaseCss: useNgBaseCss,
updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents);
}

/**
Expand All @@ -369,14 +386,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(
Expand All @@ -386,7 +405,8 @@ class Decorator extends Directive {
selector: selector,
visibility: visibility,
exportExpressions: exportExpressions,
exportExpressionAttrs: exportExpressionAttrs);
exportExpressionAttrs: exportExpressionAttrs,
updateBoundElementPropertiesOnEvents: updateBoundElementPropertiesOnEvents);
}

/**
Expand Down
26 changes: 19 additions & 7 deletions lib/core_dom/element_binder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ class ElementBinder {
bool get shouldCompileChildren =>
childMode == Directive.COMPILE_CHILDREN;

List<String> _bindAssignablePropsOnCache;
List<String> get _bindAssignablePropsOn {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC, when do we call bind multiple times on a single ElementBinder (otherwise the cache is not necessary)?

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<DirectiveRef> get _usableDirectiveRefs {
if (_directiveCache != null) return _directiveCache;
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions test/core/annotation_src_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ void main() => describe('annotations', () {
visibility: Directive.LOCAL_VISIBILITY,
exportExpressions: [],
exportExpressionAttrs: [],
useShadowDom: true
useShadowDom: true,
updateBoundElementPropertiesOnEvents: []
);

// Check that no fields are null
Expand All @@ -66,7 +67,8 @@ void main() => describe('annotations', () {
module: (i){},
visibility: Directive.LOCAL_VISIBILITY,
exportExpressions: [],
exportExpressionAttrs: []
exportExpressionAttrs: [],
updateBoundElementPropertiesOnEvents: []
);

// Check that no fields are null
Expand Down
25 changes: 25 additions & 0 deletions test/core_dom/web_components_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ main() {
beforeEach((TestBed tb) {
_ = tb;
});

beforeEachModule((Module m) {
m.bind(TestsTwoWayCustom);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC, why add to each module?

});

it('should create custom elements', () {
registerElement('tests-basic', {'prop-x': 6});
Expand Down Expand Up @@ -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('<tests-twoway-custom bind-prop="x"></tests-twoway-custom>');

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 {}