Skip to main content

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

Loading CnWidgetWrapper playground…

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

PropTypeDefaultDescription
titleString'Widget'Widget title shown in the header
showTitleBooleantrueWhether to render the header bar
borderlessBooleanfalseRemove border and background — makes the wrapper transparent
flushBooleanfalseRemove content padding — lets content extend edge-to-edge
iconUrlStringnullImage URL for the header icon
iconClassStringnullCSS class for the header icon (e.g. Nextcloud icon class)
titleIconPositionString'right'Position of the title-icon slot in the header: 'left' places it before the title group; 'right' places it after the actions
titleIconColorStringnullCSS color value applied to the title-icon slot container (e.g. '#e74c3c')
buttonsArray[]Footer button links: [{ text, link }]
styleConfigObject{}Runtime style overrides: { backgroundColor?, borderStyle?, borderWidth?, borderColor?, borderRadius?, padding?: { top, right, bottom, left } }
refreshingBooleanfalseWhen 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
optimisticSpinMsNumber800Duration (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

SlotDescription
defaultWidget content rendered in the scrollable body area
actionsButtons or controls placed in the right side of the header
title-iconExtra icon element rendered in the header at the position controlled by titleIconPosition (left of title or right of actions)
footerCustom 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 calls event.preventDefault()) emits on the @nextcloud/event-bus channel cn:widget:refresh with payload { widgetId, title }. Widgets that care subscribe and filter by widgetId.
  • Documentation — rendered only when a documentationUrl is 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 with documentationLabel.
  • Request a feature — emits @request-feature, then (unless suppressed) auto-mounts CnSuggestFeatureModal with app + page + surface=widget:<id> context auto-filled from CnAppRoot injects. The host can override the default by binding @request-feature and calling event.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

NameTypeRequiredDefaultDescription
titlestring() =&gt; t('nextcloud-vue', 'Widget')Widget title
showTitlebooleantrueWhether to show the header with title
borderlessbooleanfalseRemove border and background — makes the wrapper transparent. Useful for widgets that are self-contained cards (e.g. CnStatsBlock).
flushbooleanfalseRemove content padding — allows content to go edge-to-edge. Useful for list-style widgets where items should span the full width.
iconUrlstringnullIcon URL (image)
iconClassstringnullIcon CSS class (e.g., Nextcloud icon class)
titleIconPositionstring'right'Position of the title-icon slot in the header. 'left' places it before the title; 'right' places it after the actions.
titleIconColorstringnullCSS color value applied to the title-icon slot container
buttonsarray[]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.
hideRefreshbooleanfalseHide 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.
hideRequestFeaturebooleanfalseHide 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.
showRefreshbooleantrueInverse 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.
showRequestFeaturebooleantrueInverse of hideRequestFeature. Defaults to true so the action renders. Set :show-request-feature="false" to hide it. Either flag hides the action.
documentationUrlstring''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.
documentationLabelstring() =&gt; t('nextcloud-vue', 'Documentation')Optional pre-translated label for the Documentation action. Defaults to the lib's translation of "Documentation".
widgetIdstring''Widget id for the built-in default Refresh / Request-a-feature handlers (B2). Forwarded as the surface: "widget:&lt;id&gt;" 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.
specRefstring''Optional specRef forwarded to the auto-mounted CnSuggestFeatureModal so the resulting GitHub issue links to the spec capability this widget belongs to.
refreshingbooleanfalseWhether 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.
optimisticSpinMsnumber800Duration (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).
refreshLabelstring() =&gt; 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.
requestFeatureLabelstring() =&gt; 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".
actionsMenuLabelstring() =&gt; t('nextcloud-vue', 'Actions')Pre-translated aria-label / tooltip for the overflow menu trigger. Defaults to "Actions".

Events

NamePayloadDescription
refreshundefinedUser 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-featureundefinedUser 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

NameBindingsDescription
title-icon
title-meta
actionsactions Custom action buttons rendered before the
action-items
default
footer

Additional props & slots

PropTypeDefaultDescription
specRefString''Forwarded to the auto-mounted CnSuggestFeatureModal so the resulting issue links to the widget's spec capability.
documentationUrlString''When set, renders a Documentation item in the overflow menu that opens this link in a new tab. Empty hides it.
documentationLabelStringt('Documentation')Pre-translated label for the Documentation action.
refreshLabelStringt('Refresh')Pre-translated label for the Refresh action.
requestFeatureLabelStringt('Request a feature')Pre-translated label for the Request-a-feature action.
actionsMenuLabelStringt('Actions')Pre-translated aria-label / tooltip for the overflow menu trigger.
SlotDescription
title-metaInline content rendered next to the title (e.g. the dashboard date-range chip).