- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Dropdown Menu
- Empty
- Field
- Hover Card
- Icon
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner (Toast)
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
import { Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDialogImports } from '@spartan-ng/helm/dialog';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmLabelImports } from '@spartan-ng/helm/label';
@Component({
selector: 'spartan-dialog-preview',
imports: [HlmDialogImports, HlmLabelImports, HlmInputImports, HlmButtonImports],
template: `
<hlm-dialog>
<button id="edit-profile" hlmDialogTrigger hlmBtn variant="outline">Open Dialog</button>
<hlm-dialog-content class="sm:max-w-[425px]" *hlmDialogPortal="let ctx">
<hlm-dialog-header>
<h3 hlmDialogTitle>Edit profile</h3>
<p hlmDialogDescription>Make changes to your profile here. Click save when you're done.</p>
</hlm-dialog-header>
<div class="grid gap-4">
<div class="grid gap-3">
<label hlmLabel for="name" class="text-right">Name</label>
<input hlmInput id="name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label hlmLabel for="username" class="text-right">Username</label>
<input hlmInput id="username" value="@peduarte" class="col-span-3" />
</div>
</div>
<hlm-dialog-footer>
<button hlmBtn variant="outline" hlmDialogClose>Cancel</button>
<button hlmBtn type="submit">Save changes</button>
</hlm-dialog-footer>
</hlm-dialog-content>
</hlm-dialog>
`,
})
export class DialogPreview {}Installation
ng g @spartan-ng/cli:ui dialognx g @spartan-ng/cli:ui dialogimport { DestroyRef, ElementRef, HostAttributeToken, Injector, PLATFORM_ID, effect, inject, runInInjectionContext } from '@angular/core';
import { clsx, type ClassValue } from 'clsx';
import { isPlatformBrowser } from '@angular/common';
import { twMerge } from 'tailwind-merge';
export function hlm(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Global map to track class managers per element
const elementClassManagers = new WeakMap<HTMLElement, ElementClassManager>();
// Global mutation observer for all elements
let globalObserver: MutationObserver | null = null;
const observedElements = new Set<HTMLElement>();
interface ElementClassManager {
element: HTMLElement;
sources: Map<number, { classes: Set<string>; order: number }>;
baseClasses: Set<string>;
isUpdating: boolean;
nextOrder: number;
hasInitialized: boolean;
restoreRafId: number | null;
/** Transitions are suppressed until the first effect writes correct classes */
transitionsSuppressed: boolean;
/** Original inline transition value to restore after suppression (empty string = none was set) */
previousTransition: string;
/** Original inline transition priority to preserve !important when restoring */
previousTransitionPriority: string;
}
let sourceCounter = 0;
/**
* This function dynamically adds and removes classes for a given element without requiring
* the a class binding (e.g. `[class]="..."`) which may interfere with other class bindings.
*
* 1. This will merge the existing classes on the element with the new classes.
* 2. It will also remove any classes that were previously added by this function but are no longer present in the new classes.
* 3. Multiple calls to this function on the same element will be merged efficiently.
*/
export function classes(computed: () => ClassValue[] | string, options: ClassesOptions = {}) {
runInInjectionContext(options.injector ?? inject(Injector), () => {
const elementRef = options.elementRef ?? inject(ElementRef);
const platformId = inject(PLATFORM_ID);
const destroyRef = inject(DestroyRef);
const baseClasses = inject(new HostAttributeToken('class'), { optional: true });
const element = elementRef.nativeElement;
// Create unique identifier for this source
const sourceId = sourceCounter++;
// Get or create the class manager for this element
let manager = elementClassManagers.get(element);
if (!manager) {
// Initialize base classes from variation (host attribute 'class')
const initialBaseClasses = new Set<string>();
if (baseClasses) {
toClassList(baseClasses).forEach((cls) => initialBaseClasses.add(cls));
}
manager = {
element,
sources: new Map(),
baseClasses: initialBaseClasses,
isUpdating: false,
nextOrder: 0,
hasInitialized: false,
restoreRafId: null,
transitionsSuppressed: false,
previousTransition: '',
previousTransitionPriority: '',
};
elementClassManagers.set(element, manager);
// Setup global observer if needed and register this element
setupGlobalObserver(platformId);
observedElements.add(element);
// Suppress transitions until the first effect writes correct classes and
// the browser has painted them. This prevents CSS transition animations
// during hydration when classes change from SSR state to client state.
if (isPlatformBrowser(platformId)) {
manager.previousTransition = element.style.getPropertyValue('transition');
manager.previousTransitionPriority = element.style.getPropertyPriority('transition');
element.style.setProperty('transition', 'none', 'important');
manager.transitionsSuppressed = true;
}
}
// Assign order once at registration time
const sourceOrder = manager.nextOrder++;
function updateClasses(): void {
// Get the new classes from the computed function
const newClasses = toClassList(computed());
// Update this source's classes, keeping the original order
manager!.sources.set(sourceId, {
classes: new Set(newClasses),
order: sourceOrder,
});
// Update the element
updateElement(manager!);
// Re-enable transitions after the first effect writes correct classes.
// Deferred to next animation frame so the browser paints the class change
// with transitions disabled first, then re-enables them.
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
manager!.restoreRafId = requestAnimationFrame(() => {
manager!.restoreRafId = null;
restoreTransitionSuppression(manager!);
});
}
}
// Register cleanup with DestroyRef
destroyRef.onDestroy(() => {
if (manager!.restoreRafId !== null) {
cancelAnimationFrame(manager!.restoreRafId);
manager!.restoreRafId = null;
}
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
restoreTransitionSuppression(manager!);
}
// Remove this source from the manager
manager!.sources.delete(sourceId);
// If no more sources, clean up the manager
if (manager!.sources.size === 0) {
cleanupManager(element);
} else {
// Update element without this source's classes
updateElement(manager!);
}
});
/**
* We need this effect to track changes to the computed classes. Ideally, we would use
* afterRenderEffect here, but that doesn't run in SSR contexts, so we use a standard
* effect which works in both browser and SSR.
*/
effect(updateClasses);
});
}
function restoreTransitionSuppression(manager: ElementClassManager): void {
const prev = manager.previousTransition;
if (prev) {
manager.element.style.setProperty('transition', prev, manager.previousTransitionPriority || undefined);
} else {
manager.element.style.removeProperty('transition');
}
}
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
function setupGlobalObserver(platformId: Object): void {
if (isPlatformBrowser(platformId) && !globalObserver) {
// Create single global observer that watches the entire document
globalObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const element = mutation.target as HTMLElement;
const manager = elementClassManagers.get(element);
// Only process elements we're managing
if (manager && observedElements.has(element)) {
if (manager.isUpdating) continue; // Ignore changes we're making
// Update base classes to include any externally added classes
const currentClasses = toClassList(element.className);
const allSourceClasses = new Set<string>();
// Collect all classes from all sources
for (const source of manager.sources.values()) {
for (const className of source.classes) {
allSourceClasses.add(className);
}
}
// Any classes not from sources become new base classes
manager.baseClasses.clear();
for (const className of currentClasses) {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
}
updateElement(manager);
}
}
}
});
// Start observing the entire document for class attribute changes
globalObserver.observe(document, {
attributes: true,
attributeFilter: ['class'],
subtree: true, // Watch all descendants
});
}
}
function updateElement(manager: ElementClassManager): void {
if (manager.isUpdating) return; // Prevent recursive updates
manager.isUpdating = true;
// Handle initialization: capture base classes after first source registration
if (!manager.hasInitialized && manager.sources.size > 0) {
// Get current classes on element (may include SSR classes)
const currentClasses = toClassList(manager.element.className);
// Get all classes that will be applied by sources
const allSourceClasses = new Set<string>();
for (const source of manager.sources.values()) {
source.classes.forEach((className) => allSourceClasses.add(className));
}
// Only consider classes as "base" if they're not produced by any source
// This prevents SSR-rendered classes from being preserved as base classes
currentClasses.forEach((className) => {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
});
manager.hasInitialized = true;
}
// Get classes from all sources, sorted by registration order (later takes precedence)
const sortedSources = Array.from(manager.sources.entries()).sort(([, a], [, b]) => a.order - b.order);
const allSourceClasses: string[] = [];
for (const [, source] of sortedSources) {
allSourceClasses.push(...source.classes);
}
// Combine base classes with all source classes, ensuring base classes take precedence
const classesToApply =
allSourceClasses.length > 0 || manager.baseClasses.size > 0
? hlm([...allSourceClasses, ...manager.baseClasses])
: '';
// Apply the classes to the element
if (manager.element.className !== classesToApply) {
manager.element.className = classesToApply;
}
manager.isUpdating = false;
}
function cleanupManager(element: HTMLElement): void {
// Remove from global tracking
observedElements.delete(element);
elementClassManagers.delete(element);
// If no more elements being tracked, cleanup global observer
if (observedElements.size === 0 && globalObserver) {
globalObserver.disconnect();
globalObserver = null;
}
}
interface ClassesOptions {
elementRef?: ElementRef<HTMLElement>;
injector?: Injector;
}
// Cache for parsed class lists to avoid repeated string operations
const classListCache = new Map<string, string[]>();
function toClassList(className: string | ClassValue[]): string[] {
// For simple string inputs, use cache to avoid repeated parsing
if (typeof className === 'string' && classListCache.has(className)) {
return classListCache.get(className)!;
}
const result = clsx(className)
.split(' ')
.filter((c) => c.length > 0);
// Cache string results, but limit cache size to prevent memory growth
if (typeof className === 'string' && classListCache.size < 1000) {
classListCache.set(className, result);
}
return result;
}import type, { BooleanInput } from '@angular/cdk/coercion';
import type, { ComponentType } from '@angular/cdk/portal';
import { BrnDialog, BrnDialogClose, BrnDialogContent, BrnDialogDescription, BrnDialogOverlay, BrnDialogRef, BrnDialogService, BrnDialogTitle, BrnDialogTrigger, cssClassesToArray, injectBrnDialogContext, provideBrnDialogDefaultOptions, type BrnDialogOptions } from '@spartan-ng/brain/dialog';
import { ChangeDetectionStrategy, Component, Directive, Injectable, booleanAttribute, computed, effect, forwardRef, inject, input, untracked, type TemplateRef } from '@angular/core';
import { ClassValue } from 'clsx';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { NgComponentOutlet } from '@angular/common';
import { classes, hlm } from '@spartan-ng/helm/utils';
import { injectCustomClassSettable } from '@spartan-ng/brain/core';
import { lucideX } from '@ng-icons/lucide';
import { provideIcons } from '@ng-icons/core';
@Directive({
selector: 'button[hlmDialogClose]',
hostDirectives: [{ directive: BrnDialogClose, inputs: ['delay'] }],
host: {
'data-slot': 'dialog-close',
},
})
export class HlmDialogClose {}
@Component({
selector: 'hlm-dialog-content',
imports: [NgComponentOutlet, HlmIconImports, HlmButton, HlmDialogClose],
providers: [provideIcons({ lucideX })],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'data-slot': 'dialog-content',
'[attr.data-state]': 'state()',
},
template: `
@if (component) {
<ng-container [ngComponentOutlet]="component" />
} @else {
<ng-content />
}
@if (showCloseButton()) {
<button hlmBtn variant="ghost" size="icon-sm" class="absolute end-4 top-4" hlmDialogClose>
<span class="sr-only">close</span>
<ng-icon hlm size="sm" name="lucideX" />
</button>
}
`,
})
export class HlmDialogContent {
private readonly _dialogRef = inject(BrnDialogRef);
private readonly _dialogContext = injectBrnDialogContext({ optional: true });
public readonly showCloseButton = input<boolean, BooleanInput>(true, { transform: booleanAttribute });
public readonly state = computed(() => this._dialogRef?.state() ?? 'closed');
public readonly component = this._dialogContext?.$component;
private readonly _dynamicComponentClass = this._dialogContext?.$dynamicComponentClass;
constructor() {
classes(() => [
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 relative z-50 mx-auto grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border p-6 shadow-lg data-[state=closed]:duration-200 data-[state=open]:duration-200 sm:mx-0 sm:max-w-lg',
this._dynamicComponentClass,
]);
}
}
@Directive({
selector: '[hlmDialogDescription]',
hostDirectives: [BrnDialogDescription],
host: {
'data-slot': 'dialog-description',
},
})
export class HlmDialogDescription {
constructor() {
classes(() => 'text-muted-foreground text-sm');
}
}
@Directive({
selector: '[hlmDialogFooter],hlm-dialog-footer',
host: {
'data-slot': 'dialog-footer',
},
})
export class HlmDialogFooter {
constructor() {
classes(() => 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end');
}
}
@Directive({
selector: '[hlmDialogHeader],hlm-dialog-header',
host: {
'data-slot': 'dialog-header',
},
})
export class HlmDialogHeader {
constructor() {
classes(() => 'flex flex-col gap-2 text-center sm:text-start');
}
}
export const hlmDialogOverlayClass =
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/50';
@Directive({
selector: '[hlmDialogOverlay],hlm-dialog-overlay',
hostDirectives: [BrnDialogOverlay],
})
export class HlmDialogOverlay {
private readonly _classSettable = injectCustomClassSettable({ optional: true, host: true });
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() => hlm(hlmDialogOverlayClass, this.userClass()));
constructor() {
effect(() => {
const newClass = this._computedClass();
untracked(() => this._classSettable?.setClassToCustomElement(newClass));
});
}
}
@Directive({
selector: '[hlmDialogPortal]',
hostDirectives: [{ directive: BrnDialogContent, inputs: ['context', 'class'] }],
})
export class HlmDialogPortal {}
@Directive({
selector: '[hlmDialogTitle]',
hostDirectives: [BrnDialogTitle],
host: {
'data-slot': 'dialog-title',
},
})
export class HlmDialogTitle {
constructor() {
classes(() => 'text-lg leading-none font-semibold');
}
}
@Directive({
selector: 'button[hlmDialogTrigger],button[hlmDialogTriggerFor]',
hostDirectives: [{ directive: BrnDialogTrigger, inputs: ['id', 'brnDialogTriggerFor: hlmDialogTriggerFor', 'type'] }],
host: {
'data-slot': 'dialog-trigger',
},
})
export class HlmDialogTrigger {}
export type HlmDialogOptions<DialogContext = unknown> = BrnDialogOptions & {
contentClass?: string;
context?: DialogContext;
};
@Injectable({
providedIn: 'root',
})
export class HlmDialogService {
private readonly _brnDialogService = inject(BrnDialogService);
public open(component: ComponentType<unknown> | TemplateRef<unknown>, options?: Partial<HlmDialogOptions>) {
const mergedOptions = {
...(options ?? {}),
backdropClass: cssClassesToArray(`${hlmDialogOverlayClass} ${options?.backdropClass ?? ''}`),
context: {
...(options?.context && typeof options.context === 'object' ? options.context : {}),
$component: component,
$dynamicComponentClass: options?.contentClass,
},
};
return this._brnDialogService.open(HlmDialogContent, undefined, mergedOptions.context, mergedOptions);
}
}
@Component({
selector: 'hlm-dialog',
exportAs: 'hlmDialog',
imports: [HlmDialogOverlay],
providers: [
{
provide: BrnDialog,
useExisting: forwardRef(() => HlmDialog),
},
provideBrnDialogDefaultOptions({
// add custom options here
}),
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<hlm-dialog-overlay />
<ng-content />
`,
})
export class HlmDialog extends BrnDialog {}
export const HlmDialogImports = [
HlmDialog,
HlmDialogContent,
HlmDialogDescription,
HlmDialogFooter,
HlmDialogHeader,
HlmDialogOverlay,
HlmDialogPortal,
HlmDialogTitle,
HlmDialogTrigger,
HlmDialogClose,
] as const;Usage
import { HlmDialogImports } from '@spartan-ng/helm/dialog';<hlm-dialog>
<button hlmDialogTrigger hlmBtn variant="outline">Open Dialog</button>
<hlm-dialog-content *hlmDialogPortal="let ctx">
<hlm-dialog-header>
<h3 hlmDialogTitle>Edit profile</h3>
<p hlmDialogDescription>Make changes to your profile here. Click save when you're done.</p>
</hlm-dialog-header>
<hlm-dialog-footer>
<button hlmBtn variant="outline" hlmDialogClose>Cancel</button>
<button hlmBtn type="submit">Save changes</button>
</hlm-dialog-footer>
</hlm-dialog-content>
</hlm-dialog>Examples
Declarative Usage
Spartan's dialog supports declarative usage. Simply set it's state input to open or closed and let spartan handle the rest. This allows you to leverage the power of declarative code, like listening to changes in an input field, debouncing the value, and opening the dialog only if the user's enters the correct passphrase.
Enter passphrase to open dialog
Hint: It's sparta
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { HlmDialogImports } from '@spartan-ng/helm/dialog';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmLabelImports } from '@spartan-ng/helm/label';
import { HlmTypographyImports } from '@spartan-ng/helm/typography';
import { debounceTime, map } from 'rxjs/operators';
@Component({
selector: 'spartan-dialog-declarative-preview',
imports: [FormsModule, HlmDialogImports, HlmLabelImports, HlmInputImports, HlmTypographyImports],
template: `
<div class="mt-6 space-y-4">
<p hlmH4>Enter passphrase to open dialog</p>
<div class="flex flex-col gap-2">
<label hlmLabel for="passphrase" class="px-1">Passphrase</label>
<input
id="passphrase"
name="passphrase"
hlmInput
[ngModelOptions]="{ standalone: true }"
[ngModel]="_passphrase()"
(ngModelChange)="_passphrase.set($event)"
/>
<p hlmMuted class="px-1">Hint: It's sparta</p>
</div>
</div>
<hlm-dialog [state]="_state()" (closed)="_passphrase.set('')">
<hlm-dialog-content *hlmDialogPortal="let ctx">
<hlm-dialog-header class="w-[250px]">
<h3 hlmDialogTitle>Welcome to Sparta</h3>
<p hlmDialogDescription>Enjoy declarative dialogs.</p>
</hlm-dialog-header>
</hlm-dialog-content>
</hlm-dialog>
`,
})
export class DialogDeclarativePreview {
protected readonly _passphrase = signal<string>('');
private readonly _debouncedState$ = toObservable(this._passphrase).pipe(
debounceTime(500),
map((passphrase) => (passphrase === 'sparta' ? 'open' : 'closed')),
);
protected readonly _state = toSignal(this._debouncedState$, { initialValue: 'closed' as 'open' | 'closed' });
}Inside Menu
You can nest dialogs inside context or dropdown menus. Make sure to wrap the menu-item inside the hlm-dialog component and apply the HlmDialogTrigger directive. Another option is to use the hlmDialogTriggerFor alternative, which takes in a reference to the hlm-dialog. That way you can avoid nesting the template.
Note
Do not use the HlmDropdownMenuItem directives as they conflict with HlmDialogTrigger & hlmDialogTriggerFor ! We expose the hlm variants so you can directly use them to style your elements. Check out the code of the example below!
import { Component } from '@angular/core';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmContextMenuImports } from '@spartan-ng/helm/context-menu';
import { HlmDialogImports } from '@spartan-ng/helm/dialog';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
@Component({
selector: 'spartan-dialog-context-menu',
imports: [HlmDialogImports, HlmButtonImports, HlmDropdownMenuImports, HlmContextMenuImports],
template: `
<div
[hlmContextMenuTrigger]="menu"
class="border-border flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm"
>
Right click here
</div>
<ng-template #menu>
<hlm-dropdown-menu class="w-64">
<hlm-dropdown-menu-group>
<button inset hlmDropdownMenuItem>
Save
<hlm-dropdown-menu-shortcut>⌘S</hlm-dropdown-menu-shortcut>
</button>
<button disabled inset hlmDropdownMenuItem>
Archive
<hlm-dropdown-menu-shortcut>⌘A</hlm-dropdown-menu-shortcut>
</button>
<hlm-dialog>
<button hlmDropdownMenuItem inset="true" hlmDialogTrigger>
Print
<hlm-dropdown-menu-shortcut>⌘P</hlm-dropdown-menu-shortcut>
</button>
<hlm-dialog-content class="sm:max-w-[425px]" *hlmDialogPortal="let ctx">
<hlm-dialog-header>
<h3 hlmDialogTitle>Print this page</h3>
<p hlmDialogDescription>
Are you sure you want to print this page? Only print if absolutely necessary! The less we print, the
less paper we need, the better it is for our environment!
</p>
</hlm-dialog-header>
<hlm-dialog-footer>
<button hlmBtn variant="ghost" (click)="ctx.close()">Cancel</button>
<button hlmBtn>Print</button>
</hlm-dialog-footer>
</hlm-dialog-content>
</hlm-dialog>
</hlm-dropdown-menu-group>
</hlm-dropdown-menu>
</ng-template>
`,
})
export class DialogContextMenuPreview {}Dynamic Component
You can dynamically open a dialog with a component rendered as the content. The dialog context can be injected into the dynamic component using the provided injectBrnDialogContext function.
Note
Avoid using the <hlm-dialog-content> tag when your dialog relies on dynamic content. Using it in this case can cause the dialog to repeatedly render itself in a loop. The tag is meant to wrap static content for the dialog, but with a dynamic component, the component automatically acts as the wrapper.
import { Component, inject } from '@angular/core';
import { provideIcons } from '@ng-icons/core';
import { lucideCheck } from '@ng-icons/lucide';
import { BrnDialogRef, injectBrnDialogContext } from '@spartan-ng/brain/dialog';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmDialogDescription, HlmDialogHeader, HlmDialogService, HlmDialogTitle } from '@spartan-ng/helm/dialog';
import { HlmTableImports } from '@spartan-ng/helm/table';
type ExampleUser = {
name: string;
email: string;
phone: string;
};
@Component({
selector: 'spartan-dialog-dynamic-component-preview',
imports: [HlmButton],
template: `
<button hlmBtn (click)="openDynamicComponent()">Select User</button>
`,
})
export class DialogDynamicPreview {
private readonly _hlmDialogService = inject(HlmDialogService);
private readonly _users: ExampleUser[] = [
{
name: 'Helena Chambers',
email: 'helenachambers@chorizon.com',
phone: '+1 (812) 588-3759',
},
{
name: 'Josie Crane',
email: 'josiecrane@hinway.com',
phone: '+1 (884) 523-3324',
},
{
name: 'Lou Hartman',
email: 'louhartman@optyk.com',
phone: '+1 (912) 479-3998',
},
{
name: 'Lydia Zimmerman',
email: 'lydiazimmerman@ultrasure.com',
phone: '+1 (944) 511-2111',
},
];
public openDynamicComponent() {
const dialogRef = this._hlmDialogService.open(SelectUser, {
context: {
users: this._users,
},
contentClass: 'sm:!max-w-[750px]',
});
dialogRef.closed$.subscribe((user) => {
if (user) {
console.log('Selected user:', user);
}
});
}
}
@Component({
selector: 'spartan-dynamic-content',
imports: [HlmDialogHeader, HlmDialogTitle, HlmDialogDescription, ...HlmTableImports],
providers: [provideIcons({ lucideCheck })],
host: {
class: 'flex flex-col gap-4',
},
template: `
<hlm-dialog-header>
<h3 hlmDialogTitle>Select user</h3>
<p hlmDialogDescription>Click a row to select a user.</p>
</hlm-dialog-header>
<table hlmTable class="w-full">
<tr hlmTr>
<th hlmTh>Name</th>
<th hlmTh>Email</th>
<th hlmTh>Phone</th>
</tr>
@for (user of _users; track user.name) {
<tr hlmTr (click)="selectUser(user)" class="cursor-pointer">
<td hlmTd truncate class="font-medium">{{ user.name }}</td>
<td hlmTd>{{ user.email }}</td>
<td hlmTd>{{ user.phone }}</td>
</tr>
}
</table>
`,
})
class SelectUser {
private readonly _dialogRef = inject<BrnDialogRef<ExampleUser>>(BrnDialogRef);
private readonly _dialogContext = injectBrnDialogContext<{ users: ExampleUser[] }>();
protected readonly _users = this._dialogContext.users;
public selectUser(user: ExampleUser) {
this._dialogRef.close(user);
}
}Close Dialog
You can close the dialog by using a directive, a template reference, or a viewchild/contentchild reference to the dialog.
import { Component, viewChild } from '@angular/core';
import { BrnDialog } from '@spartan-ng/brain/dialog';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDialogImports } from '@spartan-ng/helm/dialog';
import { HlmLabelImports } from '@spartan-ng/helm/label';
@Component({
selector: 'spartan-dialog-close-preview',
imports: [HlmDialogImports, HlmLabelImports, HlmButtonImports],
template: `
<hlm-dialog #dialogRef>
<button id="edit-profile" hlmDialogTrigger hlmBtn>Open</button>
<hlm-dialog-content class="sm:max-w-[425px]" *hlmDialogPortal="let ctx">
<hlm-dialog-header>
<h3 hlmDialogTitle>Dialog</h3>
</hlm-dialog-header>
<div class="grid gap-4 py-4">
<div class="flex items-center justify-between gap-4">
<label hlmLabel>Close dialog by directive</label>
<button hlmBtn hlmDialogClose>Close</button>
</div>
<div class="flex items-center justify-between gap-4">
<label hlmLabel>Close dialog by reference</label>
<button hlmBtn (click)="dialogRef.close({})">Close</button>
</div>
<div class="flex items-center justify-between gap-4">
<label hlmLabel>Close dialog by viewchild reference</label>
<button hlmBtn (click)="closeDialog()">Close</button>
</div>
</div>
</hlm-dialog-content>
</hlm-dialog>
`,
})
export class DialogClosePreview {
public readonly viewchildDialogRef = viewChild(BrnDialog);
closeDialog() {
this.viewchildDialogRef()?.close({});
}
}Brain API
BrnDialogClose
Selector: button[brnDialogClose]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| delay | number | undefined | undefined | - |
BrnDialogContent
Selector: [brnDialogContent]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | null | undefined | undefined | - |
| context | T | undefined | undefined | - |
BrnDialogDescription
Selector: [brnDialogDescription]
BrnDialogOverlay
Selector: [brnDialogOverlay],brn-dialog-overlay
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | null | undefined | undefined | - |
BrnDialogTitle
Selector: [brnDialogTitle]
BrnDialogTrigger
Selector: button[brnDialogTrigger],button[brnDialogTriggerFor]
ExportAs: brnDialogTrigger
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | `brn-dialog-trigger-${++idSequence}` | - |
| type | 'button' | 'submit' | 'reset' | button | - |
| brnDialogTriggerFor | BrnDialog | undefined | undefined | - |
BrnDialog
Selector: [brnDialog],brn-dialog
ExportAs: brnDialog
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| state | BrnDialogState | null | null | - |
| role | BrnDialogOptions['role'] | this._defaultOptions.role | - |
| hasBackdrop | boolean | this._defaultOptions.hasBackdrop | - |
| positionStrategy | BrnDialogOptions['positionStrategy'] | this._defaultOptions.positionStrategy | - |
| scrollStrategy | BrnDialogOptions['scrollStrategy'] | 'close' | 'reposition' | null | this._defaultOptions.scrollStrategy | - |
| restoreFocus | BrnDialogOptions['restoreFocus'] | this._defaultOptions.restoreFocus | - |
| closeOnOutsidePointerEvents | boolean | this._defaultOptions.closeOnOutsidePointerEvents | - |
| closeOnBackdropClick | boolean | this._defaultOptions.closeOnBackdropClick | - |
| attachTo | BrnDialogOptions['attachTo'] | null | - |
| attachPositions | BrnDialogOptions['attachPositions'] | this._defaultOptions.attachPositions | - |
| autoFocus | BrnDialogOptions['autoFocus'] | this._defaultOptions.autoFocus | - |
| closeDelay | number | this._defaultOptions.closeDelay | - |
| disableClose | boolean | this._defaultOptions.disableClose | - |
| aria-describedby | BrnDialogOptions['ariaDescribedBy'] | null | - |
| aria-labelledby | BrnDialogOptions['ariaLabelledBy'] | null | - |
| aria-label | BrnDialogOptions['ariaLabel'] | null | - |
| aria-modal | boolean | true | - |
Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| closed | TResult | - | - |
| stateChanged | BrnDialogState | - | - |
Helm API
HlmDialogClose
Selector: button[hlmDialogClose]
HlmDialogContent
Selector: hlm-dialog-content
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| showCloseButton | boolean | true | - |
HlmDialogDescription
Selector: [hlmDialogDescription]
HlmDialogFooter
Selector: [hlmDialogFooter],hlm-dialog-footer
HlmDialogHeader
Selector: [hlmDialogHeader],hlm-dialog-header
HlmDialogOverlay
Selector: [hlmDialogOverlay],hlm-dialog-overlay
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| class | ClassValue | - | - |
HlmDialogPortal
Selector: [hlmDialogPortal]
HlmDialogTitle
Selector: [hlmDialogTitle]
HlmDialogTrigger
Selector: button[hlmDialogTrigger],button[hlmDialogTriggerFor]
HlmDialog
Selector: hlm-dialog
ExportAs: hlmDialog
On This Page