diff --git a/packages/elements/src/component-factory-strategy.ts b/packages/elements/src/component-factory-strategy.ts index 3e5fcc5212..799cfba683 100644 --- a/packages/elements/src/component-factory-strategy.ts +++ b/packages/elements/src/component-factory-strategy.ts @@ -7,8 +7,8 @@ */ import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; -import {merge, Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {merge, Observable, ReplaySubject} from 'rxjs'; +import {map, switchMap} from 'rxjs/operators'; import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy'; import {extractProjectableNodes} from './extract-projectable-nodes'; @@ -43,9 +43,11 @@ export class ComponentNgElementStrategyFactory implements NgElementStrategyFacto * @publicApi */ export class ComponentNgElementStrategy implements NgElementStrategy { + // Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs. + private eventEmitters = new ReplaySubject[]>(1); + /** Merged stream of the component's output events. */ - // TODO(issue/24571): remove '!'. - events!: Observable; + readonly events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters))); /** Reference to the component that was created on connect. */ private componentRef: ComponentRef|null = null; @@ -187,12 +189,13 @@ export class ComponentNgElementStrategy implements NgElementStrategy { /** Sets up listeners for the component's outputs so that the events stream emits the events. */ protected initializeOutputs(componentRef: ComponentRef): void { - const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => { - const emitter: EventEmitter = componentRef.instance[propName]; - return emitter.pipe(map(value => ({name: templateName, value}))); - }); + const eventEmitters: Observable[] = + this.componentFactory.outputs.map(({propName, templateName}) => { + const emitter: EventEmitter = componentRef.instance[propName]; + return emitter.pipe(map(value => ({name: templateName, value}))); + }); - this.events = merge(...eventEmitters); + this.eventEmitters.next(eventEmitters); } /** Calls ngOnChanges with all the inputs that have changed since the last call. */ diff --git a/packages/elements/src/create-custom-element.ts b/packages/elements/src/create-custom-element.ts index 53b55890fe..be3c82e9b3 100644 --- a/packages/elements/src/create-custom-element.ts +++ b/packages/elements/src/create-custom-element.ts @@ -187,13 +187,13 @@ export function createCustomElement

( } connectedCallback(): void { - this.ngElementStrategy.connect(this); - // Listen for events from the strategy and dispatch them as custom events this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => { const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value); this.dispatchEvent(customEvent); }); + + this.ngElementStrategy.connect(this); } disconnectedCallback(): void { diff --git a/packages/elements/test/component-factory-strategy_spec.ts b/packages/elements/test/component-factory-strategy_spec.ts index c05f6a08a5..53c9fbd0fc 100644 --- a/packages/elements/test/component-factory-strategy_spec.ts +++ b/packages/elements/test/component-factory-strategy_spec.ts @@ -41,6 +41,33 @@ describe('ComponentFactoryNgElementStrategy', () => { expect(strategyFactory.create(injector)).toBeTruthy(); }); + describe('before connected', () => { + it('should allow subscribing to output events', () => { + const events: NgElementStrategyEvent[] = []; + strategy.events.subscribe(e => events.push(e)); + + // No events before connecting (since `componentRef` is not even on the strategy yet). + componentRef.instance.output1.next('output-1a'); + componentRef.instance.output1.next('output-1b'); + componentRef.instance.output2.next('output-2a'); + expect(events).toEqual([]); + + // No events upon connecting (since events are not cached/played back). + strategy.connect(document.createElement('div')); + expect(events).toEqual([]); + + // Events emitted once connected. + componentRef.instance.output1.next('output-1c'); + componentRef.instance.output1.next('output-1d'); + componentRef.instance.output2.next('output-2b'); + expect(events).toEqual([ + {name: 'templateOutput1', value: 'output-1c'}, + {name: 'templateOutput1', value: 'output-1d'}, + {name: 'templateOutput2', value: 'output-2b'}, + ]); + }); + }); + describe('after connected', () => { beforeEach(() => { // Set up an initial value to make sure it is passed to the component diff --git a/packages/elements/test/create-custom-element_spec.ts b/packages/elements/test/create-custom-element_spec.ts index 231b3f3a34..1520b4faab 100644 --- a/packages/elements/test/create-custom-element_spec.ts +++ b/packages/elements/test/create-custom-element_spec.ts @@ -117,6 +117,16 @@ 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 properly set getters/setters on the element', () => { const element = new NgElementCtor(injector); element.fooFoo = 'foo-foo-value'; @@ -255,6 +265,7 @@ if (browserDetection.supportsCustomElements) { events = new Subject(); connect(element: HTMLElement): void { + this.events.next({name: 'strategy-event', value: 'connect'}); this.connectedElement = element; }