Widget
Widget lets one module put a component on a page that another module owns. Subscriptions adds a membership card to
the account overview, and Events adds cards to the admin dashboard, without either page knowing them.
Use it for dashboards and tabbed detail pages that other modules should fill. When only the page itself decides its
sections, use PageTabs instead.
The package is @frigear-nu/widget. A page names a widget area, such as admin.dashboard,
and draws whatever is registered for it. Modules register Vue components to that area at build time. Like
Menu, it is a registry.
What you get
- Components (on by default):
<WidgetArea>draws an area's widgets in a grid, and<WidgetTabs>draws its tabs. - Composables and helpers (on by default):
useWidgetsanduseTabsread an area, andextendWidgetsandextendTabsadd to one from a module. - Hooks (on by default):
widgets:filterandtabs:filterhide entries the current visitor should not see, by the entry'spermission.
A widget's component loads only when its area is drawn.
@frigear-nu/module-kit.You add: nothing else. <WidgetTabs> draws its icons with Nuxt UI's UIcon.Optional:@nuxtjs/i18n, to translate tab labels.Quick setup
Install Widget
pnpm add @frigear-nu/widget
Add it to nuxt.config.ts
export default defineNuxtConfig({
modules: ['@frigear-nu/widget'],
})
Draw an area
<WidgetArea name="admin.dashboard">
<template #empty>Nothing to show yet.</template>
</WidgetArea>
extendWidgets(nuxt, 'admin.dashboard', …).
Then its component appears in the grid.Options
The module has no options. The widgets config key only holds internal state.
App
Components
<WidgetArea> and <WidgetTabs> draw an area on a page.
| Component | Props | Slots | Purpose |
|---|---|---|---|
<WidgetArea> | name: string, context?: Record<string, unknown>, gridClass?: string | empty, item with { widget, component, context } | Draws every widget of the area in a responsive grid. |
<WidgetTabs> | name: string, context?: Record<string, unknown> | empty | Draws the area's tabs, with the active tab's component below. Emits refresh. |
contextis what the page draws the area for, such as{ user }. Each component receives its staticprops, then every entry ofcontextas a prop. The filter hooks get it asctx.context.gridClassreplaces the grid classes of the wrapper. Theitemslot lays the widgets out yourself.<WidgetTabs>keeps the active tab in the?tab=query parameter, without a navigation. It supports arrow keys, and passes a tab'srefreshevent on so the page can reload its data.
<WidgetTabs> looks exactly like PageTabs. When to use which is on Pages.
Composables
useWidgets and useTabs return an area's entries as plain data, ordered and filtered. They are auto-imported and
return the shape of useAsyncData. context may be a ref or a getter.
useWidgets(area: string, context?: MaybeRefOrGetter<Record<string, unknown> | undefined>): AsyncData<WidgetMeta[]>
useTabs(area: string, context?: MaybeRefOrGetter<Record<string, unknown> | undefined>): AsyncData<TabMeta[]> // labels already translated
Add widgets and tabs from a module
Call extendWidgets or extendTabs in a module's setup(). The order between modules does not matter.
import { createResolver, defineNuxtModule } from '@nuxt/kit'
import { extendWidgets, extendTabs } from '@frigear-nu/widget/utils'
export default defineNuxtModule({
setup(_options, nuxt) {
const resolver = createResolver(import.meta.url)
extendWidgets(nuxt, 'admin.dashboard', {
component: resolver.resolve('./runtime/app/widgets/NewMembers.vue'),
order: 10,
span: 2,
props: { days: 30 },
permission: 'admin:access',
})
extendTabs(nuxt, 'admin.users', {
id: 'payments',
component: resolver.resolve('./runtime/app/tabs/Payments.vue'),
order: 20,
label: 'Payments',
labelKey: 'payments.admin.tab',
icon: 'i-lucide-credit-card',
permission: 'payments:manage',
})
},
})
id in the same area. The id defaults to the
component's file name.type WidgetDefinition = {
id?: string // unique within an area; default: component file name
component: string // resolved path of the Vue component
order?: number // lower first; unordered last, in registration order
span?: 1 | 2 | 3 | 4 // grid columns in the default layout (default 1)
props?: Record<string, unknown> // serializable
requires?: string // free-form, for the widgets:filter hook; never interpreted by the module
permission?: string // `resource:action`: shown only to viewers who hold it
tags?: string[]
meta?: Record<string, unknown>
}
type TabDefinition = {
id?: string // default: component file name; also the `?tab=` value
component: string
order?: number
props?: Record<string, unknown>
label?: string // plain fallback label; default: the id
labelKey?: string // i18n key, translated at render time with @nuxtjs/i18n
icon?: string // for example 'i-lucide-credit-card'
requires?: string // free-form, for the tabs:filter hook
permission?: string // `resource:action`: shown only to viewers who hold it
tags?: string[]
meta?: Record<string, unknown>
}
type RegisteredWidget = WidgetDefinition & { id: string, area: string }
type RegisteredTab = TabDefinition & { id: string, area: string }
type WidgetMeta = Omit<RegisteredWidget, 'component'> // what useWidgets returns
type TabMeta = Omit<RegisteredTab, 'component' | 'labelKey' | 'label'> & { label: string, labelKey?: string } // what useTabs returns
Utils
@frigear-nu/widget/utils is framework-agnostic. The types above also come from @frigear-nu/widget/types.
| Export | Purpose |
|---|---|
extendWidgets(nuxt, area, widgets) | Adds one or more widgets to an area, from a module's setup(). |
extendTabs(nuxt, area, tabs) | Adds one or more tabs to an area. |
deriveWidgetId(widget), deriveTabId(tab) | The id an entry gets: its id, else the component file name. |
sortWidgets(list) | Orders by order, lowest first. Entries without one come last, in registration order. |
Hide entries per visitor
The module itself never reads permission or requires. It calls widgets:filter and tabs:filter, and a listener
drops what the current visitor should not see. With Authorization installed, a
listener is already registered: it keeps an entry when it has no permission or when can(entry.permission) is true.
extendTabs(nuxt, 'admin.users', {
id: 'payments',
component: resolver.resolve('./runtime/app/tabs/Payments.vue'),
permission: 'payments:manage',
})
requires stays free-form, for your own rules such as a feature flag. Your app listens to the same hook:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('widgets:filter', (ctx) => {
const flags = useRuntimeConfig().public.flags as string[]
ctx.widgets = ctx.widgets.filter(widget => !widget.requires || flags.includes(widget.requires))
})
})
Entries are cached under a key that includes an access version. Authorization raises the version on sign-in, sign-out and role changes, so widgets and tabs filter again without a reload.
authorize on the server for the data a widget loads.Hooks
Module authors listen to the build hooks; your app listens to the filter hooks.
| Hook | Where | When | Payload |
|---|---|---|---|
widgets:extend | Nuxt (build) | extendWidgets adds widgets | (area: string, widgets: RegisteredWidget[]) |
widgets:resolved | Nuxt (build) | Once, after every module has set up | (areas: Record<string, RegisteredWidget[]>) |
tabs:extend | Nuxt (build) | extendTabs adds tabs | (area: string, tabs: RegisteredTab[]) |
tabs:resolved | Nuxt (build) | Once, after every module has set up | (areas: Record<string, RegisteredTab[]>) |
widgets:filter | Nuxt app | Before an area's widgets are drawn | { area, widgets: WidgetMeta[], context? }; reassign or filter ctx.widgets |
tabs:filter | Nuxt app | Before an area's tabs are drawn | { area, tabs: TabMeta[], context? }; reassign or filter ctx.tabs |
Next steps
- Admin – fill the three widget areas of the admin dashboard.
- Pages – choose between
WidgetTabsandPageTabson a detail page. - Extension points – how widgets, menus and hooks let modules extend each other.