Fixed getComponent not triggering again when it's input values change by creating inputNamesDependentForComponent

Also simplified the way @Input() values are passed to their child component and how ngOnChanges is called by using setInput instead
This commit is contained in:
Alexandre Vryghem
2023-12-10 22:19:57 +01:00
parent 26e0bc81c7
commit 14d42b0d93
5 changed files with 71 additions and 36 deletions

View File

@@ -32,10 +32,22 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
*/ */
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
protected inAndOutputNames: (keyof this)[] = [ /**
* The @{@link Input}() that are used to find the matching component using {@link getComponent}. When the value of
* one of these @{@link Input}() change this loader needs to retrieve the best matching component again using the
* {@link getComponent} method.
*/
protected inputNamesDependentForComponent: (keyof this & string)[] = [
'context', 'context',
]; ];
protected inputNames: (keyof this & string)[] = [
'context',
];
protected outputNames: (keyof this & string)[] = [
];
constructor( constructor(
protected themeService: ThemeService, protected themeService: ThemeService,
) { ) {
@@ -45,7 +57,9 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
* Set up the dynamic child component * Set up the dynamic child component
*/ */
ngOnInit(): void { ngOnInit(): void {
this.instantiateComponent(); if (hasNoValue(this.compRef)) {
this.instantiateComponent();
}
} }
/** /**
@@ -55,14 +69,14 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
if (hasNoValue(this.compRef)) { if (hasNoValue(this.compRef)) {
// sometimes the component has not been initialized yet, so it first needs to be initialized // sometimes the component has not been initialized yet, so it first needs to be initialized
// before being called again // before being called again
this.instantiateComponent(changes); this.instantiateComponent();
} else { } else {
// if an input or output has changed if (this.inputNamesDependentForComponent.some((name: any) => hasValue(changes[name]) && changes[name].previousValue !== changes[name].currentValue)) {
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) { // Recreate the component when the @Input()s used by getComponent() aren't up-to-date anymore
this.destroyComponentInstance();
this.instantiateComponent();
} else {
this.connectInputsAndOutputs(); this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
} }
} }
} }
@@ -73,7 +87,10 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
.forEach((subscription: Subscription) => subscription.unsubscribe()); .forEach((subscription: Subscription) => subscription.unsubscribe());
} }
public instantiateComponent(changes?: SimpleChanges): void { /**
* Creates the component and connects the @Input() & @Output() from the ThemedComponent to its child Component.
*/
public instantiateComponent(): void {
const component: GenericConstructor<T> = this.getComponent(); const component: GenericConstructor<T> = this.getComponent();
const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef; const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef;
@@ -86,10 +103,16 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
}, },
); );
if (hasValue(changes)) { this.connectInputsAndOutputs();
this.ngOnChanges(changes); }
} else {
this.connectInputsAndOutputs(); /**
* Destroys the themed component and calls it's `ngOnDestroy`
*/
public destroyComponentInstance(): void {
if (hasValue(this.compRef)) {
this.compRef.destroy();
this.compRef = null;
} }
} }
@@ -99,12 +122,18 @@ export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnC
public abstract getComponent(): GenericConstructor<T>; public abstract getComponent(): GenericConstructor<T>;
/** /**
* Connect the in and outputs of this component to the dynamic component, * Connect the inputs and outputs of this component to the dynamic component,
* to ensure they're in sync * to ensure they're in sync
*/ */
protected connectInputsAndOutputs(): void { protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) { if (isNotEmpty(this.inputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => { this.inputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => {
// Using setInput will automatically trigger the ngOnChanges
this.compRef.setInput(name, this[name]);
});
}
if (isNotEmpty(this.outputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.outputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => {
this.compRef.instance[name] = this[name]; this.compRef.instance[name] = this[name];
}); });
} }

View File

@@ -24,8 +24,8 @@ export class MetadataRepresentationLoaderComponent extends AbstractComponentLoad
*/ */
@Input() mdRepresentation: MetadataRepresentation; @Input() mdRepresentation: MetadataRepresentation;
protected inAndOutputNames: (keyof this)[] = [ protected inputNames: (keyof this & string)[] = [
...this.inAndOutputNames, ...this.inputNames,
'mdRepresentation', 'mdRepresentation',
]; ];

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, } from '@angular/core'; import { Component, EventEmitter, Input, Output, } from '@angular/core';
import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator'; import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { MyDSpaceActionsResult } from '../../mydspace-actions'; import { MyDSpaceActionsResult } from '../../mydspace-actions';
@@ -16,7 +16,7 @@ import { GenericConstructor } from '../../../../core/shared/generic-constructor'
* Component for loading a ClaimedTaskAction component depending on the "option" input * Component for loading a ClaimedTaskAction component depending on the "option" input
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output * Passes on the ClaimedTask to the component and subscribes to the processCompleted output
*/ */
export class ClaimedTaskActionsLoaderComponent extends AbstractComponentLoaderComponent<ClaimedTaskActionsAbstractComponent> implements OnInit, OnChanges { export class ClaimedTaskActionsLoaderComponent extends AbstractComponentLoaderComponent<ClaimedTaskActionsAbstractComponent> {
/** /**
* The item object that belonging to the ClaimedTask object * The item object that belonging to the ClaimedTask object
*/ */
@@ -46,12 +46,16 @@ export class ClaimedTaskActionsLoaderComponent extends AbstractComponentLoaderCo
/** /**
* The list of input and output names for the dynamic component * The list of input and output names for the dynamic component
*/ */
protected inAndOutputNames: (keyof this)[] = [ protected inputNames: (keyof this & string)[] = [
...this.inAndOutputNames, ...this.inputNames,
'item', 'item',
'object', 'object',
'option', 'option',
'workflowitem', 'workflowitem',
];
protected outputNames: (keyof this & string)[] = [
...this.outputNames,
'processCompleted', 'processCompleted',
]; ];

View File

@@ -1,5 +1,4 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { ListableObject } from '../listable-object.model'; import { ListableObject } from '../listable-object.model';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
@@ -74,8 +73,8 @@ export class ListableObjectComponentLoaderComponent extends AbstractComponentLoa
/** /**
* The list of input and output names for the dynamic component * The list of input and output names for the dynamic component
*/ */
protected inAndOutputNames: (keyof this)[] = [ protected inputNames: (keyof this & string)[] = [
...this.inAndOutputNames, ...this.inputNames,
'object', 'object',
'index', 'index',
'linkType', 'linkType',
@@ -84,6 +83,10 @@ export class ListableObjectComponentLoaderComponent extends AbstractComponentLoa
'showThumbnails', 'showThumbnails',
'viewMode', 'viewMode',
'value', 'value',
];
protected outputNames: (keyof this & string)[] = [
...this.outputNames,
'contentChange', 'contentChange',
]; ];
@@ -94,17 +97,16 @@ export class ListableObjectComponentLoaderComponent extends AbstractComponentLoa
super(themeService); super(themeService);
} }
public instantiateComponent(changes?: SimpleChanges): void { public instantiateComponent(): void {
super.instantiateComponent(changes); super.instantiateComponent();
if ((this.compRef.instance as any).reloadedObject) { if ((this.compRef.instance as any).reloadedObject) {
combineLatest([ (this.compRef.instance as any).reloadedObject.pipe(
observableOf(changes), take(1),
(this.compRef.instance as any).reloadedObject.pipe(take(1)) as Observable<DSpaceObject>, ).subscribe((reloadedObject: DSpaceObject) => {
]).subscribe(([simpleChanges, reloadedObject]: [SimpleChanges, DSpaceObject]) => {
if (reloadedObject) { if (reloadedObject) {
this.compRef.destroy(); this.destroyComponentInstance();
this.object = reloadedObject; this.object = reloadedObject;
this.instantiateComponent(simpleChanges); this.instantiateComponent();
this.cdr.detectChanges(); this.cdr.detectChanges();
this.contentChange.emit(reloadedObject); this.contentChange.emit(reloadedObject);
} }

View File

@@ -24,8 +24,8 @@ export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoa
*/ */
@Input() type: string; @Input() type: string;
protected inAndOutputNames: (keyof this)[] = [ protected inputNames: (keyof this & string)[] = [
...this.inAndOutputNames, ...this.inputNames,
'type', 'type',
]; ];