fix(elements): fire custom element output events during component initialization (#37570)
Previously, event listeners for component output events attached on an Angular custom element before inserting it into the DOM (i.e. before instantiating the underlying component) didn't fire for events emitted during initialization lifecycle hooks, such as `ngAfterContentInit`, `ngAfterViewInit`, `ngOnChanges` (initial call) and `ngOnInit`. The reason was that `NgElementImpl` [subscribed to events][1] _after_ calling [ngElementStrategy#connect()][2], which is where the [initial change detection][3] takes place (running the initialization lifecycle hooks). This commit fixes this by: 1. Ensuring `ComponentNgElementStrategy#events` is defined and available for subscribing to, even before instantiating the component. 2. Changing `NgElementImpl` to subscribe to `NgElementStrategy#events` (if available) before calling `NgElementStrategy#connect()` (which initializes the component instance) if available. 3. Falling back to the old behavior (subscribing to `events` after calling `connect()` for strategies that do not initialize `events` before their `connect()` is run). NOTE: By falling back to the old behavior when `NgElementStrategy#events` is not initialized before calling `NgElementStrategy#connect()`, we avoid breaking existing custom `NgElementStrategy` implementations (with @remackgeek's [ElementZoneStrategy][4] being a commonly used example). Jira issue: [FW-2010](https://angular-team.atlassian.net/browse/FW-2010) [1]:c0143cb2ab/packages/elements/src/create-custom-element.ts (L167-L170)
[2]:c0143cb2ab/packages/elements/src/create-custom-element.ts (L164)
[3]:c0143cb2ab/packages/elements/src/component-factory-strategy.ts (L158)
[4]:f1b6699495/projects/elements-zone-strategy/src/lib/element-zone-strategy.ts
Fixes #36141 PR Close #37570
This commit is contained in:

committed by
Andrew Kushnir

parent
1a1f99af37
commit
89e16ed6a5
@ -40,12 +40,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
strategyFactory = new TestStrategyFactory();
|
||||
strategy = strategyFactory.testStrategy;
|
||||
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
NgElementCtor = ElementCtor;
|
||||
|
||||
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
|
||||
// new instances of the NgElement which extends HTMLElement, as long as we define it.
|
||||
customElements.define(selector, NgElementCtor);
|
||||
NgElementCtor = createAndRegisterTestCustomElement(strategyFactory);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
});
|
||||
@ -117,6 +112,47 @@ if (browserDetection.supportsCustomElements) {
|
||||
expect(eventValue).toEqual(null);
|
||||
});
|
||||
|
||||
it('should listen to output events during initialization', () => {
|
||||
const events: string[] = [];
|
||||
|
||||
const element = new NgElementCtor(injector);
|
||||
element.addEventListener('strategy-event', evt => events.push((evt as CustomEvent).detail));
|
||||
element.connectedCallback();
|
||||
|
||||
expect(events).toEqual(['connect']);
|
||||
});
|
||||
|
||||
it('should not break if `NgElementStrategy#events` is not available before calling `NgElementStrategy#connect()`',
|
||||
() => {
|
||||
class TestStrategyWithLateEvents extends TestStrategy {
|
||||
events: Subject<NgElementStrategyEvent> = undefined!;
|
||||
|
||||
connect(element: HTMLElement): void {
|
||||
this.connectedElement = element;
|
||||
this.events = new Subject<NgElementStrategyEvent>();
|
||||
this.events.next({name: 'strategy-event', value: 'connect'});
|
||||
}
|
||||
}
|
||||
|
||||
const strategyWithLateEvents = new TestStrategyWithLateEvents();
|
||||
const capturedEvents: string[] = [];
|
||||
|
||||
const NgElementCtorWithLateEventsStrategy =
|
||||
createAndRegisterTestCustomElement({create: () => strategyWithLateEvents});
|
||||
|
||||
const element = new NgElementCtorWithLateEventsStrategy(injector);
|
||||
element.addEventListener(
|
||||
'strategy-event', evt => capturedEvents.push((evt as CustomEvent).detail));
|
||||
element.connectedCallback();
|
||||
|
||||
// The "connect" event (emitted during initialization) was missed, but things didn't break.
|
||||
expect(capturedEvents).toEqual([]);
|
||||
|
||||
// Subsequent events are still captured.
|
||||
strategyWithLateEvents.events.next({name: 'strategy-event', value: 'after-connect'});
|
||||
expect(capturedEvents).toEqual(['after-connect']);
|
||||
});
|
||||
|
||||
it('should properly set getters/setters on the element', () => {
|
||||
const element = new NgElementCtor(injector);
|
||||
element.fooFoo = 'foo-foo-value';
|
||||
@ -144,7 +180,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
|
||||
it('should capture properties set before upgrading the element', () => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -165,7 +201,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
it('should capture properties set after upgrading the element but before inserting it into the DOM',
|
||||
() => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -193,7 +229,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
it('should allow overwriting properties with attributes after upgrading the element but before inserting it into the DOM',
|
||||
() => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -219,7 +255,17 @@ if (browserDetection.supportsCustomElements) {
|
||||
});
|
||||
|
||||
// Helpers
|
||||
function createTestCustomElement() {
|
||||
function createAndRegisterTestCustomElement(strategyFactory: NgElementStrategyFactory) {
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
|
||||
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
|
||||
// new instances of the NgElement which extends HTMLElement, as long as we define it.
|
||||
customElements.define(selector, ElementCtor);
|
||||
|
||||
return ElementCtor;
|
||||
}
|
||||
|
||||
function createTestCustomElement(strategyFactory: NgElementStrategyFactory) {
|
||||
return {
|
||||
selector: `test-element-${++selectorUid}`,
|
||||
ElementCtor: createCustomElement<WithFooBar>(TestComponent, {injector, strategyFactory}),
|
||||
@ -255,6 +301,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
events = new Subject<NgElementStrategyEvent>();
|
||||
|
||||
connect(element: HTMLElement): void {
|
||||
this.events.next({name: 'strategy-event', value: 'connect'});
|
||||
this.connectedElement = element;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user