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,
})
})