@omnitend/dashboard-for-laravel 0.19.2 → 0.21.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/extended/DXDashboard.vue.d.ts +8 -2
- package/dist/components/extended/DXFieldLabel.vue.d.ts +11 -1
- package/dist/components/extended/DXFormField.vue.d.ts +41 -0
- package/dist/components/extended/DXSwitch.vue.d.ts +30 -0
- package/dist/components/extended/dxFieldContext.d.ts +11 -0
- package/dist/dashboard-for-laravel.js +10930 -10862
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/utils/api.d.ts +1 -1
- package/docs/public/api-reference.json +211 -93
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +6 -5
- package/resources/css/theme.scss +5 -1
- package/resources/js/components/extended/DXDashboard.vue +13 -3
- package/resources/js/components/extended/DXField.vue +66 -77
- package/resources/js/components/extended/DXFieldLabel.vue +18 -3
- package/resources/js/components/extended/DXForm.vue +31 -109
- package/resources/js/components/extended/DXFormField.vue +98 -0
- package/resources/js/components/extended/DXSwitch.vue +153 -0
- package/resources/js/components/extended/dxFieldContext.ts +14 -0
- package/resources/js/composables/useForm.ts +10 -3
- package/resources/js/index.ts +1 -0
- package/resources/js/types/index.ts +4 -1
- package/resources/js/utils/api.ts +19 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `hint` and `repeater-row` slots for per-field customisation.
|
|
3
|
+
Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint` and `repeater-row` slots for per-field customisation.
|
|
4
4
|
-->
|
|
5
5
|
<template>
|
|
6
6
|
<!-- Full-width span field: delegate entirely to the #span slot -->
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
duplicated popover trigger. -->
|
|
129
129
|
<DFormGroup
|
|
130
130
|
v-else-if="field.type === 'switch' && isHorizontal"
|
|
131
|
-
:class="
|
|
131
|
+
:class="field.class || 'mb-3'"
|
|
132
132
|
v-bind="horizontalAttrs"
|
|
133
133
|
>
|
|
134
134
|
<!-- The row label (left column) names the control. The switch's own
|
|
@@ -156,10 +156,9 @@
|
|
|
156
156
|
:value="fieldValue"
|
|
157
157
|
:update="setValue"
|
|
158
158
|
/>
|
|
159
|
-
<
|
|
159
|
+
<DXSwitch
|
|
160
160
|
v-else
|
|
161
161
|
v-model="switchModel"
|
|
162
|
-
switch
|
|
163
162
|
:disabled="isDisabled || isReadonly"
|
|
164
163
|
v-bind="field.inputProps"
|
|
165
164
|
>
|
|
@@ -167,7 +166,7 @@
|
|
|
167
166
|
:label="switchText"
|
|
168
167
|
:class="switchHasContextualText ? undefined : 'visually-hidden'"
|
|
169
168
|
/>
|
|
170
|
-
</
|
|
169
|
+
</DXSwitch>
|
|
171
170
|
|
|
172
171
|
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
173
172
|
{{ form.getError(errorKey) }}
|
|
@@ -179,10 +178,10 @@
|
|
|
179
178
|
<DFormText v-if="field.help">{{ field.help }}</DFormText>
|
|
180
179
|
</DFormGroup>
|
|
181
180
|
|
|
182
|
-
<!-- Switch (vertical, default): toggle with contextual on/off text
|
|
181
|
+
<!-- Switch (vertical, default): toggle with contextual on/off text (DXSwitch owns the filled-box on-state style) -->
|
|
183
182
|
<div
|
|
184
183
|
v-else-if="field.type === 'switch'"
|
|
185
|
-
:class="
|
|
184
|
+
:class="field.class || 'mb-3'"
|
|
186
185
|
>
|
|
187
186
|
<!--
|
|
188
187
|
@slot Replaces the built-in control with a custom value editor.
|
|
@@ -199,15 +198,14 @@
|
|
|
199
198
|
:value="fieldValue"
|
|
200
199
|
:update="setValue"
|
|
201
200
|
/>
|
|
202
|
-
<
|
|
201
|
+
<DXSwitch
|
|
203
202
|
v-else
|
|
204
203
|
v-model="switchModel"
|
|
205
|
-
switch
|
|
206
204
|
:disabled="isDisabled || isReadonly"
|
|
207
205
|
v-bind="field.inputProps"
|
|
208
206
|
>
|
|
209
207
|
<DXFieldLabel :label="switchText" :info="resolvedInfo" />
|
|
210
|
-
</
|
|
208
|
+
</DXSwitch>
|
|
211
209
|
|
|
212
210
|
<DFormInvalidFeedback v-if="form.hasError(errorKey)" force-show>
|
|
213
211
|
{{ form.getError(errorKey) }}
|
|
@@ -442,7 +440,7 @@
|
|
|
442
440
|
<!-- Text-based inputs (text/email/password/number/url/tel/date/time/datetime) -->
|
|
443
441
|
<DFormInput
|
|
444
442
|
v-else
|
|
445
|
-
v-model="
|
|
443
|
+
v-model="textFieldModel"
|
|
446
444
|
:type="inputType"
|
|
447
445
|
:required="field.required"
|
|
448
446
|
:placeholder="field.placeholder"
|
|
@@ -490,16 +488,20 @@ import {
|
|
|
490
488
|
defineAsyncComponent,
|
|
491
489
|
onBeforeUnmount,
|
|
492
490
|
onMounted,
|
|
491
|
+
provide,
|
|
493
492
|
ref,
|
|
494
493
|
useId,
|
|
494
|
+
useSlots,
|
|
495
495
|
watch,
|
|
496
496
|
} from "vue";
|
|
497
|
+
import { dxFieldInfoPopoverKey } from "./dxFieldContext";
|
|
497
498
|
import DFormGroup from "../base/DFormGroup.vue";
|
|
498
499
|
import DFormInput from "../base/DFormInput.vue";
|
|
499
500
|
import DFormTextarea from "../base/DFormTextarea.vue";
|
|
500
501
|
import DFormSelect from "../base/DFormSelect.vue";
|
|
501
502
|
import DFormRadioGroup from "../base/DFormRadioGroup.vue";
|
|
502
503
|
import DFormCheckbox from "../base/DFormCheckbox.vue";
|
|
504
|
+
import DXSwitch from "./DXSwitch.vue";
|
|
503
505
|
import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
|
|
504
506
|
import DFormText from "../base/DFormText.vue";
|
|
505
507
|
import DInputGroup from "../base/DInputGroup.vue";
|
|
@@ -559,6 +561,24 @@ const props = defineProps<Props>();
|
|
|
559
561
|
|
|
560
562
|
const effectiveModel = computed(() => props.model ?? props.form.data);
|
|
561
563
|
|
|
564
|
+
// Forward the `info-popover` slot to whichever DXFieldLabel this field renders,
|
|
565
|
+
// once — so the label sites stay plain `<DXFieldLabel :label :info />` and can't
|
|
566
|
+
// drift out of sync (each rendering its own copy of the forward). DXFieldLabel
|
|
567
|
+
// injects this; standalone use ignores it.
|
|
568
|
+
const slots = useSlots();
|
|
569
|
+
provide(
|
|
570
|
+
dxFieldInfoPopoverKey,
|
|
571
|
+
computed(() =>
|
|
572
|
+
slots["info-popover"]
|
|
573
|
+
? () =>
|
|
574
|
+
slots["info-popover"]!({
|
|
575
|
+
field: props.field,
|
|
576
|
+
model: props.model,
|
|
577
|
+
})
|
|
578
|
+
: null,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
|
|
562
582
|
// ————————————————— horizontal layout (#66)
|
|
563
583
|
|
|
564
584
|
// A `span` field always bypasses the label/content column split — it's
|
|
@@ -662,6 +682,41 @@ const fieldValue = computed({
|
|
|
662
682
|
set: (value: any) => setValue(value),
|
|
663
683
|
});
|
|
664
684
|
|
|
685
|
+
// Native <input type=date/datetime-local/time> only accept a strict format
|
|
686
|
+
// (YYYY-MM-DD, YYYY-MM-DDTHH:MM, HH:MM) and render EMPTY for anything else —
|
|
687
|
+
// including Laravel's ISO-8601 (`…Z`) and `Y-m-d H:i:s` serialisations, so a
|
|
688
|
+
// field seeded with `created_at` shows a blank placeholder. Reshape the
|
|
689
|
+
// *displayed* value to the input's format for those types; the setter writes
|
|
690
|
+
// the input's native value straight back. This is a pure string reshape, not a
|
|
691
|
+
// timezone conversion — the stored wall-clock is shown as-is, so a UTC `…Z`
|
|
692
|
+
// value displays its UTC time (convert before seeding if you need local).
|
|
693
|
+
const DATE_FAMILY_TYPES = new Set<FieldType>(["date", "datetime", "time"]);
|
|
694
|
+
|
|
695
|
+
function normaliseDateForInput(value: unknown, type: FieldType): string {
|
|
696
|
+
if (value === null || value === undefined || value === "") return "";
|
|
697
|
+
const iso = String(value).replace(" ", "T"); // `Y-m-d H:i:s` → ISO-ish
|
|
698
|
+
if (type === "date") {
|
|
699
|
+
return iso.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
|
|
700
|
+
}
|
|
701
|
+
if (type === "time") {
|
|
702
|
+
// Anchor to the start or the time part of an ISO value, so a malformed
|
|
703
|
+
// string can't yield a stray HH:MM from the middle.
|
|
704
|
+
return iso.match(/(?:^|T)(\d{2}:\d{2})/)?.[1] ?? "";
|
|
705
|
+
}
|
|
706
|
+
// datetime → datetime-local
|
|
707
|
+
return iso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})/)?.[1] ?? "";
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Model for the text-based input branch. Reshapes date-family values for the
|
|
711
|
+
// native picker; every other type passes straight through.
|
|
712
|
+
const textFieldModel = computed({
|
|
713
|
+
get: () =>
|
|
714
|
+
DATE_FAMILY_TYPES.has(props.field.type)
|
|
715
|
+
? normaliseDateForInput(fieldValue.value, props.field.type)
|
|
716
|
+
: fieldValue.value,
|
|
717
|
+
set: (value: any) => setValue(value),
|
|
718
|
+
});
|
|
719
|
+
|
|
665
720
|
// For `percentage` fields with `asFraction`, the model holds a 0–1 fraction but
|
|
666
721
|
// the input shows/edits a 0–100 percentage. Scale on read/write, rounding away
|
|
667
722
|
// binary-float artefacts (0.2 * 100 = 20.000000000000004). Currency and plain
|
|
@@ -913,70 +968,4 @@ onBeforeUnmount(() => {
|
|
|
913
968
|
border-radius: var(--bs-border-radius);
|
|
914
969
|
}
|
|
915
970
|
|
|
916
|
-
/* Switch field: a compact "filled box" toggle, sized to its content — like the
|
|
917
|
-
plain `checkbox` field type, it doesn't stretch to fill the form row — but
|
|
918
|
-
matching standard input height so it lines up with sibling text/select
|
|
919
|
-
fields. Colour-codes its state: a neutral box when off, a filled success
|
|
920
|
-
green when on. The label sits on the left and the toggle on the right.
|
|
921
|
-
Renders the underlying `.form-check` (from the switch checkbox) as the box.
|
|
922
|
-
`justify-content: space-between` on a full-width box used to leave a wide
|
|
923
|
-
unclickable gap between the label and the toggle (only the tiny toggle
|
|
924
|
-
itself was a real click target); shrinking the box to fit its content
|
|
925
|
-
removes that dead zone entirely. Uses block-level `flex` (not `inline-flex`)
|
|
926
|
-
so the box stays shrink-to-fit yet still takes its own line: an inline-level
|
|
927
|
-
box let a following `<small class="form-text">` hint flow inline beside it
|
|
928
|
-
instead of dropping below the control like every other field type (#79). */
|
|
929
|
-
.dx-switch :deep(.form-check) {
|
|
930
|
-
display: flex;
|
|
931
|
-
align-items: center;
|
|
932
|
-
gap: 0.75rem;
|
|
933
|
-
width: fit-content;
|
|
934
|
-
max-width: 100%; /* wrap/ellipsize rather than overflow a narrow field */
|
|
935
|
-
min-height: var(--dx-input-height, calc(1.5em + 0.75rem + 2px)); /* match .form-control height */
|
|
936
|
-
margin: 0;
|
|
937
|
-
/* Override Bootstrap `.form-switch`'s 2.5em left padding for the floated input. */
|
|
938
|
-
padding: 0.375rem 0.75rem;
|
|
939
|
-
border: 1px solid var(--bs-border-color);
|
|
940
|
-
border-radius: var(--bs-border-radius);
|
|
941
|
-
background-color: var(--bs-body-bg);
|
|
942
|
-
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/* Label on the left, toggle on the right (DOM order is input then label). */
|
|
946
|
-
.dx-switch :deep(.form-check-label) {
|
|
947
|
-
order: 1;
|
|
948
|
-
margin: 0;
|
|
949
|
-
color: var(--bs-secondary-color);
|
|
950
|
-
cursor: pointer;
|
|
951
|
-
transition: color 0.15s ease-in-out;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
.dx-switch :deep(.form-check-input) {
|
|
955
|
-
order: 2;
|
|
956
|
-
flex-shrink: 0;
|
|
957
|
-
/* Override `.form-switch`'s negative left margin now that we're flex-laid-out. */
|
|
958
|
-
margin: 0;
|
|
959
|
-
cursor: pointer;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/* On-state: filled success green. */
|
|
963
|
-
.dx-switch--on :deep(.form-check) {
|
|
964
|
-
background-color: var(--bs-success-bg-subtle);
|
|
965
|
-
border-color: var(--bs-success);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
.dx-switch--on :deep(.form-check-label) {
|
|
969
|
-
color: var(--bs-success-text-emphasis);
|
|
970
|
-
font-weight: 500;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
.dx-switch--on :deep(.form-check-input:checked) {
|
|
974
|
-
background-color: var(--bs-success);
|
|
975
|
-
border-color: var(--bs-success);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
.dx-switch--on :deep(.form-check-input:focus) {
|
|
979
|
-
border-color: var(--bs-success);
|
|
980
|
-
box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
|
|
981
|
-
}
|
|
982
971
|
</style>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<template>
|
|
7
7
|
<span class="dx-field-label">
|
|
8
8
|
<span class="dx-field-label__text">{{ label }}</span>
|
|
9
|
-
<template v-if="info">
|
|
9
|
+
<template v-if="info || $slots.popover || popoverRender">
|
|
10
10
|
<button
|
|
11
11
|
:id="infoId"
|
|
12
12
|
type="button"
|
|
@@ -22,15 +22,24 @@
|
|
|
22
22
|
focus
|
|
23
23
|
placement="top"
|
|
24
24
|
>
|
|
25
|
-
|
|
25
|
+
<!--
|
|
26
|
+
@slot Rich popover body. Overrides the plain `info` text — use
|
|
27
|
+
it for lists, bold, paragraphs. Falls back to `{{ info }}`.
|
|
28
|
+
-->
|
|
29
|
+
<slot name="popover">
|
|
30
|
+
<!-- DXField forwards its `info-popover` slot here via inject. -->
|
|
31
|
+
<component v-if="popoverRender" :is="popoverRender" />
|
|
32
|
+
<template v-else>{{ info }}</template>
|
|
33
|
+
</slot>
|
|
26
34
|
</DPopover>
|
|
27
35
|
</template>
|
|
28
36
|
</span>
|
|
29
37
|
</template>
|
|
30
38
|
|
|
31
39
|
<script setup lang="ts">
|
|
32
|
-
import { useId } from "vue";
|
|
40
|
+
import { computed, inject, useId } from "vue";
|
|
33
41
|
import DPopover from "../base/DPopover.vue";
|
|
42
|
+
import { dxFieldInfoPopoverKey } from "./dxFieldContext";
|
|
34
43
|
|
|
35
44
|
interface Props {
|
|
36
45
|
/** Visible label text. */
|
|
@@ -42,6 +51,12 @@ interface Props {
|
|
|
42
51
|
|
|
43
52
|
defineProps<Props>();
|
|
44
53
|
|
|
54
|
+
// A rich popover body forwarded by a DXField ancestor (its `info-popover`
|
|
55
|
+
// slot), if any. The explicit `popover` slot still takes precedence for
|
|
56
|
+
// standalone use. Null when there's no DXField ancestor / no such slot.
|
|
57
|
+
const injectedPopover = inject(dxFieldInfoPopoverKey, null);
|
|
58
|
+
const popoverRender = computed(() => injectedPopover?.value ?? null);
|
|
59
|
+
|
|
45
60
|
// Stable, SSR-safe id so the popover can target the trigger button.
|
|
46
61
|
const infoId = `dx-field-info-${useId()}`;
|
|
47
62
|
</script>
|
|
@@ -65,52 +65,22 @@
|
|
|
65
65
|
-->
|
|
66
66
|
<slot :name="`tab-before(${tab.key})`" :tab="tab" :model="model" />
|
|
67
67
|
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<template v-
|
|
81
|
-
|
|
82
|
-
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
83
|
-
@binding {FieldDefinition} field The field about to be rendered.
|
|
84
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
85
|
-
-->
|
|
86
|
-
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
87
|
-
|
|
88
|
-
<DXField
|
|
89
|
-
:field="field"
|
|
90
|
-
:form="resolvedForm"
|
|
91
|
-
:model="model"
|
|
92
|
-
:layout="field.layout ?? layout"
|
|
93
|
-
:label-cols="field.labelCols ?? labelCols"
|
|
94
|
-
:hide-label="field.hideLabel"
|
|
95
|
-
>
|
|
96
|
-
<template
|
|
97
|
-
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
98
|
-
:key="target"
|
|
99
|
-
#[target]="slotProps"
|
|
100
|
-
>
|
|
101
|
-
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
102
|
-
<slot :name="slotName" v-bind="slotProps" />
|
|
103
|
-
</template>
|
|
104
|
-
</DXField>
|
|
105
|
-
|
|
106
|
-
<!--
|
|
107
|
-
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
108
|
-
@binding {FieldDefinition} field The field that was just rendered.
|
|
109
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
110
|
-
-->
|
|
111
|
-
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
68
|
+
<DXFormField
|
|
69
|
+
v-for="field in visibleFieldsFor(tab)"
|
|
70
|
+
:key="field.key"
|
|
71
|
+
:field="field"
|
|
72
|
+
:form="resolvedForm"
|
|
73
|
+
:model="model"
|
|
74
|
+
:layout="layout"
|
|
75
|
+
:label-cols="labelCols"
|
|
76
|
+
>
|
|
77
|
+
<!-- Forward every DXForm slot so the field can render
|
|
78
|
+
its keyed field(<key>)/field-before/field-after/
|
|
79
|
+
value/span/info/hint/repeater-row slots. -->
|
|
80
|
+
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
81
|
+
<slot :name="name" v-bind="slotProps" />
|
|
112
82
|
</template>
|
|
113
|
-
</
|
|
83
|
+
</DXFormField>
|
|
114
84
|
|
|
115
85
|
<!--
|
|
116
86
|
@slot Content inserted below a tab's fields, keyed by tab (slot name `tab-after(<tabKey>)`).
|
|
@@ -129,52 +99,22 @@
|
|
|
129
99
|
instead, above). -->
|
|
130
100
|
<template v-if="!hasTabs">
|
|
131
101
|
<component :is="card ? DCard : 'div'">
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<template v-
|
|
145
|
-
|
|
146
|
-
@slot Content inserted directly above a field, keyed by field key: `field-before(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
147
|
-
@binding {FieldDefinition} field The field about to be rendered.
|
|
148
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
149
|
-
-->
|
|
150
|
-
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
151
|
-
|
|
152
|
-
<DXField
|
|
153
|
-
:field="field"
|
|
154
|
-
:form="resolvedForm"
|
|
155
|
-
:model="model"
|
|
156
|
-
:layout="field.layout ?? layout"
|
|
157
|
-
:label-cols="field.labelCols ?? labelCols"
|
|
158
|
-
:hide-label="field.hideLabel"
|
|
159
|
-
>
|
|
160
|
-
<template
|
|
161
|
-
v-for="(slotName, target) in fieldSlotMap(field.key)"
|
|
162
|
-
:key="target"
|
|
163
|
-
#[target]="slotProps"
|
|
164
|
-
>
|
|
165
|
-
<!-- @slot Per-field overrides forwarded to DXField, keyed by field key: `value(<key>)`, `span(<key>)`, `info(<key>)`, `hint(<key>)`, `repeater-row(<key>)`. -->
|
|
166
|
-
<slot :name="slotName" v-bind="slotProps" />
|
|
167
|
-
</template>
|
|
168
|
-
</DXField>
|
|
169
|
-
|
|
170
|
-
<!--
|
|
171
|
-
@slot Content inserted directly below a field, keyed by field key: `field-after(<key>)`. Not rendered when `field(<key>)` replaces the same field.
|
|
172
|
-
@binding {FieldDefinition} field The field that was just rendered.
|
|
173
|
-
@binding {object} model Live form data merged with `context`, for predicates.
|
|
174
|
-
-->
|
|
175
|
-
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
102
|
+
<DXFormField
|
|
103
|
+
v-for="field in visibleFlatFields"
|
|
104
|
+
:key="field.key"
|
|
105
|
+
:field="field"
|
|
106
|
+
:form="resolvedForm"
|
|
107
|
+
:model="model"
|
|
108
|
+
:layout="layout"
|
|
109
|
+
:label-cols="labelCols"
|
|
110
|
+
>
|
|
111
|
+
<!-- Forward every DXForm slot so the field can render its
|
|
112
|
+
keyed field(<key>)/field-before/field-after/value/span/
|
|
113
|
+
info/hint/repeater-row slots. -->
|
|
114
|
+
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
115
|
+
<slot :name="name" v-bind="slotProps" />
|
|
176
116
|
</template>
|
|
177
|
-
</
|
|
117
|
+
</DXFormField>
|
|
178
118
|
|
|
179
119
|
<!-- Submit button -->
|
|
180
120
|
<DButton
|
|
@@ -233,7 +173,7 @@ import DButton from "../base/DButton.vue";
|
|
|
233
173
|
import DCard from "../base/DCard.vue";
|
|
234
174
|
import DTabs from "../base/DTabs.vue";
|
|
235
175
|
import DTab from "../base/DTab.vue";
|
|
236
|
-
import
|
|
176
|
+
import DXFormField from "./DXFormField.vue";
|
|
237
177
|
import type { UseFormReturn } from "../../composables/useForm";
|
|
238
178
|
import type { DefineFormReturn } from "../../composables/defineForm";
|
|
239
179
|
import type { FieldDefinition, FormTab, LabelCols, MaybeFn } from "../../types";
|
|
@@ -408,24 +348,6 @@ const visibleFlatFields = computed<FieldDefinition[]>(() =>
|
|
|
408
348
|
resolvedFields.value.filter(isFieldVisible),
|
|
409
349
|
);
|
|
410
350
|
|
|
411
|
-
// ————————————————— per-field slot forwarding
|
|
412
|
-
|
|
413
|
-
/** Map a DXField slot name to the parent's keyed slot, when present. */
|
|
414
|
-
function fieldSlotMap(key: string): Record<string, string> {
|
|
415
|
-
const map: Record<string, string> = {};
|
|
416
|
-
const candidates: Array<[string, string]> = [
|
|
417
|
-
["value", `value(${key})`],
|
|
418
|
-
["span", `span(${key})`],
|
|
419
|
-
["info", `info(${key})`],
|
|
420
|
-
["hint", `hint(${key})`],
|
|
421
|
-
["repeater-row", `repeater-row(${key})`],
|
|
422
|
-
];
|
|
423
|
-
for (const [target, source] of candidates) {
|
|
424
|
-
if (slots[source]) map[target] = source;
|
|
425
|
-
}
|
|
426
|
-
return map;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
351
|
// ————————————————— auto-switch to the first error tab
|
|
430
352
|
|
|
431
353
|
/** Keys that currently carry at least one validation error. */
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
DXFormField — internal per-field renderer for DXForm.
|
|
4
|
+
|
|
5
|
+
Owns the single per-field render block that DXForm's tabbed and flat layouts
|
|
6
|
+
both need: the `field(<key>)` full-replacement slot, the
|
|
7
|
+
`field-before(<key>)`/`field-after(<key>)` wrapper slots, and the `<DXField>`
|
|
8
|
+
element with its `value`/`span`/`info`/`hint`/`repeater-row` slot forwarding.
|
|
9
|
+
|
|
10
|
+
Extracted so those per-field props/slots are wired in ONE place — before this,
|
|
11
|
+
DXForm carried two near-identical copies (tabbed + flat) and a new per-field
|
|
12
|
+
prop had to be added to both, which was easy to miss (see #78/#83). DXForm
|
|
13
|
+
forwards all of its slots into each instance; this component only reads the
|
|
14
|
+
ones matching its field's key.
|
|
15
|
+
|
|
16
|
+
Internal: not exported from the package index — consumers use DXForm.
|
|
17
|
+
-->
|
|
18
|
+
<template>
|
|
19
|
+
<!-- Full replacement slot bypasses DXField entirely (mirrors tab-content):
|
|
20
|
+
also supersedes field-before/field-after for the same key. -->
|
|
21
|
+
<slot
|
|
22
|
+
v-if="$slots[`field(${field.key})`]"
|
|
23
|
+
:name="`field(${field.key})`"
|
|
24
|
+
:field="field"
|
|
25
|
+
:model="model"
|
|
26
|
+
/>
|
|
27
|
+
<template v-else>
|
|
28
|
+
<!-- Content inserted directly above the field. -->
|
|
29
|
+
<slot :name="`field-before(${field.key})`" :field="field" :model="model" />
|
|
30
|
+
|
|
31
|
+
<DXField
|
|
32
|
+
:field="field"
|
|
33
|
+
:form="form"
|
|
34
|
+
:model="model"
|
|
35
|
+
:layout="field.layout ?? layout"
|
|
36
|
+
:label-cols="field.labelCols ?? labelCols"
|
|
37
|
+
:hide-label="field.hideLabel"
|
|
38
|
+
>
|
|
39
|
+
<template
|
|
40
|
+
v-for="(slotName, target) in fieldSlotMap()"
|
|
41
|
+
:key="target"
|
|
42
|
+
#[target]="slotProps"
|
|
43
|
+
>
|
|
44
|
+
<!-- Per-field overrides forwarded to DXField: value/span/info/hint/repeater-row. -->
|
|
45
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
46
|
+
</template>
|
|
47
|
+
</DXField>
|
|
48
|
+
|
|
49
|
+
<!-- Content inserted directly below the field. -->
|
|
50
|
+
<slot :name="`field-after(${field.key})`" :field="field" :model="model" />
|
|
51
|
+
</template>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { useSlots } from "vue";
|
|
56
|
+
import DXField from "./DXField.vue";
|
|
57
|
+
import type { UseFormReturn } from "../../composables/useForm";
|
|
58
|
+
import type { FieldDefinition, LabelCols } from "../../types";
|
|
59
|
+
|
|
60
|
+
interface Props {
|
|
61
|
+
/** The field to render. */
|
|
62
|
+
field: FieldDefinition;
|
|
63
|
+
/** The resolved form instance (state, errors, submit helpers). */
|
|
64
|
+
form: UseFormReturn<any>;
|
|
65
|
+
/** Live form data merged with context, for slot bindings/predicates. */
|
|
66
|
+
model: Record<string, any>;
|
|
67
|
+
/** Form-level layout; overridden per-field by `field.layout`. */
|
|
68
|
+
layout?: "vertical" | "horizontal";
|
|
69
|
+
/** Form-level label column width; overridden per-field by `field.labelCols`. */
|
|
70
|
+
labelCols?: LabelCols;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const props = defineProps<Props>();
|
|
74
|
+
const slots = useSlots();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map a DXField slot name to this field's keyed parent slot, when present.
|
|
78
|
+
* A plain function (re-read each render) rather than a computed, so a slot the
|
|
79
|
+
* consumer adds after mount is still picked up — matching DXForm's original
|
|
80
|
+
* per-render behaviour before this component was extracted.
|
|
81
|
+
*/
|
|
82
|
+
function fieldSlotMap(): Record<string, string> {
|
|
83
|
+
const key = props.field.key;
|
|
84
|
+
const map: Record<string, string> = {};
|
|
85
|
+
const candidates: Array<[string, string]> = [
|
|
86
|
+
["value", `value(${key})`],
|
|
87
|
+
["span", `span(${key})`],
|
|
88
|
+
["info", `info(${key})`],
|
|
89
|
+
["info-popover", `info-popover(${key})`],
|
|
90
|
+
["hint", `hint(${key})`],
|
|
91
|
+
["repeater-row", `repeater-row(${key})`],
|
|
92
|
+
];
|
|
93
|
+
for (const [target, source] of candidates) {
|
|
94
|
+
if (slots[source]) map[target] = source;
|
|
95
|
+
}
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
</script>
|