Skip to content

Useful Composables ​

Common composables for everyday development tasks.

useConfirm ​

Prompt users for confirmation with dialogs.

vue
<template>
  <UiButton @click="handleDelete">Delete</UiButton>
</template>

<script setup>
const { confirm } = useConfirm()

const handleDelete = async () => {
  const confirmed = await confirm('Delete this item?', {
    description: 'This action cannot be undone.',
    variant: 'destructive',
    confirmButtonText: 'Delete'
  })
  
  if (confirmed) {
    // User clicked confirm
    await deleteItem()
  }
}
</script>

Variants ​

  • default - Standard confirmation (blue)
  • destructive - Dangerous actions (red)
  • warning - Caution required (yellow)
  • info - Informational
  • success - Positive confirmation (green)

With i18n ​

vue
<script setup>
const { t } = useI18n()
const { confirm } = useConfirm()

const handleDelete = async () => {
  const confirmed = await confirm(
    () => t('employees.deleteTitle'),
    {
      description: () => t('employees.deleteDescription'),
      confirmButtonText: () => t('common.actions.delete'),
      variant: 'destructive'
    }
  )
}
</script>

Reactive Translations

Use getter functions () => t('key') to make dialog text reactive to language changes.

With Async Handler ​

Manage edit mode state for forms and cards.

vue
<template>
  <UiCard>
    <UiCardHeader>
      <UiCardTitle>Employee Details</UiCardTitle>
      
      <!-- Edit/Save/Cancel buttons -->
      <div v-if="isEditing" class="flex gap-2">
        <UiButton @click="cancel">Cancel</UiButton>
        <UiButton @click="handleSave" :disabled="!isDirty">Save</UiButton>
      </div>
      <UiButton v-else @click="startEdit">Edit</UiButton>
    </UiCardHeader>
    
    <UiCardContent>
      <!-- View mode -->
      <div v-if="!isEditing">
        <p>{{ employee.name }}</p>
        <p>{{ employee.email }}</p>
      </div>
      
      <!-- Edit mode -->
      <div v-else>
        <UiInput v-model="editableValue.name" />
        <UiInput v-model="editableValue.email" />
      </div>
    </UiCardContent>
  </UiCard>
</template>

<script setup>
const props = defineProps<{
  employee: Employee
}>()

const emit = defineEmits<{
  'save': [Employee]
}>()

// Set up edit mode
const { 
  isEditing,      // Boolean: currently editing?
  editableValue,  // Ref: working copy of data
  isDirty,        // Boolean: has data changed?
  startEdit,      // Function: enter edit mode
  cancel,         // Function: cancel and revert changes
  save            // Function: save and exit edit mode
} = useEdit(() => props.employee)

const handleSave = () => {
  const saved = save()
  if (saved) {
    emit('save', saved)
  }
}
</script>

What useEdit provides:

  • isEditing - Boolean indicating edit mode
  • editableValue - Ref with working copy of data
  • isDirty - Boolean indicating if data changed
  • startEdit() - Enter edit mode
  • cancel() - Cancel and revert changes
  • save() - Save and exit, returns saved data
  • reset() - Reset to initial value

Clean Edit Flow

useEdit handles cloning, change detection, and state management automatically!

useRouteLeaveGuard ​

Prevent navigation when there are unsaved changes.

vue
<script setup>
const formData = ref({ name: '', email: '' })
const isDirty = ref(false)

// Block navigation if form has unsaved changes
useRouteLeaveGuard(isDirty)

const handleInput = () => {
  isDirty.value = true
}

const handleSave = async () => {
  await saveData()
  isDirty.value = false  // Allow navigation after save
}
</script>

With i18n ​

Use getter functions for reactive translations:

vue
<script setup>
const { t } = useI18n()
const hasUnsavedChanges = ref(false)

useRouteLeaveGuard(hasUnsavedChanges, {
  confirmTitle: () => t('forms.unsavedChanges'),
  confirmDescription: () => t('forms.unsavedWarning'),
  confirmButtonText: () => t('common.actions.discard'),
  cancelButtonText: () => t('common.actions.keepEditing'),
  confirmVariant: 'warning'
})
</script>

Options ​

  • confirmTitle - Dialog title (string, ref, or getter function)
  • confirmDescription - Dialog description
  • confirmVariant - 'default' | 'destructive' | 'warning' | 'info' | 'success'
  • confirmButtonText - Confirm button text
  • cancelButtonText - Cancel button text
  • guardRouteUpdate - Also guard when route params change (default: false)

Default translations (if no custom text provided):

  • Title: common.routeLeave.title - "You have unsaved changes"
  • Description: common.routeLeave.description - "Are you sure you want to leave? Your changes will be lost."
  • Confirm: common.routeLeave.leave - "Leave"
  • Cancel: common.routeLeave.stay - "Stay"
  • Variant: 'warning'

When to Use

Perfect for forms with unsaved changes, multi-step wizards, or any scenario where leaving the page would lose user data.

useRtl ​

Access RTL/LTR direction information.

vue
<script setup>
const { dir, resolvedDir, isRtl } = useRtl()

// dir: 'ltr' | 'rtl' | 'auto'
// resolvedDir: 'ltr' | 'rtl' (never 'auto')
// isRtl: boolean
</script>

<template>
  <!-- Conditional rendering -->
  <ChevronLeft v-if="!isRtl" />
  <ChevronRight v-else />
  
  <!-- Conditional classes -->
  <div :class="isRtl && 'scale-x-[-1]'">
    Icon
  </div>
</template>

See RTL Support → for complete guide.

useAuthUserStore ​

Access current user and organization context.

vue
<script setup>
const { 
  user,                  // Current user
  currentOrganization,   // Current org context
  currentEmployee,       // Current employee context
  currentAccess,         // 'organization' | 'employee' | 'none'
  organizations,         // Available organizations
  employees,             // Available employee accounts
  hasCompanyAccess,      // Boolean
  hasEmployeeAccess      // Boolean
} = useAuthUserStore()
</script>

<template>
  <div>
    <p>Welcome, {{ user?.name }}</p>
    <p v-if="currentOrganization">
      Organization: {{ currentOrganization.name }}
    </p>
  </div>
</template>

useOrganizationDataStore ​

Access cached organization data (departments, positions).

vue
<script setup>
const { 
  departments,        // Ref<Department[]>
  positions,          // Ref<Position[]>
  getDepartmentName,  // (id) => string
  getPositionName,    // (id) => string
  addDepartment,      // (dept) => Promise<Department>
  addPosition         // (pos) => Promise<Position>
} = useOrganizationDataStore()

// Data is automatically lazy-loaded and cached
</script>

<template>
  <UiSelect v-model="selectedDept">
    <UiSelectTrigger>
      <UiSelectValue placeholder="Select department" />
    </UiSelectTrigger>
    <UiSelectContent>
      <UiSelectItem 
        v-for="dept in departments" 
        :key="dept.id" 
        :value="dept.id"
      >
        {{ dept.name }}
      </UiSelectItem>
    </UiSelectContent>
  </UiSelect>
</template>

Automatic Caching

Data is cached using useState and persists across navigation. The store automatically fetches on first use.

Input Masking ​

For formatting inputs (phone numbers, dates, SSNs), see Input Masking →.

Best Practices ​

Do's âś…

  • Use useConfirm for all user confirmations
  • Use useEdit for consistent edit mode behavior
  • Use useRouteLeaveGuard to prevent data loss on navigation
  • Use useOrganizationDataStore for cached org data
  • Use getter functions with useConfirm for reactive i18n

Don'ts ❌

  • Don't create custom confirmation dialogs (use useConfirm)
  • Don't manually manage edit state (use useEdit)
  • Don't let users lose unsaved data (use useRouteLeaveGuard)
  • Don't fetch departments/positions directly (use store)

Quick Reference ​

vue
<script setup>
// Confirmation
const { confirm } = useConfirm()
await confirm('Delete?', { variant: 'destructive' })

// Edit mode
const { isEditing, editableValue, isDirty, startEdit, save, cancel } = useEdit(() => data)

// Route leave guard
useRouteLeaveGuard(hasUnsavedChanges, { confirmVariant: 'warning' })

// RTL
const { isRtl } = useRtl()

// Auth
const { user, currentOrganization } = useAuthUserStore()

// Org data
const { departments, positions } = useOrganizationDataStore()
</script>

Related: UI Components → | API Calls → | Forms → | Input Masking →