@omnitend/dashboard-for-laravel 0.16.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/base/DButton.vue.d.ts +57 -2
- package/dist/components/extended/DXField.vue.d.ts +75 -25
- package/dist/components/extended/DXForm.vue.d.ts +23 -1
- package/dist/components/extended/DXRepeater.vue.d.ts +16 -0
- package/dist/dashboard-for-laravel.js +10611 -10154
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +5 -1
- package/dist/types/index.d.ts +50 -1
- package/docs/public/api-reference.json +172 -4
- package/docs/public/docs-map.md +1 -1
- package/package.json +2 -1
- package/resources/css/theme.scss +28 -0
- package/resources/js/components/base/DButton.vue +170 -6
- package/resources/js/components/extended/DXField.vue +154 -7
- package/resources/js/components/extended/DXForm.vue +206 -80
- package/resources/js/components/extended/DXRepeater.vue +270 -4
- package/resources/js/components/extended/DXTable.vue +6 -7
- package/resources/js/types/index.ts +52 -1
|
@@ -21,7 +21,51 @@
|
|
|
21
21
|
/>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
|
-
<!-- Checkbox
|
|
24
|
+
<!-- Checkbox (horizontal layout): wrapped in DFormGroup for the same
|
|
25
|
+
label/content column split as other field types, instead of the
|
|
26
|
+
label-beside-control layout below. The inline label beside the
|
|
27
|
+
checkbox itself is kept (not deduplicated against the outer label)
|
|
28
|
+
so the control's accessible name doesn't depend on aria-labelledby
|
|
29
|
+
association through the fieldset alone — but its `info` popover is
|
|
30
|
+
omitted here, since rendering the same popover trigger twice (once
|
|
31
|
+
per label) would be a duplicated interactive element, not just
|
|
32
|
+
duplicated text. -->
|
|
33
|
+
<DFormGroup
|
|
34
|
+
v-else-if="field.type === 'checkbox' && isHorizontal"
|
|
35
|
+
:class="field.class || 'mb-3'"
|
|
36
|
+
v-bind="horizontalAttrs"
|
|
37
|
+
>
|
|
38
|
+
<template #label>
|
|
39
|
+
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
40
|
+
</template>
|
|
41
|
+
<slot
|
|
42
|
+
v-if="$slots.value"
|
|
43
|
+
name="value"
|
|
44
|
+
:field="field"
|
|
45
|
+
:model="model"
|
|
46
|
+
:value="fieldValue"
|
|
47
|
+
:update="setValue"
|
|
48
|
+
/>
|
|
49
|
+
<DFormCheckbox
|
|
50
|
+
v-else
|
|
51
|
+
v-model="fieldValue"
|
|
52
|
+
:disabled="isDisabled || isReadonly"
|
|
53
|
+
v-bind="field.inputProps"
|
|
54
|
+
>
|
|
55
|
+
<DXFieldLabel :label="resolvedLabel" />
|
|
56
|
+
</DFormCheckbox>
|
|
57
|
+
|
|
58
|
+
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
59
|
+
{{ form.getError(errorKey) }}
|
|
60
|
+
</DFormInvalidFeedback>
|
|
61
|
+
<slot name="info" :field="field" :model="model" />
|
|
62
|
+
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
63
|
+
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
64
|
+
</DFormText>
|
|
65
|
+
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
66
|
+
</DFormGroup>
|
|
67
|
+
|
|
68
|
+
<!-- Checkbox (vertical, default): no label wrapper, label sits beside the control -->
|
|
25
69
|
<div v-else-if="field.type === 'checkbox'" :class="field.class || 'mb-3'">
|
|
26
70
|
<!--
|
|
27
71
|
@slot Replaces the built-in control with a custom value editor.
|
|
@@ -67,7 +111,46 @@
|
|
|
67
111
|
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
68
112
|
</div>
|
|
69
113
|
|
|
70
|
-
<!-- Switch
|
|
114
|
+
<!-- Switch (horizontal layout): same column-split treatment as checkbox
|
|
115
|
+
above, including omitting `info` from the inner label to avoid a
|
|
116
|
+
duplicated popover trigger. -->
|
|
117
|
+
<DFormGroup
|
|
118
|
+
v-else-if="field.type === 'switch' && isHorizontal"
|
|
119
|
+
:class="[field.class || 'mb-3', 'dx-switch', { 'dx-switch--on': switchIsOn }]"
|
|
120
|
+
v-bind="horizontalAttrs"
|
|
121
|
+
>
|
|
122
|
+
<template #label>
|
|
123
|
+
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
124
|
+
</template>
|
|
125
|
+
<slot
|
|
126
|
+
v-if="$slots.value"
|
|
127
|
+
name="value"
|
|
128
|
+
:field="field"
|
|
129
|
+
:model="model"
|
|
130
|
+
:value="fieldValue"
|
|
131
|
+
:update="setValue"
|
|
132
|
+
/>
|
|
133
|
+
<DFormCheckbox
|
|
134
|
+
v-else
|
|
135
|
+
v-model="switchModel"
|
|
136
|
+
switch
|
|
137
|
+
:disabled="isDisabled || isReadonly"
|
|
138
|
+
v-bind="field.inputProps"
|
|
139
|
+
>
|
|
140
|
+
<DXFieldLabel :label="switchText" />
|
|
141
|
+
</DFormCheckbox>
|
|
142
|
+
|
|
143
|
+
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
144
|
+
{{ form.getError(errorKey) }}
|
|
145
|
+
</DFormInvalidFeedback>
|
|
146
|
+
<slot name="info" :field="field" :model="model" />
|
|
147
|
+
<DFormText v-if="resolvedHint || $slots.hint" class="text-muted">
|
|
148
|
+
<slot name="hint" :field="field" :model="model">{{ resolvedHint }}</slot>
|
|
149
|
+
</DFormText>
|
|
150
|
+
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
151
|
+
</DFormGroup>
|
|
152
|
+
|
|
153
|
+
<!-- Switch (vertical, default): toggle with contextual on/off text and an on-state style -->
|
|
71
154
|
<div
|
|
72
155
|
v-else-if="field.type === 'switch'"
|
|
73
156
|
:class="[field.class || 'mb-3', 'dx-switch', { 'dx-switch--on': switchIsOn }]"
|
|
@@ -119,7 +202,7 @@
|
|
|
119
202
|
|
|
120
203
|
<!-- Repeater: nested, repeatable sub-form -->
|
|
121
204
|
<div v-else-if="field.type === 'repeater'" :class="field.class || 'mb-3'">
|
|
122
|
-
<DFormGroup>
|
|
205
|
+
<DFormGroup v-bind="repeaterHorizontalAttrs">
|
|
123
206
|
<template #label>
|
|
124
207
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
125
208
|
</template>
|
|
@@ -154,9 +237,11 @@
|
|
|
154
237
|
</div>
|
|
155
238
|
|
|
156
239
|
<!-- Standard labelled field -->
|
|
157
|
-
<DFormGroup v-else :class="field.class || 'mb-3'">
|
|
158
|
-
<!-- Label with optional info popover
|
|
159
|
-
|
|
240
|
+
<DFormGroup v-else :class="field.class || 'mb-3'" v-bind="horizontalAttrs">
|
|
241
|
+
<!-- Label with optional info popover. Omitted entirely (not just
|
|
242
|
+
emptied) when hideLabel is set, so BFormGroup doesn't reserve a
|
|
243
|
+
label column/row for it (see DXRepeater's table layout). -->
|
|
244
|
+
<template v-if="!hideLabel" #label>
|
|
160
245
|
<DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
|
|
161
246
|
</template>
|
|
162
247
|
|
|
@@ -380,7 +465,7 @@ import DFormText from "../base/DFormText.vue";
|
|
|
380
465
|
import DInputGroup from "../base/DInputGroup.vue";
|
|
381
466
|
import DXFieldLabel from "./DXFieldLabel.vue";
|
|
382
467
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
383
|
-
import type { FieldDefinition, FieldOption, FieldType } from "../../types";
|
|
468
|
+
import type { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
|
|
384
469
|
import { getByPath, setByPath } from "../../utils/objectPath";
|
|
385
470
|
|
|
386
471
|
// Async to break the DXField <-> DXRepeater circular import.
|
|
@@ -405,12 +490,74 @@ interface Props {
|
|
|
405
490
|
|
|
406
491
|
/** Error key for validation lookups (defaults to keyPath/field.key). */
|
|
407
492
|
errorKey?: string;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Field layout: "vertical" (default, label above input) or "horizontal"
|
|
496
|
+
* (label left, input right). Normally set by DXForm (form-level, with
|
|
497
|
+
* `field.layout` taking precedence); pass directly when using DXField
|
|
498
|
+
* standalone.
|
|
499
|
+
*/
|
|
500
|
+
layout?: "vertical" | "horizontal";
|
|
501
|
+
|
|
502
|
+
/** Label column width for horizontal layout (mirrors BFormGroup). */
|
|
503
|
+
labelCols?: LabelCols;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Skip rendering the field's own label entirely (only affects the
|
|
507
|
+
* standard labelled-field branch). Used by DXRepeater's `table` layout,
|
|
508
|
+
* where a column header already names the field and a per-row label
|
|
509
|
+
* would waste space in the cell.
|
|
510
|
+
*/
|
|
511
|
+
hideLabel?: boolean;
|
|
408
512
|
}
|
|
409
513
|
|
|
410
514
|
const props = defineProps<Props>();
|
|
411
515
|
|
|
412
516
|
const effectiveModel = computed(() => props.model ?? props.form.data);
|
|
413
517
|
|
|
518
|
+
// ————————————————— horizontal layout (#66)
|
|
519
|
+
|
|
520
|
+
// A `span` field always bypasses the label/content column split — it's
|
|
521
|
+
// rendered as fully custom full-width content via the very first template
|
|
522
|
+
// branch, before layout is ever considered.
|
|
523
|
+
const isHorizontal = computed(
|
|
524
|
+
() => props.layout === "horizontal" && !props.field.span,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
/** Translate a `LabelCols` value into the BFormGroup attrs it expects. */
|
|
528
|
+
function labelColsAttrs(cols: LabelCols | undefined): Record<string, any> {
|
|
529
|
+
if (cols === undefined) return {};
|
|
530
|
+
// BFormGroup's own `labelCols` prop accepts a number OR a numeric string
|
|
531
|
+
// (BVN's `Numberish` type) — pass anything that isn't the breakpoint
|
|
532
|
+
// object straight through, rather than gatekeeping on `typeof === "number"`,
|
|
533
|
+
// so a numeric string (e.g. from an unbound template attribute or a
|
|
534
|
+
// JSON-driven field config) still activates horizontal layout instead of
|
|
535
|
+
// silently falling through to an empty breakpoint-object read.
|
|
536
|
+
if (typeof cols !== "object" || cols === null) {
|
|
537
|
+
return { labelCols: cols };
|
|
538
|
+
}
|
|
539
|
+
const attrs: Record<string, any> = {};
|
|
540
|
+
if (cols.sm !== undefined) attrs.labelColsSm = cols.sm;
|
|
541
|
+
if (cols.md !== undefined) attrs.labelColsMd = cols.md;
|
|
542
|
+
if (cols.lg !== undefined) attrs.labelColsLg = cols.lg;
|
|
543
|
+
if (cols.xl !== undefined) attrs.labelColsXl = cols.xl;
|
|
544
|
+
return attrs;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Default to a 3-column label when horizontal but no width was configured,
|
|
548
|
+
// so `layout: "horizontal"` alone is enough to see the effect.
|
|
549
|
+
const horizontalAttrs = computed<Record<string, any>>(() =>
|
|
550
|
+
isHorizontal.value ? labelColsAttrs(props.labelCols ?? 3) : {},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// A table-layout repeater is inherently wide — squeezing its own label into a
|
|
554
|
+
// narrow left column (as if it were a single-line input) reads badly, unlike
|
|
555
|
+
// a normal horizontal field. Its label always renders above, regardless of
|
|
556
|
+
// the form/field's horizontal layout setting (#68 follow-up).
|
|
557
|
+
const repeaterHorizontalAttrs = computed<Record<string, any>>(() =>
|
|
558
|
+
props.field.repeaterLayout === "table" ? {} : horizontalAttrs.value,
|
|
559
|
+
);
|
|
560
|
+
|
|
414
561
|
// Path semantics (getByPath/setByPath) are only used when a parent passes an
|
|
415
562
|
// explicit keyPath (e.g. a repeater binding `lines.0.price`). For top-level
|
|
416
563
|
// fields the key is used literally, so a field whose key legitimately contains
|
|
@@ -24,99 +24,199 @@
|
|
|
24
24
|
</DAlert>
|
|
25
25
|
|
|
26
26
|
<!-- Tabbed layout. BTabs exposes the active *index* via v-model:index
|
|
27
|
-
(plain v-model is the active tab id), which is what we track.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
:model="model"
|
|
46
|
-
/>
|
|
47
|
-
|
|
48
|
-
<div v-else class="pt-3">
|
|
27
|
+
(plain v-model is the active tab id), which is what we track.
|
|
28
|
+
`card` needs BOTH: DTabs' own `card` prop (adds `card-header`/
|
|
29
|
+
`card-body` classes to its nav/content internally) AND an outer
|
|
30
|
+
`.card` element wrapping it (BVN's `card` prop does not add the
|
|
31
|
+
outer wrapper itself — the consumer supplies it, per BVN's docs
|
|
32
|
+
pattern). `no-body` stops DCard from adding its own
|
|
33
|
+
`.card-body` wrapper — DTabs already provides `.card-header`/
|
|
34
|
+
`.card-body` internally via its own `card` prop, so wrapping
|
|
35
|
+
that in another `.card-body` would double up. -->
|
|
36
|
+
<component :is="card ? DCard : 'div'" v-if="hasTabs" v-bind="card ? { noBody: true } : {}">
|
|
37
|
+
<DTabs v-model:index="activeTab" :card="card">
|
|
38
|
+
<DTab
|
|
39
|
+
v-for="(tab, index) in visibleTabs"
|
|
40
|
+
:key="tab.key"
|
|
41
|
+
:title="resolveTabLabel(tab)"
|
|
42
|
+
:lazy="tab.lazy"
|
|
43
|
+
:active="index === 0"
|
|
44
|
+
>
|
|
49
45
|
<!--
|
|
50
|
-
@slot
|
|
46
|
+
@slot Replaces the entire body of a tab, keyed by tab (slot name `tab-content(<tabKey>)`).
|
|
51
47
|
@binding {FormTab} tab The tab definition being rendered.
|
|
52
48
|
@binding {object} model Live form data merged with `context`, for predicates.
|
|
53
49
|
-->
|
|
54
|
-
<slot
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
:key="field.key"
|
|
59
|
-
:field="field"
|
|
60
|
-
:form="resolvedForm"
|
|
50
|
+
<slot
|
|
51
|
+
v-if="$slots[`tab-content(${tab.key})`]"
|
|
52
|
+
:name="`tab-content(${tab.key})`"
|
|
53
|
+
:tab="tab"
|
|
61
54
|
:model="model"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<div v-else class="pt-3">
|
|
58
|
+
<!--
|
|
59
|
+
@slot Content inserted above a tab's fields, keyed by tab (slot name `tab-before(<tabKey>)`).
|
|
60
|
+
@binding {FormTab} tab The tab definition being rendered.
|
|
61
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
62
|
+
-->
|
|
63
|
+
<slot :name="`tab-before(${tab.key})`" :tab="tab" :model="model" />
|
|
64
|
+
|
|
65
|
+
<template v-for="field in visibleFieldsFor(tab)" :key="field.key">
|
|
66
|
+
<!--
|
|
67
|
+
@slot Fully replaces a field's rendering (including its label), keyed by field key: `field(<key>)`. Unlike `value(<key>)`, bypasses DXField entirely — like `tab-content`, this also supersedes `field-before(<key>)`/`field-after(<key>)` for the same key.
|
|
68
|
+
@binding {FieldDefinition} field The field definition being replaced.
|
|
69
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
70
|
+
-->
|
|
71
|
+
<slot
|
|
72
|
+
v-if="$slots[`field(${field.key})`]"
|
|
73
|
+
:name="`field(${field.key})`"
|
|
74
|
+
:field="field"
|
|
75
|
+
:model="model"
|
|
76
|
+
/>
|
|
77
|
+
<template v-else>
|
|
78
|
+
<!--
|
|
79
|
+
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
80
|
+
@binding {FieldDefinition} field The field about to be rendered.
|
|
81
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
82
|
+
-->
|
|
83
|
+
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
84
|
+
|
|
85
|
+
<DXField
|
|
86
|
+
:field="field"
|
|
87
|
+
:form="resolvedForm"
|
|
88
|
+
:model="model"
|
|
89
|
+
:layout="field.layout ?? layout"
|
|
90
|
+
:label-cols="field.labelCols ?? labelCols"
|
|
91
|
+
>
|
|
92
|
+
<template
|
|
93
|
+
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
94
|
+
:key="target"
|
|
95
|
+
#[target]="slotProps"
|
|
96
|
+
>
|
|
97
|
+
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
98
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
99
|
+
</template>
|
|
100
|
+
</DXField>
|
|
101
|
+
|
|
102
|
+
<!--
|
|
103
|
+
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
104
|
+
@binding {FieldDefinition} field The field that was just rendered.
|
|
105
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
106
|
+
-->
|
|
107
|
+
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
108
|
+
</template>
|
|
70
109
|
</template>
|
|
71
|
-
</DXField>
|
|
72
110
|
|
|
111
|
+
<!--
|
|
112
|
+
@slot Content inserted below a tab's fields, keyed by tab (slot name `tab-after(<tabKey>)`).
|
|
113
|
+
@binding {FormTab} tab The tab definition being rendered.
|
|
114
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
115
|
+
-->
|
|
116
|
+
<slot :name="`tab-after(${tab.key})`" :tab="tab" :model="model" />
|
|
117
|
+
</div>
|
|
118
|
+
</DTab>
|
|
119
|
+
</DTabs>
|
|
120
|
+
</component>
|
|
121
|
+
|
|
122
|
+
<!-- Flat layout (no tabs). When `card` is set, the fields + submit
|
|
123
|
+
button + footer render inside a DCard, giving the form a visual
|
|
124
|
+
boundary (the tabbed case gets this from DTabs' own `card` prop
|
|
125
|
+
instead, above). -->
|
|
126
|
+
<template v-if="!hasTabs">
|
|
127
|
+
<component :is="card ? DCard : 'div'">
|
|
128
|
+
<template v-for="field in visibleFlatFields" :key="field.key">
|
|
73
129
|
<!--
|
|
74
|
-
@slot
|
|
75
|
-
@binding {
|
|
130
|
+
@slot Fully replaces a field's rendering (including its label), keyed by field key: `field(<key>)`. Unlike `value(<key>)`, bypasses DXField entirely — like `tab-content`, this also supersedes `field-before(<key>)`/`field-after(<key>)` for the same key.
|
|
131
|
+
@binding {FieldDefinition} field The field definition being replaced.
|
|
76
132
|
@binding {object} model Live form data merged with `context`, for predicates.
|
|
77
133
|
-->
|
|
78
|
-
<slot
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
<slot
|
|
135
|
+
v-if="$slots[`field(${field.key})`]"
|
|
136
|
+
:name="`field(${field.key})`"
|
|
137
|
+
:field="field"
|
|
138
|
+
:model="model"
|
|
139
|
+
/>
|
|
140
|
+
<template v-else>
|
|
141
|
+
<!--
|
|
142
|
+
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
143
|
+
@binding {FieldDefinition} field The field about to be rendered.
|
|
144
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
145
|
+
-->
|
|
146
|
+
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
147
|
+
|
|
148
|
+
<DXField
|
|
149
|
+
:field="field"
|
|
150
|
+
:form="resolvedForm"
|
|
151
|
+
:model="model"
|
|
152
|
+
:layout="field.layout ?? layout"
|
|
153
|
+
:label-cols="field.labelCols ?? labelCols"
|
|
154
|
+
>
|
|
155
|
+
<template
|
|
156
|
+
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
157
|
+
:key="target"
|
|
158
|
+
#[target]="slotProps"
|
|
159
|
+
>
|
|
160
|
+
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
161
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
162
|
+
</template>
|
|
163
|
+
</DXField>
|
|
164
|
+
|
|
165
|
+
<!--
|
|
166
|
+
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
167
|
+
@binding {FieldDefinition} field The field that was just rendered.
|
|
168
|
+
@binding {object} model Live form data merged with `context`, for predicates.
|
|
169
|
+
-->
|
|
170
|
+
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
171
|
+
</template>
|
|
172
|
+
</template>
|
|
82
173
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<template
|
|
93
|
-
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
94
|
-
:key="target"
|
|
95
|
-
#[target]="slotProps"
|
|
174
|
+
<!-- Submit button -->
|
|
175
|
+
<DButton
|
|
176
|
+
v-if="showSubmit"
|
|
177
|
+
type="submit"
|
|
178
|
+
variant="primary"
|
|
179
|
+
block
|
|
180
|
+
:loading="resolvedForm.processing"
|
|
181
|
+
:loading-text="submitLoadingText"
|
|
182
|
+
class="mt-3"
|
|
96
183
|
>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
184
|
+
{{ submitText }}
|
|
185
|
+
</DButton>
|
|
186
|
+
|
|
187
|
+
<!--
|
|
188
|
+
@slot Content rendered below the submit button (e.g. a cancel link or secondary actions).
|
|
189
|
+
@binding {UseFormReturn} form The resolved form instance (state, errors, submit helpers).
|
|
190
|
+
-->
|
|
191
|
+
<slot name="footer" :form="resolvedForm" />
|
|
192
|
+
</component>
|
|
101
193
|
</template>
|
|
102
194
|
|
|
103
|
-
<!--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
195
|
+
<!-- Tabbed layout: submit button + footer always render as siblings
|
|
196
|
+
below DTabs (outside the card, when `card` is set) — unlike the
|
|
197
|
+
flat layout, they aren't pulled inside the card here, since
|
|
198
|
+
DTabs' own card-body padding belongs to its tab content, not to
|
|
199
|
+
trailing form-level actions. -->
|
|
200
|
+
<template v-else>
|
|
201
|
+
<!-- Submit button -->
|
|
202
|
+
<DButton
|
|
203
|
+
v-if="showSubmit"
|
|
204
|
+
type="submit"
|
|
205
|
+
variant="primary"
|
|
206
|
+
block
|
|
207
|
+
:loading="resolvedForm.processing"
|
|
208
|
+
:loading-text="submitLoadingText"
|
|
209
|
+
class="mt-3"
|
|
210
|
+
>
|
|
211
|
+
{{ submitText }}
|
|
212
|
+
</DButton>
|
|
213
|
+
|
|
214
|
+
<!--
|
|
215
|
+
@slot Content rendered below the submit button (e.g. a cancel link or secondary actions).
|
|
216
|
+
@binding {UseFormReturn} form The resolved form instance (state, errors, submit helpers).
|
|
217
|
+
-->
|
|
218
|
+
<slot name="footer" :form="resolvedForm" />
|
|
219
|
+
</template>
|
|
120
220
|
</BForm>
|
|
121
221
|
</template>
|
|
122
222
|
|
|
@@ -125,12 +225,13 @@ import { computed, watch } from "vue";
|
|
|
125
225
|
import { BForm } from "bootstrap-vue-next";
|
|
126
226
|
import DAlert from "../base/DAlert.vue";
|
|
127
227
|
import DButton from "../base/DButton.vue";
|
|
228
|
+
import DCard from "../base/DCard.vue";
|
|
128
229
|
import DTabs from "../base/DTabs.vue";
|
|
129
230
|
import DTab from "../base/DTab.vue";
|
|
130
231
|
import DXField from "./DXField.vue";
|
|
131
232
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
132
233
|
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
133
|
-
import type { FieldDefinition, FormTab, MaybeFn } from "../../types";
|
|
234
|
+
import type { FieldDefinition, FormTab, LabelCols, MaybeFn } from "../../types";
|
|
134
235
|
|
|
135
236
|
interface Props {
|
|
136
237
|
/**
|
|
@@ -164,6 +265,29 @@ interface Props {
|
|
|
164
265
|
|
|
165
266
|
/** Auto-switch to the first tab containing a validation error. */
|
|
166
267
|
autoErrorTab?: boolean;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Wrap the form in a card for a visual boundary (mirrors DXTable's
|
|
271
|
+
* `card` prop). Tabbed forms render the tab nav as a BS5
|
|
272
|
+
* card-header-tabs instead of double-wrapping. Off by default since
|
|
273
|
+
* DXForm is commonly embedded in a page card or modal already.
|
|
274
|
+
*/
|
|
275
|
+
card?: boolean;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Form-wide field layout: "vertical" (default, label above input) or
|
|
279
|
+
* "horizontal" (label left, input right). Overridable per-field via
|
|
280
|
+
* `field.layout`. A field with `span: true` always renders full-width,
|
|
281
|
+
* regardless of layout.
|
|
282
|
+
*/
|
|
283
|
+
layout?: "vertical" | "horizontal";
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Label column width for horizontal layout (mirrors BFormGroup's
|
|
287
|
+
* `labelCols`/`labelCols*` props). Overridable per-field via
|
|
288
|
+
* `field.labelCols`. Ignored when `layout` is "vertical".
|
|
289
|
+
*/
|
|
290
|
+
labelCols?: LabelCols;
|
|
167
291
|
}
|
|
168
292
|
|
|
169
293
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -171,6 +295,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
171
295
|
submitLoadingText: "Submitting...",
|
|
172
296
|
showSubmit: true,
|
|
173
297
|
autoErrorTab: true,
|
|
298
|
+
card: false,
|
|
299
|
+
layout: "vertical",
|
|
174
300
|
});
|
|
175
301
|
|
|
176
302
|
const emit = defineEmits<{
|