vue-guarded-save
v0.2.0
Published
Save bar + unsaved-changes leave guard + saving lifecycle, in one Vue component.
Maintainers
Readme
vue-guarded-save
Save bar + unsaved-changes leave guard + saving lifecycle — in one Vue 3 component.
Install
pnpm add vue-guarded-savePeer deps: vue ^3.4, vue-router ^4 || ^5.
Use
<script setup lang="ts">
import { GuardedSave } from "vue-guarded-save";
const isDirty = computed(() => draft.value !== original.value);
async function onSave() {
await save();
return true;
} // return false → no "✓ Saved" flash
function onDiscard() {
draft.value = original.value;
}
</script>
<template>
<GuardedSave :dirty="isDirty" :on-save="onSave" :on-discard="onDiscard" />
</template>GuardedSave wraps everything: the sticky save/discard bar, the leave dialog,
and the saving/saved lifecycle (flashing "✓ Saved" 2s on success). onSave
may be async and return false (or throw) to suppress the flash when your
page reports the error itself. A spacer keeps the fixed bar from covering
content — no bottom padding needed on your page.
Props
| Prop | Type | Default | Purpose |
| ---------------- | --------------------------------------- | -------------- | ---------------------------------------------------------------------------- |
| dirty | boolean | — | Show the bar / arm the leave guard. |
| onSave | () => boolean \| void \| Promise<...> | — | The page's save; wrapped with saving state. Return false → no flash. |
| onDiscard | () => void | — | Resets the page's draft. |
| sidebarInset | boolean | true | Shift the bar right of a fixed lg:left-64 sidebar; false for full width. |
| insetClass | string | 'lg:left-64' | Override the inset (e.g. 'lg:left-72'). |
| barClass | string | '' | Extra classes on the bar row. |
| saveClass | string | '' | Extra classes appended to the Save button (default markup only). |
| discardClass | string | '' | Extra classes appended to the Discard button (default markup only). |
| leaveGuard | boolean | true | Arm the SPA route-leave guard — in-app navigation shows the dialog. |
| unloadGuard | boolean | true | Warn on refresh / tab close while dirty (beforeunload). |
| saveShortcut | boolean | false | Bind Cmd/Ctrl+S to save while dirty. |
| labels | object | EN strings | Override any user-facing string. |
| v-model:saving | boolean | — | Optional mirror of the saving state — bind to disable your inputs mid-save. |
Also exported separately: SaveBar (presentation only), UnsavedLeaveDialog,
and useUnsavedLeaveGuard(isDirty, saving) if you want to compose your own.
Slots
Replace either bar button entirely — the clean escape hatch when appended classes aren't enough. Slot props mirror everything the default button knows:
<GuardedSave :dirty="isDirty" :on-save="onSave" :on-discard="onDiscard">
<template #save="{ save, saving, disabled }">
<button type="button" :disabled="disabled" @click="save()">
{{ saving ? "Saving…" : "Save" }}
</button>
</template>
</GuardedSave>#save receives { save, saving, saved, disabled }; #discard receives
{ discard, disabled }.
Guards & shortcut
Two independent guards run while dirty: in-app navigation opens the leave
dialog (leaveGuard), and browser refresh / tab close triggers the browser's
own leave confirmation (unloadGuard) — the SPA guard cannot catch those.
Both default on. saveShortcut (off by default, to avoid clashing with pages
that own Cmd/Ctrl+S themselves) binds Cmd/Ctrl+S to save while dirty.
Styling
Ships Tailwind utility classes against shadcn-style tokens (bg-background,
bg-primary, text-muted-foreground, …). Bring your own Tailwind v4 and
define those tokens (see playground/src/style.css for a minimal set).
Two things the consumer must wire up:
Scan the package for utility classes. Tailwind v4's auto-detection skips
node_modules, so the classes this library ships in its dist JS are never generated by default — the save bar renders unstyled/unsafely stacked. Add to your main CSS:@source "../node_modules/vue-guarded-save/dist";Import the animations. The bar's slide and the dialog's fade are scoped styles the Tailwind scan cannot produce:
import "vue-guarded-save/style.css";
