From 3e06fc2235f2a8d6c072e57770dcb30c4babe814 Mon Sep 17 00:00:00 2001 From: markuczy <129275100+markuczy@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:37:10 +0200 Subject: [PATCH] feat: remote component - slot - slot owner binding (#352) * feat: slot passes down inputs and outputs * feat: update remote components inputs and outputs * feat: change type of emitters * fix: review fix * feat: removed unsubscribe and added examples * feat: enchance output example --- .../src/lib/components/slot/slot.component.ts | 132 +++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/libs/angular-remote-components/src/lib/components/slot/slot.component.ts b/libs/angular-remote-components/src/lib/components/slot/slot.component.ts index 23197587..0fd370b1 100644 --- a/libs/angular-remote-components/src/lib/components/slot/slot.component.ts +++ b/libs/angular-remote-components/src/lib/components/slot/slot.component.ts @@ -1,6 +1,8 @@ import { Component, + ComponentRef, ContentChild, + EventEmitter, Inject, Input, OnDestroy, @@ -25,6 +27,99 @@ export class SlotComponent implements OnInit, OnDestroy { @Input() name!: string + private _assignedComponents$ = new BehaviorSubject<(ComponentRef | HTMLElement)[]>([]) + + /** + * Inputs to be passed to components inside a slot. + * + * @example + * + * ## Slot usage + * ``` + * + * + * ``` + * + * ## Remote component definition + * ``` + * export class MyRemoteComponent: { + * ⁣@Input() header: string = '' + * } + * ``` + * + * ## Remote component template + * ``` + *

myInput = {{header}}

+ * ``` + */ + private _inputs$ = new BehaviorSubject>({}) + @Input() + get inputs(): Record { + return this._inputs$.getValue() + } + set inputs(value: Record) { + this._inputs$.next({ + ...this._inputs$.getValue(), + ...value, + }) + } + + /** + * Outputs to be passed to components inside a slot as EventEmitters. It is important that the output property is annotated with ⁣@Input(). + * + * @example + * + * ## Component with slot in a template + * ``` + * ⁣@Component({ + * selector: 'my-component', + * templateUrl: './my-component.component.html', + * }) + * export class MyComponent { + * buttonClickedEmitter = new EventEmitter() + * constructor() { + * this.buttonClickedEmitter.subscribe((msg) => { + * console.log(msg) + * }) + * } + * } + * ``` + * + * ## Slot usage in my-component.component.html + * ``` + * + * + * ``` + * + * ## Remote component definition + * ``` + * export class MyRemoteComponent: { + * ⁣@Input() buttonClicked = EventEmitter() + * onButtonClick() { + * buttonClicked.emit('payload') + * } + * } + * ``` + * + * ## Remote component template + * ``` + * + * ``` + */ + private _outputs$ = new BehaviorSubject>>({}) + @Input() + get outputs(): Record> { + return this._outputs$.getValue() + } + set outputs(value: Record>) { + this._outputs$.next({ + ...this._outputs$.getValue(), + ...value, + }) + } + + updateDataSub: Subscription | undefined + _viewContainers$ = new BehaviorSubject | undefined>(undefined) @ViewChildren('slot', { read: ViewContainerRef }) set viewContainers(value: QueryList) { @@ -40,6 +135,13 @@ export class SlotComponent implements OnInit, OnDestroy { ngOnInit(): void { this.components$ = this.slotService.getComponentsForSlot(this.name) + combineLatest([this._assignedComponents$, this._inputs$, this._outputs$]).subscribe( + ([components, inputs, outputs]) => { + components.forEach((component) => { + this.updateComponentData(component, inputs, outputs) + }) + } + ) this.subscription = combineLatest([this._viewContainers$, this.components$]).subscribe( ([viewContainers, components]) => { if (viewContainers && viewContainers.length === components.length) { @@ -49,7 +151,8 @@ export class SlotComponent implements OnInit, OnDestroy { Promise.resolve(componentInfo.componentType), Promise.resolve(componentInfo.permissions), ]).then(([componentType, permissions]) => { - this.createComponent(componentType, componentInfo, permissions, viewContainers, i) + const component = this.createComponent(componentType, componentInfo, permissions, viewContainers, i) + if (component) this._assignedComponents$.next([...this._assignedComponents$.getValue(), component]) }) } }) @@ -64,7 +167,7 @@ export class SlotComponent implements OnInit, OnDestroy { permissions: string[], viewContainers: QueryList, i: number - ) { + ): ComponentRef | HTMLElement | undefined { const viewContainer = viewContainers.get(i) viewContainer?.clear() viewContainer?.element.nativeElement.replaceChildren() @@ -79,6 +182,7 @@ export class SlotComponent implements OnInit, OnDestroy { }) } componentRef?.changeDetectorRef.detectChanges() + return componentRef } else if ( componentInfo.remoteComponent.technology === Technologies.WebComponentModule || componentInfo.remoteComponent.technology === Technologies.WebComponentScript @@ -92,8 +196,32 @@ export class SlotComponent implements OnInit, OnDestroy { permissions: permissions, } satisfies RemoteComponentConfig viewContainer?.element.nativeElement.appendChild(element) + return element } } + + return + } + + private updateComponentData( + component: ComponentRef | HTMLElement | undefined, + inputs: Record, + outputs: Record> + ) { + this.setProps(component, inputs) + this.setProps(component, outputs) + } + + private setProps(component: ComponentRef | HTMLElement | undefined, props: Record) { + if (!component) return + + Object.entries(props).map(([name, value]) => { + if (component instanceof HTMLElement) { + ;(component as any)[name] = value + } else { + component.setInput(name, value) + } + }) } ngOnDestroy(): void {