inertia-bootstrap-forms 2.1.20 → 3.0.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/README.fa.md +468 -0
- package/README.md +322 -714
- package/dist/inertia-bootstrap-forms.cjs.js +1 -0
- package/dist/inertia-bootstrap-forms.es.js +3524 -30
- package/index.d.ts +143 -507
- package/package.json +89 -70
- package/src/AmountInput.vue +81 -76
- package/src/CaptchaInput.vue +87 -88
- package/src/CheckboxButtonInput.vue +56 -69
- package/src/CheckboxInput.vue +60 -79
- package/src/CheckboxToggle.vue +53 -136
- package/src/ChipsInput.vue +157 -0
- package/src/ComboboxInput.vue +556 -0
- package/src/DropzoneInput.vue +125 -103
- package/src/EditorInput.vue +47 -106
- package/src/EmailInput.vue +16 -15
- package/src/FileInput.vue +189 -179
- package/src/FormAdornInput.vue +75 -0
- package/src/FormContainer.vue +288 -251
- package/src/FormField.vue +59 -0
- package/src/FormLabel.vue +27 -27
- package/src/GroupControl.vue +55 -53
- package/src/LocationInput.vue +79 -72
- package/src/MobileInput.vue +21 -13
- package/src/MultiQuantityInput.vue +109 -181
- package/src/OTPInput.vue +144 -76
- package/src/PasswordInput.vue +151 -17
- package/src/PersianDatePickerInput.vue +63 -166
- package/src/QuantityInput.vue +100 -119
- package/src/RangeInput.vue +96 -0
- package/src/SimpleUploader.vue +8 -143
- package/src/StarRatingInput.vue +114 -138
- package/src/SubmitButton.vue +39 -24
- package/src/SwitchInput.vue +58 -83
- package/src/TelInput.vue +86 -120
- package/src/TextAreaInput.vue +29 -33
- package/src/TextInput.vue +42 -51
- package/src/UppyInput.vue +237 -324
- package/src/composables/useBsInstance.js +66 -0
- package/src/composables/useChoice.js +34 -0
- package/src/composables/useDisposables.js +59 -0
- package/src/composables/useField.js +121 -0
- package/src/composables/useFormApi.js +12 -0
- package/src/i18n.js +120 -0
- package/src/index.js +123 -42
- package/src/utils/bootstrap.js +48 -0
- package/src/utils/digits.js +21 -0
- package/src/utils/http.js +15 -0
- package/dist/choices-KiM-I_0P.js +0 -2955
- package/dist/dropdown-yh2aQrbw-XuCU1v1F.js +0 -1543
- package/dist/index-BRtKc2RA.js +0 -604
- package/dist/index-_x4UqcXz.js +0 -4938
- package/dist/inertia-bootstrap-forms.umd.js +0 -19
- package/dist/range-slider-pips-DTDOg3Q9.js +0 -3506
- package/dist/style.css +0 -1
- package/src/css/from-select.scss +0 -384
- package/src/css/loader.gif +0 -0
- package/src/css/location.scss +0 -44
- package/src/css/simple-upload.scss +0 -54
package/src/FormContainer.vue
CHANGED
|
@@ -1,251 +1,288 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import {useForm} from
|
|
3
|
-
import {computed, defineComponent, reactive,
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1
|
+
<script>
|
|
2
|
+
import { useForm } from '@inertiajs/vue3';
|
|
3
|
+
import { computed, defineComponent, provide, reactive, ref, watch } from 'vue';
|
|
4
|
+
import { toEnglishDigits } from './utils/digits.js';
|
|
5
|
+
|
|
6
|
+
const isPlainObject = (v) =>
|
|
7
|
+
v !== null && typeof v === 'object' && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
8
|
+
|
|
9
|
+
/** Recursively converts Persian/Arabic digits inside strings; leaves File/Blob/Date untouched. */
|
|
10
|
+
function normalizeDigits(value) {
|
|
11
|
+
if (typeof value === 'string') return toEnglishDigits(value);
|
|
12
|
+
if (Array.isArray(value)) return value.map(normalizeDigits);
|
|
13
|
+
if (isPlainObject(value)) {
|
|
14
|
+
return Object.keys(value).reduce((carry, key) => {
|
|
15
|
+
carry[key] = normalizeDigits(value[key]);
|
|
16
|
+
return carry;
|
|
17
|
+
}, {});
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeEqual(a, b) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default defineComponent({
|
|
31
|
+
name: 'FormContainer',
|
|
32
|
+
emits: [
|
|
33
|
+
'update:modelValue',
|
|
34
|
+
'change',
|
|
35
|
+
'submit',
|
|
36
|
+
'reset',
|
|
37
|
+
'before',
|
|
38
|
+
'start',
|
|
39
|
+
'progress',
|
|
40
|
+
'success',
|
|
41
|
+
'error',
|
|
42
|
+
'cancel',
|
|
43
|
+
'finish',
|
|
44
|
+
// Legacy (v2) event names
|
|
45
|
+
'onStart',
|
|
46
|
+
'onFinish',
|
|
47
|
+
'onSuccess',
|
|
48
|
+
'onError',
|
|
49
|
+
],
|
|
50
|
+
props: {
|
|
51
|
+
/** The object that holds the value of every field (v-model). */
|
|
52
|
+
modelValue: { type: Object, default: () => ({}) },
|
|
53
|
+
url: { type: String, default: '' },
|
|
54
|
+
method: { type: String, default: 'post' },
|
|
55
|
+
/** Only reload these props after a successful visit (Inertia `only`). */
|
|
56
|
+
only: { type: Array, default: () => [] },
|
|
57
|
+
/** Extra form fields that are not part of `modelValue`. */
|
|
58
|
+
options: { type: Object, default: () => ({}) },
|
|
59
|
+
/** Extra Inertia visit options (preserveScroll, preserveState, headers, …). */
|
|
60
|
+
visitOptions: { type: Object, default: () => ({}) },
|
|
61
|
+
autoTab: { type: Boolean, default: true },
|
|
62
|
+
resetOnSuccess: { type: Boolean, default: false },
|
|
63
|
+
/** Replace the default submit behaviour. Receives (event, form). */
|
|
64
|
+
submitHandler: { type: Function, default: null },
|
|
65
|
+
/** Convert Persian/Arabic digits to English digits before submitting. */
|
|
66
|
+
normalizeDigits: { type: Boolean, default: true },
|
|
67
|
+
/** Enable Bootstrap 6 client-side validation (`data-bs-validate`). */
|
|
68
|
+
validate: { type: Boolean, default: false },
|
|
69
|
+
/** Render server messages as HTML (only enable for trusted messages). */
|
|
70
|
+
htmlMessages: { type: Boolean, default: false },
|
|
71
|
+
/** Hide the built-in error / success alerts. */
|
|
72
|
+
hideAlerts: { type: Boolean, default: false },
|
|
73
|
+
},
|
|
74
|
+
setup(props, { emit, expose }) {
|
|
75
|
+
const formEl = ref(null);
|
|
76
|
+
const ui = reactive({ hasMessage: false, successMessage: null });
|
|
77
|
+
|
|
78
|
+
const form = useForm({
|
|
79
|
+
...props.options,
|
|
80
|
+
...props.modelValue,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Upload components write their progress here; SubmitButton reads it.
|
|
84
|
+
form.uploading = null;
|
|
85
|
+
|
|
86
|
+
form.getID = function (el = {}) {
|
|
87
|
+
const output = [];
|
|
88
|
+
|
|
89
|
+
if (formEl.value?.id) output.push(formEl.value.id);
|
|
90
|
+
if (typeof el === 'string') output.push(el);
|
|
91
|
+
|
|
92
|
+
if (el.group) {
|
|
93
|
+
if (el.group.name) output.push(el.group.name);
|
|
94
|
+
if (el.group.index !== undefined && el.group.index !== null) output.push(el.group.index);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (el.name) output.push(el.name);
|
|
98
|
+
if (el.value !== undefined && el.value !== null && el.value !== '') output.push(el.value);
|
|
99
|
+
|
|
100
|
+
return output.join('-');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Keys that exist on the Inertia form object right now (its own state, methods and the initial fields).
|
|
104
|
+
// Fields that are not part of the initial v-model are added later by the inputs themselves.
|
|
105
|
+
const baseKeys = new Set(Object.keys(form));
|
|
106
|
+
const dynamicKeys = () =>
|
|
107
|
+
Object.keys(form).filter((key) => !baseKeys.has(key) && typeof form[key] !== 'function' && form[key] !== undefined);
|
|
108
|
+
|
|
109
|
+
const currentData = () => {
|
|
110
|
+
const data = typeof form.data === 'function'
|
|
111
|
+
? { ...form.data() }
|
|
112
|
+
: Object.keys(form).reduce((carry, key) => {
|
|
113
|
+
if (baseKeys.has(key) && typeof form[key] !== 'function') carry[key] = form[key];
|
|
114
|
+
return carry;
|
|
115
|
+
}, {});
|
|
116
|
+
|
|
117
|
+
// v-model objects do not have to declare every field up front
|
|
118
|
+
dynamicKeys().forEach((key) => {
|
|
119
|
+
data[key] = form[key];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return data;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resetAll = () => {
|
|
126
|
+
dynamicKeys().forEach((key) => delete form[key]);
|
|
127
|
+
form.reset();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// form -> parent (v-model)
|
|
131
|
+
watch(
|
|
132
|
+
() => currentData(),
|
|
133
|
+
(data) => {
|
|
134
|
+
emit('change', data);
|
|
135
|
+
emit('update:modelValue', data);
|
|
136
|
+
},
|
|
137
|
+
{ deep: true }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// parent -> form (external changes of the v-model object)
|
|
141
|
+
watch(
|
|
142
|
+
() => props.modelValue,
|
|
143
|
+
(incoming) => {
|
|
144
|
+
if (!incoming || safeEqual(incoming, currentData())) return;
|
|
145
|
+
Object.keys(incoming).forEach((key) => {
|
|
146
|
+
if (!safeEqual(form[key], incoming[key])) form[key] = incoming[key];
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
{ deep: true }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
function focusNextField(current) {
|
|
153
|
+
const focusable = Array.from(formEl.value.querySelectorAll('input, select, textarea')).filter(
|
|
154
|
+
(el) => !el.disabled && el.tabIndex !== -1 && el.type !== 'hidden' && el.offsetParent !== null
|
|
155
|
+
);
|
|
156
|
+
const next = focusable[focusable.indexOf(current) + 1];
|
|
157
|
+
|
|
158
|
+
if (next) {
|
|
159
|
+
next.focus();
|
|
160
|
+
if (next.tagName !== 'SELECT' && typeof next.select === 'function') next.select();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handleInput(event) {
|
|
165
|
+
const el = event.target;
|
|
166
|
+
if (!['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
|
|
167
|
+
|
|
168
|
+
if (props.autoTab && el.maxLength > 0 && el.value.length >= el.maxLength) {
|
|
169
|
+
focusNextField(el);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const autoSubmitLength = parseInt(el.dataset.autoSubmitLength, 10);
|
|
173
|
+
if (!isNaN(autoSubmitLength) && el.value.length >= autoSubmitLength) {
|
|
174
|
+
submit();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function reset() {
|
|
179
|
+
resetAll();
|
|
180
|
+
form.clearErrors();
|
|
181
|
+
ui.hasMessage = false;
|
|
182
|
+
ui.successMessage = null;
|
|
183
|
+
emit('reset');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function submit(event) {
|
|
187
|
+
if (props.submitHandler) {
|
|
188
|
+
return props.submitHandler(event, form);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
emit('submit', event);
|
|
192
|
+
|
|
193
|
+
form
|
|
194
|
+
.transform((data) => {
|
|
195
|
+
const merged = { ...props.modelValue, ...data, ...currentData() };
|
|
196
|
+
return props.normalizeDigits ? normalizeDigits(merged) : merged;
|
|
197
|
+
})
|
|
198
|
+
.submit((props.method || 'post').toString().toLowerCase(), props.url, {
|
|
199
|
+
only: props.only,
|
|
200
|
+
...props.visitOptions,
|
|
201
|
+
onBefore: (visit) => {
|
|
202
|
+
emit('before', visit);
|
|
203
|
+
return props.visitOptions.onBefore?.(visit);
|
|
204
|
+
},
|
|
205
|
+
onStart: (visit) => {
|
|
206
|
+
ui.hasMessage = false;
|
|
207
|
+
ui.successMessage = null;
|
|
208
|
+
form.clearErrors();
|
|
209
|
+
emit('start', visit);
|
|
210
|
+
emit('onStart', visit);
|
|
211
|
+
props.visitOptions.onStart?.(visit);
|
|
212
|
+
},
|
|
213
|
+
onProgress: (progress) => {
|
|
214
|
+
emit('progress', progress);
|
|
215
|
+
props.visitOptions.onProgress?.(progress);
|
|
216
|
+
},
|
|
217
|
+
onCancel: () => {
|
|
218
|
+
emit('cancel');
|
|
219
|
+
props.visitOptions.onCancel?.();
|
|
220
|
+
},
|
|
221
|
+
onFinish: (visit) => {
|
|
222
|
+
emit('finish', visit);
|
|
223
|
+
emit('onFinish', visit);
|
|
224
|
+
props.visitOptions.onFinish?.(visit);
|
|
225
|
+
},
|
|
226
|
+
onError: (errors) => {
|
|
227
|
+
emit('error', errors);
|
|
228
|
+
emit('onError', errors);
|
|
229
|
+
props.visitOptions.onError?.(errors);
|
|
230
|
+
},
|
|
231
|
+
onSuccess: (page) => {
|
|
232
|
+
if (props.resetOnSuccess) resetAll();
|
|
233
|
+
|
|
234
|
+
if (page?.props?.message) {
|
|
235
|
+
ui.hasMessage = true;
|
|
236
|
+
ui.successMessage = page.props.message;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
emit('success', page);
|
|
240
|
+
emit('onSuccess', page);
|
|
241
|
+
props.visitOptions.onSuccess?.(page);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
provide('form', computed(() => form));
|
|
247
|
+
provide('formApi', { submit, reset, el: formEl });
|
|
248
|
+
|
|
249
|
+
expose({ submit, reset, form, el: formEl });
|
|
250
|
+
|
|
251
|
+
return { form, formEl, ui, handleInput, reset, submit };
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<template>
|
|
257
|
+
<form
|
|
258
|
+
ref="formEl"
|
|
259
|
+
:action="url || undefined"
|
|
260
|
+
:method="method"
|
|
261
|
+
:data-bs-validate="validate ? '' : undefined"
|
|
262
|
+
:novalidate="!validate || form.hasErrors ? true : undefined"
|
|
263
|
+
:class="{ 'form-processing': form.processing }"
|
|
264
|
+
@input="handleInput"
|
|
265
|
+
@submit.prevent="submit"
|
|
266
|
+
@reset.prevent="reset"
|
|
267
|
+
>
|
|
268
|
+
<template v-if="!hideAlerts">
|
|
269
|
+
<slot name="errors" :errors="form.errors" :form="form">
|
|
270
|
+
<div v-if="form.hasErrors" class="alert theme-danger" role="alert">
|
|
271
|
+
<ul class="list-unstyled p-0 m-0 fanum">
|
|
272
|
+
<li v-for="(error, key) in form.errors" :key="key">
|
|
273
|
+
<span v-if="htmlMessages" v-html="error"></span>
|
|
274
|
+
<template v-else>{{ error }}</template>
|
|
275
|
+
</li>
|
|
276
|
+
</ul>
|
|
277
|
+
</div>
|
|
278
|
+
</slot>
|
|
279
|
+
<slot name="message" :message="ui.successMessage">
|
|
280
|
+
<div v-if="ui.hasMessage && ui.successMessage" class="alert theme-success" role="status">
|
|
281
|
+
<span v-if="htmlMessages" v-html="ui.successMessage"></span>
|
|
282
|
+
<span v-else>{{ ui.successMessage }}</span>
|
|
283
|
+
</div>
|
|
284
|
+
</slot>
|
|
285
|
+
</template>
|
|
286
|
+
<slot :form="form" :submit="submit" :reset="reset" />
|
|
287
|
+
</form>
|
|
288
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { computed, defineComponent, inject, provide } from 'vue';
|
|
3
|
+
import * as Vue from 'vue';
|
|
4
|
+
import FormLabel from './FormLabel.vue';
|
|
5
|
+
|
|
6
|
+
let uid = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Bootstrap 6 `.form-field` wrapper: label + control + help text + validation feedback.
|
|
10
|
+
* Child inputs pick up the generated `id` automatically, so the label always points to the control.
|
|
11
|
+
*/
|
|
12
|
+
export default defineComponent({
|
|
13
|
+
name: 'FormField',
|
|
14
|
+
components: { FormLabel },
|
|
15
|
+
props: {
|
|
16
|
+
/** Field key. Used to read the server-side error of the field. */
|
|
17
|
+
name: { type: String, default: null },
|
|
18
|
+
label: { type: String, default: null },
|
|
19
|
+
help: { type: String, default: null },
|
|
20
|
+
/** Override the error text (otherwise it is read from the form errors). */
|
|
21
|
+
error: { type: String, default: null },
|
|
22
|
+
required: { type: Boolean, default: false },
|
|
23
|
+
id: { type: String, default: null },
|
|
24
|
+
},
|
|
25
|
+
setup(props) {
|
|
26
|
+
const form = inject('form', null);
|
|
27
|
+
const generated = Vue.useId ? Vue.useId() : `ff-${++uid}`;
|
|
28
|
+
const fieldId = computed(() => props.id || (props.name ? `${props.name}-${generated}` : generated));
|
|
29
|
+
|
|
30
|
+
provide('formField', {
|
|
31
|
+
get id() {
|
|
32
|
+
return fieldId.value;
|
|
33
|
+
},
|
|
34
|
+
get name() {
|
|
35
|
+
return props.name;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const errorText = computed(() => props.error ?? (props.name ? form?.value?.errors?.[props.name] : null) ?? null);
|
|
40
|
+
|
|
41
|
+
return { fieldId, errorText };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="form-field">
|
|
48
|
+
<slot name="label">
|
|
49
|
+
<FormLabel v-if="label" :for="fieldId" :required="required">{{ label }}</FormLabel>
|
|
50
|
+
</slot>
|
|
51
|
+
<slot />
|
|
52
|
+
<slot name="help">
|
|
53
|
+
<div v-if="help" :id="fieldId + '-help'" class="form-text">{{ help }}</div>
|
|
54
|
+
</slot>
|
|
55
|
+
<slot name="error" :error="errorText">
|
|
56
|
+
<div v-if="errorText" :id="fieldId + '-error'" class="invalid-feedback d-block" role="alert">{{ errorText }}</div>
|
|
57
|
+
</slot>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|