diff --git a/src/app/shared/theme-support/themed.component.spec.ts b/src/app/shared/theme-support/themed.component.spec.ts
index a0cead6dc5c..e38bf337fe5 100644
--- a/src/app/shared/theme-support/themed.component.spec.ts
+++ b/src/app/shared/theme-support/themed.component.spec.ts
@@ -85,6 +85,54 @@ describe('ThemedComponent', () => {
expect(component.usedTheme).toEqual('custom');
});
}));
+
+ describe('it checks for ngContent and', () => {
+
+ it('returns all child nodes when selector is *', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '12';
+ const result = (component as any).getNgContent(element, ['*']);
+ expect(result.length).toBe(1);
+ expect(result[0].length).toBe(2);
+ expect(result[0][0].textContent).toBe('1');
+ expect(result[0][1].textContent).toBe('2');
+ });
+
+ it('returns nodes matching specific selector', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '12';
+ const result = (component as any).getNgContent(element, ['.match']);
+ expect(result.length).toBe(1);
+ expect(result[0].length).toBe(1);
+ expect(result[0][0].textContent).toBe('1');
+ });
+
+ it('removes selected elements from the DOM', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '12';
+ (component as any).getNgContent(element, ['.match']);
+ expect(element.querySelectorAll('.match').length).toBe(0);
+ });
+
+ it('returns empty array when no elements match the selector', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '12';
+ const result = (component as any).getNgContent(element, ['.no-match']);
+ expect(result.length).toBe(1);
+ expect(result[0].length).toBe(0);
+ });
+
+ it('handles multiple selectors', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '12';
+ const result = (component as any).getNgContent(element, ['.match1', '.match2']);
+ expect(result.length).toBe(2);
+ expect(result[0].length).toBe(1);
+ expect(result[0][0].textContent).toBe('1');
+ expect(result[1].length).toBe(1);
+ expect(result[1][0].textContent).toBe('2');
+ });
+ });
});
describe('when the current theme doesn\'t match a themed component', () => {
diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts
index 84b1163d7b6..fd9aa592673 100644
--- a/src/app/shared/theme-support/themed.component.ts
+++ b/src/app/shared/theme-support/themed.component.ts
@@ -5,6 +5,8 @@ import {
ComponentRef,
ElementRef,
HostBinding,
+ inject,
+ Injector,
OnChanges,
OnDestroy,
SimpleChanges,
@@ -39,6 +41,7 @@ import { ThemeService } from './theme.service';
selector: 'ds-themed',
styleUrls: ['./themed.component.scss'],
templateUrl: './themed.component.html',
+ standalone: true,
})
export abstract class ThemedComponent implements AfterViewInit, OnDestroy, OnChanges {
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
@@ -57,6 +60,8 @@ export abstract class ThemedComponent implements AfterViewInit
protected inAndOutputNames: (keyof T & keyof this)[] = [];
+ protected injector = inject(Injector);
+
/**
* A data attribute on the ThemedComponent to indicate which theme the rendered component came from.
*/
@@ -133,8 +138,15 @@ export abstract class ThemedComponent implements AfterViewInit
this.lazyLoadSub = this.lazyLoadObs.subscribe(([simpleChanges, constructor]: [SimpleChanges, GenericConstructor]) => {
this.destroyComponentInstance();
+
+ // Get the ng-content selectors from the component metadata
+ const ngContentSelectors = this.getComponentNgContentSelectors(constructor);
+ const projectableNodes = this.getNgContent(this.themedElementContent.nativeElement, ngContentSelectors);
+
+ // Create component without using ComponentFactoryResolver
this.compRef = this.vcr.createComponent(constructor, {
- projectableNodes: [this.themedElementContent.nativeElement.childNodes],
+ injector: this.injector,
+ projectableNodes: projectableNodes,
});
if (hasValue(simpleChanges)) {
this.ngOnChanges(simpleChanges);
@@ -147,6 +159,17 @@ export abstract class ThemedComponent implements AfterViewInit
});
}
+ /**
+ * Retrieves ng-content selectors from a component type
+ */
+ private getComponentNgContentSelectors(componentType: GenericConstructor): string[] {
+ const componentDef = (componentType as any).ɵcmp;
+ if (componentDef && componentDef.ngContentSelectors) {
+ return componentDef.ngContentSelectors;
+ }
+ return ['*']; // Default fallback
+ }
+
protected destroyComponentInstance(): void {
if (hasValue(this.compRef)) {
this.compRef.destroy();
@@ -157,6 +180,28 @@ export abstract class ThemedComponent implements AfterViewInit
}
}
+ /**
+ * Extracts and returns the content nodes from the given element based on the provided ng-content selectors.
+ *
+ * @param {Element} element - The DOM element from which to extract content nodes.
+ * @param {string[]} ngSelectors - An array of ng-content selectors to match against the element's children.
+ * @returns {Node[][]} - A 2D array where each sub-array contains the nodes matching a specific selector.
+ */
+ protected getNgContent(element: Element, ngSelectors: string[]): Node[][] {
+ return ngSelectors.map(selector => {
+ if (selector === '*') {
+ // If the selector is '*', return all child nodes of the element.
+ return Array.from(element.childNodes);
+ } else {
+ // Otherwise, select and return the nodes matching the specific selector.
+ const selectedElements = Array.from(element.querySelectorAll(selector));
+ // Remove the selected elements from the DOM.
+ selectedElements.forEach(e => e.remove());
+ return selectedElements;
+ }
+ });
+ }
+
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) => {