Dialog Loader for Quasar

🇪🇸 Versión en español | 🇧🇷 Versão em português

npm version license Vue Quasar TypeScript Node

Quasar extension for a global loading modal dialog in Vue 3 + Quasar apps, with an imperative composable-based API.

Table of contents

In 30 seconds

Recommended flow:

  1. Install the extension in your Quasar app (quasar ext add).
  2. Ensure the host Pinia boot runs before this extension’s boot.
  3. Import useDialogLoader() where you run async work.
  4. Wrap the task with await loader.run({ task: () => api.save() }) — or { label, task } for a one-off label override.

The extension registers boots and CSS in the host project automatically after quasar ext add.

What it is and what it includes

Global modal dialog controlled by useDialogLoader() — not an embeddable component.

Requirements and compatibility

Item Value
Node >= 20.0.0
Quasar ^2.6.0
Vue ^3.4.18
Pinia ^2.0.11 | ^3.0.0
Package format ES modules
Recommended CLI @quasar/app-vite ^2.x or ^3.x

The host Pinia boot must run before this extension’s boot.

Install in a Quasar app

quasar ext add @benjaminor-dev/dialog-loader

Remove:

quasar ext remove @benjaminor-dev/dialog-loader

After quasar ext add, the extension registers boots and CSS automatically. No extra steps for a normal npm install.

What the extension adds to the host

Resources installed or registered by the extension:

Resource npm path
Dialog boot ~@benjaminor-dev/quasar-app-extension-dialog-loader/boot/dialog
Styles ~@benjaminor-dev/quasar-app-extension-dialog-loader/main.css
Host defaults boot src/boot/bor/bor-dialog-loader-defaults.ts — created on install; Skip/Overwrite if it exists

Install (quasar ext add / invoke): creates src/boot/bor/bor-dialog-loader-defaults.ts (or .js) and registers it in quasar.configboot: [] automatically. No manual editing needed in a normal install. Package boot (boot/dialog) and main.css are injected on every dev/build (they do not appear as short entries in quasar.config).

Remove (quasar ext remove): removes the defaults boot and its quasar.config entry.

Minimum boot order:

1. Pinia (host)
2. Dialog Loader — boot/dialog
3. bor/bor-dialog-loader-defaults (host)

In quasar.config you only see bor/bor-dialog-loader-defaults; the package boot/dialog is added by the extension runner at compile time.

The boot mounts the dialog in the document and wires the host Pinia store. You do not import DialogLoader in App.vue.

Global defaults (boot)

Avoid repeating variant, spinner, gap, font, delays, and zIndex on every call. Label priority: run({ label }) / show() → boot label → i18n (locale / $q.lang / en).

After quasar ext add, review src/boot/bor/bor-dialog-loader-defaults.ts — template with all boot options (// recommended: …) and language notes in the file header comment.

// src/boot/bor/bor-dialog-loader-defaults.ts
import { configureDialogLoaderDefaults } from "@benjaminor-dev/quasar-app-extension-dialog-loader";

configureDialogLoaderDefaults({
  // locale: "es", // uncomment to pin a specific language for Dialog Loader
  variant: "glass",
  spinner: "ios",
  openDelayMs: 120,
  hideDelayMs: 450,
  minVisibleMs: 600,
  backdropBlur: "blur(10px)",
  zIndex: 6000,
  gap: "2rem",
  font: {
    family: "Arial, sans-serif",
    size: "1.35rem",
  },
  spinnerColor: {
    light: "primary",
    dark: "white",
  },
  spinnerTrackColor: {
    light: "grey-4",
    dark: "grey-7",
  },
});

Language (i18n)

Built-in en, es, and pt. By default the label follows Quasar language ($q.lang.isoName); if Quasar has no language, fallback is en ("Loading...").

Locale resolution: boot locale$q.lang.isoName prefix (es*es, pt*pt) → en.

Label resolution: run({ label }) / show(label) → boot label → boot messages.loadingLabel → built-in catalog → en.

Built-in message keys: loadingLabel (default loader text), operationCancelled (DialogLoaderRunCancelledError when cancel() aborts the task).

Option When to use
framework.lang in quasar.config.ts Recommended — aligns Dialog Loader with the rest of Quasar
locale: 'es' in boot Pin a specific language (en | es | pt); ignores $q.lang changes
messages: { loadingLabel: '…' } Change one string without setting locale
label: '…' in boot Fixed label always (ignores i18n)

Quasar config example (Spanish for the whole app):

// quasar.config.ts
framework: {
  lang: "es",
},

configureDialogLoader is an alias of configureDialogLoaderDefaults.

Boot option Built-in default Description
locale (follows $q.lang) Lock en | es | pt for extension strings
messages (per locale) Partial override (loadingLabel, operationCancelled)
label "Loading..." (i18n) Fixed text when show() / run() omit label
variant "glass" Dialog presentation
spinner "ios" Loading indicator (see spinners)
openDelayMs 120 Delay before show (avoids flashes on very fast requests)
hideDelayMs 450 Extra delay on close (matches fade animation)
minVisibleMs 600 Minimum visible time even if the task finishes sooner
backdropBlur "blur(10px)" CSS backdrop-filter on the overlay
zIndex 6000 Below Quasar Notify (~9500) so toasts stay visible
gap "2rem" Label↔spinner spacing
font.family "Arial, sans-serif" CSS font-family of the label
font.size "1.35rem" CSS font-size of the label
spinnerColor (per variant) Quasar spinner color — flat or { light, dark }
spinnerTrackColor (per variant) Quasar track color on circular spinners — flat or { light, dark }

If you omit spinnerColor / spinnerTrackColor, fallback depends on variant: on primary the spinner is white; on glass and minimal it uses primary and grey-4. Set { light, dark } in boot to match the host theme ($q.dark).

Quick start

<script setup lang="ts">
import { useDialogLoader } from "@benjaminor-dev/quasar-app-extension-dialog-loader";

const loader = useDialogLoader();

async function onSave() {
  try {
    await loader.run({ task: () => api.save(formData) });
    // notify.success('Saved');
  } catch (error) {
    // notify.error('Save failed');
  }
}
</script>

<template>
  <q-btn label="Save" @click="onSave" />
</template>

No explicit label — uses i18n from $q.lang (e.g. "Cargando..." with lang: 'es'):

await loader.run(() => fetchUsers());
// equivalent:
await loader.run({ task: () => fetchUsers() });

One-off label override:

await loader.run({ label: "Saving...", task: () => api.save(formData) });

Optional controls inside task:

await loader.run({
  task: async ({ pause, resume, cancel, signal }) => {
    await api.prepare();
    pause(); // hide overlay; task keeps running
    await api.finishInBackground();
    resume(); // show loader again
    await fetch("/api/finalize", { signal }); // cancel() aborts this fetch
  },
});
Control What it does
pause() Hides the loader; does not cancel the task
resume() Reopens the loader after pause()
cancel() Aborts signal, closes the loader, rejects run() with DialogLoaderRunCancelledError
signal Pass to fetch / axios for cooperative cancellation

pause(), resume(), and cancel() are idempotent and only affect that run() call.

import { DialogLoaderRunCancelledError } from "@benjaminor-dev/quasar-app-extension-dialog-loader";

try {
  await loader.run({ task: ({ signal }) => fetch(url, { signal }) });
} catch (error) {
  if (error instanceof DialogLoaderRunCancelledError) return;
  throw error;
}

Manual control (less common):

loader.show("Processing...");
try {
  await longTask();
} finally {
  loader.hide();
}

TypeScript in your IDE

Do not memorize the API — let autocomplete guide you.

If something does not autocomplete, it is usually a nonexistent prop or wrong import — check the main entrypoint before searching this README.

Public API

Entry point: useDialogLoader().

import { useDialogLoader } from "@benjaminor-dev/quasar-app-extension-dialog-loader";

const loader = useDialogLoader();

Methods

Method Description
run(task) Runs the task with loader; boot label. Closes in finally.
run({ task }) Same as above (object form).
run({ label, task }) Same with a one-off label for that operation.
show(label?) Increments the internal counter and schedules open.
hide() Decrements the counter; closes UI only when no operations remain.

Reactive state (read-only)

Ref Description
visible Whether the dialog is visible.
label Text shown in the dialog.

When to use run vs show / hide

Approach When
run() Async requests — prefer run({ task }) or run(task); label only when you need an override. Recommended.
show() / hide() Manual flows or multi-phase work with the same label.

Visual variants

Set variant in the host boot (or built-in).

Value Presentation
glass Glassmorphism — semi-transparent card with blur (default)
primary Solid card with --q-primary; white text and spinner
minimal Compact horizontal pill, less intrusive

Loading indicators (spinner)

Set spinner in boot. Curated subset of Quasar spinners:

Value Style
ios iOS-style radial bars (default)
circular Ring with track (QCircularProgress)
dots Three animated dots
oval Rotating oval
tail Rotating tail
puff Pulsing circle
rings Concentric rings
bars Vertical bars
hourglass Hourglass
infinity Infinity symbol

Exported constant: DIALOG_LOADER_SPINNER_TYPES — useful to validate values at runtime.

Anti-flicker and concurrent operations

The store keeps a pendingCount: each show() / run() start increments it; each hide() / run() end decrements it. UI closes only when the counter reaches zero.

Configurable delays in boot:

Concurrent run() or show() calls share the same dialog until all finish.

Exported TypeScript types

From the main entrypoint:

import {
  useDialogLoader,
  DialogLoaderRunCancelledError,
  resolveDialogLoaderMessages,
  resolveExtensionLocale,
  type DialogLoaderApi,
  type DialogLoaderRunControls,
  type DialogLoaderRunOptions,
  type DialogLoaderRunTask,
  type DialogLoaderDefaults,
  type DialogLoaderFontDefaults,
  type DialogLoaderMessages,
  type DialogLoaderThemeColor,
  type DialogLoaderThemeColorValue,
  type DialogLoaderThemeMode,
  type DialogLoaderVariant,
  type DialogLoaderSpinnerType,
  type ExtensionLocale,
  type ResolvedDialogLoaderDefaults,
} from "@benjaminor-dev/quasar-app-extension-dialog-loader";
Type Use
DialogLoaderApi Return type of useDialogLoader()
ExtensionLocale "en" | "es" | "pt" — boot locale
DialogLoaderMessages Shape of boot messages (loadingLabel, operationCancelled)
DialogLoaderRunOptions Object argument of run({ label?, task })
DialogLoaderRunControls { pause, resume, cancel, signal } passed to task
DialogLoaderRunTask Type of the task callback
DialogLoaderRunPause Hides loader without cancelling
DialogLoaderRunResume Reopens loader after pause()
DialogLoaderRunCancel Cancels the operation
DialogLoaderRunCancelledError Error when cancelling with cancel()
DialogLoaderDefaults Options for configureDialogLoaderDefaults
DialogLoaderFontDefaults { family?, size? } inside DialogLoaderDefaults.font
DialogLoaderThemeColorValue Flat color or { light?, dark? } for spinner
DialogLoaderThemeColor { light?, dark? } — part of DialogLoaderThemeColorValue
DialogLoaderThemeMode "light" | "dark" — resolved with $q.dark.isActive
DialogLoaderVariant "glass" | "primary" | "minimal"
DialogLoaderSpinnerType Union of supported spinners
ResolvedDialogLoaderDefaults Merged defaults (internal / reference)

Also exported: BUILTIN_DIALOG_LOADER_DEFAULTS, DIALOG_LOADER_SPINNER_TYPES, resolveDialogLoaderMessages, resolveExtensionLocale, configureDialogLoader (alias), and DIALOG_LOADER_API_KEY (boot provide key for advanced integrations).

Public entrypoints

What you import in your code:

Entrypoint Purpose
@benjaminor-dev/quasar-app-extension-dialog-loader useDialogLoader, configureDialogLoaderDefaults, i18n helpers (resolveDialogLoaderMessages, resolveExtensionLocale), types, and DIALOG_LOADER_SPINNER_TYPES

Boot (boot/dialog) and styles (main.css) are registered by the extension in quasar.config on install — no manual import in a normal setup.

Troubleshooting

I see "Loading..." and want Spanish (or Portuguese)

Without Quasar language config, the default is English. Pick one line:

// quasar.config.ts — recommended (whole Quasar app)
framework: {
  lang: "es", // or "pt-BR" via Quasar lang pack in boot
},
// src/boot/bor/bor-dialog-loader-defaults.ts — pin Dialog Loader locale only
configureDialogLoaderDefaults({ locale: "es" });

Override a single string:

configureDialogLoaderDefaults({ messages: { loadingLabel: "Cargando..." } });

Label does not update when switching language during run()

If the user changes app language while the loader is visible, the on-screen label keeps the text from when run() / show() started. After the loader closes, the idle label follows the new locale. To pin a language for the whole extension, use locale in boot.

Boots or styles missing from quasar.config

Under normal conditions, install registers the defaults boot in boot: [] and the runner injects boot/dialog + CSS at compile time. If the defaults boot is missing:

npx quasar ext invoke @benjaminor-dev/dialog-loader

Error: Pinia not initialized

useDialogLoader() outside setup

Call it inside setup, another composable, or <script setup>. The boot must have run (app mounted).

Loader flickers on fast requests

Adjust boot: increase openDelayMs, minVisibleMs, or both.

Loader does not close after show()

Each show() needs a matching hide() (or use run(), which balances automatically).

Dialog stays behind another overlay

Check zIndex in boot (default 6000).

Changed spinner in boot and nothing updates

Restart the dev server. Confirm you edit the host’s src/boot/bor/bor-dialog-loader-defaults.ts, not only the extension template.


MIT © Benjamín Olvera R.