CnWidgetWrapper
Container shell around a dashboard widget. Provides a header with icon and title, a scrollable content area, and an optional footer with action links. Accepts a styleConfig object for runtime style overrides (background, border, padding). Used internally by CnDashboardPage for all non-tile widgets.
Try it
Usage
<!-- Basic wrapper -->
<CnWidgetWrapper title="My Cases" icon-url="/apps/myapp/img/icon.svg">
<MyCasesChart :data="chartData" />
</CnWidgetWrapper>
<!-- Without header (borderless, flush — for self-contained card widgets) -->
<CnWidgetWrapper :show-title="false" :borderless="true">
<CnStatsBlock :stats="kpis" />
</CnWidgetWrapper>
<!-- With NC widget object (used by CnDashboardPage internally) -->
<CnWidgetWrapper
:title="widget.title"
:icon-url="widget.iconUrl"
:icon-class="widget.iconClass"
:buttons="widget.buttons">
<CnWidgetRenderer :widget="widget" />
</CnWidgetWrapper>
<!-- With custom footer and header actions -->
<CnWidgetWrapper title="Tasks">
<template #actions>
<NcButton type="tertiary" @click="refresh">Refresh</NcButton>
</template>
<TaskList :items="tasks" />
<template #footer>
<a href="/apps/tasks">View all</a>
</template>
</CnWidgetWrapper>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | String | 'Widget' | Widget title shown in the header |
showTitle | Boolean | true | Whether to render the header bar |
borderless | Boolean | false | Remove border and background — makes the wrapper transparent |
flush | Boolean | false | Remove content padding — lets content extend edge-to-edge |
iconUrl | String | null | Image URL for the header icon |
iconClass | String | null | CSS class for the header icon (e.g. Nextcloud icon class) |
titleIconPosition | String | 'right' | Position of the title-icon slot in the header: 'left' places it before the title group; 'right' places it after the actions |
titleIconColor | String | null | CSS color value applied to the title-icon slot container (e.g. '#e74c3c') |
buttons | Array | [] | Footer button links: [{ text, link }] |
styleConfig | Object | {} | Runtime style overrides: { backgroundColor?, borderStyle?, borderWidth?, borderColor?, borderRadius?, padding?: { top, right, bottom, left } } |
refreshing | Boolean | false | When bound, the Refresh icon spins for exactly as long as this stays true (host-driven, accurate feedback). When left false, clicking Refresh spins optimistically for optimisticSpinMs |
optimisticSpinMs | Number | 800 | Duration (ms) of the optimistic Refresh spin shown on click when refreshing is not bound. ~2 rotations at the icon's spin speed. Set 0 to disable the optimistic spin (spin only while refreshing is true) |
Slots
| Slot | Description |
|---|---|
| default | Widget content rendered in the scrollable body area |
actions | Buttons or controls placed in the right side of the header |
title-icon | Extra icon element rendered in the header at the position controlled by titleIconPosition (left of title or right of actions) |
footer | Custom footer content (replaces the buttons prop rendering) |
Built-in Actions menu
CnWidgetWrapper ships with a small overflow … menu in the header — the shared CnActionsMenu — containing up to three actions, all functional without any host wiring when the wrapper is mounted under CnAppRoot:
- Refresh — emits
@refresh, then (unless the host listener callsevent.preventDefault()) emits on the@nextcloud/event-buschannelcn:widget:refreshwith payload{ widgetId, title }. Widgets that care subscribe and filter bywidgetId. - Documentation — rendered only when a
documentationUrlis supplied. Opens the host-provided link in a new tab (target="_blank",rel="noopener noreferrer"); no JS handler. Apps pass the URL from the widget configuration (:documentation-url="widget.documentationUrl"); customise the wording withdocumentationLabel. - Request a feature — emits
@request-feature, then (unless suppressed) auto-mountsCnSuggestFeatureModalwithapp + page + surface=widget:<id>context auto-filled fromCnAppRootinjects. The host can override the default by binding@request-featureand callingevent.preventDefault()to handle it themselves.
Opt out per-instance with :show-refresh="false" and/or :show-request-feature="false" (the legacy hide-refresh / hide-request-feature aliases also still work for back-compat). When everything is hidden — both built-ins opted out, no documentationUrl, and no #action-items slot content — the overflow menu disappears entirely.
Set :widget-id so the event-bus payload + modal surface tag are stable across renames; otherwise the wrapper falls back to a slugified title.
Opting into Refresh
A widget can opt in to Refresh in one of three ways. Pick whichever fits the widget's existing reactivity model — all three are first-class.
1. Ref-callable refresh() method (canonical)
The cleanest pattern: expose a method, let the host call it through the ref.
<template>
<CnWidgetWrapper title="Outgoing calls" widget-id="outgoing-calls-daily">
<CallsChart ref="chart" />
</CnWidgetWrapper>
</template>
<script>
export default {
// Inside CallsChart.vue:
methods: {
async refresh() {
this.data = await this.$store.dispatch('callLogs/refetch')
},
},
}
</script>
The host listens via the event-bus channel and routes by id (see mode 3 below for the wiring), or the wrapper invokes the method directly when integrated.
2. Reactive refreshTrigger prop
Useful when the widget cannot easily expose a ref (e.g. async-loaded). The host increments a timestamp; the widget watches it.
<template>
<CnWidgetWrapper title="Outgoing calls" widget-id="outgoing-calls-daily">
<CallsChart :refresh-trigger="callsRefreshTrigger" />
</CnWidgetWrapper>
</template>
<script>
import { subscribe } from '@nextcloud/event-bus'
export default {
data() {
return { callsRefreshTrigger: 0 }
},
mounted() {
subscribe('cn:widget:refresh', ({ widgetId }) => {
if (widgetId === 'outgoing-calls-daily') {
this.callsRefreshTrigger = Date.now()
}
})
},
}
</script>
3. Event-bus subscription (direct)
The widget subscribes itself; no host plumbing needed.
<script>
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
export default {
props: { widgetId: { type: String, required: true } },
mounted() {
this._onRefresh = ({ widgetId }) => {
if (widgetId === this.widgetId) this.refetch()
}
subscribe('cn:widget:refresh', this._onRefresh)
},
beforeDestroy() {
unsubscribe('cn:widget:refresh', this._onRefresh)
},
methods: {
async refetch() { /* ... */ },
},
}
</script>
Reference (auto-generated)
The tables below are generated from the SFC source via vue-docgen-cli. They reflect what's actually in CnWidgetWrapper.vue and update automatically whenever the component changes.
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | () => t('nextcloud-vue', 'Widget') | Widget title | |
showTitle | boolean | true | Whether to show the header with title | |
borderless | boolean | false | Remove border and background — makes the wrapper transparent. Useful for widgets that are self-contained cards (e.g. CnStatsBlock). | |
flush | boolean | false | Remove content padding — allows content to go edge-to-edge. Useful for list-style widgets where items should span the full width. | |
iconUrl | string | null | Icon URL (image) | |
iconClass | string | null | Icon CSS class (e.g., Nextcloud icon class) | |
titleIconPosition | string | 'right' | Position of the title-icon slot in the header. 'left' places it before the title; 'right' places it after the actions. | |
titleIconColor | string | null | CSS color value applied to the title-icon slot container | |
buttons | array | [] | Footer action buttons: [{ text, link }] | |
styleConfig | { backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } } | \{\} | Style configuration for the wrapper. | |
hideRefresh | boolean | false | Hide the built-in Refresh item from the overflow action menu. The Refresh item is shown by default — set this when the widget has no refreshable data source (e.g. a static tile). Alias for the inverse :show-refresh="false"; either form opts out. | |
hideRequestFeature | boolean | false | Hide the built-in Request-a-feature item from the overflow action menu. Shown by default; set when the consuming app has no public issue tracker to link out to. Alias for the inverse :show-request-feature="false"; either form opts out. | |
showRefresh | boolean | true | Inverse of hideRefresh. Defaults to true so the action renders. Set :show-refresh="false" to hide. Either hideRefresh OR !showRefresh hides the action — the spec scenario in widget-wrapper declares the show-prefixed form as canonical; the hide- flags remain for back-compat. | |
showRequestFeature | boolean | true | Inverse of hideRequestFeature. Defaults to true so the action renders. Set :show-request-feature="false" to hide it. Either flag hides the action. | |
documentationUrl | string | '' | Documentation link for this widget. When a non-empty URL is set, the overflow menu renders a "Documentation" item that opens the link in a new tab. Host apps supply it from the widget configuration. Empty (the default) hides the item. | |
documentationLabel | string | () => t('nextcloud-vue', 'Documentation') | Optional pre-translated label for the Documentation action. Defaults to the lib's translation of "Documentation". | |
widgetId | string | '' | Widget id for the built-in default Refresh / Request-a-feature handlers (B2). Forwarded as the surface: "widget:<id>" value on the auto-mounted CnSuggestFeatureModal AND as the widgetId field on the cn:widget:refresh event-bus payload. When unset, the wrapper falls back to a slugified displayTitle, which works but is less stable than an explicit id. | |
specRef | string | '' | Optional specRef forwarded to the auto-mounted CnSuggestFeatureModal so the resulting GitHub issue links to the spec capability this widget belongs to. | |
refreshing | boolean | false | Whether a refresh is currently in flight. When bound by the host (e.g. :refreshing="loading" around its refetch), the Refresh icon spins for exactly as long as this stays true — accurate feedback. When left at its default false, clicking Refresh still spins optimistically for a short fixed duration (optimisticSpinMs) so the action feels responsive even without host wiring. | |
optimisticSpinMs | number | 800 | Duration (ms) of the optimistic spin shown on Refresh click when the host has NOT bound :refreshing. Defaults to 800ms — roughly two rotations at the icon's spin speed. Set to 0 to disable the optimistic spin entirely (icon only spins while refreshing is true). | |
refreshLabel | string | () => t('nextcloud-vue', 'Refresh') | Optional pre-translated label for the Refresh action. Defaults to the lib's translation of "Refresh" so callers usually don't need to set this. | |
requestFeatureLabel | string | () => t('nextcloud-vue', 'Request a feature') | Optional pre-translated label for the Request-a-feature action. Defaults to the lib's translation of "Request a feature". | |
actionsMenuLabel | string | () => t('nextcloud-vue', 'Actions') | Pre-translated aria-label / tooltip for the overflow menu trigger. Defaults to "Actions". |
Events
| Name | Payload | Description |
|---|---|---|
refresh | undefined | User clicked the Refresh item in the overflow action menu. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (event-bus emit on cn:widget:refresh). |
request-feature | undefined | User clicked the Request a feature item. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (auto-opening CnSuggestFeatureModal). |
Slots
| Name | Bindings | Description |
|---|---|---|
title-icon | — | |
title-meta | — | |
actions | — | actions Custom action buttons rendered before the |
action-items | — | |
default | — | |
footer | — |
Additional props & slots
| Prop | Type | Default | Description |
|---|---|---|---|
specRef | String | '' | Forwarded to the auto-mounted CnSuggestFeatureModal so the resulting issue links to the widget's spec capability. |
documentationUrl | String | '' | When set, renders a Documentation item in the overflow menu that opens this link in a new tab. Empty hides it. |
documentationLabel | String | t('Documentation') | Pre-translated label for the Documentation action. |
refreshLabel | String | t('Refresh') | Pre-translated label for the Refresh action. |
requestFeatureLabel | String | t('Request a feature') | Pre-translated label for the Request-a-feature action. |
actionsMenuLabel | String | t('Actions') | Pre-translated aria-label / tooltip for the overflow … menu trigger. |
| Slot | Description |
|---|---|
title-meta | Inline content rendered next to the title (e.g. the dashboard date-range chip). |