df-actions Component
The df-actions component provides a clean and responsive way to display action buttons or links, with support for responsive behavior across different screen sizes.
Basic Example
Here's a simple example of the df-actions component in action:
Features
- Renders actions as buttons or text links
- Supports responsive display based on screen size
- Automatic icon and label handling
- Grouping options for button layouts
- Integration with DynamicForms action system
defaultConfirm/defaultRejectactions are automatically coloredprimary/secondary
Props
| Prop | Type | Default | Description |
|---|---|---|---|
actions | Action[] or Ref<Action[]> | required | Array of Action objects to render - this library's Action, or a bare @dynamicforms/vue-forms one |
buttonSize | string or number | 'default' | Size of buttons (see Vuetify's v-btn size prop) |
showAsGroup | 'no' | 'grouped' | 'grouped-no-borders' | 'no' | Controls how buttons are grouped |
Action Object
The component expects an array of Action objects created with new Action() with the following structure:
interface ActionBreakpointOptions {
name?: string; // Unique identifier for the action
label?: string; // Display text
icon?: string; // Icon name, resolved by vue-cached-icon (e.g. ion-save-outline, mdi-content-save)
renderAs?: ActionDisplayStyle; // BUTTON or TEXT
showIcon?: boolean; // Whether to show the icon
showLabel?: boolean; // Whether to show the label
defaultConfirm?: boolean; // Marks the action as the "confirm" one - colored primary by default
defaultReject?: boolean; // Marks the action as the "reject/dismiss" one - colored secondary by default
passthroughAttrs?: Record<string, any>; // Extra props forwarded straight to the rendered <v-btn>
// Responsive breakpoints
xs?: ActionBreakpointRenderOptions;
sm?: ActionBreakpointRenderOptions;
md?: ActionBreakpointRenderOptions;
lg?: ActionBreakpointRenderOptions;
xl?: ActionBreakpointRenderOptions;
}A breakpoint states how the action is drawn at that width: renderAs, label, icon, showLabel, showIcon and passthroughAttrs. name, defaultConfirm and defaultReject are not among them - which key a modal resolves with, and which key fires the action, are properties of the action itself and do not depend on the width of the screen. They are stated once, in the base options, and ActionBreakpointRenderOptions is ActionRenderOptions without them.
passthroughAttrs cascades key by key, like any plain object: a breakpoint restating color leaves the density below it standing.
The Action object is the core component that defines how actions behave in the df-actions component. It extends the Action class of @dynamicforms/vue-forms to provide responsive behavior and visual configuration.
Actions declared in @dynamicforms/vue-forms
The component reads what it draws off the action's value rather than through this subclass's accessors, so an action declared as the Action of @dynamicforms/vue-forms - which states a label and an icon and nothing else - is rendered as well: as a button showing whichever of the two it carries. What the value does not state comes out as the default, and a label or icon that is not a string is not drawn, since this library renders both as text.
import { Action } from '@dynamicforms/vue-forms';
const cancel = new Action({ value: { label: 'Cancel' }, actions: [cancelHandler] });The subclass earns its keep where an action is to render responsively, as a text link, or in a color the confirm / reject flags pick: those members live in ActionRenderOptions, which only it declares. An action that renders as a plain button needs none of them.
The read itself is exported: getRenderOptionsForBreakpoint(value, breakpoint) answers the ActionRenderOptions a value renders as at one breakpoint, defaults filled in and label / icon filtered by their flags. Action.getBreakpointValue(breakpointRef) is a computed() over the same call.
Creating Actions
Actions are created with the Action constructor:
import { Action, ActionDisplayStyle } from '@dynamicforms/vuetify-inputs';
import { ExecuteAction } from '@dynamicforms/vue-forms';
const saveAction = new Action({
value: {
name: 'save',
label: 'Save',
icon: 'save-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: true,
showLabel: true
},
actions: [saveFormAction] // ExecuteAction handlers that run when the action is executed
});Action Value Configuration
The value object defines the visual appearance and behavior:
| Property | Type | Description |
|---|---|---|
name | string | Unique identifier for the action |
label | string | Display text |
icon | string | Icon name, resolved by vue-cached-icon; both ion- and mdi- prefixed names work |
renderAs | ActionDisplayStyle | How to render: BUTTON or TEXT |
showIcon | boolean | Whether to display the icon |
showLabel | boolean | Whether to display the label |
defaultConfirm | boolean | Marks this as the "confirm" action of the set; colors the button primary in <df-actions> (unless overridden via passthroughAttrs.color) |
defaultReject | boolean | Marks this as the "reject/dismiss" action of the set; colors the button secondary in <df-actions> (unless overridden via passthroughAttrs.color) |
passthroughAttrs | Record<string, any> | Extra props/attrs (e.g. color, density, rounded, block, prependIcon) forwarded to the rendered <v-btn>, overriding <df-actions>'s own computed props |
Every one of these but name, defaultConfirm and defaultReject can also be stated per breakpoint.
label and icon are @dynamicforms/vue-forms' own accessors: each reads the member of the same name off the value and each takes a write, which is an ordinary value change. renderedLabel and renderedIcon are this subclass's, and answer what the action draws — the text where showLabel states it is shown, the icon name where showIcon does, and undefined otherwise. showLabel and showIcon answer false where the text they govern is missing or empty, whatever the value holds. The breakpoint-resolved options <df-actions> renders from are filtered the same way, which is why the component reads none of these accessors.
enabled, busy and visibility aren't part of ActionRenderOptions - they're standard Action/Field members of @dynamicforms/vue-forms (enabled and visibility settable at the top level of the new Action() parameters, or via action.enabled / action.visibility directly; busy is a read) - but <df-actions> reacts to them too:
enabled: falsedisables the button (<v-btn disabled>), and so does a disabled container above the action: what the button binds iseffectiveEnabled, which isfalsewhere the action or anyGrouporListholding it is disabled.busydisables the button and draws itloadingfor as long as a run of the action has yet to settle.visibility: DisplayMode.HIDDENkeeps the button in the DOM with ad-noneclass.visibility: DisplayMode.INVISIBLEkeeps the button in the layout with aninvisibleclass (visibility: hidden).visibility: DisplayMode.SUPPRESSremoves the button from the rendered list entirely.
ActionDisplayStyle
ActionDisplayStyle.BUTTON: Renders as a Material Design button with backgroundActionDisplayStyle.TEXT: Renders as a text link without background
Responsive Breakpoints
Actions support responsive behavior through breakpoint-specific configurations:
const responsiveAction = new Action({
value: {
name: 'save',
label: 'Save Document',
icon: 'save-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: false, // Default: no icon
showLabel: true, // Default: show label
// Small screens: only show icon
sm: {
showIcon: true,
showLabel: false
},
// Medium screens: show both
md: {
showIcon: true,
showLabel: true
},
// Large screens: render as text link
lg: {
renderAs: ActionDisplayStyle.TEXT
}
}
});Breakpoint inheritance: Values cascade from smaller to larger breakpoints. If lg doesn't specify showIcon, it inherits the value from md (or the closest smaller breakpoint that defines it). Note that the "original values" (without breakpoint) represent the smallest breakpoint size.
All of an action's options but one are single values, and a breakpoint restating one replaces it. passthroughAttrs is an object and merges key by key instead, so a breakpoint states the attrs it changes and inherits the rest. The class behind the cascade, ResponsiveRenderOptions, also carries options that are lists, for the subclasses that need them - see responsive options if you are writing one.
Action Execution
Actions are executed through the actions array, which holds FieldActionBase handlers from @dynamicforms/vue-forms; the one that runs on execution is ExecuteAction:
import { ExecuteAction } from '@dynamicforms/vue-forms';
// Create the execution logic
const saveFormAction = new ExecuteAction(async (action, supr, params) => {
await saveDocument();
return supr(action, params); // Call the super method
});
// Attach to Action
const saveAction = new Action({
value: { /* visual config */ },
actions: [saveFormAction]
});Awaiting a run
action.execute(params?) runs the ExecuteAction chain and answers a promise carrying what the chain returned. The chain is entered synchronously - a handler has already run by the time execute() returns - but a handler that throws rejects that promise rather than throwing out of the call, so the failure is reported to whoever awaits the answer:
try {
await saveAction.execute();
showSuccessMessage('Document saved successfully');
} catch (error) {
showErrorMessage('Failed to save document');
}A call that neither awaits the answer nor attaches a .catch() leaves the rejection unhandled: it surfaces through the runtime rather than through the form, and the action carries no trace of it. A handler rendered through <df-actions> needs no change on that count - the component calls execute(event) from a template event handler, and Vue attaches its own catch to the promise such a handler answers with, so the rejection goes to Vue's error handling instead of nowhere. Where the user has to be told what failed, catch it in the ExecuteAction handler.
See handling a failed run for the whole contract.
Reporting a run in flight
action.busy is true from the call to execute() until the run it started settles, whether it resolves or rejects; overlapping runs are counted, so it stands until the last of them is done. <df-actions> binds it: the button is disabled and drawn loading while it stands, so a second click cannot start a second run of a handler that has yet to settle, and the wait is visible where the handler is slow.
execute() itself consults neither enabled nor busy, so this stops the click rather than the call: a programmatic execute() runs whatever either says, and an action that is to refuse an overlapping run refuses it in its own handler.
Predefined Actions
The library provides common action templates:
// Close action with default "Close" label and close icon
const closeAction = Action.closeAction();
// Yes/No actions for confirmation dialogs
const yesAction = Action.yesAction();
const noAction = Action.noAction();
// Override default properties
const customCloseAction = Action.closeAction({
value: {
label: 'Cancel',
renderAs: ActionDisplayStyle.TEXT
}
});Default Confirm / Reject Actions
defaultConfirm and defaultReject mark which action in a set represents "confirm" (e.g. Save, Yes, OK) versus "reject/dismiss" (e.g. Cancel, No). <df-actions> uses these flags to pick a sensible default button color - primary for defaultConfirm, secondary for defaultReject - so the important action in a set stands out without you having to set color by hand on every action:
const yes = Action.yesAction({ value: { defaultConfirm: true } }); // colored primary
const no = Action.noAction({ value: { defaultReject: true } }); // colored secondary
// A single close action can be both, e.g. when there's only one way to close
const close = Action.closeAction({ value: { defaultConfirm: true, defaultReject: true } });At most one action in a given set should set defaultConfirm, and at most one should set defaultReject.
Side note: if you're using
@dynamicforms/vuetify-modal-form-kit, its<df-modal>component also reads these same flags off the actions passed to itsactionsprop, to decide which action Enter / Escape should trigger. Since<df-modal>'sactionsslot is normally rendered via<df-actions>too, the two concerns - keyboard shortcut and default button color - line up for free.
Passthrough Attributes
passthroughAttrs forwards arbitrary props straight to the rendered <v-btn>, taking precedence over <df-actions>'s own computed props (variant, color, disabled, loading). Use it for anything <v-btn> supports that isn't already modeled by ActionRenderOptions:
const deleteAction = new Action({
value: {
name: 'delete',
label: 'Delete',
icon: 'trash-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: true,
showLabel: true,
passthroughAttrs: { color: 'error', variant: 'flat' },
},
actions: [deleteFormAction],
});Complete Example
import { Action, ActionDisplayStyle } from '@dynamicforms/vuetify-inputs';
import { ExecuteAction } from '@dynamicforms/vue-forms';
// Business logic
const submitFormAction = new ExecuteAction(async (action, supr, params) => {
await api.submitForm(form.value);
router.push('/success');
return supr(action, params);
});
// Responsive action configuration
const submitAction = new Action({
value: {
name: 'submit',
label: 'Submit Form',
icon: 'checkmark-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: false,
showLabel: true,
// Mobile: icon only
xs: { showIcon: true, showLabel: false },
// Tablet and up: both icon and label
md: { showIcon: true, showLabel: true },
// Desktop: text link style
xl: { renderAs: ActionDisplayStyle.TEXT }
},
actions: [submitFormAction]
});form here is a Group: it hands out what it holds through the value property, or through fullValue where disabled members have to be in the payload.
This approach separates visual presentation (the Action and its render options) from business logic (the ExecuteAction handlers), and lets each screen size get the presentation that fits it.
Responsive Behavior
The component automatically adapts to different screen sizes based on the breakpoint configuration:
const saveAction = new Action({
value: {
name: 'save',
label: 'Save',
icon: 'save-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: true,
showLabel: false,
md: { showLabel: true, showIcon: false }, // Medium screens and up
lg: { showIcon: true } // Large screens and up
},
actions: [formSaveAction]
});With this configuration:
- On small screens: Only the icon is shown
- On medium screens: Only the label is shown
- On large screens: Both icon and label are shown
Button Grouping
The showAsGroup property allows you to control the visual grouping of buttons:
'no': Buttons are displayed as separate elements'grouped': Buttons are grouped with borders'grouped-no-borders': Buttons are grouped without borders
Usage with DynamicForms
<template>
<df-actions :actions="actions" button-size="small" />
</template>
<script setup>
import { ExecuteAction } from '@dynamicforms/vue-forms';
import { Action, ActionDisplayStyle } from '@dynamicforms/vuetify-inputs';
// Create form action
const saveFormAction = new ExecuteAction((action, supr, params) => {
// Your save logic here
console.log('Save action executed');
return supr(action, params);
});
// Create actions for the component
const actions = ref([
new Action({
value: {
name: 'save',
label: 'Save',
icon: 'save-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: true,
showLabel: true
},
actions: [saveFormAction]
}),
new Action({
value: {
name: 'cancel',
label: 'Cancel',
icon: 'close-outline',
renderAs: ActionDisplayStyle.BUTTON,
showIcon: true,
showLabel: true
}
})
]);
</script>