useDialogStore
A Pinia store for managing dynamic dialog state across your application. This store provides a centralized way to open, close, and manage multiple dialogs with lazy-loaded components.
Auto-Setup with Plugin (Recommended)
The easiest way to use the dialog store is with the ftxDialogPlugin. It automatically registers and mounts the FTxDynamicDialog component, so you don't need to add it manually to your app layout.
Quasar Boot File Setup
For Quasar projects, create a boot file:
// src/boot/ftx-dialog.js
import { boot } from 'quasar/wrappers'
import { ftxDialogPlugin } from '@ftx/ui'
export default boot(({ app }) => {
app.use(ftxDialogPlugin)
})Then register it in quasar.config.js:
// quasar.config.js
module.exports = function (ctx) {
return {
boot: [
// ... other boot files
'ftx-dialog'
],
// ... rest of config
}
}That's it! The plugin automatically:
- Registers the
FTxDynamicDialogcomponent - Mounts it to your app (defaults to
body) - Shares the same Pinia instance so dialogs work seamlessly
- Handles all dialog rendering automatically
Manual Setup (Alternative)
If you prefer to manually control where the dialog component is rendered, you can use FTxDynamicDialog directly:
<template>
<div id="app">
<!-- Your app content -->
<router-view />
<!-- Render all active dialogs -->
<FTxDynamicDialog />
</div>
</template>
<script setup>
import { FTxDynamicDialog } from '@ftx/ui';
</script>The component automatically:
- Renders all dialogs in the
dialogStack(active dialogs) - Passes the
is-dialogprop to identify dialog components - Only renders dialogs when their components are loaded in
dialogComponent
Note: The FTxDynamicDialog component only passes the is-dialog prop to dialog components. Dialog components should access properties from the store using useDialogStore() and getProperties(name) if they need to receive properties from the store. Alternatively, properties can be passed directly by projects in their dialog component implementations.
Installation
The store is exported from @ftx/ui:
import { useDialogStore } from '@ftx/ui';Setup
Option 1: Register Component Map (Recommended for frequently used dialogs)
You can optionally register your dialog components upfront in your application initialization (e.g., main.js, boot file, or a store setup file):
import { useDialogStore } from '@ftx/ui';
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);
// Optionally register your dialog components
const dialogStore = useDialogStore();
dialogStore.registerComponentMap({
PriceListForm: () => import('src/modules/main/price-list/components/PriceListForm.vue'),
CustomerGroupForm: () => import('src/modules/main/customer-group/components/CustomerGroupForm.vue'),
// ... add frequently used dialog components
});Option 2: On-Demand Import (No registration needed)
You can also import components on-demand by providing an importPath when opening a dialog. This is useful for dialogs that are used infrequently or loaded dynamically.
Usage
Using Registered Components
<template>
<div>
<q-btn label="Open Dialog" @click="openDialog" />
<q-btn label="Close Dialog" @click="closeDialog" />
</div>
</template>
<script setup>
import { useDialogStore } from '@ftx/ui';
const dialogStore = useDialogStore();
async function openDialog() {
// If component is registered in componentMap, just use the name
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
id: 123,
mode: 'edit'
}
});
}
function closeDialog() {
dialogStore.closeDialog('ProductForm');
}
</script>Using On-Demand Import
<template>
<div>
<q-btn label="Open Dialog" @click="openDialog" />
</div>
</template>
<script setup>
import { useDialogStore } from '@ftx/ui';
const dialogStore = useDialogStore();
async function openDialog() {
// Import component on-demand without pre-registration
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
importPath: 'src/components/ProductForm.vue', // Dynamic import path
properties: {
id: 123,
mode: 'edit'
}
});
}
</script>Note: The store will automatically check if a component is already loaded, then check if it's registered in componentMap, and finally use importPath if provided. Components are cached after first load for better performance.
API Reference
State
| Property | Type | Description |
|---|---|---|
dialogStack | Array | Array of active dialogs |
dialogComponent | Object | Cached dialog components |
componentMap | Object | Registered component loaders |
Getters
getDialog(name)
Returns a dialog object by name, or null if not found.
const dialog = dialogStore.getDialog('ProductForm');
// Returns: { name: 'ProductForm', properties: {...} } or nullgetProperties(name)
Returns the properties of a dialog by name, or null if not found.
const props = dialogStore.getProperties('ProductForm');
// Returns: { id: 123, mode: 'edit' } or nullActions
registerComponentMap(componentMap)
Registers a map of dialog names to component import functions. This allows the store to lazy-load dialog components on demand.
Parameters:
componentMap(Object) - Object where keys are dialog names and values are functions that return component imports
Example:
dialogStore.registerComponentMap({
MyDialog: () => import('src/components/MyDialog.vue'),
AnotherDialog: () => import('src/components/AnotherDialog.vue')
});unregisterComponent(name)
Unregisters a single component from the component map. This removes the component from the registered components but does not affect already loaded components.
Parameters:
name(String, required) - Name of the component to unregister
Example:
dialogStore.unregisterComponent('MyDialog');clearComponentMap()
Clears all registered components from the component map. This removes all registrations but does not affect already loaded components.
Example:
dialogStore.clearComponentMap();getRegisteredComponents()
Returns an array of all registered component names.
Returns:
string[]- Array of registered component names
Example:
const components = dialogStore.getRegisteredComponents();
// Returns: ['ProductForm', 'CustomerForm', 'TaxTypeForm', ...]
// Check if a component is registered
if (dialogStore.getRegisteredComponents().includes('ProductForm')) {
console.log('ProductForm is registered');
}Component Map Management Example:
// Register multiple components
dialogStore.registerComponentMap({
ProductForm: () => import('src/components/ProductForm.vue'),
CustomerForm: () => import('src/components/CustomerForm.vue'),
TaxForm: () => import('src/components/TaxForm.vue')
});
// List all registered components
console.log(dialogStore.getRegisteredComponents());
// Output: ['ProductForm', 'CustomerForm', 'TaxForm']
// Unregister a specific component
dialogStore.unregisterComponent('TaxForm');
// Check remaining components
console.log(dialogStore.getRegisteredComponents());
// Output: ['ProductForm', 'CustomerForm']
// Clear all registrations
dialogStore.clearComponentMap();
console.log(dialogStore.getRegisteredComponents());
// Output: []Note: Unregistering or clearing the component map does not affect already loaded components in dialogComponent. It only prevents future imports from using those registrations.
Component Map vs Loaded Components:
- Component Map (
componentMap): Registry of import functions - tells the store HOW to import components - Loaded Components (
dialogComponent): Registry of already imported components - prevents re-import errors and Vue component conflicts
// Register how to import (component map)
dialogStore.registerComponentMap({
ProductForm: () => import('src/components/ProductForm.vue')
});
// Component gets loaded when first opened
await dialogStore.updateDialog({ name: 'ProductForm', value: true });
// Now ProductForm is in dialogComponent registry
// Component stays loaded to prevent Vue import errors
// Once loaded, it's reused for all subsequent opens
await dialogStore.updateDialog({ name: 'ProductForm', value: true });Important: Components are kept in the registry once loaded. Unloading and re-importing the same component can cause Vue errors, so components should remain loaded for the lifetime of the application.
updateDialog({ name, value, properties, importPath })
Opens or closes a dialog and manages its state. Components are lazy-loaded on first open.
Parameters:
name(String, required) - Name of the dialogvalue(Boolean, default:true) - Whether to open (true) or close (false) the dialogproperties(Object, optional) - Properties to pass to the dialog component (can include functions, objects, primitives, etc.)importPath(String, optional) - Import path for on-demand component loading (used if component is not registered in componentMap)
Properties with Functions: The properties parameter can include functions, making it useful for callbacks:
await dialogStore.updateDialog({
name: 'MyDialog',
value: true,
properties: {
callback: (data) => {
form.value.customerGroupId = data
},
id: 123,
config: { option: true }
}
})Component Loading Priority:
- If component is already loaded (cached), it's reused
- If component is registered in
componentMap, it's imported from there - If
importPathis provided, component is imported dynamically - If none of the above, the dialog is still added to the stack (component might already be loaded elsewhere or will be handled by the project)
Note: If a component is not registered and no importPath is provided, the dialog will still be added to the stack. This allows projects to load components in other ways (e.g., globally registered components, already imported elsewhere). The dialog will render once the component becomes available in dialogComponent.
Example:
// Open a dialog with registered component
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
id: 123,
title: 'Edit Product'
}
});
// Open a dialog with on-demand import
await dialogStore.updateDialog({
name: 'CustomDialog',
value: true,
importPath: 'src/components/CustomDialog.vue',
properties: {
data: someData
}
});
// Close a dialog
await dialogStore.updateDialog({
name: 'ProductForm',
value: false
});closeDialog(name)
Closes a dialog by name.
Parameters:
name(String, required) - Name of the dialog to close
Example:
dialogStore.closeDialog('ProductForm');resetAllDialogs()
Closes all dialogs and clears the dialog stack.
Example:
await dialogStore.resetAllDialogs();isComponentLoaded(name)
Checks if a component is currently loaded in the registry.
Parameters:
name(String, required) - Component name to check
Returns:
boolean-trueif component is loaded,falseotherwise
Example:
if (dialogStore.isComponentLoaded('ProductForm')) {
console.log('ProductForm is already loaded');
} else {
console.log('ProductForm needs to be imported');
}Note: Components are kept in the registry once loaded to prevent Vue import errors. They should not be unloaded as re-importing the same component can cause Vue errors.
Complete Example
App Initialization
Quasar Boot File (Recommended)
// src/boot/ftx-dialog.js
import { boot } from 'quasar/wrappers'
import { useDialogStore, ftxDialogPlugin } from '@ftx/ui'
export default boot(({ app }) => {
// Auto-setup dialog system (automatically registers FTxDynamicDialog)
app.use(ftxDialogPlugin)
// Optionally register frequently used dialog components
const dialogStore = useDialogStore()
dialogStore.registerComponentMap({
ProductForm: () => import('src/components/ProductForm.vue'),
CustomerForm: () => import('src/components/CustomerForm.vue'),
// Note: Other dialogs can be imported on-demand without registration
})
})Register in quasar.config.js:
// quasar.config.js
module.exports = function (ctx) {
return {
boot: [
'ftx-dialog', // Add this
// ... other boot files
],
// ... rest of config
}
}Component Usage
Using FTxDynamicDialog Component (Recommended)
The easiest way to render dialogs is using the FTxDynamicDialog component:
<template>
<div>
<q-btn label="Add Product" @click="openProductForm" />
<q-btn label="Edit Product" @click="editProduct" />
<q-btn label="Open Custom Dialog" @click="openCustomDialog" />
<!-- Render all active dialogs automatically -->
<FTxDynamicDialog />
</div>
</template>
<script setup>
import { FTxDynamicDialog, useDialogStore } from '@ftx/ui';
const dialogStore = useDialogStore();
// Using registered component (from componentMap)
async function openProductForm() {
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
mode: 'create'
}
});
}
// Using registered component with different properties
async function editProduct() {
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
mode: 'edit',
id: 123
}
});
}
// Using on-demand import (not registered in componentMap)
async function openCustomDialog() {
await dialogStore.updateDialog({
name: 'CustomDialog',
value: true,
importPath: './components/CustomDialog.vue', // Import on-demand
properties: {
title: 'Custom Dialog',
data: someData
}
});
}
</script>Manual Rendering (Alternative)
If you need more control, you can manually render dialogs:
<template>
<div>
<q-btn label="Add Product" @click="openProductForm" />
<!-- Render dialogs manually -->
<template v-for="dialog in dialogStore.dialogStack" :key="dialog.name">
<component
:is="dialogStore.dialogComponent[dialog.name]"
v-if="dialogStore.dialogComponent[dialog.name]"
v-bind="dialog.properties"
@close="dialogStore.closeDialog(dialog.name)"
/>
</template>
</div>
</template>
<script setup>
import { useDialogStore } from '@ftx/ui';
const dialogStore = useDialogStore();
// Using registered component (from componentMap)
async function openProductForm() {
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
mode: 'create'
}
});
}
// Using registered component with different properties
async function editProduct() {
await dialogStore.updateDialog({
name: 'ProductForm',
value: true,
properties: {
mode: 'edit',
id: 123
}
});
}
// Using on-demand import (not registered in componentMap)
async function openCustomDialog() {
await dialogStore.updateDialog({
name: 'CustomDialog',
value: true,
importPath: './components/CustomDialog.vue', // Import on-demand
properties: {
title: 'Custom Dialog',
data: someData
}
});
}
</script>Dialog Component Example
Dialog components receive the is-dialog prop from FTxDynamicDialog and should access properties from the store:
<!-- ProductForm.vue -->
<template>
<q-dialog :model-value="true" @update:model-value="handleClose">
<q-card style="min-width: 500px">
<q-card-section>
<div class="text-h6">{{ properties?.mode === 'edit' ? 'Edit Product' : 'Add Product' }}</div>
</q-card-section>
<q-card-section>
<!-- Your form content here -->
<q-input v-model="formData.name" label="Product Name" />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" @click="handleClose" />
<q-btn flat label="Save" @click="handleSave" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useDialogStore } from '@ftx/ui'
const props = defineProps({
'is-dialog': {
type: Boolean,
default: false
}
})
const dialogStore = useDialogStore()
const dialogName = 'ProductForm' // The name used when opening the dialog
// Get properties from store
const properties = computed(() => dialogStore.getProperties(dialogName))
const formData = ref({ name: '' })
// Watch for property changes
watch(() => properties.value, (newProps) => {
if (newProps?.id) {
// Load data for edit mode
formData.value = { name: 'Loaded Name' } // Load from API
}
}, { immediate: true })
function handleClose() {
dialogStore.closeDialog(dialogName)
}
function handleSave() {
// Call callback if provided
if (properties.value?.callback) {
properties.value.callback(formData.value)
}
// Save logic here
handleClose()
}
</script>Alternative: Using Properties Directly
If you prefer to receive properties as props (requires manual rendering with v-bind), you can structure your component differently:
<!-- ProductForm.vue -->
<script setup>
const props = defineProps({
mode: {
type: String,
default: 'create'
},
id: {
type: Number,
default: null
},
callback: {
type: Function,
default: null
},
'is-dialog': {
type: Boolean,
default: false
}
})
</script>In this case, you would need to manually render dialogs with v-bind="dialog.properties" instead of using FTxDynamicDialog.
Migration from showcase.store.js
If you're migrating from a project-specific showcase.store.js, follow these steps:
Remove the old store:
- Delete your project's
showcase.store.jsfile
- Delete your project's
Update imports:
javascript// Old import { useShowcaseStore } from 'src/stores/showcase.store.js'; // New import { useDialogStore } from '@ftx/ui';Register component map:
- Move your
componentMapfrom the old store to your app initialization - Use
registerComponentMap()to register all your dialogs
- Move your
Update store usage:
javascript// Old const showcaseStore = useShowcaseStore(); await showcaseStore.updateDialog({ name: 'MyDialog', value: true }); // New const dialogStore = useDialogStore(); await dialogStore.updateDialog({ name: 'MyDialog', value: true });
Notes
- Components are lazy-loaded on first use, improving initial bundle size
- The store supports multiple dialogs open simultaneously (dialog stack)
- Components are cached after first load for better performance
- Hot Module Replacement (HMR) is supported during development