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.
Files changed (59) hide show
  1. package/README.fa.md +468 -0
  2. package/README.md +322 -714
  3. package/dist/inertia-bootstrap-forms.cjs.js +1 -0
  4. package/dist/inertia-bootstrap-forms.es.js +3524 -30
  5. package/index.d.ts +143 -507
  6. package/package.json +89 -70
  7. package/src/AmountInput.vue +81 -76
  8. package/src/CaptchaInput.vue +87 -88
  9. package/src/CheckboxButtonInput.vue +56 -69
  10. package/src/CheckboxInput.vue +60 -79
  11. package/src/CheckboxToggle.vue +53 -136
  12. package/src/ChipsInput.vue +157 -0
  13. package/src/ComboboxInput.vue +556 -0
  14. package/src/DropzoneInput.vue +125 -103
  15. package/src/EditorInput.vue +47 -106
  16. package/src/EmailInput.vue +16 -15
  17. package/src/FileInput.vue +189 -179
  18. package/src/FormAdornInput.vue +75 -0
  19. package/src/FormContainer.vue +288 -251
  20. package/src/FormField.vue +59 -0
  21. package/src/FormLabel.vue +27 -27
  22. package/src/GroupControl.vue +55 -53
  23. package/src/LocationInput.vue +79 -72
  24. package/src/MobileInput.vue +21 -13
  25. package/src/MultiQuantityInput.vue +109 -181
  26. package/src/OTPInput.vue +144 -76
  27. package/src/PasswordInput.vue +151 -17
  28. package/src/PersianDatePickerInput.vue +63 -166
  29. package/src/QuantityInput.vue +100 -119
  30. package/src/RangeInput.vue +96 -0
  31. package/src/SimpleUploader.vue +8 -143
  32. package/src/StarRatingInput.vue +114 -138
  33. package/src/SubmitButton.vue +39 -24
  34. package/src/SwitchInput.vue +58 -83
  35. package/src/TelInput.vue +86 -120
  36. package/src/TextAreaInput.vue +29 -33
  37. package/src/TextInput.vue +42 -51
  38. package/src/UppyInput.vue +237 -324
  39. package/src/composables/useBsInstance.js +66 -0
  40. package/src/composables/useChoice.js +34 -0
  41. package/src/composables/useDisposables.js +59 -0
  42. package/src/composables/useField.js +121 -0
  43. package/src/composables/useFormApi.js +12 -0
  44. package/src/i18n.js +120 -0
  45. package/src/index.js +123 -42
  46. package/src/utils/bootstrap.js +48 -0
  47. package/src/utils/digits.js +21 -0
  48. package/src/utils/http.js +15 -0
  49. package/dist/choices-KiM-I_0P.js +0 -2955
  50. package/dist/dropdown-yh2aQrbw-XuCU1v1F.js +0 -1543
  51. package/dist/index-BRtKc2RA.js +0 -604
  52. package/dist/index-_x4UqcXz.js +0 -4938
  53. package/dist/inertia-bootstrap-forms.umd.js +0 -19
  54. package/dist/range-slider-pips-DTDOg3Q9.js +0 -3506
  55. package/dist/style.css +0 -1
  56. package/src/css/from-select.scss +0 -384
  57. package/src/css/loader.gif +0 -0
  58. package/src/css/location.scss +0 -44
  59. package/src/css/simple-upload.scss +0 -54
package/package.json CHANGED
@@ -1,70 +1,89 @@
1
- {
2
- "name": "inertia-bootstrap-forms",
3
- "version": "2.1.20",
4
- "description": "Create bootstrap forms with inertia and twitter bootstrap",
5
- "main": "dist/inertia-bootstrap-forms.cjs.js",
6
- "module": "dist/inertia-bootstrap-forms.es.js",
7
- "type": "module",
8
- "types": "./index.d.ts",
9
- "scripts": {
10
- "dev": "vite build --watch",
11
- "build": "vite build"
12
- },
13
- "files": [
14
- "dist",
15
- "src",
16
- "index.d.ts"
17
- ],
18
- "exports": {
19
- ".": {
20
- "import": "./dist/inertia-bootstrap-forms.es.js"
21
- },
22
- "./dist/*.css": {
23
- "import": "./dist/style.css"
24
- }
25
- },
26
- "author": "NovinVision",
27
- "license": "MIT",
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/novinvision/inertia-bootstrap-forms.git"
31
- },
32
- "keywords": [
33
- "inertia",
34
- "vue",
35
- "laravel",
36
- "bootstrap",
37
- "forms"
38
- ],
39
- "dependencies": {
40
- "@tinymce/tinymce-vue": "^6.1.0",
41
- "@uppy/audio": "^3.1.0",
42
- "@uppy/core": "^5.2.0",
43
- "@uppy/dashboard": "^5.1.1",
44
- "@uppy/drag-drop": "^5.1.0",
45
- "@uppy/locales": "^5.1.1",
46
- "@uppy/vue": "^3.2.0",
47
- "@uppy/xhr-upload": "^5.1.1",
48
- "@vitejs/plugin-vue": "^5.2.4",
49
- "@vue-leaflet/vue-leaflet": "^0.10.1",
50
- "choices.js": "^11.1.0",
51
- "svelte-range-slider-pips": "^4.1.0",
52
- "vue-tel-input": "^9.3.0",
53
- "vue3-bootstrap-components": "^2.0.16",
54
- "vue3-persian-datetime-picker": "^1.2.2"
55
- },
56
- "peerDependencies": {
57
- "@inertiajs/vue3": ">3.0",
58
- "dropzone": "^6.0.0-beta.2",
59
- "vue": "^3.0"
60
- },
61
- "devDependencies": {
62
- "dropzone": "^6.0.0-beta.2",
63
- "sass-embedded": "^1.89.2",
64
- "vite": "^5"
65
- },
66
- "bugs": {
67
- "url": "https://github.com/novinvision/inertia-bootstrap-forms/issues"
68
- },
69
- "homepage": "https://github.com/novinvision/inertia-bootstrap-forms#readme"
70
- }
1
+ {
2
+ "name": "inertia-bootstrap-forms",
3
+ "version": "3.0.0",
4
+ "description": "Bootstrap 6 form components for Vue 3 + Inertia.js (Laravel). Every field is bound to a single FormContainer v-model.",
5
+ "type": "module",
6
+ "main": "dist/inertia-bootstrap-forms.cjs.js",
7
+ "module": "dist/inertia-bootstrap-forms.es.js",
8
+ "types": "./index.d.ts",
9
+ "sideEffects": false,
10
+ "scripts": {
11
+ "dev": "vite build --watch",
12
+ "build": "vite build",
13
+ "test": "vitest run"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src",
18
+ "index.d.ts",
19
+ "README.md",
20
+ "README.fa.md"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./index.d.ts",
25
+ "import": "./dist/inertia-bootstrap-forms.es.js",
26
+ "require": "./dist/inertia-bootstrap-forms.cjs.js"
27
+ },
28
+ "./src/*": "./src/*",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "author": "NovinVision",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/novinvision/inertia-bootstrap-forms.git"
36
+ },
37
+ "keywords": [
38
+ "inertia",
39
+ "vue",
40
+ "laravel",
41
+ "bootstrap",
42
+ "bootstrap6",
43
+ "forms",
44
+ "combobox",
45
+ "chips",
46
+ "otp"
47
+ ],
48
+ "dependencies": {
49
+ "@tinymce/tinymce-vue": "^6.1.0",
50
+ "@uppy/core": "^5.2.0",
51
+ "@uppy/vue": "^3.2.0",
52
+ "@uppy/xhr-upload": "^5.1.1",
53
+ "@vue-leaflet/vue-leaflet": "^0.10.1",
54
+ "vue-tel-input": "^9.3.0",
55
+ "vue3-persian-datetime-picker": "^1.2.2"
56
+ },
57
+ "peerDependencies": {
58
+ "@floating-ui/dom": "^1.7.6",
59
+ "@inertiajs/vue3": ">=2.0.0",
60
+ "bootstrap": "^6.0.0-alpha.1",
61
+ "dropzone": "^6.0.0-beta.2",
62
+ "leaflet": "^1.9.0",
63
+ "vue": "^3.3.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "dropzone": {
67
+ "optional": true
68
+ },
69
+ "leaflet": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@floating-ui/dom": "^1.7.6",
75
+ "@inertiajs/vue3": "^2.0.0",
76
+ "@vitejs/plugin-vue": "^5.2.4",
77
+ "@vue/test-utils": "^2.4.6",
78
+ "bootstrap": "6.0.0-alpha.1",
79
+ "dropzone": "^6.0.0-beta.2",
80
+ "jsdom": "^25.0.0",
81
+ "vite": "^5",
82
+ "vitest": "^2.1.0",
83
+ "vue": "^3.5.0"
84
+ },
85
+ "bugs": {
86
+ "url": "https://github.com/novinvision/inertia-bootstrap-forms/issues"
87
+ },
88
+ "homepage": "https://github.com/novinvision/inertia-bootstrap-forms#readme"
89
+ }
@@ -1,86 +1,91 @@
1
1
  <script>
2
- import {InputGroup, InputGroupText} from 'vue3-bootstrap-components';
3
- import {computed, defineComponent, inject} from "vue";
2
+ import { computed, defineComponent } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useI18n } from './i18n.js';
5
+ import { formatNumber, parseDigits, toEnglishDigits } from './utils/digits.js';
4
6
 
7
+ /** Integer input with thousands separators and a unit (Bootstrap 6 `.input-group`). */
5
8
  export default defineComponent({
6
- components: {InputGroup, InputGroupText},
7
- props: {
8
- name: {
9
- type: String,
10
- required: true,
9
+ name: 'AmountInput',
10
+ emits: ['update:modelValue', 'change', 'input', 'focus', 'blur'],
11
+ props: {
12
+ ...fieldProps,
13
+ placeholder: { type: String, default: null },
14
+ unit: { type: String, default: null },
15
+ min: { type: Number, default: null },
16
+ max: { type: Number, default: null },
17
+ inputClass: { type: [String, Array, Object], default: null },
18
+ size: { type: String, default: null },
19
+ locale: { type: String, default: null },
11
20
  },
12
- required: Boolean,
13
- disabled: Boolean,
14
- readonly: Boolean,
15
- inputClass: String,
16
- placeholder: {
17
- type: String,
18
- default: 'عدد را وارد کنید'
19
- },
20
- unit: {
21
- type: String,
22
- default: 'عدد'
23
- },
24
- },
25
- setup(props) {
26
- let form = inject('form', {
27
- errors: {},
28
- getID(name) {
29
- return name;
30
- }
31
- });
32
- let group = inject('group', {});
21
+ setup(props, { emit }) {
22
+ const field = useField(props, emit);
23
+ const { t } = useI18n(() => props.locale);
33
24
 
34
- const modelValue = computed({
35
- get() {
36
- return (group.value && form.value[group.value.name]) ? group.value?.getData(props.name) : form.value[props.name];
37
- },
38
- set(value) {
39
- if (group?.value?.name) {
40
- group.value.setData(props.name, value);
41
- } else {
42
- form.value[props.name] = value;
43
- }
44
- }
45
- });
25
+ const displayValue = computed(() => formatNumber(field.model.value));
26
+ const unitText = computed(() => props.unit ?? t('amount.unit'));
27
+ const sizeClass = computed(() => (props.size ? `form-control-${props.size}` : null));
28
+
29
+ function onInput(event) {
30
+ const el = event.target;
31
+ const before = el.value.slice(0, el.selectionStart ?? 0);
32
+ const digitsBefore = toEnglishDigits(before).replace(/\D/g, '').length;
33
+
34
+ const parsed = parseDigits(el.value);
35
+ field.model.value = parsed;
46
36
 
47
- const persianDigits = '۰۱۲۳۴۵۶۷۸۹';
48
- const arabicDigits = '٠١٢٣٤٥٦٧٨٩';
49
- function toEnglishDigits(str) {
50
- return String(str).replace(/[۰-۹٠-٩]/g, (ch) => {
51
- const pIndex = persianDigits.indexOf(ch);
52
- if (pIndex !== -1) return pIndex;
53
- return arabicDigits.indexOf(ch);
54
- });
55
- }
37
+ // Re-format immediately and keep the caret after the same number of digits
38
+ const formatted = formatNumber(parsed);
39
+ el.value = formatted;
40
+ let pos = 0;
41
+ let seen = 0;
42
+ while (pos < formatted.length && seen < digitsBefore) {
43
+ if (/\d/.test(formatted[pos])) seen++;
44
+ pos++;
45
+ }
46
+ if (document.activeElement === el) el.setSelectionRange(pos, pos);
56
47
 
57
- const displayValue = computed({
58
- get() {
59
- const raw = modelValue.value;
60
- if (raw === null || raw === undefined || raw === '') return '';
61
- return Number(raw).toLocaleString('en-US');
62
- },
63
- set(value) {
64
- const digitsOnly = toEnglishDigits(value).replace(/\D/g, '');
65
- modelValue.value = digitsOnly === '' ? null : parseInt(digitsOnly, 10);
66
- }
67
- });
48
+ emit('input', parsed, event);
49
+ }
50
+
51
+ function clamp(event) {
52
+ let v = field.model.value;
53
+ if (v !== null && v !== undefined) {
54
+ if (props.max !== null && v > props.max) v = props.max;
55
+ if (props.min !== null && v < props.min) v = props.min;
56
+ if (v !== field.model.value) field.model.value = v;
57
+ }
58
+ field.emitChange(v, event);
59
+ }
68
60
 
69
- return {modelValue, displayValue, form, group};
70
- },
71
- })
61
+ return { ...field, displayValue, unitText, sizeClass, onInput, clamp, t };
62
+ },
63
+ });
72
64
  </script>
65
+
73
66
  <template>
74
- <InputGroup>
75
- <input
76
- :name="name"
77
- v-model="displayValue"
78
- :class="['form-control fanum', inputClass, {'is-invalid': form.errors[name]}]"
79
- :disabled="disabled || form.processing"
80
- :readonly="readonly"
81
- :placeholder="placeholder"
82
- type="tel">
83
- <InputGroupText class="fanum">{{ unit }}</InputGroupText>
84
- <slot name="suffix"/>
85
- </InputGroup>
86
- </template>
67
+ <div class="input-group" :class="{ 'is-invalid': isInvalid }">
68
+ <slot name="prefix" />
69
+ <input
70
+ :name="name"
71
+ :id="inputId"
72
+ :value="displayValue"
73
+ :placeholder="placeholder ?? t('amount.placeholder')"
74
+ :required="required"
75
+ :readonly="readonly"
76
+ :disabled="isDisabled"
77
+ :aria-invalid="isInvalid || undefined"
78
+ type="tel"
79
+ inputmode="numeric"
80
+ autocomplete="off"
81
+ class="form-control fanum"
82
+ :class="[sizeClass, inputClass, { 'is-invalid': isInvalid }]"
83
+ @input="onInput"
84
+ @change="clamp"
85
+ @focus="$emit('focus', $event)"
86
+ @blur="$emit('blur', $event)"
87
+ />
88
+ <span v-if="unitText" class="input-group-text fanum">{{ unitText }}</span>
89
+ <slot name="suffix" />
90
+ </div>
91
+ </template>
@@ -1,106 +1,105 @@
1
1
  <script>
2
- import TextInput from "./TextInput.vue";
3
- import {defineComponent} from "vue";
2
+ import { defineComponent, onMounted, ref, watch } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useDisposables } from './composables/useDisposables.js';
5
+ import { useI18n } from './i18n.js';
4
6
 
7
+ /** Captcha image + input + refresh button. The image is refreshed after every finished submit. */
5
8
  export default defineComponent({
6
- components: {TextInput},
9
+ name: 'CaptchaInput',
10
+ emits: ['update:modelValue', 'change', 'input', 'refresh'],
7
11
  props: {
8
- name: {
9
- type: String,
10
- default: 'captcha',
11
- required: false,
12
- },
13
- url: {
14
- type: String,
15
- default: '/captcha/default',
16
- required: false,
17
- },
18
- modelValue: '',
19
- required: Boolean,
20
- placeholder: {
21
- default: 'کد امنیتی روبرو را وارد کنید',
22
- },
12
+ ...fieldProps,
13
+ name: { type: String, default: 'captcha' },
14
+ /** Endpoint that returns the captcha image. */
15
+ url: { type: String, default: '/captcha/default' },
16
+ placeholder: { type: String, default: null },
17
+ /** Reload the image (and clear the input) when a submit finishes. */
18
+ refreshOnSubmit: { type: Boolean, default: true },
19
+ imageWidth: { type: [Number, String], default: 82 },
20
+ imageHeight: { type: [Number, String], default: 38 },
21
+ locale: { type: String, default: null },
23
22
  },
24
- emits: ['update:modelValue'],
25
- inject: ['form'],
26
- watch: {
27
- 'form.processing': {
28
- handler: function (val, old) {
29
- if (val !== old && !val) {
30
- this.getSrc();
31
- this.emptyField();
32
- }
33
- },
34
- deep: false,
35
- immediate: false,
23
+ setup(props, { emit, expose }) {
24
+ const field = useField(props, emit);
25
+ const { t } = useI18n(() => props.locale);
26
+ const d = useDisposables();
27
+
28
+ const src = ref(null);
29
+ const loading = ref(true);
30
+
31
+ function refresh() {
32
+ loading.value = true;
33
+ src.value = `${props.url}${props.url.includes('?') ? '&' : '?'}t=${Date.now()}`;
34
+ d.timeout(() => {
35
+ loading.value = false;
36
+ }, 500);
37
+ emit('refresh');
36
38
  }
37
- },
38
- methods: {
39
- emptyField() {
40
- if (this.form) {
41
- this.form[this.name] = null;
39
+
40
+ onMounted(refresh);
41
+
42
+ watch(
43
+ () => field.form.value?.processing,
44
+ (now, before) => {
45
+ if (props.refreshOnSubmit && before && !now) {
46
+ field.model.value = null;
47
+ refresh();
48
+ }
42
49
  }
43
- },
44
- async getSrc() {
45
- this.loading = true;
46
- this.src = this.url + '?t=' + Math.random();
50
+ );
47
51
 
48
- let _this = this;
49
- setTimeout(function () {
50
- _this.loading = false;
51
- }, 500)
52
- }
52
+ expose({ refresh });
53
+
54
+ return { ...field, t, src, loading, refresh };
53
55
  },
54
- data() {
55
- return {
56
- loading: false,
57
- src: this.url + Math.random(),
58
- };
59
- }
60
56
  });
61
-
62
57
  </script>
58
+
63
59
  <template>
64
- <div class="input-group captcha">
65
- <TextInput
60
+ <div class="input-group captcha" :class="{ 'is-invalid': isInvalid }">
61
+ <input
62
+ v-model="model"
66
63
  :name="name"
67
- class="text-start"
68
- type="tel"/>
69
- <img
70
- :src="src"
71
- alt="captcha"
72
- @click="getSrc"
73
- v-if="!loading"
74
- class="captcha-holder">
75
- <div class="placeholder-wave captcha-holder" v-else>
76
- <div class="placeholder w-100 h-100"></div>
77
- </div>
64
+ :id="inputId"
65
+ type="text"
66
+ inputmode="numeric"
67
+ autocomplete="off"
68
+ :placeholder="placeholder ?? t('captcha.placeholder')"
69
+ :required="required"
70
+ :readonly="readonly"
71
+ :disabled="isDisabled"
72
+ :aria-invalid="isInvalid || undefined"
73
+ class="form-control text-start"
74
+ :class="{ 'is-invalid': isInvalid }"
75
+ @input="$emit('input', model, $event)"
76
+ @change="emitChange(model, $event)"
77
+ />
78
+ <span class="input-group-text p-0 overflow-hidden" :style="{ width: imageWidth + 'px', height: 'auto' }">
79
+ <img
80
+ v-if="src && !loading"
81
+ :src="src"
82
+ :alt="t('captcha.alt')"
83
+ :width="imageWidth"
84
+ :height="imageHeight"
85
+ class="captcha-holder"
86
+ :style="{ cursor: 'pointer', objectFit: 'contain' }"
87
+ @click="refresh"
88
+ />
89
+ <span v-else class="placeholder-wave w-100" aria-hidden="true">
90
+ <span class="placeholder w-100" :style="{ height: imageHeight + 'px' }"></span>
91
+ </span>
92
+ </span>
78
93
  <button
79
94
  type="button"
80
- :disabled="form.processing || loading"
81
- @click="getSrc"
82
- class="btn btn-secondary border-0">
83
- <i class="fal fa-sync-alt align-middle"></i>
95
+ class="btn-outline theme-secondary btn-icon"
96
+ :disabled="isDisabled || loading"
97
+ :aria-label="t('captcha.refresh')"
98
+ @click="refresh"
99
+ >
100
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
101
+ <path d="M13.5 3v3.5H10M2.5 13V9.5H6" /><path d="M3.2 6.5A5.5 5.5 0 0 1 13 6.5M12.8 9.5A5.5 5.5 0 0 1 3 9.5" />
102
+ </svg>
84
103
  </button>
85
104
  </div>
86
105
  </template>
87
- <style>
88
- .captcha .captcha-holder {
89
- cursor: pointer;
90
- width: 82px;
91
- height: 43px;
92
- object-fit: contain;
93
- border-top: 1px solid var(--bs-border-color, #eeeeee);
94
- border-bottom: 1px solid var(--bs-border-color, #eeeeee);
95
- }
96
-
97
- .input-group-lg.captcha .captcha-holder {
98
- width: 100px;
99
- height: auto;
100
- }
101
-
102
- .captcha .btn-refresh {
103
- background-color: var(--bs-body-bg, #ffffff);
104
- border-color: var(--bs-border-color, #eeeeee) !important;
105
- }
106
- </style>
@@ -1,69 +1,56 @@
1
- <script>
2
- import {defineComponent, inject} from "vue";
3
-
4
- export default defineComponent({
5
- emits: ['change'],
6
- props: {
7
- name: {
8
- type: String,
9
- required: true,
10
- },
11
- id: {
12
- type: String,
13
- default: '',
14
- required: false,
15
- },
16
- value: {
17
- type: String,
18
- default: 'yes',
19
- required: false,
20
- },
21
- type: {
22
- type: String,
23
- default: 'radio',
24
- required: false,
25
- },
26
- },
27
- methods:{
28
- change(e){
29
- this.$emit('change', e);
30
- }
31
- },
32
- setup(props) {
33
- let form = inject('form');
34
-
35
- if (form === undefined) {
36
- form = {
37
- errors: {},
38
- getID(name) {
39
- return name;
40
- }
41
- };
42
- }
43
-
44
- return {form};
45
- },
46
- })
47
- </script>
48
- <template>
49
- <label class="btn-label" :for="form.getID(name)+(id || value)" :class="{'active': (form[name] === value || form[name] === true)}">
50
- <input
51
- :name="name"
52
- :id="form.getID(name)+(id || value)"
53
- v-model="form[name]"
54
- :class="{'is-invalid': form?.errors[name]}"
55
- :disabled="form?.processing"
56
- :type="type"
57
- :value="value"
58
- @change="change"
59
- autocomplete="off"
60
- class="btn-check">
61
- <slot/>
62
- </label>
63
- </template>
64
- <style>
65
- .btn-label:not(.active){
66
- --bs-bg-opacity: .3;
67
- opacity: .6;
68
- }
69
- </style>
1
+ <script>
2
+ import { defineComponent, ref } from 'vue';
3
+ import { fieldProps, useField } from './composables/useField.js';
4
+ import { useChoice } from './composables/useChoice.js';
5
+
6
+ /** Toggle button (Bootstrap 6 `.btn-check`): the input is nested inside the label. */
7
+ export default defineComponent({
8
+ name: 'CheckboxButtonInput',
9
+ emits: ['update:modelValue', 'change', 'focus', 'blur'],
10
+ props: {
11
+ ...fieldProps,
12
+ type: { type: String, default: 'radio', validator: (v) => ['checkbox', 'radio'].includes(v) },
13
+ value: { type: [String, Number, Boolean, Object], default: 'yes' },
14
+ trueValue: { type: null, default: true },
15
+ falseValue: { type: null, default: false },
16
+ checked: { type: Boolean, default: false },
17
+ /** solid | outline | subtle | text | styled */
18
+ variant: { type: String, default: 'outline' },
19
+ theme: { type: String, default: 'primary' },
20
+ },
21
+ setup(props, { emit }) {
22
+ const input = ref(null);
23
+ const field = useField(props, emit, { valued: () => props.value });
24
+ useChoice(props, field, input);
25
+
26
+ return { ...field, input, onChange: (event) => field.emitChange(field.model.value, event) };
27
+ },
28
+ expose: ['input'],
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <label
34
+ class="btn-check"
35
+ :class="[`btn-${variant}`, `theme-${theme}`, { 'is-invalid': isInvalid, disabled: isDisabled }]"
36
+ >
37
+ <input
38
+ ref="input"
39
+ v-model="model"
40
+ :name="name"
41
+ :type="type"
42
+ :value="value"
43
+ :true-value="trueValue"
44
+ :false-value="falseValue"
45
+ :required="required"
46
+ :disabled="isDisabled"
47
+ :aria-invalid="isInvalid || undefined"
48
+ autocomplete="off"
49
+ :class="{ 'is-invalid': isInvalid }"
50
+ @change="onChange"
51
+ @focus="$emit('focus', $event)"
52
+ @blur="$emit('blur', $event)"
53
+ />
54
+ <slot />
55
+ </label>
56
+ </template>