Dynamic component support for Sheet
Which scope/s are relevant/related to the feature request?
sheet
Information
It would be great if we can use dynamic component in the sheet component, just like we can do in the dialog component - after all, sheet is just another form of dialog!
Describe any alternatives/workarounds you're currently using
No response
I would be willing to submit a PR to fix this issue
- [x] Yes
- [ ] No
I've got a working implementation. Since I suppose there should be some magnitude of rework/refactor to the sheet component to be done in order to get this properly implemented, I'm posting the code here rather than creating a PR for now. Please feel free to grab my code, and let me know if I can help to put a PR together.
hlm-sheet-dynamic-content.component.ts
Ideally we should reuse the HlmSheetContentComponent, but it requires injection of ExposesStateProvider and ExposesSideProvider. Some refactors have to be done before we can reuse it. For now we just create a copy without the injections for dynamic use.
import { Component, ElementRef, Renderer2, computed, effect, inject, input } from '@angular/core';
import { lucideX } from '@ng-icons/lucide';
import { BrnSheetCloseDirective } from '@spartan-ng/brain/sheet';
import { hlm } from '@spartan-ng/ui-core';
import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm';
import type { ClassValue } from 'clsx';
import { HlmSheetCloseDirective } from './hlm-sheet-close.directive';
import { BrnDialogRef, injectBrnDialogContext } from '@spartan-ng/brain/dialog';
import { sheetVariants } from './hlm-sheet-content.component';
import { NgComponentOutlet } from '@angular/common';
@Component({
selector: 'hlm-sheet-dynamic-content',
standalone: true,
imports: [NgComponentOutlet, HlmSheetCloseDirective, BrnSheetCloseDirective, HlmIconComponent],
providers: [provideIcons({ lucideX })],
host: {
'[class]': '_computedClass()',
'[attr.data-state]': 'state()',
},
template: `
@if (component) {
<ng-container [ngComponentOutlet]="component" />
} @else {
<ng-content />
}
<button brnSheetClose hlm>
<span class="sr-only">Close</span>
<hlm-icon class="flex h-4 w-4" size="100%" name="lucideX" />
</button>
`,
})
export class HlmSheetDynamicContentComponent {
private readonly _dialogRef = inject(BrnDialogRef);
private readonly _dialogContext = injectBrnDialogContext({ optional: true });
public readonly component = this._dialogContext?.$component;
public readonly state = computed(() => this._dialogRef?.state() ?? 'closed');
private readonly _renderer = inject(Renderer2);
private readonly _element = inject(ElementRef);
constructor() {
effect(() => {
this._renderer.setAttribute(this._element.nativeElement, 'data-state', this.state());
});
}
public readonly userClass = input<ClassValue>('', { alias: 'class' });
// dirty: need to figure out how to pass the side option here
protected _computedClass = computed(() => hlm(sheetVariants({ side: (this._dialogRef as any)._options.side }), this.userClass()));
}
hlm-sheet.service.ts
import type { ComponentType } from '@angular/cdk/portal';
import { Injectable, type TemplateRef, inject } from '@angular/core';
import {
type BrnDialogOptions,
BrnDialogService,
DEFAULT_BRN_DIALOG_OPTIONS,
cssClassesToArray,
} from '@spartan-ng/brain/dialog';
import { HlmSheetContentComponent } from './hlm-sheet-content.component';
import { hlmSheetOverlayClass } from './hlm-sheet-overlay.directive';
import { HlmDialogOptions } from '@spartan-ng/ui-dialog-helm';
import { OverlayPositionBuilder } from '@angular/cdk/overlay';
import { HlmSheetDynamicContentComponent } from './hlm-sheet-dynamic-content.component';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HlmSheetOptions<DialogContext = any> = HlmDialogOptions<DialogContext> & {
side: 'top' | 'bottom' | 'left' | 'right';
};
@Injectable({
providedIn: 'root',
})
export class HlmSheetService {
private readonly _brnDialogService = inject(BrnDialogService);
private readonly _positionBuilder = inject(OverlayPositionBuilder);
private getPositionStrategy(side?: 'top' | 'bottom' | 'left' | 'right') {
switch (side) {
case 'top':
return this._positionBuilder.global().top();
case 'bottom':
return this._positionBuilder.global().bottom();
case 'left':
return this._positionBuilder.global().left();
case 'right':
default:
return this._positionBuilder.global().right();
}
}
public open(component: ComponentType<unknown> | TemplateRef<unknown>, options?: Partial<HlmSheetOptions>) {
const mergedOptions = {
...DEFAULT_BRN_DIALOG_OPTIONS,
closeDelay: 100,
positionStrategy: this.getPositionStrategy(options?.side),
...(options ?? {}),
backdropClass: cssClassesToArray(`${hlmSheetOverlayClass} ${options?.backdropClass ?? ''}`),
context: { ...options?.context, $component: component, $dynamicComponentClass: options?.contentClass },
};
return this._brnDialogService.open(HlmSheetDynamicContentComponent, undefined, mergedOptions.context, mergedOptions);
}
}
Usage:
private readonly _sheets = inject(HlmSheetService);
const sheetRef = this._sheets.open(MySheetComponent, {
contentClass: "!w-[540px]",
side: 'left'
});
sheetRef.closed$.subscribe((result) => {
// ...
});