Dialog
The Aurelia dialog plugin - a modular system bridging Aurelia with various UI framework dialog implementations.
Introduction
The Aurelia dialog plugin is a modular and pluggable system that serves as a bridge between Aurelia and different UI framework dialog implementations. Rather than being a single dialog implementation, it provides an extensible architecture where different renderers can be plugged in to support various dialog styles and behaviors.
The plugin comes with two built-in renderer implementations:
Standard: Uses the modern HTML5
<dialog>elementClassic: A light DOM implementation similar to Aurelia v1 (ideal for migration)
The modular design makes it easy to create custom renderers for specific UI frameworks or design requirements.
Here's what you'll learn...
How to install & configure the plugin with different renderers
Understanding the modular renderer architecture
How to use the Standard and Classic implementations
How to create your own custom dialog renderer
Advanced configuration and lifecycle management
Plugin Architecture
The dialog plugin is built around a modular renderer system that bridges Aurelia with any UI framework:
Core Interfaces
IDialogService
Main API for opening/managing dialogs
Single service per app
IDialogController
Controls individual dialog instances
One per dialog
IDialogDomRenderer<TOptions>
Pluggable renderer interface
Standard, Classic, or Custom
IDialogDom
Dialog DOM abstraction
Renderer-specific implementation
Modular Benefits
Choose between built-in renderers (Standard, Classic)
Create custom renderers for specific UI frameworks
Mix different renderers within the same application
Migrate gradually from v1 dialog implementations
Installing The Plugin
Aurelia provides three configuration options for the dialog plugin:
Option 1: Standard Configuration (Recommended)
Uses the modern HTML5 <dialog> element with native modal behavior:
import { DialogConfigurationStandard } from '@aurelia/dialog';
import { Aurelia } from 'aurelia';
Aurelia.register(DialogConfigurationStandard).app(MyApp).start();Best for: New applications, modern browsers, native accessibility features
Option 2: Classic Configuration (Migration-Friendly)
Uses a light DOM implementation similar to Aurelia v1:
import { DialogConfigurationClassic } from '@aurelia/dialog';
import { Aurelia } from 'aurelia';
Aurelia.register(DialogConfigurationClassic).app(MyApp).start();Best for: Migrating from Aurelia v1, custom styling requirements, older browser support
Option 3: Base Configuration (Custom Renderer)
Provides the core infrastructure without a default renderer:
import { DialogConfiguration } from '@aurelia/dialog';
import { Aurelia } from 'aurelia';
Aurelia.register(DialogConfiguration.customize(settings => {
// Register your custom renderer here
settings.renderer = MyCustomRenderer;
})).app(MyApp).start();Best for: Custom UI framework integration, specific design requirements
Choosing the Right Configuration
Use this guide to select the best dialog configuration for your project:
Browser Support
Modern (dialog support)
All browsers
Depends on implementation
Accessibility
Built-in
Manual setup
Manual setup
Styling Control
Limited backdrop
Full control
Full control
Animation Support
Via callbacks
Via callbacks
Full control
Migration from v1
Requires changes
Minimal changes
Complete rewrite
UI Framework Integration
Limited
Some styling
Perfect integration
Choose Standard When:
Building a new application
Modern browser support is acceptable
You want native accessibility features
Simple styling requirements
You prefer web standards
Choose Classic When:
Migrating from Aurelia v1
Need full styling control
Supporting older browsers
Complex z-index management required
Custom event handling needed
Choose Custom When:
Integrating with UI frameworks (Material, Bootstrap, Ant Design)
Specific design system requirements
Need complete control over DOM structure
Building reusable dialog library
Advanced animation requirements
Global Configuration
Each configuration can be customized to set global defaults for all dialogs. Call .customize() on your chosen configuration:
Configure global defaults for the Standard renderer:
import { DialogConfigurationStandard } from '@aurelia/dialog';
import { Aurelia } from 'aurelia';
Aurelia
.register(DialogConfigurationStandard.customize((settings) => {
// Global service settings
settings.rejectOnCancel = true; // Treat cancellation as promise rejection
// Global Standard renderer options
settings.options.modal = true; // Always open as modal by default
settings.options.show = (dom) => {
// Custom show animation for all dialogs
return dom.root.animate([
{ transform: 'scale(0.8)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
], { duration: 200 }).finished;
};
settings.options.hide = (dom) => {
// Custom hide animation for all dialogs
return dom.root.animate([
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(0.8)', opacity: 0 }
], { duration: 200 }).finished;
};
settings.options.overlayStyle = 'background: rgba(0, 0, 0, 0.6)';
}))
.app(MyApp)
.start();Configure global defaults for the Classic renderer:
import { DialogConfigurationClassic } from '@aurelia/dialog';
import { Aurelia } from 'aurelia';
Aurelia
.register(DialogConfigurationClassic.customize((settings) => {
// Global service settings
settings.rejectOnCancel = true; // Treat cancellation as promise rejection
// Global Classic renderer options
settings.options.lock = false; // Allow ESC key and overlay clicks
settings.options.overlayDismiss = true; // Click outside to close
settings.options.keyboard = ['Escape', 'Enter']; // Keys that close dialog
settings.options.startingZIndex = 2000; // Higher z-index for overlays
settings.options.show = (dom) => {
// Custom show animation
return dom.contentHost.animate([
{ transform: 'translateY(-50px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], { duration: 300 }).finished;
};
settings.options.hide = (dom) => {
// Custom hide animation
return dom.contentHost.animate([
{ transform: 'translateY(0)', opacity: 1 },
{ transform: 'translateY(-50px)', opacity: 0 }
], { duration: 300 }).finished;
};
}))
.app(MyApp)
.start();If it's desirable to change some of the default implementations, we can instead use the export named DialogConfiguration and pass in the list of implementation for the main interfaces:
import { DialogConfiguration } from '@aurelia/dialog';
Aurelia
.register(DialogConfiguration.customize(
settings => {
// customize settings here if needed
}))
.app(MyApp)
.start();Using The Default Implementations
The Dialog Settings
There are two levels where dialog behaviors can be configured:
Global level via
IDialogGlobalSettingsSingle dialog level via dialog service
.open()call, or the propertysettingson a dialog controller.
Normally, the global settings would be changed during the app startup/or before, while the single dialog settings would be changed during the contruction of the dialog view model, via the open method.
An example of configuring the global dialog settings:
For the standard implementation: Make all dialogs, by default:
show as modal
has some basic animation in & out
Aurelia .reigster(DialogConfigurationStandard.customize((settings) => { settings.options.modal = true; settings.options.show = (dom) => dom.root.animate(...); settings.options.hide = (dom) => dom.root.animate(...); })) .app(MyApp) .start();For the classic implementation: Make all dialogs, by default:
not dismissable by clicking outside of it, or hitting the ESC key
have starting CSS
z-indexof 5if not locked, closable by hitting the
ESCkeyAurelia .register(DialogConfigurationClassic.customize((settings) => { settings.options.lock = true; settings.options.startingZIndex = 5; settings.options.keyboard = true; })) .app(MyApp) .start();
An example of configuring a single dialog, via
openmethod of the dialog service:For the standard implementation: Displaying an alert dialog
dialogService.open({ component: Alert, options: { // block the entire screen with this alert box modal: true, show: dom => dom.root.animate(...), hide: dom => dom.root.animate(...) } })For the classic implementation: Displaying an alert dialog, which has
z-indexvalue as 10 to stay on top of all other dialogs, and will be dismissed when the user hits theESCkey.dialogService.open({ component: Alert, options: { lock: false, startingZIndex: 10, } });
The main settings that are available in the open method of the dialog service:
componentcan be class reference or instance, or a function that resolves to such, or a promise of such.templatecan be HTML elements, string or a function that resolves to such, or a promise of such.modelthe data to be passed to thecanActivateandactivatemethods of the view model if implemented.hostallows providing the element which will parent the dialog - if not provided the document body will be used.rendererallows providing a custom renderer to be used, instead of the pre-registered one. This allows a single dialog service to be able to use multiple renderers for different purposes: default for modals, and a different one for notifications, alert etc...containerallows specifying the DI Container instance to be used for the dialog. If not provided a new child container will be created from the root one.rejectOnCancelis a boolean that must be set totrueif cancellations should be treated as rejection. The reason will be anIDialogCancelError- the propertywasCancelledwill be set totrueand if cancellation data was provided it will be set to thevalueproperty.optionsoptions passed to the renderer.
Renderer Options Reference
Each renderer supports different configuration options. Here are the complete option sets:
Standard Renderer Options (DialogRenderOptionsStandard)
DialogRenderOptionsStandard)interface DialogRenderOptionsStandard {
modal?: boolean; // Default: true
overlayStyle?: string | Partial<CSSStyleDeclaration>;
show?: (dom: DialogDomStandard) => void | Promise<void>;
hide?: (dom: DialogDomStandard) => void | Promise<void>;
closedby?: 'any' | 'closerequest' | 'none';
}Option Details:
modal: Controls whether the dialog opens as modal (showModal()) or modeless (show()). Modal dialogs block interaction with the rest of the page and display a backdrop.overlayStyle: CSS styling for the::backdroppseudo-element (only applies whenmodal: true). Can be a CSS string or style object.show: Animation callback called when dialog is shown. Return a Promise to wait for animations.hide: Animation callback called when dialog is hidden. Return a Promise to wait for animations.closedby: HTML5 dialog attribute controlling which user actions can close the dialog. See MDN documentation.
Default Global Settings:
{
modal: true,
rejectOnCancel: false
}Classic Renderer Options (DialogRenderOptionsClassic)
DialogRenderOptionsClassic)interface DialogRenderOptionsClassic {
lock?: boolean; // Default: true
keyboard?: DialogActionKey[]; // ['Escape', 'Enter'] when lock: false, [] when lock: true
mouseEvent?: 'click' | 'mouseup' | 'mousedown'; // Default: 'click'
overlayDismiss?: boolean; // Default: !lock
startingZIndex?: number; // Default: 1000
show?: (dom: DialogDomClassic) => void | Promise<void>;
hide?: (dom: DialogDomClassic) => void | Promise<void>;
}
type DialogActionKey = 'Escape' | 'Enter';Option Details:
lock: Whentrue, prevents dialog dismissal via ESC key or overlay clicks. Takes precedence overkeyboardandoverlayDismiss.keyboard: Array of keys that close the dialog.'Escape'cancels,'Enter'confirms. Whenlock: false, defaults to['Enter', 'Escape']. Whenlock: true, defaults to[].mouseEvent: Which mouse event type triggers overlay dismiss logic.overlayDismiss: Whentrue, clicking outside the dialog cancels it. Defaults to!lockvalue.startingZIndex: Base z-index for dialog wrapper and overlay elements.show/hide: Animation callbacks with access toDialogDomClassicinstance.
Default Global Settings:
{
lock: true,
startingZIndex: 1000,
rejectOnCancel: false
}DOM Structure Comparison
Uses the native HTML5 <dialog> element:
<dialog> <!-- HTMLDialogElement, root -->
<div> <!-- contentHost -->
<!-- Your component content renders here -->
</div>
</dialog>Characteristics:
Native modal behavior with
showModal()Built-in ESC key handling
Native
::backdroppseudo-elementAccessibility features built-in
Limited styling control of backdrop
Uses traditional light DOM elements:
<au-dialog-container> <!-- wrapper/root -->
<au-dialog-overlay> <!-- overlay for backdrop -->
</au-dialog-overlay>
<div> <!-- contentHost -->
<!-- Your component content renders here -->
</div>
</au-dialog-container>Characteristics:
Full styling control over all elements
Custom event management system
Z-index stacking control
Compatible with older browsers
Similar to Aurelia v1 behavior
Service & Controller APIs
IDialogService Interface
interface IDialogService {
readonly controllers: IDialogController[];
/**
* Opens a new dialog.
* @param settings - Dialog settings for this dialog instance.
* @returns DialogOpenPromise - Promise resolving to dialog controller and open result
*/
open<TOptions, TModel = any, TComponent extends object = any>(
settings: IDialogSettings<TOptions, TModel, TComponent>
): DialogOpenPromise;
/**
* Closes all open dialogs at the time of invocation.
* @returns Promise<IDialogController[]> - Controllers whose close operation was cancelled
*/
closeAll(): Promise<IDialogController[]>;
}IDialogController Interface
interface IDialogController {
readonly settings: IDialogLoadedSettings;
/**
* A promise that will be fulfilled once this dialog has been closed
*/
readonly closed: Promise<DialogCloseResult>;
ok(value?: unknown): Promise<DialogCloseResult<'ok'>>;
cancel(value?: unknown): Promise<DialogCloseResult<'cancel'>>;
error(value?: unknown): Promise<void>;
}DialogOpenPromise Interface
The open() method returns a special promise with additional methods:
interface DialogOpenPromise extends Promise<DialogOpenResult> {
/**
* Add a callback that will be invoked when a dialog has been closed
*/
whenClosed<TResult1, TResult2>(
onfulfilled?: (value: DialogCloseResult) => TResult1 | Promise<TResult1>,
onrejected?: (reason: unknown) => TResult2 | Promise<TResult2>
): Promise<TResult1 | TResult2>;
}Dialog Settings Interface
Complete settings interface with all available options:
interface IDialogSettings<TOptions = any, TModel = any, TComponent extends object = object> {
/**
* Custom renderer for the dialog
*/
renderer?: Constructable<IDialogDomRenderer<TOptions>> | IDialogDomRenderer<TOptions>;
/**
* Component constructor, instance, or function returning component/promise
*/
component?: CustomElementType<Constructable<TComponent>>
| Constructable<TComponent>
| (() => (Constructable<TComponent> | TComponent | Promise<TComponent | Constructable<TComponent>>));
/**
* Template string, Element, or function returning template/promise
*/
template?: string | Element | Promise<string | Element>
| (() => string | Element | Promise<string | Element>);
/**
* Data passed to component lifecycle hooks
*/
model?: TModel;
/**
* Element that will parent the dialog (defaults to document.body)
*/
host?: Element;
/**
* DI container for dialog creation (child container created if not provided)
*/
container?: IContainer;
/**
* Renderer-specific configuration options
*/
options?: TOptions;
/**
* When true, cancellation is treated as promise rejection
*/
rejectOnCancel?: boolean;
}An important feature of the dialog plugin is that it is possible to resolve and close a dialog (using cancel/ok/error methods on the controller) in the same context where it's open.
Example of controlling the opening and closing of a dialog in promise style:
import { EditPerson } from './edit-person'; import { IDialogService, DialogDeactivationStatuses } from '@aurelia/dialog'; export class Welcome { static inject = [IDialogService]; person = { firstName: 'Wade', middleName: 'Owen', lastName: 'Watts' }; constructor(dialogService) { this.dialogService = dialogService; } submit() { this.dialogService .open({ component: () => EditPerson, model: this.person }) .then(openDialogResult => { // Note: // We get here when the dialog is opened, // and we are able to close dialog setTimeout(() => { openDialogResult.dialog.cancel('Failed to finish editing after 3 seconds'); }, 3000); // each dialog controller should expose a promise for attaching callbacks // to be executed for when it has been closed return openDialogResult.dialog.closed; }) .then((response) => { if (response.status === 'ok') { console.log('good'); } else { console.log('bad'); } console.log(response); }); } }Example of controlling the opening and closing of a dialog using
async/await:import { EditPerson } from './edit-person'; import { IDialogService, DialogDeactivationStatuses } from '@aurelia/dialog'; export class Welcome { static inject = [IDialogService]; person = { firstName: 'Wade', middleName: 'Owen', lastName: 'Watts' }; constructor(dialogService) { this.dialogService = dialogService; } async submit() { const { dialog } = await this.dialogService.open({ component: () => EditPerson, model: this.person }); // Note: // We get here when the dialog is opened, // and we are able to close dialog setTimeout(() => { dialog.cancel('Failed to finish editing after 3 seconds'); }, 3000); const response = await dialog.closed; if (response.status === 'ok') { console.log('good'); } else { console.log('bad'); } console.log(response); } }
By default, when an application is destroyed, the dialog service of that application will also try to close all the open dialogs that are registered with it via closeAll method. It can also be used whenever there's a need to forcefully close all open dialogs, as per following example:
Given an error list, open a dialog for each error, and close all of them after 5 seconds.
import { Alert } from './dialog-alert';
import { IDialogService, DialogDeactivationStatuses } from '@aurelia/dialog';
export class Welcome {
static inject = [IDialogService];
constructor(dialogService) {
this.dialogService = dialogService;
}
notifyErrors(errors) {
// for each of the error in the given error
errors.forEach(error => {
this.dialogService.open({ component: () => Alert, model: error });
});
setTimeout(() => this.dialogService.closeAll(), 5000);
}
}If there's no need for the opening result of a dialog, and only the response of it after the dialog has been closed, there is a whenClosed method exposed on the returned promise of the open method of the dialog service, that should help reduce some boilerplate code, per following example:
import { EditPerson } from './edit-person';
import { IDialogService, DialogDeactivationStatuses } from '@aurelia/dialog';
export class Welcome {
static inject = [IDialogService];
person = { firstName: 'Wade', middleName: 'Owen', lastName: 'Watts' };
constructor(dialogService) {
this.dialogService = dialogService;
}
submit() {
this.dialogService
.open({ component: () => EditPerson, model: this.person })
.whenClosed(response => {
console.log('The edit dialog has been closed');
if (response.status === 'ok') {
console.log('good');
} else {
console.log('bad');
}
console.log(response);
})
.catch(err => {
console.log('Failed to edit person information');
});
}
}Template Only Dialogs
The dialog service supports rendering dialogs with only template specified. A template only dialog can be open like the following examples:
dialogService.open({
template: () => fetch('https://some-server.com/alert-dialog.html').then(r => r.text()),
template: () => '<div>Welcome to Aurelia</div>',
template: '<div>Are you ready?</div>'
})Retrieving the dialog controller
By default, the dialog controller of a dialog will be assigned automatically to the property $dialog on the component view model. To specify this in TypeScript, the component class can implement the interface IDialogCustomElementViewModel:
import { IDialogController, IDialogCustomElementViewModel } from '@aurelia/dialog';
class MyDialog implements IDialogCustomElementViewModel {
$dialog: IDialogController;
closeDialog() {
this.$dialog.ok('All good!');
}
}Note that the property $dialog will only be ready after the contructor.
If it's desirable to retrieve the associated dialog controller of a dialog during the constructor of the component, IDialogController can be inject to achieve the same effect:
import { resolve } from 'aurelia';
import { IDialogController } from '@aurelia/dialog';
class MyDialog {
constructor() {
// change some settings
resolve(IDialogController).settings.zIndex = 100;
}
}This means it's also possible to control the dialog from template only dialog via the $dialog property. An example of this is: Open an alert dialog, and display an "Ok" button to close it, without using any component:
dialogService.open({
template: `<div>
Please check the oven!
<button click.trigger="$dialog.ok()">Close and check</button>
</div>`
})The Default Dialog Renderer
By default, the dialog DOM structure is rendered as follow:
> (1) Dialog host element
> (2) Dialog Wrapper Element
> (3) Dialog Overlay Element
> (4) Dialog Content Host ElementThe Dialog host element is the target where an application chooses to add the dialog to, this is normally the document body, if not supplied in the settings of the open method of the dialog service.
An example of the html structure when document body is the dialog host:
<body>
<au-dialog-container> <!-- wrapper -->
<au-dialog-overlay> <!-- overlay -->
<div> <!-- dialog content host -->Centering/Uncentering dialog position
By default, the dialog content host is centered horizontally and vertically. It can be changed via IDialogDom injection:
import { resolve } from 'aurelia';
import { IDialogDom, DefaultDialogDom } from '@aurelia/dialog';
export class MyDialog {
constructor(dialogDom: DefaultDialogDom = resolve(IDialogDom)) {
dialogDom.contentHost.style.margin = "0 auto"; // only center horizontally
}
}Styling the overlay
For the standard implementation
The overlay of a
<dialog>element is only "rendered" when it's shown as modal. Which means the following example will not "trigger" any overlay:dialogService.open({ component: Alert, options: { overlayStyle: 'rgba(0,0,0,0.5)' } })as
options.modalvalue is nottrue.When
options.modalistrue, the overlay can be specified usingoptions.overlayStylelike the following example:dialogService.open({ component: Alert, options: { modal: true, overlayStyle: 'rgba(0,0,0,0.5)' } })Note that the overlay of a modal dialog dom can also be configured from the dialog dom itself, like the following example:
import { resolve } from 'aurelia'; import { IDialogDom, DialogDomStandard } from '@aurelia/dialog'; export class MyDialog { constructor(dialogDom = resolve(IDialogDom) as DialogDomStandard) { dialogDom.setOverlayStyle("rgba(0, 0, 0, 0.5)"); } }For the classic implementation
By default, the overlay of a dialog is transparent. Though it's often desirable to add 50% opacity and a background color of black to the modal. To achieve this in dialog, retrieve the
IDialogDominstance and modify theoverlayelementstyle:import { resolve } from 'aurelia'; import { IDialogDom, DefaultDialogDom } from '@aurelia/dialog'; export class MyDialog { constructor(dialogDom: DefaultDialogDom = resolve(IDialogDom)) { dialogDom.overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; } }
Creating Custom Dialog Renderers
The dialog plugin's modular architecture makes it easy to create custom renderers for specific UI frameworks, design systems, or unique requirements. A renderer is responsible for creating the DOM structure and managing the dialog's presentation.
Understanding the Renderer Interface
All dialog renderers must implement the IDialogDomRenderer<TOptions> interface:
interface IDialogDomRenderer<TOptions = any> {
render(dialogHost: Element, controller: IDialogController, options: TOptions): IDialogDom;
}The renderer's render method returns an IDialogDom object that provides the dialog's DOM structure:
interface IDialogDom extends IDisposable {
readonly contentHost: HTMLElement; // Container for dialog content
show?(): void | Promise<void>; // Optional show animation
hide?(): void | Promise<void>; // Optional hide animation
}Note: The IDialogDom interface only requires contentHost. The Standard and Classic implementations extend this with their own properties (root, overlay, etc.).
Example: Creating a Material Design Renderer
Let's create a custom renderer that follows Material Design principles:
import { DI, IDisposable } from 'aurelia';
import { IDialogDomRenderer, IDialogDom, IDialogController } from '@aurelia/dialog';
// Define custom options for your renderer
interface MaterialDialogOptions {
elevation?: 1 | 2 | 3 | 4 | 5;
fullscreen?: boolean;
persistent?: boolean;
showBackdrop?: boolean;
}
// Custom DOM implementation
class MaterialDialogDom implements IDialogDom {
// IDialogDom interface requirement
public readonly contentHost: HTMLElement;
// Additional properties for this implementation
public readonly root: HTMLElement;
public readonly overlay: HTMLElement | null;
constructor(
root: HTMLElement,
overlay: HTMLElement | null,
contentHost: HTMLElement,
private readonly host: Element,
private readonly options: MaterialDialogOptions
) {
this.root = root;
this.overlay = overlay;
this.contentHost = contentHost;
}
async show(): Promise<void> {
// Material Design entrance animation
const animation = this.root.animate([
{
transform: 'translateY(100px) scale(0.8)',
opacity: 0
},
{
transform: 'translateY(0) scale(1)',
opacity: 1
}
], {
duration: 225,
easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)'
});
if (this.overlay) {
this.overlay.animate([
{ opacity: 0 },
{ opacity: 1 }
], { duration: 150 });
}
await animation.finished;
}
async hide(): Promise<void> {
// Material Design exit animation
const animation = this.root.animate([
{
transform: 'translateY(0) scale(1)',
opacity: 1
},
{
transform: 'translateY(100px) scale(0.8)',
opacity: 0
}
], {
duration: 195,
easing: 'cubic-bezier(0.4, 0.0, 1, 1)'
});
if (this.overlay) {
this.overlay.animate([
{ opacity: 1 },
{ opacity: 0 }
], { duration: 195 });
}
await animation.finished;
}
dispose(): void {
this.host.removeChild(this.root);
if (this.overlay) {
this.host.removeChild(this.overlay);
}
}
}
// Custom renderer implementation
export class MaterialDialogRenderer implements IDialogDomRenderer<MaterialDialogOptions> {
render(host: Element, controller: IDialogController, options: MaterialDialogOptions): IDialogDom {
// Create backdrop if requested
let overlay: HTMLElement | null = null;
if (options.showBackdrop !== false) {
overlay = document.createElement('div');
overlay.className = 'material-dialog-backdrop';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.32);
z-index: 1000;
`;
// Handle backdrop clicks
if (!options.persistent) {
overlay.addEventListener('click', () => controller.cancel());
}
host.appendChild(overlay);
}
// Create main dialog container
const root = document.createElement('div');
root.className = 'material-dialog-container';
root.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
min-width: 280px;
max-width: 560px;
max-height: calc(100vh - 64px);
`;
// Create content host with Material Design styling
const contentHost = document.createElement('div');
contentHost.className = 'material-dialog-content';
const elevation = options.elevation || 3;
const boxShadow = [
'0 3px 1px -2px rgba(0,0,0,.2)',
'0 2px 2px 0 rgba(0,0,0,.14)',
'0 1px 5px 0 rgba(0,0,0,.12)'
][elevation - 1] || '0 8px 10px 1px rgba(0,0,0,.14)';
contentHost.style.cssText = `
background: white;
border-radius: 4px;
box-shadow: ${boxShadow};
overflow: hidden;
${options.fullscreen ? 'width: 100vw; height: 100vh; border-radius: 0;' : ''}
`;
root.appendChild(contentHost);
host.appendChild(root);
// Handle ESC key for non-persistent dialogs
if (!options.persistent) {
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
controller.cancel();
}
};
document.addEventListener('keydown', handleKeydown);
// Clean up listener when dialog is disposed
const originalDispose = MaterialDialogDom.prototype.dispose;
MaterialDialogDom.prototype.dispose = function() {
document.removeEventListener('keydown', handleKeydown);
originalDispose.call(this);
};
}
return new MaterialDialogDom(root, overlay, contentHost, host, options);
}
// Registration helper
static register(container: any) {
return container.register(
DI.createInterface<IDialogDomRenderer>('IDialogDomRenderer', x => x.singleton(MaterialDialogRenderer))
);
}
}Registering Your Custom Renderer
There are several ways to use your custom renderer:
1. As the Default Renderer
Replace the default renderer globally:
import { DialogConfiguration } from '@aurelia/dialog';
import { MaterialDialogRenderer } from './material-dialog-renderer';
Aurelia.register(
DialogConfiguration.customize(settings => {
settings.renderer = MaterialDialogRenderer;
// Set default options for your renderer
settings.options = {
elevation: 2,
showBackdrop: true,
persistent: false
};
})
).app(MyApp).start();2. Per-Dialog Renderer
Use a specific renderer for individual dialogs:
dialogService.open({
component: MyDialogComponent,
renderer: new MaterialDialogRenderer(),
options: {
elevation: 4,
fullscreen: false,
persistent: true
}
});3. Multiple Renderer Support
Register multiple renderers and choose them based on dialog type:
// In your dialog service wrapper
class MyDialogService {
constructor(private dialogService: IDialogService) {}
openAlert(message: string) {
return this.dialogService.open({
template: `<div>${message}</div>`,
renderer: new MaterialDialogRenderer(),
options: { elevation: 1, persistent: false }
});
}
openModal(component: any, model?: any) {
return this.dialogService.open({
component,
model,
renderer: new MaterialDialogRenderer(),
options: { elevation: 3, showBackdrop: true }
});
}
openFullscreen(component: any, model?: any) {
return this.dialogService.open({
component,
model,
renderer: new MaterialDialogRenderer(),
options: { fullscreen: true, persistent: true }
});
}
}Best Practices for Custom Renderers
TypeScript Support: Define strong types for your renderer options
Accessibility: Ensure proper ARIA attributes and focus management
Animation: Use CSS animations or Web Animations API for smooth transitions
Responsive Design: Handle different screen sizes appropriately
Cleanup: Always implement proper disposal in the
dispose()methodEvent Handling: Handle keyboard and mouse events according to your design system
Consistent API: Keep the same patterns as built-in renderers for familiarity
This modular approach allows you to integrate any UI framework (Bootstrap, Ant Design, Chakra UI, etc.) or create completely custom dialog behaviors while leveraging Aurelia's powerful dialog lifecycle management.
Error Handling & Promise Rejection
The dialog system includes comprehensive error handling for different scenarios:
DialogCancelError vs DialogCloseError
When rejectOnCancel: true is set, cancellations and errors result in different error types:
interface DialogError<T> extends Error {
wasCancelled: boolean;
value?: T;
}
// When user cancels dialog (ESC, overlay click, controller.cancel())
type DialogCancelError<T> = DialogError<T> & { wasCancelled: true };
// When dialog.error() is called
type DialogCloseError<T> = DialogError<T> & { wasCancelled: false };Example: Handling Different Outcomes
try {
const result = await dialogService.open({
component: ConfirmDialog,
model: { message: "Delete this item?" },
rejectOnCancel: true
});
// If we reach here, user clicked OK
console.log('Confirmed:', result.dialog.closed);
} catch (error) {
if (error.wasCancelled) {
// User cancelled (ESC, overlay click, Cancel button)
console.log('User cancelled dialog');
} else {
// Error occurred (dialog.error() was called)
console.error('Dialog error:', error);
}
}Using whenClosed for Non-Rejecting Pattern
If you prefer not to use try/catch, use whenClosed():
dialogService.open({
component: MyDialog,
rejectOnCancel: false // Default
}).whenClosed(result => {
if (result.status === 'ok') {
console.log('Dialog confirmed:', result.value);
} else if (result.status === 'cancel') {
console.log('Dialog cancelled:', result.value);
} else if (result.status === 'error') {
console.log('Dialog error occurred:', result.value);
}
});Animation
Using the IDialogDomAnimator
IDialogDomAnimatorIf you use either the default implementations of the interface IDialogRenderer, then the in/out animations of a dialog can be configured via show/hide the options settings, like the following examples:
Global animations for all dialogs:
Aurelia.register(DialogStandardConfiguration.customize(settings => {
settings.options.show = (dom) => dom.root.animate([{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { duration: 150 });
}))Different animation per dialog
.open()
dialogService.open({
component: SuccessNotification,
options: {
show: dom => dom.root.animate([{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { duration: 150 }),
hide: dom => dom.root.animate([{ transform: 'scale(1)' }, { transform: 'scale(0)' }], { duration: 150 }),
}
})Using component lifecycles
The lifecycles attaching and detaching can be used to animate a dialog, as in those lifecycles, if a promise is returned, it will be awaited during the activation/deactivation phases.
An example of animating a dialog on attaching and detaching, with the animation duration of 200 milliseconds:
import { resolve } from 'aurelia';
export class MyDialog {
host: Element = resolve(Element);
constructor(host: Element) {
this.host = host;
}
attaching() {
const animation = this.host.animate(
[{ transform: 'translateY(0px)' }, { transform: 'translateY(-300px)' }],
{ duration: 200 },
);
return animation.finished;
}
detaching() {
const animation = this.host.animate(
[{ transform: 'translateY(-300px)' }, { transform: 'translateY(0)' }],
{ duration: 200 },
);
return animation.finished;
}
}Component Lifecycles With The Dialog Plugin
In adition to the lifecycle hooks defined in the core templating, the dialog defines additional ones. All dialog specific hooks can return a Promise, that resolves to the appropriate value for the hook, and will be awaited.
.canActivate()
.canActivate()This hook can be used to cancel the opening of a dialog. It is invoked with one parameter - the value of the model setting passed to .open(). To cancel the opening of the dialog return false - null and undefined will be coerced to true.
.activate()
.activate()This hook can be used to do any necessary init work. The hook is invoked with one parameter - the value of the model setting passed to .open().
.canDeactivate(result: DialogCloseResult)
.canDeactivate(result: DialogCloseResult)This hook can be used to cancel the closing of a dialog. To do so return false - null and undefined will be coerced to true. The passed in result parameter has a property status, indicating if the dialog was closed or cancelled, or the deactivation process itself has been aborted, and an value property with the dialog result which can be manipulated before dialog deactivation.
The DialogCloseResult has the following interface:
class DialogCloseResult<T extends DialogDeactivationStatuses = DialogDeactivationStatuses, TVal = unknown> {
readonly status: T;
readonly value?: TVal;
}
type DialogDeactivationStatuses = 'ok' | 'error' | 'cancel' | 'abort';Warning When the
errormethod of aDialogControlleris called this hook will be skipped.
.deactivate(result: DialogCloseResult)
.deactivate(result: DialogCloseResult)This hook can be used to do any clean up work. The hook is invoked with one result parameter that has a property status, indicating if the dialog was closed (Ok) or cancelled (Cancel), and an value property with the dialog result.
Lifecycle Execution Order
Each dialog instance goes through the complete lifecycle once:
Aurelia v1 to v2 Migration
Quick Migration Steps
Change your registration:
// v1 .plugin(PLATFORM.moduleName('aurelia-dialog')) // v2 .register(DialogConfigurationClassic)Update property names:
v1 Propertyv2 PropertyNotesviewModelcomponentSame functionality
viewtemplateSame functionality
controller$dialogProperty on view model
closeResultdialog.closedNow a promise on controller
Move renderer options:
// v1 - options on dialog settings dialogService.open({ viewModel: MyDialog, lock: true, keyboard: true, overlayDismiss: false }); // v2 - options moved to 'options' object dialogService.open({ component: MyDialog, options: { lock: true, keyboard: ['Escape', 'Enter'], overlayDismiss: false } });
Breaking Changes Detail
DialogService.open() Changes:
// v1 Result
interface DialogOpenResult {
wasCancelled: boolean;
controller: DialogController;
closeResult: Promise<DialogCloseResult>;
}
// v2 Result
interface DialogOpenResult {
wasCancelled: boolean;
dialog: IDialogController; // renamed from 'controller'
// closeResult removed - use dialog.closed instead
}DialogCloseResult Changes:
// v1
interface DialogCloseResult {
wasCancelled: boolean;
output?: unknown;
}
// v2
interface DialogCloseResult {
status: 'ok' | 'cancel' | 'error' | 'abort'; // more specific
value?: unknown; // renamed from 'output'
}Boolean to Array Format:
// v1 - Boolean options
{
keyboard: true, // Allow ESC to close
keyboard: false, // No keyboard interaction
keyboard: 'Enter' // String format
}
// v2 - Array format only
{
keyboard: ['Escape'], // ESC cancels dialog
keyboard: ['Enter'], // ENTER confirms dialog
keyboard: ['Enter', 'Escape'], // Both keys work
keyboard: [] // No keyboard interaction
}Boolean and string variants for keyboard are no longer supported in v2.
Controller Property:
// v1 - 'controller' property
export class MyDialog {
controller: DialogController; // Injected automatically
close() {
this.controller.ok('result');
}
}
// v2 - '$dialog' property
export class MyDialog {
$dialog: IDialogController; // Assigned automatically
close() {
this.$dialog.ok('result');
}
}TypeScript Support:
// v2 - Implement interface for type safety
import { IDialogCustomElementViewModel } from '@aurelia/dialog';
export class MyDialog implements IDialogCustomElementViewModel {
readonly $dialog: IDialogController; // Type-safe property
}Complete Migration Example
Migration Strategy:
Start with
DialogConfigurationClassicfor immediate compatibilityTest all existing dialogs work correctly
Gradually migrate to
DialogConfigurationStandardfor modern featuresConsider custom renderers for UI framework integration
Last updated
Was this helpful?