Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
import { Component } from '@angular/core';
import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
import { BrnDialogContentDirective, BrnDialogTriggerDirective } from '@spartan-ng/brain/dialog';
import {
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogDescriptionDirective,
HlmDialogFooterComponent,
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmInputDirective } from '@spartan-ng/ui-input-helm';
import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
@Component({
selector: 'spartan-dialog-preview',
standalone: true,
imports: [
BrnDialogTriggerDirective,
BrnDialogContentDirective,
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogHeaderComponent,
HlmDialogFooterComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmLabelDirective,
HlmInputDirective,
HlmButtonDirective,
],
template: `
<hlm-dialog>
<button id="edit-profile" brnDialogTrigger hlmBtn>Edit Profile</button>
<hlm-dialog-content class="sm:max-w-[425px]" *brnDialogContent="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="py-4 grid gap-4">
<div class="items-center grid grid-cols-4 gap-4">
<label hlmLabel for="name" class="text-right">Name</label>
<input hlmInput id="name" value="Pedro Duarte" class="col-span-3" />
</div>
<div class="items-center grid grid-cols-4 gap-4">
<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 type="submit">Save changes</button>
</hlm-dialog-footer>
</hlm-dialog-content>
</hlm-dialog>
`,
})
export class DialogPreviewComponent {}
Installation
npx nx g @spartan-ng/cli:ui dialog
ng g @spartan-ng/cli:ui dialog
Usage
import { BrnDialogContentDirective, BrnDialogTriggerDirective } from '@spartan-ng/brain/dialog';
import {
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogDescriptionDirective,
HlmDialogFooterComponent,
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
<hlm-dialog>
<button brnDialogTrigger hlmBtn>Edit Profile</button>
<hlm-dialog-content *brnDialogContent="let ctx">
<hlm-dialog-header>
<h3 brnDialogTitle hlm>Edit profile</h3>
<p brnDialogDescription hlm>Make changes to your profile here. Click save when you're done.</p>
</hlm-dialog-header>
<hlm-dialog-footer>
<button hlmBtn type="submit">Save changes</button>
</hlm-dialog-footer>
</hlm-dialog-content>
</hlm-dialog>
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
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { HlmBadgeDirective } from '@spartan-ng/ui-badge-helm';
import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
import { BrnDialogContentDirective } from '@spartan-ng/brain/dialog';
import {
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogDescriptionDirective,
HlmDialogFooterComponent,
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmInputDirective } from '@spartan-ng/ui-input-helm';
import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
import { HlmH4Directive, HlmMutedDirective } from '@spartan-ng/ui-typography-helm';
import { debounceTime, map } from 'rxjs';
@Component({
selector: 'spartan-dialog-declarative-preview',
standalone: true,
imports: [
FormsModule,
BrnDialogContentDirective,
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogHeaderComponent,
HlmDialogFooterComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmLabelDirective,
HlmInputDirective,
HlmButtonDirective,
HlmBadgeDirective,
HlmMutedDirective,
HlmH4Directive,
],
template: `
<div class="space-y-4">
<p hlmH4>Enter passphrase to open dialog</p>
<label hlmLabel>
Passphrase
<input
name="passphrase"
hlmInput
[ngModelOptions]="{ standalone: true }"
[ngModel]="passphrase()"
(ngModelChange)="passphrase.set($event)"
/>
<span hlmMuted>Hint: It's sparta</span>
</label>
</div>
<hlm-dialog [state]="state()" (closed)="passphrase.set('')">
<hlm-dialog-content *brnDialogContent="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 DialogDeclarativePreviewComponent {
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$);
}
Inside Menu
You can nest dialogs inside context or dropdown menus. Make sure to wrap the menu-item inside the brn-dialog
component and apply the BrnDialogTrigger
directive. Another option is to use the brnDialogTriggerFor
alternative, which takes in a reference to the brn-dialog. That way you can avoid nesting the template.
Note
Do not use the HlmMenuItem
or BrnMenuItem
directives as they conflict with BrnDialogTrigger
& brnDialogTriggerFor!
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 { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
import { BrnDialogContentDirective, BrnDialogTriggerDirective } from '@spartan-ng/brain/dialog';
import {
HlmDialogComponent,
HlmDialogContentComponent,
HlmDialogDescriptionDirective,
HlmDialogFooterComponent,
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmInputDirective } from '@spartan-ng/ui-input-helm';
import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
import { BrnContextMenuTriggerDirective } from '@spartan-ng/brain/menu';
import {
HlmMenuComponent,
HlmMenuGroupComponent,
HlmMenuItemDirective,
HlmMenuShortcutComponent,
hlmMenuItemVariants,
} from '@spartan-ng/ui-menu-helm';
@Component({
selector: 'spartan-dialog-context-menu',
standalone: true,
imports: [
BrnDialogTriggerDirective,
BrnDialogContentDirective,
HlmDialogContentComponent,
HlmDialogComponent,
HlmDialogHeaderComponent,
HlmDialogFooterComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmLabelDirective,
HlmButtonDirective,
HlmInputDirective,
BrnContextMenuTriggerDirective,
HlmMenuItemDirective,
HlmMenuShortcutComponent,
HlmMenuComponent,
HlmMenuGroupComponent,
],
template: `
<div
[brnCtxMenuTriggerFor]="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-menu class="w-64">
<hlm-menu-group>
<button inset hlmMenuItem>
Save
<hlm-menu-shortcut>⌘S</hlm-menu-shortcut>
</button>
<button disabled inset hlmMenuItem>
Archive
<hlm-menu-shortcut>⌘A</hlm-menu-shortcut>
</button>
<hlm-dialog>
<button [class]="_hlmMenuItemClasses" brnDialogTrigger>
Print
<hlm-menu-shortcut>⌘P</hlm-menu-shortcut>
</button>
<hlm-dialog-content *brnDialogContent="let ctx">
<hlm-dialog-header>
<h3 brnDialogTitle hlm>Print this page</h3>
<p brnDialogDescription hlm>
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-menu-group>
</hlm-menu>
</ng-template>
`,
})
export class DialogContextMenuPreviewComponent {
protected readonly _hlmMenuItemClasses = hlmMenuItemVariants({ inset: true });
}
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 {
HlmDialogDescriptionDirective,
HlmDialogHeaderComponent,
HlmDialogService,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm';
import { HlmTableComponent, HlmTdComponent, HlmThComponent, HlmTrowComponent } from '@spartan-ng/ui-table-helm';
type ExampleUser = {
name: string;
email: string;
phone: string;
};
@Component({
selector: 'spartan-dialog-dynamic-component-preview',
standalone: true,
imports: [HlmButtonDirective],
template: `
<button hlmBtn (click)="openDynamicComponent()">Select User</button>
`,
})
export class DialogDynamicComponentPreviewComponent {
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(SelectUserComponent, {
context: {
users: this._users,
},
contentClass: 'sm:!max-w-[750px]',
});
dialogRef.closed$.subscribe((user) => {
if (user) {
console.log('Selected user:', user);
}
});
}
}
@Component({
selector: 'dynamic-content',
standalone: true,
imports: [
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmTableComponent,
HlmThComponent,
HlmTrowComponent,
HlmTdComponent,
HlmButtonDirective,
HlmIconComponent,
],
providers: [provideIcons({ lucideCheck })],
template: `
<hlm-dialog-header>
<h3 hlmDialogTitle>Select user</h3>
<p hlmDialogDescription>Click a row to select a user.</p>
</hlm-dialog-header>
<hlm-table>
<hlm-trow>
<hlm-th class="w-44">Name</hlm-th>
<hlm-th class="w-60">Email</hlm-th>
<hlm-th class="w-48">Phone</hlm-th>
</hlm-trow>
@for (user of users; track user.name) {
<button class="text-left" (click)="selectUser(user)">
<hlm-trow>
<hlm-td truncate class="font-medium w-44">{{ user.name }}</hlm-td>
<hlm-td class="w-60">{{ user.email }}</hlm-td>
<hlm-td class="w-48">{{ user.phone }}</hlm-td>
</hlm-trow>
</button>
}
</hlm-table>
`,
})
class SelectUserComponent {
@HostBinding('class') private readonly _class: string = 'flex flex-col gap-4';
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);
}
}