diff --git a/src/app/shared/abstract-component-loader/abstract-component-loader.component.html b/src/app/shared/abstract-component-loader/abstract-component-loader.component.html
new file mode 100644
index 0000000000..2035dbadd0
--- /dev/null
+++ b/src/app/shared/abstract-component-loader/abstract-component-loader.component.html
@@ -0,0 +1 @@
+
diff --git a/src/app/shared/abstract-component-loader/abstract-component-loader.component.ts b/src/app/shared/abstract-component-loader/abstract-component-loader.component.ts
new file mode 100644
index 0000000000..6f934f5b4b
--- /dev/null
+++ b/src/app/shared/abstract-component-loader/abstract-component-loader.component.ts
@@ -0,0 +1,113 @@
+import { Component, ComponentRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
+import { Context } from '../../core/shared/context.model';
+import { ThemeService } from '../theme-support/theme.service';
+import { GenericConstructor } from '../../core/shared/generic-constructor';
+import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
+import { Subscription } from 'rxjs';
+import { DynamicComponentLoaderDirective } from './dynamic-component-loader.directive';
+
+@Component({
+ selector: 'ds-abstract-component-loader',
+ templateUrl: './abstract-component-loader.component.html',
+})
+export abstract class AbstractComponentLoaderComponent implements OnInit, OnChanges, OnDestroy {
+
+ /**
+ * The optional context
+ */
+ @Input() context: Context;
+
+ /**
+ * Directive to determine where the dynamic child component is located
+ */
+ @ViewChild(DynamicComponentLoaderDirective, { static: true }) componentDirective: DynamicComponentLoaderDirective;
+
+ /**
+ * The reference to the dynamic component
+ */
+ protected compRef: ComponentRef;
+
+ /**
+ * Array to track all subscriptions and unsubscribe them onDestroy
+ */
+ protected subs: Subscription[] = [];
+
+ protected inAndOutputNames: (keyof this)[] = [
+ 'context',
+ ];
+
+ constructor(
+ protected themeService: ThemeService,
+ ) {
+ }
+
+ /**
+ * Set up the dynamic child component
+ */
+ ngOnInit(): void {
+ this.instantiateComponent();
+ }
+
+ /**
+ * 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(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(): void {
+ this.subs
+ .filter((subscription: Subscription) => hasValue(subscription))
+ .forEach((subscription: Subscription) => subscription.unsubscribe());
+ }
+
+ public instantiateComponent(changes?: SimpleChanges): void {
+ const component: GenericConstructor = this.getComponent();
+
+ const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef;
+ viewContainerRef.clear();
+
+ this.compRef = viewContainerRef.createComponent(
+ component, {
+ index: 0,
+ injector: undefined,
+ },
+ );
+
+ if (hasValue(changes)) {
+ this.ngOnChanges(changes);
+ } else {
+ this.connectInputsAndOutputs();
+ }
+ }
+
+ /**
+ * Fetch the component depending on the item's entity type, metadata representation type and context
+ */
+ public abstract getComponent(): GenericConstructor;
+
+ /**
+ * 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];
+ });
+ }
+ }
+
+}
diff --git a/src/app/shared/abstract-component-loader/dynamic-component-loader.directive.ts b/src/app/shared/abstract-component-loader/dynamic-component-loader.directive.ts
new file mode 100644
index 0000000000..8c77df1cdb
--- /dev/null
+++ b/src/app/shared/abstract-component-loader/dynamic-component-loader.directive.ts
@@ -0,0 +1,16 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+/**
+ * Directive used as a hook to know where to inject the dynamic loaded component
+ */
+@Directive({
+ selector: '[dsDynamicComponentLoader]'
+})
+export class DynamicComponentLoaderDirective {
+
+ constructor(
+ public viewContainerRef: ViewContainerRef,
+ ) {
+ }
+
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 0f7871f7f9..2f9d3317fb 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -284,6 +284,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
+import { DynamicComponentLoaderDirective } from './abstract-component-loader/dynamic-component-loader.directive';
const MODULES = [
CommonModule,
@@ -491,6 +492,7 @@ const DIRECTIVES = [
MetadataFieldValidator,
HoverClassDirective,
ContextHelpDirective,
+ DynamicComponentLoaderDirective,
];
@NgModule({