Responsive options
ResponsiveRenderOptions is the class behind an Action's breakpoints: it holds one set of options per breakpoint and resolves them into the set that applies at a given screen width. Action uses it for its own render options, and it is exported so you can build your own responsive object on top of it - @dynamicforms/vuetify-modal-form-kit builds its form, row and column layouts that way.
If all you do is set sm / md / lg on an action, df-actions covers what you need. This page is about writing a subclass.
How a breakpoint is resolved
Options cascade from smaller breakpoints to larger ones. The options written without a breakpoint are the smallest size, and each breakpoint states only what changes from the one below it. Resolving md therefore walks xs, sm, md in order and lets each one state its part.
What "state its part" means depends on the kind of value:
| Value | At a breakpoint |
|---|---|
undefined or null | says nothing - the field keeps what it inherited |
| a single value (string, number, boolean, enum) | replaces what it inherited |
| a plain object | is merged into what it inherited, key by key |
anything else - an array, a Date, a Map, a class instance | replaces what it inherited |
// base: { cols: 8, offset: 2 }, sm: { cols: 12 }
options.getOptionsForBreakpoint('sm'); // { cols: 12, offset: 2 } - offset carried overThe object merge is shallow: a nested object is a value like any other, so restating it replaces it whole.
// base: { style: { color: 'red', margin: 10 } }, sm: { style: { color: 'blue' } }
options.getOptionsForBreakpoint('sm'); // { style: { color: 'blue' } } - margin is goneOnly plain objects merge. A Date, a Map, a Set or an instance of your own class is replaced, because merging one would strip it of its prototype and leave a bare copy of its own properties behind.
Writing a subclass
A subclass declares the shape of its options and implements cleanBreakpoint(), which normalizes one breakpoint's worth of them - it is where you validate input and drop what you do not accept.
import { ResponsiveRenderOptions } from '@dynamicforms/vuetify-inputs';
interface PanelOptions {
title?: string;
props?: Record<string, any>;
items?: string[];
}
class ResponsivePanelOptions extends ResponsiveRenderOptions<PanelOptions> {
protected cleanBreakpoint(bp?: PanelOptions, defaultIfEmpty: boolean = false): PanelOptions | null {
if (!bp && !defaultIfEmpty) return null;
return {
title: isString(bp?.title) ? bp.title : undefined,
// a field the breakpoint does not state comes back undefined, never as an empty value
props: bp?.props ? { ...bp.props } : undefined,
items: bp?.items ? [...bp.items] : undefined,
};
}
}One rule makes the cascade behave: return undefined for a field the breakpoint does not state. An empty array or an empty string is a value, and the merge treats it as one - it will replace what the breakpoint inherited. That is what lets a breakpoint clear a list on purpose:
// base: { items: ['a', 'b'] }, md: { items: [] }
options.getOptionsForBreakpoint('sm'); // items: ['a', 'b']
options.getOptionsForBreakpoint('md'); // items: [] - emptied deliberatelyWatch for a class field that initializes a collection: items: string[] = [] makes every breakpoint object state an empty list from the moment it is constructed, and every one of them will then clear the list instead of inheriting it. Leave such a field optional and fill it in when something is actually added.
A field takes part in the cascade because a breakpoint states it, not because the base does: a field named at md and nowhere else resolves from md upwards, and comes back undefined below it.
// base: {}, md: { title: 'only at md' }
options.getOptionsForBreakpoint('sm'); // { }
options.getOptionsForBreakpoint('md'); // { title: 'only at md' }Options a breakpoint may not state
Some options belong to the object as a whole rather than to one screen width - an identifier, a flag another component keys on. BreakpointsJSON takes a second type argument for what a breakpoint may state, so such an option is a type error at a breakpoint instead of a value that quietly does nothing:
interface PanelOptions {
name?: string;
title?: string;
}
type PanelBreakpointOptions = BreakpointsJSON<PanelOptions, Omit<PanelOptions, 'name'>>;
const panel: PanelBreakpointOptions = { name: 'main', md: { name: 'wide' } }; // error: `name` is not per-breakpointThis is how an action's options are declared: ActionBreakpointOptions is ActionRenderOptions with name, defaultConfirm and defaultReject left out of every breakpoint.
See also: df-actions, input base
