Notification

Toasts for things the user should notice but not be stopped by. For anything that needs an answer, use a dialog.

Source in the kit: composables/useNotification.ts

The four kinds

examples/feedback/Notification.vue
<template>
  <div class="flex flex-wrap gap-2">
    <Button
      label="success"
      color="success"
      @click="
        notification.success({
          title: 'Saved',
          description: 'Your profile was updated.',
        })
      "
    />
    <Button
      label="error"
      color="error"
      @click="notification.error({ title: 'Could not save' })"
    />
    <Button
      label="warning"
      color="warning"
      @click="notification.warning({ title: 'Your session ends in 5 minutes' })"
    />
    <Button
      label="info"
      color="info"
      @click="notification.info({ title: '3 new comments' })"
    />
    <Button
      label="With an action"
      color="neutral"
      variant="outline"
      @click="withAction"
    />
  </div>
</template>

<script lang="ts" setup>
// A thin wrapper over Nuxt UI toasts that fixes colour and icon per kind.
// Every other toast option still works: description, duration, actions, id, icon…
const notification = useNotification()

const withAction = () =>
  notification.info({
    title: 'User archived',
    duration: 8000,
    actions: [
      {
        label: 'Undo',
        onClick: () =>
          notification.success({
            title: 'Restored',
          }),
      },
    ],
  })
</script>

Reporting the result of a request

examples/feedback/NotificationApi.vue
<template>
  <div class="flex flex-wrap gap-2">
    <Button
      label="Request that succeeds"
      @click="profile.run()"
    />
    <Button
      label="Request that fails"
      color="error"
      variant="soft"
      @click="profile.run({ params: { fail: 1 } })"
    />
  </div>
</template>

<script lang="ts" setup>
import { useProfileLoader } from '~/loaders/users'

// The everyday pairing: a loader reports, a notification tells the user
const profile = useProfileLoader()
const notification = useNotification()

useWatchTrue(
  () => profile.status.isSuccess,
  () => {
    notification.success({
      title: `Loaded ${profile.data?.name}`,
    })
  }
)

useWatchTrue(
  () => profile.status.isError,
  () => {
    notification.error({
      title: 'Request failed',
      // getError picks the API message, or the field message for INVALID_PARAMS
      description: StringHelper.getError(profile.status.errorData),
    })
  }
)
</script>

loaders/users.ts

import { defineStore } from 'pinia'

// One file per API resource: its types, then one store per loader.
// `defineStore` makes the loader a singleton, so a list page and its edit form share the same state.

export interface IUser {
  id: string
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  salary: number
  isActive: boolean
  avatar: string
  createdAt: string
}

export interface IRole {
  id: string
  name: string
}

// Paginated CRUD: GET /users, GET /users/:id, POST /users, PUT /users/:id, DELETE /users/:id
export const createUsersLoader = () => {
  const options = useRequestOptions()

  return usePageLoader<IUser>({
    baseURL: '/users',
    getBaseRequestOptions: options.getMock,
  })
}

// The store is the shared instance: a list page and the form that edits it see the same state.
// Call createUsersLoader() instead when a component wants a private one (each docs example does).
export const useUsersLoader = defineStore('users', createUsersLoader)

// A plain array: GET /roles
export const useRolesLoader = defineStore('roles', () => {
  const options = useRequestOptions()

  return useListLoader<IRole>({
    url: '/roles',
    getRequestOptions: options.getMock,
  })
})

// A single object, or any one-off request: GET /profile
export const useProfileLoader = defineStore('profile', () => {
  const options = useRequestOptions()

  return useObjectLoader<IUser & { loadedAt: string }>({
    method: 'get',
    url: '/profile',
    getRequestOptions: options.getMock,
  })
})