Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
import { Component } from '@angular/core';
import { BrnDialogContent, BrnDialogTrigger } from '@spartan-ng/brain/dialog';
import { HlmButton } from '@spartan-ng/helm/button';
import {
HlmDialog,
HlmDialogContent,
HlmDialogDescription,
HlmDialogFooter,
HlmDialogHeader,
HlmDialogTitle,
} from '@spartan-ng/helm/dialog';
import { HlmInput } from '@spartan-ng/helm/input';
import { HlmLabel } from '@spartan-ng/helm/label';
@Component({
selector: 'spartan-dialog-preview',
imports: [
BrnDialogTrigger,
BrnDialogContent,
HlmDialog,
HlmDialogContent,
HlmDialogHeader,
HlmDialogFooter,
HlmDialogTitle,
HlmDialogDescription,
HlmLabel,
HlmInput,
HlmButton,
],
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="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center 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="grid grid-cols-4 items-center 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 DialogPreview {}
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 {
HlmDialog
HlmDialogContent
HlmDialogDescription
HlmDialogFooter
HlmDialogHeader
HlmDialogTitle
} from '@spartan-ng/helm/dialog';
<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>
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: 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 | unknown | `brn-dialog-trigger-${idSequence++}` | - |
brnDialogTriggerFor | BrnDialog | undefined | undefined | - |
BrnDialog
Selector: brn-dialog
ExportAs: brnDialog
Inputs
Prop | Type | Default | Description |
---|---|---|---|
state | BrnDialogState | null | null | - |
role | BrnDialogOptions['role'] | this._defaultOptions.role | - |
hasBackdrop | unknown | 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 | unknown | this._defaultOptions.closeOnOutsidePointerEvents | - |
closeOnBackdropClick | unknown | this._defaultOptions.closeOnBackdropClick | - |
attachTo | BrnDialogOptions['attachTo'] | null | - |
attachPositions | BrnDialogOptions['attachPositions'] | this._defaultOptions.attachPositions | - |
autoFocus | BrnDialogOptions['autoFocus'] | this._defaultOptions.autoFocus | - |
closeDelay | unknown | this._defaultOptions.closeDelay | - |
disableClose | unknown | this._defaultOptions.disableClose | - |
aria-describedby | BrnDialogOptions['ariaDescribedBy'] | null | - |
aria-labelledby | BrnDialogOptions['ariaLabelledBy'] | null | - |
aria-label | BrnDialogOptions['ariaLabel'] | null | - |
aria-modal | unknown | true | - |
Outputs
Prop | Type | Default | Description |
---|---|---|---|
closed | any | - | - |
stateChanged | BrnDialogState | - | - |
Helm API
HlmDialogClose
Selector: [hlmDialogClose],[brnDialogClose][hlm]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogContent
Selector: hlm-dialog-content
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogDescription
Selector: [hlmDialogDescription]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogFooter
Selector: hlm-dialog-footer
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogHeader
Selector: hlm-dialog-header
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogOverlay
Selector: [hlmDialogOverlay],brn-dialog-overlay[hlm]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialogTitle
Selector: [hlmDialogTitle]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmDialog
Selector: hlm-dialog
ExportAs: hlmDialog
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 { BrnDialogContent } from '@spartan-ng/brain/dialog';
import {
HlmDialog,
HlmDialogContent,
HlmDialogDescription,
HlmDialogHeader,
HlmDialogTitle,
} from '@spartan-ng/helm/dialog';
import { HlmInput } from '@spartan-ng/helm/input';
import { HlmLabel } from '@spartan-ng/helm/label';
import { HlmH4, HlmMuted } from '@spartan-ng/helm/typography';
import { debounceTime, map } from 'rxjs/operators';
@Component({
selector: 'spartan-dialog-declarative-preview',
imports: [
FormsModule,
BrnDialogContent,
HlmDialog,
HlmDialogContent,
HlmDialogHeader,
HlmDialogTitle,
HlmDialogDescription,
HlmLabel,
HlmInput,
HlmMuted,
HlmH4,
],
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 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 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 { BrnDialogContent, BrnDialogTrigger } from '@spartan-ng/brain/dialog';
import { BrnContextMenuTrigger } from '@spartan-ng/brain/menu';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmDialog, HlmDialogContent, HlmDialogFooter, HlmDialogHeader } from '@spartan-ng/helm/dialog';
import { HlmMenu, HlmMenuGroup, HlmMenuItem, HlmMenuShortcut } from '@spartan-ng/helm/menu';
@Component({
selector: 'spartan-dialog-context-menu',
imports: [
BrnDialogTrigger,
BrnDialogContent,
HlmDialogContent,
HlmDialog,
HlmDialogHeader,
HlmDialogFooter,
HlmButton,
BrnContextMenuTrigger,
HlmMenuItem,
HlmMenuShortcut,
HlmMenu,
HlmMenuGroup,
],
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 hlmMenuItem inset="true" 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 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, ...HlmTableImports],
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({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'dynamic-content',
imports: [HlmDialogHeader, HlmDialogTitle, HlmDialogDescription, ...HlmTableImports],
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>
<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>
`,
host: {
class: 'flex flex-col gap-4',
},
})
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, BrnDialogClose, BrnDialogContent, BrnDialogTrigger } from '@spartan-ng/brain/dialog';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmDialog, HlmDialogContent, HlmDialogHeader, HlmDialogTitle } from '@spartan-ng/helm/dialog';
import { HlmLabel } from '@spartan-ng/helm/label';
@Component({
selector: 'spartan-dialog-close-preview',
imports: [
BrnDialogTrigger,
BrnDialogContent,
BrnDialogClose,
HlmDialog,
HlmDialogContent,
HlmDialogHeader,
HlmDialogTitle,
HlmLabel,
HlmButton,
],
template: `
<hlm-dialog #dialogRef>
<button id="edit-profile" brnDialogTrigger hlmBtn>Open</button>
<hlm-dialog-content class="sm:max-w-[425px]" *brnDialogContent="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 brnDialogClose>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 viewchildDialogRef = viewChild(BrnDialog);
closeDialog() {
this.viewchildDialogRef()?.close({});
}
}