Skip to content

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.

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:

javascript
// 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:

javascript
// 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 FTxDynamicDialog component
  • 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:

vue
<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-dialog prop 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:

javascript
import { useDialogStore } from '@ftx/ui';

Setup

You can optionally register your dialog components upfront in your application initialization (e.g., main.js, boot file, or a store setup file):

javascript
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

vue
<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

vue
<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

PropertyTypeDescription
dialogStackArrayArray of active dialogs
dialogComponentObjectCached dialog components
componentMapObjectRegistered component loaders

Getters

getDialog(name)

Returns a dialog object by name, or null if not found.

javascript
const dialog = dialogStore.getDialog('ProductForm');
// Returns: { name: 'ProductForm', properties: {...} } or null

getProperties(name)

Returns the properties of a dialog by name, or null if not found.

javascript
const props = dialogStore.getProperties('ProductForm');
// Returns: { id: 123, mode: 'edit' } or null

Actions

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:

javascript
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:

javascript
dialogStore.unregisterComponent('MyDialog');

clearComponentMap()

Clears all registered components from the component map. This removes all registrations but does not affect already loaded components.

Example:

javascript
dialogStore.clearComponentMap();

getRegisteredComponents()

Returns an array of all registered component names.

Returns:

  • string[] - Array of registered component names

Example:

javascript
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:

javascript
// 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
javascript
// 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 dialog
  • value (Boolean, default: true) - Whether to open (true) or close (false) the dialog
  • properties (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:

javascript
await dialogStore.updateDialog({
  name: 'MyDialog',
  value: true,
  properties: {
    callback: (data) => {
      form.value.customerGroupId = data
    },
    id: 123,
    config: { option: true }
  }
})

Component Loading Priority:

  1. If component is already loaded (cached), it's reused
  2. If component is registered in componentMap, it's imported from there
  3. If importPath is provided, component is imported dynamically
  4. 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:

javascript
// 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:

javascript
dialogStore.closeDialog('ProductForm');

resetAllDialogs()

Closes all dialogs and clears the dialog stack.

Example:

javascript
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 - true if component is loaded, false otherwise

Example:

javascript
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

javascript
// 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:

javascript
// quasar.config.js
module.exports = function (ctx) {
  return {
    boot: [
      'ftx-dialog', // Add this
      // ... other boot files
    ],
    // ... rest of config
  }
}

Component Usage

The easiest way to render dialogs is using the FTxDynamicDialog component:

vue
<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:

vue
<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:

vue
<!-- 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:

vue
<!-- 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:

  1. Remove the old store:

    • Delete your project's showcase.store.js file
  2. Update imports:

    javascript
    // Old
    import { useShowcaseStore } from 'src/stores/showcase.store.js';
    
    // New
    import { useDialogStore } from '@ftx/ui';
  3. Register component map:

    • Move your componentMap from the old store to your app initialization
    • Use registerComponentMap() to register all your dialogs
  4. 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

Released under the MIT License.