fs-dialog
Component-agnostic dialog stack with error middleware.
npm install @script-development/fs-dialogPeer dependencies: vue ^3.5.0
What It Does
fs-dialog manages a LIFO stack of modal dialogs. It handles stacking, backdrop behavior, scroll locking, and error capture. You provide your own Vue components — the service manages the lifecycle.
Basic Usage
1. Create the Service
import { createDialogService } from "@script-development/fs-dialog";
const dialog = createDialogService();2. Mount the Container
<!-- App.vue -->
<template>
<div id="app">
<router-view />
<dialog.DialogContainerComponent />
</div>
</template>3. Open Dialogs
import ConfirmDialog from "@/components/ConfirmDialog.vue";
dialog.open(ConfirmDialog, {
title: "Delete user?",
message: "This action cannot be undone.",
onConfirm: () => deleteUser(userId),
});Props are type-checked against your component's definitions — same pattern as fs-toast.
Stack Behavior
Dialogs are managed as a LIFO stack (last in, first out). Opening a new dialog pushes it on top of the stack:
dialog.open(SettingsDialog, {
/* ... */
}); // stack: [Settings]
dialog.open(ConfirmDialog, {
/* ... */
}); // stack: [Settings, Confirm]
// Confirm is on top, Settings is behind itEach dialog renders inside a native <dialog> element using showModal(), which provides:
- Backdrop — clicking outside the topmost dialog is detected
- Scroll lock — body scrolling is disabled while dialogs are open
- Focus trapping — keyboard focus stays within the dialog
- ESC key handling — managed by the service, not the browser default
Closing Dialogs
Close All
closeAll() clears the entire stack:
dialog.closeAll();Close from Within
Your dialog component can close itself. A common pattern is to accept callback props:
<!-- ConfirmDialog.vue -->
<script setup lang="ts">
const props = defineProps<{
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
}>();
</script>
<template>
<div class="dialog">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<button @click="onConfirm">Confirm</button>
<button @click="onCancel">Cancel</button>
</div>
</template>dialog.open(ConfirmDialog, {
title: "Delete?",
message: "This cannot be undone.",
onConfirm: () => {
deleteUser(userId);
dialog.closeAll();
},
onCancel: () => dialog.closeAll(),
});Error Middleware
Errors thrown inside dialog components are caught via Vue's onErrorCaptured. You can register middleware to handle them:
dialog.registerErrorMiddleware((error, { closeAll }) => {
if (error instanceof ValidationError) {
showValidationFeedback(error);
return false; // stop propagation — error is handled
}
// return true to pass the error to the next middleware
return true;
});Multiple middleware handlers form a pipeline. Return false to stop propagation, true to pass the error to the next handler.
Combining with fs-http error middleware
A powerful pattern: register HTTP error middleware that opens an error dialog, and register dialog error middleware that handles errors within dialogs. The two systems compose naturally:
// HTTP errors → open error dialog
http.registerResponseErrorMiddleware((error) => {
if (error.response?.status === 403) {
dialog.open(ForbiddenDialog, { message: "Access denied" });
}
});
// Errors inside dialogs → handle gracefully
dialog.registerErrorMiddleware((error, { closeAll }) => {
console.error("Dialog error:", error);
closeAll();
return false;
});Async Components
Dialog content is wrapped in <Suspense>, so you can use async setup in your dialog components:
// Lazy-loaded dialog — only fetched when opened
dialog.open(
defineAsyncComponent(() => import("@/components/HeavyDialog.vue")),
{ id: 42 },
);v-model Synchronization
The service supports v-model prop updates — if your dialog emits update:modelValue events, the internal state stays in sync.
API Reference
createDialogService()
Returns a dialog service. No parameters.
Service Properties
| Property | Type | Description |
|---|---|---|
open(component, props) | (component, props) => void | Push a dialog onto the stack |
closeAll() | () => void | Clear the entire stack |
registerErrorMiddleware(handler) | (handler) => UnregisterMiddleware | Register an error handler |
DialogContainerComponent | Component | Mount this in your app root |
Error Handler Signature
type DialogErrorHandler = (error: Error, context: { closeAll: () => void }) => boolean; // false = handled, true = pass to next handler