Make ListableObjectComponentLoaderComponent extend AbstractComponentLoaderComponent

This commit is contained in:
Alexandre Vryghem
2023-06-10 15:45:59 +02:00
parent 2327513dd0
commit fb7afaddd0
13 changed files with 59 additions and 162 deletions

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges>
<ng-content></ng-content>

View File

@@ -11,7 +11,7 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@@ -25,7 +25,7 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
* The component for displaying a list element for an item search result on the admin search page
*/
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnInit {
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
@ViewChild('badges', { static: true }) badges: ElementRef;
@ViewChild('buttons', { static: true }) buttons: ElementRef;
@@ -46,7 +46,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
super.ngOnInit();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges class="position-absolute ml-1">
<div class="workflow-badge">

View File

@@ -18,8 +18,8 @@ import {
ItemGridElementComponent
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
DynamicComponentLoaderDirective
} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
@@ -38,7 +38,7 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
let itemRD$;
let linkService;
let object;
let themeService;
let themeService: ThemeService;
function init() {
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
@@ -55,7 +55,11 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
init();
TestBed.configureTestingModule(
{
declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
declarations: [
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
ItemGridElementComponent,
DynamicComponentLoaderDirective,
],
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),

View File

@@ -10,7 +10,7 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { Observable } from 'rxjs';
import { LinkService } from '../../../../../core/cache/builders/link.service';
@@ -38,7 +38,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
/**
* Directive used to render the dynamic component in
*/
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
/**
* The html child that contains the badges html
@@ -77,7 +77,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
this.item$.pipe(take(1)).subscribe((item: Item) => {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges class="position-absolute ml-1">
<div class="workflow-badge">

View File

@@ -20,8 +20,8 @@ import {
ItemGridElementComponent
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
DynamicComponentLoaderDirective
} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
import {
WorkflowItemSearchResult
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
@@ -45,7 +45,7 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
let itemRD$;
let linkService;
let object;
let themeService;
let themeService: ThemeService;
let supervisionOrderDataService;
function init() {
@@ -67,7 +67,11 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
init();
TestBed.configureTestingModule(
{
declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
declarations: [
WorkspaceItemSearchResultAdminWorkflowGridElementComponent,
ItemGridElementComponent,
DynamicComponentLoaderDirective,
],
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),

View File

@@ -16,9 +16,7 @@ import {
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import {
ListableObjectDirective
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
@@ -67,7 +65,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
/**
* Directive used to render the dynamic component in
*/
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
/**
* The html child that contains the badges html
@@ -102,7 +100,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
this.item$.pipe(take(1)).subscribe((item: Item) => {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(

View File

@@ -1 +0,0 @@
<ng-template dsListableObject></ng-template>

View File

@@ -8,7 +8,7 @@ import { ViewMode } from '../../../../core/shared/view-mode.model';
import {
ItemListElementComponent
} from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
import { ListableObjectDirective } from './listable-object.directive';
import { DynamicComponentLoaderDirective } from '../../../abstract-component-loader/dynamic-component-loader.directive';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { provideMockStore } from '@ngrx/store/testing';
@@ -36,7 +36,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [ListableObjectComponentLoaderComponent, ItemListElementComponent, ListableObjectDirective],
declarations: [ListableObjectComponentLoaderComponent, ItemListElementComponent, DynamicComponentLoaderDirective],
schemas: [NO_ERRORS_SCHEMA],
providers: [
provideMockStore({}),
@@ -65,7 +65,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
describe('When the component is rendered', () => {
it('should call the getListableObjectComponent function with the right types, view mode and context', () => {
expect(comp.getComponent).toHaveBeenCalledWith([testType], testViewMode, testContext);
expect(comp.getComponent).toHaveBeenCalled();
});
it('should connectInputsAndOutputs of loaded component', () => {
@@ -78,29 +78,29 @@ describe('ListableObjectComponentLoaderComponent', () => {
let reloadedObject: any;
beforeEach(() => {
spyOn((comp as any), 'instantiateComponent').and.returnValue(null);
spyOn((comp as any).contentChange, 'emit').and.returnValue(null);
spyOn(comp, 'instantiateComponent').and.returnValue(null);
spyOn(comp.contentChange, 'emit').and.returnValue(null);
listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
reloadedObject = 'object';
});
it('should re-instantiate the listable component', fakeAsync(() => {
expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
expect(comp.instantiateComponent).not.toHaveBeenCalled();
(listableComponent as any).reloadedObject.emit(reloadedObject);
listableComponent.reloadedObject.emit(reloadedObject);
tick(200);
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject, undefined);
expect(comp.instantiateComponent).toHaveBeenCalledWith(undefined);
}));
it('should re-emit it as a contentChange', fakeAsync(() => {
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
expect(comp.contentChange.emit).not.toHaveBeenCalled();
(listableComponent as any).reloadedObject.emit(reloadedObject);
listableComponent.reloadedObject.emit(reloadedObject);
tick(200);
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
expect(comp.contentChange.emit).toHaveBeenCalledWith(reloadedObject);
}));
});

View File

@@ -1,40 +1,26 @@
import {
ChangeDetectorRef,
Component,
ComponentRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Subscription, combineLatest, of as observableOf, Observable } from 'rxjs';
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { take } from 'rxjs/operators';
import { ListableObject } from '../listable-object.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { Context } from '../../../../core/shared/context.model';
import { Context } from 'src/app/core/shared/context.model';
import { getListableObjectComponent } from './listable-object.decorator';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from './listable-object.directive';
import { CollectionElementLinkType } from '../../collection-element-link.type';
import { hasValue, isNotEmpty, hasNoValue } from '../../../empty.util';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { ThemeService } from '../../../theme-support/theme.service';
import { AbstractComponentLoaderComponent } from '../../../abstract-component-loader/abstract-component-loader.component';
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
@Component({
selector: 'ds-listable-object-component-loader',
styleUrls: ['./listable-object-component-loader.component.scss'],
templateUrl: './listable-object-component-loader.component.html'
templateUrl: '../../../abstract-component-loader/abstract-component-loader.component.html',
})
/**
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
*/
export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges, OnDestroy {
export class ListableObjectComponentLoaderComponent extends AbstractComponentLoaderComponent<Component> {
/**
* The item or metadata to determine the component for
*/
@@ -80,99 +66,36 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
*/
@Input() value: string;
/**
* Directive hook used to place the dynamic child component
*/
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
/**
* Emit when the listable object has been reloaded.
*/
@Output() contentChange = new EventEmitter<ListableObject>();
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The reference to the dynamic component
*/
protected compRef: ComponentRef<Component>;
/**
* The list of input and output names for the dynamic component
*/
protected inAndOutputNames: string[] = [
protected inAndOutputNames: (keyof this)[] = [
...this.inAndOutputNames,
'object',
'index',
'linkType',
'listID',
'showLabel',
'showThumbnails',
'context',
'viewMode',
'value',
'hideBadges',
'contentChange',
];
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
constructor(
protected themeService: ThemeService,
protected cdr: ChangeDetectorRef,
) {
super(themeService);
}
/**
* Setup the dynamic child component
*/
ngOnInit(): void {
this.instantiateComponent(this.object);
}
/**
* Whenever the inputs change, update the inputs of the dynamic component
*/
ngOnChanges(changes: SimpleChanges): void {
if (hasNoValue(this.compRef)) {
// sometimes the component has not been initialized yet, so it first needs to be initialized
// before being called again
this.instantiateComponent(this.object, changes);
} else {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
}
}
}
ngOnDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
private instantiateComponent(object: ListableObject, changes?: SimpleChanges): void {
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
viewContainerRef.clear();
this.compRef = viewContainerRef.createComponent(
component, {
index: 0,
injector: undefined
}
);
if (hasValue(changes)) {
this.ngOnChanges(changes);
} else {
this.connectInputsAndOutputs();
}
public instantiateComponent(changes?: SimpleChanges): void {
super.instantiateComponent(changes);
if ((this.compRef.instance as any).reloadedObject) {
combineLatest([
observableOf(changes),
@@ -181,7 +104,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
if (reloadedObject) {
this.compRef.destroy();
this.object = reloadedObject;
this.instantiateComponent(reloadedObject, simpleChanges);
this.instantiateComponent(simpleChanges);
this.cdr.detectChanges();
this.contentChange.emit(reloadedObject);
}
@@ -189,26 +112,8 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
}
}
/**
* Fetch the component depending on the item's entity type, view mode and context
* @returns {GenericConstructor<Component>}
*/
getComponent(renderTypes: (string | GenericConstructor<ListableObject>)[],
viewMode: ViewMode,
context: Context): GenericConstructor<Component> {
return getListableObjectComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
}
/**
* Connect the in and outputs of this component to the dynamic component,
* to ensure they're in sync
*/
protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
this.compRef.instance[name] = this[name];
});
}
public getComponent(): GenericConstructor<Component> {
return getListableObjectComponent(this.object.getRenderTypes(), this.viewMode, this.context, this.themeService.getThemeName());
}
}

View File

@@ -1,11 +0,0 @@
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dsListableObject]',
})
/**
* Directive used as a hook to know where to inject the dynamic listable object component
*/
export class ListableObjectDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

View File

@@ -177,7 +177,6 @@ import {
import {
ItemSearchResultListElementComponent
} from './object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { ListableObjectDirective } from './object-collection/shared/listable-object/listable-object.directive';
import {
ItemMetadataRepresentationListElementComponent
} from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
@@ -484,7 +483,6 @@ const DIRECTIVES = [
AutoFocusDirective,
RoleDirective,
MetadataRepresentationDirective,
ListableObjectDirective,
ClaimedTaskActionsDirective,
FileValueAccessorDirective,
FileValidator,