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/README.md CHANGED
@@ -1,859 +1,467 @@
1
1
  # inertia-bootstrap-forms
2
2
 
3
- A Vue 3 component library for building Bootstrap‑styled forms inside **Laravel + Inertia.js** applications. Every input component binds itself automatically to an [Inertia `useForm`](https://inertiajs.com/forms) instance through a `<FormContainer>` wrapper, so you stop wiring up `v-model`, error classes, and field names by hand for every form.
3
+ > **English** · [فارسی](./README.fa.md)
4
4
 
5
- This package has been developed and maintained by **[Novin Vision](https://novinvision.com)** (شرکت **[نوین ویژن](https://novinvision.com)**), a team specialized in **[طراحی سایت](https://novinvision.com)** and **[web design](https://novinvision.com)**.
5
+ Bootstrap **6** form components for **Vue 3 + Inertia.js** (Laravel). Every field binds itself to one
6
+ [Inertia `useForm`](https://inertiajs.com/forms) instance through a `<FormContainer>`, and the value of **every field
7
+ lives in the container's single `v-model`** — no per-field `v-model`, error classes or ids to wire by hand.
6
8
 
7
- # About Us
8
-
9
- Novin Vision focuses on building modern and practical web solutions for businesses and developers who want a reliable digital presence. The approach is simple: clean design, solid development, and results that can be used in real projects without complexity.
10
-
11
- If you want to learn more or get in touch, you can visit:
12
- https://novinvision.com
9
+ ```vue
10
+ <FormContainer v-model="formData" url="/tickets">
11
+ <FormField name="priority" label="Priority">
12
+ <ComboboxInput name="priority" :options="priorities" />
13
+ </FormField>
14
+
15
+ <SubmitButton>
16
+ Save
17
+ <Icon icon="arrow-left" class="align-middle ms-1" />
18
+ </SubmitButton>
19
+ </FormContainer>
20
+ ```
13
21
 
14
- ## License
22
+ Built and maintained by **[Novin Vision](https://novinvision.com)** (نوین ویژن). Free for personal and commercial use (MIT).
15
23
 
16
- The use of this package is completely free. You are allowed to use it in personal and commercial projects without any restriction or payment.
24
+ > **v3 is a rewrite for Bootstrap 6.** Bootstrap 5 projects should stay on `inertia-bootstrap-forms@2`.
25
+ > See [Migrating from v2](#migrating-from-v2).
17
26
 
18
27
  ---
19
28
 
20
- Built and maintained by **Novin Vision**
21
- ```vue
22
- <FormContainer v-model="formData" url="/products">
23
- <div class="row g-3">
24
- <div class="col-12 col-sm-6">
25
- <FormLabel>Name</FormLabel>
26
- <TextInput name="name" />
27
- </div>
28
- <div class="col-12 col-sm-3">
29
- <FormLabel>Alias</FormLabel>
30
- <TextInput name="slug" />
31
- </div>
32
- <div class="col-12 col-sm-3">
33
- <FormLabel>Status</FormLabel>
34
- <Select2Input name="status" :options="[
35
- { id: 'active', name: 'Active' },
36
- { id: 'disable', name: 'Disabled' },
37
- { id: 'draft', name: 'Draft' }
38
- ]" />
39
- </div>
40
- <div class="col-12 col-sm-6">
41
- <FormLabel>New Image</FormLabel>
42
- <FileInput name="new_thumbnail" />
43
- </div>
44
- <div class="col-12">
45
- <FormLabel>Description</FormLabel>
46
- <EditorInput name="description" />
47
- </div>
48
- <div class="col-12 text-end">
49
- <SubmitButton />
50
- </div>
51
- </div>
52
- </FormContainer>
53
- ```
29
+ ## Table of contents
30
+
31
+ 1. [What's new in v3](#whats-new-in-v3)
32
+ 2. [Requirements](#requirements)
33
+ 3. [Installation](#installation)
34
+ 4. [Core concepts](#core-concepts)
35
+ 5. [Component reference](#component-reference)
36
+ 6. [Upload endpoint contract](#upload-endpoint-contract)
37
+ 7. [Internationalisation](#internationalisation)
38
+ 8. [Lifecycle, SSR and cleanup](#lifecycle-ssr-and-cleanup)
39
+ 9. [Composables](#composables)
40
+ 10. [Migrating from v2](#migrating-from-v2)
41
+ 11. [Notes and known limitations](#notes-and-known-limitations)
42
+ 12. [Development](#development)
54
43
 
55
44
  ---
56
45
 
57
- ## Table of Contents
58
-
59
- 1. [Requirements](#requirements)
60
- 2. [Installation](#installation)
61
- 3. [Global Setup (required helpers)](#global-setup-required-helpers)
62
- 4. [Core Concepts](#core-concepts)
63
- 5. [Component Reference](#component-reference)
64
- - [FormContainer](#formcontainer)
65
- - [FormLabel](#formlabel)
66
- - [GroupControl](#groupcontrol)
67
- - [TextInput](#textinput)
68
- - [TextAreaInput](#textareainput)
69
- - [EmailInput](#emailinput)
70
- - [PasswordInput](#passwordinput)
71
- - [MobileInput](#mobileinput)
72
- - [TelInput](#telinput)
73
- - [AmountInput](#amountinput)
74
- - [QuantityInput](#quantityinput)
75
- - [MultiQuantityInput](#multiquantityinput)
76
- - [CheckboxInput](#checkboxinput)
77
- - [CheckboxButtonInput](#checkboxbuttoninput)
78
- - [CheckboxToggle](#checkboxtoggle)
79
- - [Select2Input](#select2input)
80
- - [PersianDatePickerInput](#persiandatepickerinput)
81
- - [RangeSliderInput](#rangesliderinput)
82
- - [StarRatingInput](#starratinginput)
83
- - [CaptchaInput](#captchainput)
84
- - [EditorInput](#editorinput)
85
- - [LocationInput](#locationinput)
86
- - [FileInput](#fileinput)
87
- - [SimpleUploader](#simpleuploader)
88
- - [DropzoneInput](#dropzoneinput)
89
- - [UppyInput](#uppyinput)
90
- - [SubmitButton](#submitbutton)
91
- 6. [Upload Endpoint Contract (backend)](#upload-endpoint-contract-backend)
92
- 7. [Styling & Theming](#styling--theming)
93
- 8. [TypeScript](#typescript)
94
- 9. [Known Limitations](#known-limitations)
46
+ ## What's new in v3
95
47
 
96
- ---
48
+ - Rewritten for **Bootstrap 6** markup: `.check` / `.radio` / `.switch`, `.form-field`, `.btn-solid.theme-*`, `.menu`, `.alert.theme-*`, `.form-ghost`, …
49
+ - **New components:** `ComboboxInput` (replaces `Select2Input` / choices.js), `ChipsInput`, `OTPInput` (Bootstrap OTP), `RangeInput`, `FormAdornInput`, `FormField`.
50
+ - **Zero styles shipped.** No CSS file, no `<style>` blocks. Import Bootstrap 6 in your app.
51
+ - Every field emits Vue events (`update:modelValue`, `change`, plus component specific ones) and exposes all the props it needs.
52
+ - Bootstrap JavaScript plugins (Combobox, Chips, OTP, Range, Menu) are loaded lazily **on mount** and **disposed on unmount**. Listeners, timers, XHR requests, Dropzone and Uppy instances are released too.
53
+ - SSR-safe: nothing touches `window` / `document` at import time or during server rendering.
54
+ - No more hidden global requirements (`window.axios`, `maska`, `vue3-bootstrap-components`, FontAwesome).
97
55
 
98
56
  ## Requirements
99
57
 
100
- The package itself only ships the Vue components (plus a few smaller libraries that are already bundled into `dist/`). The following must exist in the **host** application:
101
-
102
- | Requirement | Why |
58
+ | Requirement | Notes |
103
59
  |---|---|
104
- | Vue `^3.0` | The components are plain Vue 3 SFCs. |
105
- | `@inertiajs/vue3` `>2.0` | `FormContainer` calls Inertia's `useForm()` internally. |
106
- | Bootstrap 5 CSS (`.form-control`, `.btn`, `.input-group`, `.form-check`, CSS variables like `--bs-*`, etc.) | All markup/classes assume Bootstrap 5 is loaded. |
107
- | `dropzone` `^6.0.0-beta.2` | Only needed if you use `<DropzoneInput>`. |
108
- | `@tinymce/tinymce-vue` | Only needed if you use `<EditorInput>` (TinyMCE). |
109
- | `@vue-leaflet/vue-leaflet` + `leaflet` | Only needed if you use `<LocationInput>`. |
110
- | `vue-tel-input` | Only needed if you use `<TelInput>`. |
111
- | `vue3-persian-datetime-picker` | Only needed if you use `<PersianDatePickerInput>`. |
112
- | `maska` | Only needed if you use `<AmountInput>` or `<QuantityInput>` (number masking). |
113
- | `axios` available globally as `window.axios` | Used internally by `<FileInput>`. Laravel's default `resources/js/bootstrap.js` already does this (`window.axios = require('axios')`). |
114
-
115
- The following libraries are **already bundled inside `dist/`** and do **not** need to be installed separately: `vue3-bootstrap-components`, `choices.js`, `svelte-range-slider-pips`, `@uppy/core`, `@uppy/vue`, `@uppy/xhr-upload`.
60
+ | `vue` ≥ 3.3 | |
61
+ | `@inertiajs/vue3` ≥ 2.0 | `FormContainer` uses `useForm()` |
62
+ | `bootstrap` ^6.0.0-alpha.1 | **CSS** is required. The JS plugins are imported by this package on demand. |
63
+ | `@floating-ui/dom` | Peer dependency of Bootstrap 6 menus (needed by Combobox / MultiQuantityInput) |
64
+ | `dropzone` ^6 | Only for `DropzoneInput` |
65
+ | `leaflet` | Only for `LocationInput` |
116
66
 
117
- ---
67
+ Third-party widgets are regular dependencies of this package and are loaded lazily: `vue-tel-input`,
68
+ `vue3-persian-datetime-picker`, `@tinymce/tinymce-vue`, `@vue-leaflet/vue-leaflet`, `@uppy/*`.
118
69
 
119
70
  ## Installation
120
71
 
121
- ### 1. Install the package
122
-
123
- This package isn't necessarily on the public npm registry, so install it however it was distributed to you:
124
-
125
72
  ```bash
126
- # from a private/internal npm registry
127
- npm install inertia-bootstrap-forms
128
-
129
- # from a Git repository
130
- npm install git+https://github.com/novinvision/inertia-bootstrap-forms.git
131
-
132
- # from a local folder or tarball
133
- npm install file:../path/to/inertia-bootstrap-forms
73
+ npm install inertia-bootstrap-forms bootstrap@next @floating-ui/dom
74
+ # optional
75
+ npm install dropzone leaflet
134
76
  ```
135
77
 
136
- ### 2. Install the peer libraries you actually need
78
+ ### Import the styles in your application
137
79
 
138
- You don't have to install all of them — only the ones backing the components you use:
139
-
140
- ```bash
141
- npm install @inertiajs/vue3 dropzone @tinymce/tinymce-vue \
142
- @vue-leaflet/vue-leaflet leaflet vue-tel-input \
143
- vue3-persian-datetime-picker maska
144
- ```
145
-
146
- ### 3. Import the package styles
80
+ The package does not ship any CSS. Import Bootstrap 6 (compiled CSS or your own Sass build) and — only for the
81
+ widgets you use — the CSS of the third-party libraries:
147
82
 
148
83
  ```js
149
84
  // resources/js/app.js
150
- import 'inertia-bootstrap-forms/dist/style.css';
151
- ```
152
-
153
- ### 4. Register the components
85
+ import 'bootstrap/dist/css/bootstrap.min.css'; // Bootstrap 6 (required)
154
86
 
155
- You can either import components individually (recommended, keeps the bundle small):
156
-
157
- ```vue
158
- <script setup>
159
- import { FormContainer, FormLabel, TextInput, SubmitButton } from 'inertia-bootstrap-forms';
160
- </script>
87
+ import 'vue-tel-input/vue-tel-input.css'; // TelInput
88
+ import 'leaflet/dist/leaflet.css'; // LocationInput
89
+ import 'dropzone/dist/basic.css'; // DropzoneInput
90
+ import 'dropzone/dist/dropzone.css'; // DropzoneInput
91
+ import '@uppy/vue/css/style.css'; // UppyInput
161
92
  ```
162
93
 
163
- …or register everything globally once, in `app.js`:
94
+ You do **not** need to import Bootstrap's JavaScript: the plugins used by the components are loaded by the package.
95
+ If your application already bundles Bootstrap and you want a single copy, hand the classes over:
164
96
 
165
97
  ```js
166
- import { createApp, h } from 'vue';
167
- import { createInertiaApp } from '@inertiajs/vue3';
168
- import * as InertiaBootstrapForms from 'inertia-bootstrap-forms';
169
- import 'inertia-bootstrap-forms/dist/style.css';
170
-
171
- createInertiaApp({
172
- resolve: name => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
173
- setup({ el, App, props, plugin }) {
174
- const app = createApp({ render: () => h(App, props) }).use(plugin);
98
+ import { Combobox, Chips, OtpInput, Range, Menu } from 'bootstrap';
99
+ import { configureBootstrap } from 'inertia-bootstrap-forms';
175
100
 
176
- Object.entries(InertiaBootstrapForms).forEach(([name, component]) => {
177
- if (name !== 'default') app.component(name, component);
178
- });
179
-
180
- app.mount(el);
181
- },
182
- });
101
+ configureBootstrap({ Combobox, Chips, OtpInput, Range, Menu });
183
102
  ```
184
103
 
185
- ---
104
+ ### Registering components
186
105
 
187
- ## Global Setup (required helpers)
188
-
189
- Two components (`AmountInput`, `QuantityInput`) rely on globals that this package does **not** provide. You need to register them yourself in `app.js` before those components will work.
190
-
191
- ### `v-maska` directive
192
-
193
- `AmountInput` and `QuantityInput` use `v-maska` (from the [`maska`](https://www.npmjs.com/package/maska) package) to format numbers with thousand separators while typing.
194
-
195
- ```bash
196
- npm install maska
197
- ```
106
+ Import what you need, or install the plugin to register everything globally:
198
107
 
199
108
  ```js
200
- import { vMaska } from 'maska/vue';
201
-
202
- app.directive('maska', vMaska);
203
- ```
204
-
205
- You can use any version/config of `maska` you like — the components only depend on the directive name (`v-maska`) and the `@maska` event payload (`event.detail.unmasked`), so feel free to follow Maska's own documentation for advanced masking options.
206
-
207
- ### `$number` global helper
208
-
209
- The same two components call `this.$number.toEnglish(...)` to convert localized (Persian/Arabic-Indic) digits typed by the user into plain English digits before storing them in the form. This helper is **not bundled** — you need to provide it as a global property. A minimal implementation that mirrors the digit conversion already used internally by `FormContainer` on submit:
210
-
211
- ```js
212
- // resources/js/plugins/number.js
213
- const persianDigits = ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'];
214
- const arabicDigits = ['٠','١','٢','٣','٤','٥','٦','٧','٨','٩'];
215
-
216
- export default {
217
- install(app) {
218
- app.config.globalProperties.$number = {
219
- toEnglish(value) {
220
- if (value === null || value === undefined) return value;
221
- let str = value.toString();
222
- persianDigits.forEach((d, i) => { str = str.replaceAll(d, i); });
223
- arabicDigits.forEach((d, i) => { str = str.replaceAll(d, i); });
224
- return str.replaceAll(',', '');
225
- }
226
- };
227
- }
228
- };
229
- ```
230
-
231
- ```js
232
- import NumberPlugin from './plugins/number';
233
- app.use(NumberPlugin);
234
- ```
235
-
236
- If you don't use `AmountInput` or `QuantityInput`, you can skip this section entirely.
237
-
238
- ### Optional: Font Awesome
239
-
240
- `CaptchaInput` renders a refresh icon using a Font Awesome class (`fal fa-sync-alt`, Pro "light" style). If you don't load Font Awesome Pro, the button still works — you just won't see the icon. Swap in your own icon via the component's markup if needed.
241
-
242
- ---
243
-
244
- ## Core Concepts
245
-
246
- ### `FormContainer` and the injected `form`
247
-
248
- `<FormContainer>` wraps Inertia's `useForm()` and `provide()`s it to every descendant via Vue's `provide`/`inject`. All the field components below `inject` this `form` object and read/write `form[name]` automatically — that's the entire trick behind not having to wire `v-model` on every field yourself.
249
-
250
- ```vue
251
- <FormContainer v-model="formData" url="/login" method="post" reset-on-success>
252
- <TextInput name="email" />
253
- <PasswordInput name="password" />
254
- <SubmitButton />
255
- </FormContainer>
256
- ```
109
+ import InertiaBootstrapForms from 'inertia-bootstrap-forms';
257
110
 
258
- `FormContainer` props:
259
-
260
- | Prop | Type | Default | Description |
261
- |---|---|---|---|
262
- | `url` | String | `''` | Endpoint the form submits to. |
263
- | `method` | String | `'post'` | HTTP method passed to Inertia's `form.submit()`. |
264
- | `only` | Array | `[]` | Inertia partial-reload `only` option. |
265
- | `modelValue` | Object | `{}` | Initial data for the form (also works with `v-model`). |
266
- | `options` | Object | `{}` | Extra fields merged into the Inertia form state (e.g. defaults that aren't real inputs). |
267
- | `resetOnSuccess` | Boolean | `false` | Calls `form.reset()` automatically after a successful submit. |
268
- | `submitHandler` | Function | `null` | If provided, this function is called instead of the default Inertia submit flow — useful for custom submit logic (e.g. confirmation modals). |
269
-
270
- Events: `submit`, `reset`, `onStart`, `onFinish`, `onSuccess`, `onError`, `change`, `update:modelValue`.
271
-
272
- Slots:
273
- - default — scoped with `{ form, submit }`, the actual form fields.
274
- - `errors` — override the default error `<Alert>` block.
275
- - `message` — override the default success `<Alert>` block (shown when the backend response includes a `message` prop).
276
-
277
- Exposed methods (via template ref): `submit()`, `reset()`.
278
-
279
- Notable built-in behavior: on submit, `FormContainer` converts any Persian/Arabic-Indic digits present in the form's values to plain English digits before sending the request — handy for numeric fields typed with a Persian keyboard.
280
-
281
- ### Field naming & binding
282
-
283
- Every field component takes a `name` prop. As long as it lives inside a `<FormContainer>` (or any component that injects a `form`), it automatically reads/writes `form[name]` — no manual `v-model` needed:
284
-
285
- ```vue
286
- <TextInput name="title" />
287
- <!-- equivalent to manually doing -->
288
- <input v-model="form.title" name="title" class="form-control">
289
- ```
290
-
291
- Validation errors are read from `form.errors[name]` and applied as Bootstrap's `is-invalid` class automatically.
292
-
293
- ### Grouped / repeated fields — `GroupControl`
294
-
295
- `<GroupControl name="...">` lets a *block* of fields write into a nested/indexed slice of the form data instead of the top level — useful for repeating rows (e.g. multiple bid items, multiple contacts, etc.).
296
-
297
- Each `<GroupControl>` instance figures out its own index (`groupID`) based on its position among sibling `.form-control-group` blocks, so the typical pattern is to repeat the **whole** `<GroupControl>` with `v-for`:
298
-
299
- ```vue
300
- <div v-for="(row, index) in rows" :key="index">
301
- <GroupControl name="bids">
302
- <TextInput name="title" />
303
- <AmountInput name="price" />
304
- </GroupControl>
305
- </div>
306
- ```
307
-
308
- This produces `form.bids[0].title`, `form.bids[0].price`, `form.bids[1].title`, etc.
309
-
310
- `MultiQuantityInput` (documented below) uses a single `GroupControl` to collect a *map* of quantities (`form.tickets.adult`, `form.tickets.child`, …) instead of a repeated list — see its own example for that variant.
311
-
312
- ### Validation & error display
313
-
314
- `FormContainer` renders a default Bootstrap `Alert` listing every message in `form.errors` (replaceable via the `errors` slot), and every field adds `is-invalid` automatically when `form.errors[name]` exists. You don't need to render error text per-field unless you want to — Bootstrap will show `.invalid-feedback` siblings if you add them yourself.
315
-
316
- ### Element IDs
317
-
318
- `form.getID(field)` (available on the injected `form`) builds a unique, collision-free `id` attribute for fields, taking the surrounding `<FormContainer>` id and any `GroupControl` into account. Components that render a real `<label for="">`/`<input id="">` pair (e.g. `TextInput`, `CheckboxInput`) use this automatically.
319
-
320
- ---
321
-
322
- ## Component Reference
323
-
324
- ### FormContainer
325
-
326
- See [Core Concepts](#formcontainer-and-the-injected-form) above for the full prop/event/slot table.
327
-
328
- ### FormLabel
329
-
330
- Simple `<label>` wrapper that appends a red `*` when `required` is set.
331
-
332
- ```vue
333
- <FormLabel required>Email address</FormLabel>
334
- ```
335
-
336
- | Prop | Type | Default |
337
- |---|---|---|
338
- | `required` | Boolean | `false` |
339
-
340
- Slot: default — label text.
341
-
342
- ### GroupControl
343
-
344
- See [Grouped / repeated fields](#grouped--repeated-fields--groupcontrol) above.
345
-
346
- | Prop | Type | Required |
347
- |---|---|---|
348
- | `name` | String | yes |
349
-
350
- ### TextInput
351
-
352
- The base text field. Most other "shortcut" inputs (`EmailInput`, `PasswordInput`, `MobileInput`) are thin wrappers around this component.
353
-
354
- ```vue
355
- <TextInput name="full_name" />
356
- ```
357
-
358
- | Prop | Type | Required |
359
- |---|---|---|
360
- | `name` | String | yes |
361
-
362
- Renders a plain `<input type="text" class="form-control">` bound to `form[name]`.
363
-
364
- ### TextAreaInput
365
-
366
- Same binding pattern as `TextInput`, renders a `<textarea class="form-control">`.
367
-
368
- ```vue
369
- <TextAreaInput name="description" />
111
+ createInertiaApp({
112
+ setup({ el, App, props, plugin }) {
113
+ createApp({ render: () => h(App, props) })
114
+ .use(plugin)
115
+ .use(InertiaBootstrapForms, { locale: 'en' }) // options are optional
116
+ .mount(el);
117
+ },
118
+ });
370
119
  ```
371
120
 
372
- | Prop | Type | Required |
373
- |---|---|---|
374
- | `name` | String | yes |
375
-
376
- ### EmailInput
121
+ ## Core concepts
377
122
 
378
- Thin wrapper around `TextInput` (`type="email"`, Persian placeholder "ایمیل خود را وارد کنید").
123
+ ### One `v-model` for the whole form
379
124
 
380
125
  ```vue
381
- <EmailInput name="email" />
382
- ```
383
-
384
- > **Note:** see [Known Limitations](#known-limitations) — the `name` prop on this specific wrapper is not declared correctly in the current source, so passing a custom `name` may not bind to the form data as expected. If that affects you, use `<TextInput name="..." type="email" />` directly instead.
385
-
386
- ### PasswordInput
126
+ <script setup>
127
+ import { ref } from 'vue';
128
+ import { FormContainer, TextInput, ComboboxInput, SwitchInput, SubmitButton } from 'inertia-bootstrap-forms';
387
129
 
388
- Thin wrapper around `TextInput` (`type="password"`, Persian placeholder "گذرواژه خود را وارد کنید").
130
+ const formData = ref({ name: '', status: 'draft', published: false });
131
+ </script>
389
132
 
390
- ```vue
391
- <PasswordInput name="password" />
133
+ <template>
134
+ <FormContainer v-model="formData" url="/products" @success="onSaved">
135
+ <TextInput name="name" />
136
+ <ComboboxInput name="status" :options="['draft', 'active']" />
137
+ <SwitchInput name="published" label="Published" />
138
+ <SubmitButton />
139
+ </FormContainer>
140
+ </template>
392
141
  ```
393
142
 
394
- | Prop | Type | Default |
395
- |---|---|---|
396
- | `name` | String | `'password'` |
397
-
398
- ### MobileInput
399
-
400
- Thin wrapper around `TextInput` (`type="tel"`, Persian placeholder "موبایل خود را وارد کنید").
143
+ * `name` is the key inside `formData`. The object does **not** have to declare every field up front: keys that are missing are added to `formData` as soon as the field is used, and they are submitted and reset with the rest. Typing into a field updates `formData`; changing `formData` from the outside updates the fields.
144
+ * Server-side validation errors (`form.errors`) are matched by `name` and turn the control `.is-invalid`.
145
+ * Fields are disabled while the form is `processing`.
146
+ * Persian / Arabic digits are converted to English digits before submit (`:normalize-digits="false"` to disable). `File` objects are never touched.
147
+ * Values are still **not limited to the container**: a field used *outside* a `FormContainer` works with its own `v-model`.
401
148
 
402
149
  ```vue
403
- <MobileInput name="mobile" />
150
+ <TextInput v-model="title" /> <!-- standalone -->
404
151
  ```
405
152
 
406
- | Prop | Type | Default |
407
- |---|---|---|
408
- | `name` | String | `'mobile'` |
409
-
410
- ### TelInput
153
+ ### Events
411
154
 
412
- Full international phone input powered by the **external package `vue-tel-input`**. Defaults to Iran (`ir`) with a preferred-country list (`ir`, `us`, `tr`, `ca`) and a searchable country dropdown.
155
+ Every component declares its emits:
413
156
 
414
- ```vue
415
- <TelInput name="phone" placeholder="912 345 6789" />
416
- ```
417
-
418
- | Prop | Type | Default |
157
+ | Event | Payload | Emitted by |
419
158
  |---|---|---|
420
- | `name` | String | `'email'` (see [Known Limitations](#known-limitations)) |
421
- | `placeHolder` | String | `'000 000 0000'` |
422
-
423
- Since this wraps `vue-tel-input`, all of that package's own configuration (locales, validation rules, dial-code formatting, flags, styling, etc.) can be controlled by following [vue-tel-input's documentation](https://www.npmjs.com/package/vue-tel-input) and adjusting the source if you need options beyond what's hard-coded here (`mode`, `defaultCountry`, `preferredCountries`, `dropdownOptions`).
424
-
425
- ### AmountInput
159
+ | `update:modelValue` | `value` | all fields |
160
+ | `change` | `value, event?` | all fields |
161
+ | `input` | `value, event` | text-like fields, OTP, range |
162
+ | `focus` / `blur` | `event` | text-like fields, checkbox family, range, chips |
163
+ | component specific | see [reference](#component-reference) | combobox, chips, OTP, uploaders … |
426
164
 
427
- Numeric input with thousands-separator masking (via **external package `maska`** — see [Global Setup](#v-maska-directive)) and a trailing unit label.
165
+ Native listeners that are not declared (`@keydown`, `@paste`, …) fall through to the underlying `<input>` on single-root
166
+ components (`TextInput`, `TextAreaInput`, `EmailInput`, `MobileInput`, `PasswordInput`, `FormAdornInput`).
428
167
 
429
- ```vue
430
- <AmountInput name="price" unit="تومان" />
431
- ```
168
+ ### Props shared by every field
432
169
 
433
- | Prop | Type | Default |
170
+ | Prop | Type | Description |
434
171
  |---|---|---|
435
- | `name` | String | required |
436
- | `required` | Boolean | `false` |
437
- | `disabled` | Boolean | `false` |
438
- | `readonly` | Boolean | `false` |
439
- | `placeholder` | String | `'عدد را وارد کنید'` |
440
- | `unit` | String | `'عدد'` |
441
-
442
- Slot: `suffix` — extra content appended after the unit label (inside the Bootstrap `InputGroup`).
443
-
444
- Requires the `v-maska` directive and `$number.toEnglish()` global helper — see [Global Setup](#global-setup-required-helpers).
172
+ | `name` | `string` | Key in the form's v-model (also used for error lookup) |
173
+ | `modelValue` | any | Standalone `v-model` (ignored inside a `FormContainer`) |
174
+ | `id` | `string` | Override the generated id (auto-linked to `FormField` labels) |
175
+ | `disabled` | `boolean` | Disable the control |
176
+ | `readonly` | `boolean` | Read-only (or non-interactive for controls without native readonly) |
177
+ | `required` | `boolean` | Native `required` |
178
+ | `invalid` | `boolean` | Force invalid state; default follows server errors |
445
179
 
446
- ### QuantityInput
447
-
448
- Stepper-style numeric input (`+` / `-` buttons) with the same masking as `AmountInput`.
180
+ ### Grouped / repeated values
449
181
 
450
182
  ```vue
451
- <QuantityInput name="qty" :min="1" :max="10" unit="عدد" />
183
+ <GroupControl name="items" :index="0">
184
+ <TextInput name="title" /> <!-- items[0].title -->
185
+ <QuantityInput name="qty" /> <!-- items[0].qty -->
186
+ </GroupControl>
452
187
  ```
453
188
 
454
- | Prop | Type | Default |
455
- |---|---|---|
456
- | `name` | String | required |
457
- | `required` | Boolean | `false` |
458
- | `disabled` | Boolean | `false` |
459
- | `readonly` | Boolean | `false` |
460
- | `unit` | String | `'عدد'` |
461
- | `min` | Number | `0` |
462
- | `max` | Number | `null` |
189
+ `index` is optional — without it the position among sibling `.form-control-group` elements is used. Error keys such as
190
+ `items.0.title` are resolved automatically.
463
191
 
464
- Slot: `suffix`.
192
+ ---
465
193
 
466
- Also requires the `v-maska` directive and `$number.toEnglish()` — see [Global Setup](#global-setup-required-helpers).
194
+ ## Component reference
467
195
 
468
- ### MultiQuantityInput
196
+ ### Form structure
469
197
 
470
- A dropdown of multiple `QuantityInput` rows (e.g. "2 adults, 1 child") that collapses into a single summary button. Internally wraps its options in one `GroupControl`, so the result is stored as a map: `form[name][optionKey]`.
471
-
472
- ```vue
473
- <MultiQuantityInput
474
- name="tickets"
475
- unit="نفر"
476
- :options="[
477
- { name: 'بزرگسال', min: 1, max: 10 }, // key: 0
478
- { name: 'کودک', min: 0, max: 10 }, // key: 1
479
- ]"
480
- />
481
- ```
198
+ #### `FormContainer`
482
199
 
483
200
  | Prop | Type | Default | Description |
484
201
  |---|---|---|---|
485
- | `name` | String | required | |
486
- | `options` | Array | required | Each item can be a plain string or `{ name, min, max }`. The array index becomes the data key. |
487
- | `required` | Boolean | `false` | |
488
- | `disabled` | Boolean | `false` | |
489
- | `readonly` | Boolean | `false` | |
490
- | `max` | Number | `null` | Fallback max applied to options without their own `max`. |
491
- | `totalMax` | Number | `null` | (declared, not currently enforced in the template logic) |
492
- | `min` | Number | `0` | Fallback min applied to options without their own `min`. |
493
- | `totalMin` | Number | `null` | (declared, not currently enforced in the template logic) |
494
- | `unit` | String | `'Number'` | Label shown on the collapsed summary button. |
495
-
496
- ### CheckboxInput
202
+ | `modelValue` | `object` | `{}` | The form data (`v-model`) |
203
+ | `url` | `string` | `''` | Submit URL |
204
+ | `method` | `string` | `'post'` | HTTP method |
205
+ | `only` | `string[]` | `[]` | Inertia `only` option |
206
+ | `options` | `object` | `{}` | Extra fields that are not part of `modelValue` |
207
+ | `visitOptions` | `object` | `{}` | Extra Inertia visit options (`preserveScroll`, `headers`, callbacks…) |
208
+ | `autoTab` | `boolean` | `true` | Focus next field when `maxlength` is reached |
209
+ | `resetOnSuccess` | `boolean` | `false` | Reset the form after a successful submit |
210
+ | `submitHandler` | `(event, form) => any` | `null` | Replace the default submit |
211
+ | `normalizeDigits` | `boolean` | `true` | Persian/Arabic → English digits on submit |
212
+ | `validate` | `boolean` | `false` | Enable Bootstrap 6 client-side validation (`data-bs-validate`) |
213
+ | `htmlMessages` | `boolean` | `false` | Render server messages as HTML (trusted content only) |
214
+ | `hideAlerts` | `boolean` | `false` | Hide the built-in error / success alerts |
215
+
216
+ Events: `update:modelValue`, `change`, `submit`, `reset`, `before`, `start`, `progress`, `success`, `error`, `cancel`, `finish`
217
+ (legacy `onStart`, `onFinish`, `onSuccess`, `onError` are still emitted).
218
+ Slots: default `{ form, submit, reset }`, `errors { errors, form }`, `message { message }`.
219
+ Exposed: `submit()`, `reset()`, `form`, `el`.
220
+
221
+ A `message` prop returned by the server in the Inertia page props is shown as a success alert.
222
+
223
+ #### `FormField`
224
+
225
+ Bootstrap 6 `.form-field`: label + control + help + validation feedback. The generated `id` is injected into the child control and the label.
226
+
227
+ | Prop | Description |
228
+ |---|---|
229
+ | `name` | Field key — used to read the server error |
230
+ | `label`, `help` | Texts |
231
+ | `error` | Override the error text |
232
+ | `required` | Adds the required mark to the label |
233
+ | `id` | Override the generated id |
497
234
 
498
- Single checkbox/radio bound through the same `form`/`group` injection pattern as text fields.
235
+ Slots: default, `label`, `help`, `error`.
499
236
 
500
237
  ```vue
501
- <CheckboxInput name="accept_terms" value="yes">I agree to the terms</CheckboxInput>
238
+ <FormField name="email" label="Email" help="We never share it" required>
239
+ <EmailInput name="email" />
240
+ </FormField>
502
241
  ```
503
242
 
504
- | Prop | Type | Default |
505
- |---|---|---|
506
- | `name` | String | required |
507
- | `id` | String | `''` |
508
- | `value` | String / Number | `'yes'` |
509
- | `type` | String | `'checkbox'` (can be set to `'radio'`) |
510
-
511
- Event: `change`. Slot: default — label text.
243
+ #### `FormLabel`, `GroupControl`, `SubmitButton`
512
244
 
513
- To build a checkbox/radio **group**, repeat the component with the same `name` and different `value`s; bind the initial form value to an array for multi-select checkboxes, or a scalar for radios.
245
+ * `FormLabel` – `for`, `required`. Detects its control automatically inside `FormField` or from the next element.
246
+ * `GroupControl` – `name` (required), `index`.
247
+ * `SubmitButton` – `variant` (`solid`), `theme` (`primary`), `loading`, `disabled`, `locale`. Renders `class="btn-solid theme-primary"`; pass `:variant="null"` to bring your own classes. Shows a spinner while `form.processing` / `form.uploading`. Emits `click`. Slots: default, `progress`.
514
248
 
515
- ### CheckboxButtonInput
249
+ ### Text-like inputs
516
250
 
517
- Same idea as `CheckboxInput` but styled as a Bootstrap "button checkbox/radio" (`.btn-check` + label) instead of a native checkbox UI.
251
+ | Component | Extra props | Notes |
252
+ |---|---|---|
253
+ | `TextInput` | `type`, `placeholder`, `size` (`sm`/`lg`) | `.form-control` |
254
+ | `TextAreaInput` | `rows`, `placeholder`, `size` | |
255
+ | `EmailInput` | `placeholder`, `locale` | `type="email"`; all `TextInput` props forwarded |
256
+ | `MobileInput` | `placeholder`, `locale` | `type="tel"` |
257
+ | `PasswordInput` | `placeholder`, `toggle`, `locale` | `toggle` adds a show / hide button (form-adorn); event `toggle` |
258
+ | `TelInput` | `placeholder`, `defaultCountry` (`ir`), `preferredCountries`, `mode`, `validCharactersOnly`, `validate`, `dropdownOptions`, `inputOptions` | `vue-tel-input`; stores the international number; events `validate`, `country-changed` |
259
+ | `FormAdornInput` | `type`, `placeholder`, `startText`, `endText`, `size` | Bootstrap 6 `.form-adorn`. Slots `start`, `end` |
260
+ | `AmountInput` | `placeholder`, `unit`, `min`, `max`, `size`, `locale` | Integer with thousands separators, stores a `number` |
261
+ | `QuantityInput` | `min` (0), `max`, `step` (1), `unit`, `locale` | Stepper; events `increment`, `decrement` |
262
+ | `MultiQuantityInput` | `options`, `min`, `max`, `totalMin`, `totalMax`, `unit`, `placement`, `locale` | Bootstrap Menu with one stepper per option; value `{ 0: 2, 1: 1 }` |
518
263
 
519
264
  ```vue
520
- <CheckboxButtonInput name="plan" value="monthly">Monthly</CheckboxButtonInput>
521
- <CheckboxButtonInput name="plan" value="yearly">Yearly</CheckboxButtonInput>
265
+ <FormAdornInput name="website" start-text="https://">
266
+ <template #end><span class="form-adorn-icon"><svg …/></span></template>
267
+ </FormAdornInput>
522
268
  ```
523
269
 
524
- | Prop | Type | Default |
525
- |---|---|---|
526
- | `name` | String | required |
527
- | `id` | String | `''` |
528
- | `value` | String | `'yes'` |
529
- | `type` | String | `'radio'` |
530
-
531
- Event: `change`. Wrap the rendered `<label>` in your own `.btn-group`/`.btn` classes for the typical Bootstrap button-group look.
270
+ ### Choice inputs
532
271
 
533
- ### CheckboxToggle
272
+ | Component | Extra props |
273
+ |---|---|
274
+ | `CheckboxInput` | `type` (`checkbox`/`radio`), `value`, `trueValue`, `falseValue`, `checked`, `indeterminate`, `label` |
275
+ | `SwitchInput` | `value`, `trueValue`, `falseValue`, `checked`, `label` |
276
+ | `CheckboxButtonInput` | `type` (`radio`), `value`, `variant` (`outline`), `theme` (`primary`), `checked` — Bootstrap `.btn-check` |
277
+ | `CheckboxToggle` | `type`, `value`, `checked`, `hideInput` — selectable card (`.form-field-card`) |
534
278
 
535
- A larger, card-style toggle (background highlight when checked) — good for plan pickers, add-on selectors, etc.
279
+ Model rules follow Vue's native `v-model`: an **array** model collects the checked `value`s, a **radio** stores the selected `value`, a single checkbox stores `trueValue` / `falseValue` (default `true` / `false`).
536
280
 
537
281
  ```vue
538
- <CheckboxToggle name="addons" value="extra_luggage">Extra luggage</CheckboxToggle>
282
+ <CheckboxInput name="roles" value="admin" label="Admin" />
283
+ <CheckboxInput name="roles" value="editor" label="Editor" /> <!-- roles: ['admin','editor'] -->
284
+ <CheckboxInput name="plan" type="radio" value="pro" label="Pro" />
539
285
  ```
540
286
 
541
- | Prop | Type | Default |
542
- |---|---|---|
543
- | `name` | String | required |
544
- | `id` | String | `''` |
545
- | `value` | String / Number | `'yes'` |
546
- | `type` | String | `'checkbox'` |
547
- | `hideInput` | Boolean | `false` — visually hides the native input while keeping the toggle clickable. |
548
-
549
- Event: `change`.
550
-
551
- ### Select2Input
287
+ ### `ComboboxInput` (Bootstrap 6 Combobox)
552
288
 
553
- Searchable dropdown powered by the **external package `choices.js`**, with optional remote/async search.
554
-
555
- ```vue
556
- <Select2Input
557
- name="category_id"
558
- :options="categories"
559
- label="title"
560
- placeholder="Select a category"
561
- search-enabled
562
- />
563
-
564
- <!-- Remote search -->
565
- <Select2Input
566
- name="user_id"
567
- :search="{ url: '/api/users/search' }"
568
- placeholder="Search a user..."
569
- />
570
- ```
289
+ Replaces `Select2Input` (choices.js). `Select2Input` is still exported as an alias.
571
290
 
572
291
  | Prop | Type | Default | Description |
573
292
  |---|---|---|---|
574
- | `name` | String | required | |
575
- | `options` | Array | — | Array of objects (or plain strings/numbers). |
576
- | `label` | String | `null` | Object field used as the display label. Falls back to `name` / `label` / `value`. |
577
- | `placeholder` | String | `'Click to choice'` | |
578
- | `searchPlaceholder` | String | `'Type for search...'` | |
579
- | `multiple` | Boolean | `false` | |
580
- | `required` | Boolean | `false` | |
581
- | `config` | Object | `{}` | Merged into `choices.js`'s own config — use this for any Choices.js option not exposed above. |
582
- | `locale` | String | `'en'` | Set to `'fa'` (or rely on auto-detection from `document.dir === 'rtl'`) for built-in Persian UI strings. |
583
- | `searchEnabled` | Boolean | `false` | Enables local in-list searching. Automatically `true` when `search.url` is set. |
584
- | `hideDropdown` | Boolean | `false` | |
585
- | `search` | Object | `{ url: null }` | When `url` is set, typing triggers a debounced `POST` request (`?query=...`) and replaces the option list with the JSON response. |
586
-
587
- Events: `update:modelValue`, `search`, `searching`, `change`, `selected`.
588
-
589
- > **Note:** the prop named `key` (meant to pick which object field is used as the option *value*) cannot actually be set from a template, because Vue reserves `:key` for its own list-rendering mechanism. The default value-resolution (`id` → `name` → `label` → `value`) is used instead. See [Known Limitations](#known-limitations).
590
-
591
- Since this wraps Choices.js, any configuration option supported by that library can be passed through the `config` prop — see [Choices.js documentation](https://github.com/Choices-js/Choices) for the full list (custom templates, callback hooks, RTL, etc.).
592
-
593
- ### PersianDatePickerInput
594
-
595
- Persian (Jalali) date/time picker powered by the **external package `vue3-persian-datetime-picker`**.
293
+ | `options` | `Array` | `[]` | Strings, numbers or objects `{ id \| value, name \| label, description, disabled }` |
294
+ | `valueKey` / `labelKey` | `string` | auto | Property names of option objects (`label` is a legacy alias of `labelKey`) |
295
+ | `descriptionKey` | `string` | `'description'` | Secondary text of an option |
296
+ | `multiple` | `boolean` | `false` | Multiple selection (array model) |
297
+ | `placeholder` | `string` | i18n | |
298
+ | `searchEnabled` | `boolean` | `false` | Client-side search box |
299
+ | `search` | `object` | `{ url: null }` | **Remote search**: `{ url, method='POST', param='query', debounce=400, minChars=0, headers, resultsKey='data' }` |
300
+ | `clearable` | `boolean` | `false` | Adds an entry that clears a single select |
301
+ | `clearText`, `noResultsText`, `loadingText`, `searchPlaceholder`, `selectedText` | `string` | i18n | `selectedText` supports `{count}` |
302
+ | `placement` | `string` | `'bottom-start'` | Floating UI placement |
303
+ | `size`, `hideDropdown`, `searchNormalize`, `locale` | | | |
304
+
305
+ Events: `update:modelValue`, `change`, `selected` (option(s)), `clear`, `search`, `search-success`, `search-error`, `show`, `shown`, `hide`, `hidden`.
306
+ Exposed: `show()`, `hide()`, `toggle()`, `clear()`, `focus()`.
596
307
 
597
308
  ```vue
598
- <PersianDatePickerInput name="birth_date" />
599
- <PersianDatePickerInput name="appointment" calendar="datetime" view="datetime" range />
600
- ```
309
+ <ComboboxInput name="priority" :options="[{ id: 1, name: 'Low' }, { id: 2, name: 'High', description: 'Answered first' }]" clearable />
601
310
 
602
- | Prop | Type | Default |
603
- |---|---|---|
604
- | `name` | String | required |
605
- | `calendar` | String | `'date'` |
606
- | `view` | String | `'date'` |
607
- | `min` | String | — |
608
- | `max` | String | — |
609
- | `range` | Boolean | `false` |
610
- | `locale` | String | `'fa'` |
611
- | `format` | String | `'jYYYY-jMM-jDD'` |
612
- | `inputFormat` | String | `null` (falls back to `format`) |
613
- | `placeholder` | String | `'انتخاب تاریخ'` |
614
-
615
- Event: `change`. Method exposed: `clear()`.
616
-
617
- Any option supported by `vue3-persian-datetime-picker` itself (Gregorian mode, time steps, custom themes, etc.) can be used by following [that package's own documentation](https://www.npmjs.com/package/vue3-persian-datetime-picker) and extending the source where needed.
618
-
619
- ### RangeSliderInput
620
-
621
- Slider input powered by the **external package `svelte-range-slider-pips`**.
622
-
623
- ```vue
624
- <RangeSliderInput name="budget" :min="0" :max="1000" :step="10" />
625
- <RangeSliderInput name="price_range" :min="0" :max="1000" range />
626
- ```
627
-
628
- | Prop | Type | Default |
629
- |---|---|---|
630
- | `name` | String | `null` |
631
- | `modelValue` | Number | `0` |
632
- | `min` | Number | `1` |
633
- | `max` | Number | `100` |
634
- | `step` | Number | `1` |
635
- | `readonly` | Boolean | `false` |
636
- | `range` | Boolean | `false` — emits `[min, max]` instead of a single value. |
637
- | `options` | Object | `{}` | Passed straight through to `svelte-range-slider-pips` — use this for pips formatting, colors, springs, etc. See [that package's docs](https://github.com/simeydotme/svelte-range-slider-pips) for everything it supports. |
638
-
639
- Events: `update:modelValue`, `change`.
311
+ <ComboboxInput name="customer" :search="{ url: '/api/customers', debounce: 300 }" /> <!-- remote -->
640
312
 
641
- ### StarRatingInput
642
-
643
- Hover/click star rating, no external dependency.
644
-
645
- ```vue
646
- <StarRatingInput name="rating" :max-stars="5" />
313
+ <ComboboxInput name="tags" multiple search-enabled :options="tags" />
647
314
  ```
648
315
 
649
- | Prop | Type | Default |
650
- |---|---|---|
651
- | `name` | String | `null` |
652
- | `modelValue` | Number | `0` |
653
- | `maxStars` | Number | `5` |
654
- | `precision` | Number | `2` |
655
- | `showValue` | Boolean | `true` |
656
- | `readonly` | Boolean | `false` |
657
-
658
- Event: `update:modelValue`.
316
+ The remote endpoint receives `?query=…` and must answer with a JSON array (or `{ data: [] }`). The `X-XSRF-TOKEN` cookie is forwarded automatically. Selected options always keep their label even when they are not part of the latest results.
659
317
 
660
- ### CaptchaInput
318
+ ### `ChipsInput` (Bootstrap 6 Chips)
661
319
 
662
- Image captcha field with a refresh button. Expects a backend endpoint that returns a captcha **image** for `GET` and re-rolls automatically whenever the form finishes processing (e.g. after a failed submit).
663
-
664
- ```vue
665
- <CaptchaInput url="/captcha/default" />
666
- ```
320
+ Value: array of strings.
667
321
 
668
- | Prop | Type | Default |
322
+ | Prop | Default | Description |
669
323
  |---|---|---|
670
- | `name` | String | `'captcha'` |
671
- | `url` | String | `'/captcha/default'` |
672
- | `required` | Boolean | `false` |
673
- | `placeholder` | String | `'کد امنیتی روبرو را وارد کنید'` |
674
-
675
- Must be used inside a `<FormContainer>` (it injects `form` directly, without a fallback). A Persian comment in the source notes a ready-to-pair Laravel captcha endpoint is expected at `url`.
324
+ | `placeholder` | i18n | |
325
+ | `separator` | `','` | Splits typed / pasted text. `null` disables |
326
+ | `allowDuplicates` | `false` | |
327
+ | `maxChips` | `null` | |
328
+ | `dismissible` | `true` | Remove button on chips |
329
+ | `dismissIcon` | `null` | Custom remove icon markup |
330
+ | `createOnBlur` | `true` | |
331
+ | `validator` | `null` | `(value) => boolean`; rejected values emit `rejected` |
676
332
 
677
- ### EditorInput
333
+ Events: `update:modelValue`, `change`, `add`, `remove`, `select`, `rejected`, `focus`, `blur`. Exposed: `add()`, `remove()`, `clear()`, `focus()`.
678
334
 
679
- Rich-text editor powered by the **external package `@tinymce/tinymce-vue`** (TinyMCE).
335
+ ### `OTPInput` (Bootstrap 6 OTP)
680
336
 
681
- ```vue
682
- <EditorInput name="body" :options="{ height: 400 }" />
683
- ```
684
-
685
- | Prop | Type | Default |
337
+ | Prop | Default | Description |
686
338
  |---|---|---|
687
- | `name` | String | required |
688
- | `placeholder` | String | `''` |
689
- | `disabled` | Boolean | `false` |
690
- | `options` | Object | `{}` — merged into TinyMCE's own `init` config. |
691
-
692
- Event: `update:modelValue`. Exposed: `editor()` — returns the underlying TinyMCE editor instance.
339
+ | `length` (`maxLength` legacy) | `6` | Number of characters |
340
+ | `type` | `'numeric'` | `numeric` \| `alphanumeric` \| `alpha` |
341
+ | `mask` | `false` | Show `•` |
342
+ | `groups` | `null` | e.g. `[3, 3]` |
343
+ | `separator` | `'·'` | |
344
+ | `autoSubmit` | `false` | Submit the `FormContainer` when the code is complete |
345
+ | `autofocus` | `false` | |
693
346
 
694
- TinyMCE typically needs either a hosted script/API key or a self-hosted bundle — refer to [TinyMCE's Vue integration docs](https://www.tiny.cloud/docs/tinymce/latest/vue-cloud/) for licensing/configuration details and pass any extra option (toolbar, plugins, content_css, license_key, etc.) through the `options` prop.
347
+ Events: `update:modelValue`, `change`, `input`, `complete`, `focus`, `blur`. Exposed: `focus()`, `clear()`.
348
+ A single real input backs the slots, so SMS / password-manager autofill keeps working.
695
349
 
696
- ### LocationInput
350
+ ### `RangeInput` (Bootstrap 6 Range)
697
351
 
698
- Interactive map picker (click or drag a marker) powered by **external packages `@vue-leaflet/vue-leaflet` and `leaflet`**, using OpenStreetMap tiles.
699
-
700
- ```vue
701
- <LocationInput name="location" />
702
- ```
703
-
704
- | Prop | Type | Default |
352
+ | Prop | Default | Description |
705
353
  |---|---|---|
706
- | `name` | String | `'location'` |
707
- | `disabled` | Boolean / String | `false` |
708
- | `readonly` | Boolean / String | `false` |
709
- | `modelValue` | Object | `{ lat, lng }` |
710
-
711
- Event: `update:modelValue`. Stores `{ lat, lng }` into `form[name]`.
354
+ | `min` / `max` / `step` | `0` / `100` / `1` | |
355
+ | `bubble` | `false` | Value bubble above the thumb |
356
+ | `formatter` | `null` | `(value) => string` for bubble and tick labels |
357
+ | `ticks` | `[]` | `[0, 50, 100]` or `[{ value, label }]` |
712
358
 
713
- Since this wraps Vue-Leaflet, any Leaflet/Vue-Leaflet feature (custom tile providers, zoom controls, extra layers) can be added by following [Vue-Leaflet's documentation](https://vue-leaflet.github.io/vue-leaflet/) and extending the component's `<l-map>` template if you need more than a single draggable marker.
359
+ Value: `number`. Events: `update:modelValue`, `change`, `input`, `focus`, `blur`. `RangeSliderInput` is an alias.
714
360
 
715
- ### FileInput
361
+ ### Other inputs
716
362
 
717
- Single/multiple file uploader using `axios` (global `window.axios`) with a per-file progress bar, retry, and delete.
363
+ | Component | Highlights |
364
+ |---|---|
365
+ | `StarRatingInput` | `maxStars`, `precision` (steps per star), `showValue`, `clearable`, `value`; pointer + keyboard, RTL aware, `role="slider"` |
366
+ | `CaptchaInput` | `url`, `placeholder`, `refreshOnSubmit`, `imageWidth`, `imageHeight`; event `refresh` |
367
+ | `PersianDatePickerInput` | `calendar`, `view`, `min`, `max`, `range`, `format`, `inputFormat`, `pickerOptions`, `inputClass`; events `open`, `close` |
368
+ | `EditorInput` | TinyMCE: `options`, `height`, `placeholder`, `apiKey`, `tinymceScriptSrc`; event `init` |
369
+ | `LocationInput` | Leaflet: `center`, `zoom`, `height`, `tileUrl`, `attribution`; value `{ lat, lng }`; event `ready` |
370
+ | `FileInput` (`SimpleUploader`) | `multiple`, `endpoint`, `accept`, `headers`, `fieldName`; events `added`, `progress`, `uploaded`, `error`, `removed` |
371
+ | `DropzoneInput` | `multiple`, `url`, `options`, `headers`; events `ready`, `added`, `success`, `error`, `removed`, `complete` |
372
+ | `UppyInput` | `multiple`, `useXHR`, `XHRConfig`, `url`, `config`, `errorHandler`, `showRestrictionCaption`; all Uppy events are re-emitted |
718
373
 
719
- ```vue
720
- <FileInput name="avatar" endpoint="/upload" />
721
- <FileInput name="gallery" endpoint="/upload" multiple />
722
- ```
374
+ ---
723
375
 
724
- | Prop | Type | Default |
725
- |---|---|---|
726
- | `name` | String | required |
727
- | `multiple` | Boolean | `false` |
728
- | `endpoint` | String | `'/upload'` |
376
+ ## Upload endpoint contract
729
377
 
730
- Event: `update:modelValue`. See [Upload Endpoint Contract](#upload-endpoint-contract-backend).
378
+ `FileInput`, `DropzoneInput` and `UppyInput` talk to the same endpoint shape:
731
379
 
732
- ### SimpleUploader
380
+ * `POST {url}` — multipart field `file` → JSON `{ "data": { "path": "tmp/abc.jpg" } }` (or `{ "path": … }`).
381
+ * `DELETE {url}` — body is the stored `path` (`FileInput`, `DropzoneInput`) or the JSON response (`UppyInput`).
733
382
 
734
- Functionally identical to `FileInput`, but uses the native `fetch` API instead of `axios` — use this one if you don't have `window.axios` configured globally.
383
+ `FileInput` stores the `path` string(s); `DropzoneInput` / `UppyInput` store the response object(s). `form.uploading` holds the upload progress, so `SubmitButton` is disabled until uploads finish.
735
384
 
736
- ```vue
737
- <SimpleUploader name="avatar" endpoint="/upload" />
738
- ```
385
+ ## Internationalisation
739
386
 
740
- | Prop | Type | Default |
741
- |---|---|---|
742
- | `name` | String | required |
743
- | `multiple` | Boolean | `false` |
744
- | `endpoint` | String | `'/upload'` |
387
+ Built-in dictionaries: `en` and `fa`. The default `locale: 'auto'` picks `fa` when the page is RTL (`dir="rtl"` / `lang="fa"`) and `en` otherwise.
388
+ Set the locale globally to avoid any SSR / hydration difference:
745
389
 
746
- ### DropzoneInput
747
-
748
- Drag-and-drop uploader powered by the **external package `dropzone`** (Dropzone.js).
390
+ ```js
391
+ import { configureI18n } from 'inertia-bootstrap-forms';
749
392
 
750
- ```vue
751
- <DropzoneInput name="documents" url="/upload" multiple :options="{ maxFiles: 5 }" />
393
+ configureI18n({ locale: 'fa' });
394
+ configureI18n({ messages: { fa: { 'select.noResults': 'موردی نیست' } } });
752
395
  ```
753
396
 
754
- | Prop | Type | Default |
755
- |---|---|---|
756
- | `name` | String | required |
757
- | `multiple` | Boolean | `false` |
758
- | `url` | String | `'/upload'` |
759
- | `options` | Object | `{}` — merged into Dropzone's own constructor options. |
397
+ Any component with texts also accepts a `locale` prop and explicit text props (`placeholder`, `noResultsText`, …).
760
398
 
761
- Event: `update:modelValue`.
399
+ ## Lifecycle, SSR and cleanup
762
400
 
763
- Since this wraps Dropzone.js directly, any Dropzone option (`acceptedFiles`, `maxFilesize`, `dictDefaultMessage`, thumbnails, custom previews, etc.) can be passed through the `options` prop — see [Dropzone.js's documentation](https://docs.dropzone.dev/) for the full list.
401
+ * Bootstrap plugins are created in `onMounted` through `useBsInstance()` and disposed in `onBeforeUnmount`; re-created automatically when a prop that is only read at construction time changes (`multiple`, `ticks`, `length`, …).
402
+ * Plugins are imported lazily with `import('bootstrap/js/dist/<plugin>.js')` — bundlers code-split them.
403
+ * `useDisposables()` tracks listeners, timers, `AbortController`s, XHRs, Dropzone and Uppy instances and releases them on unmount (also when the component unmounts while a plugin is still loading).
404
+ * Server rendering outputs the static markup; widgets that need the DOM (Combobox, Chips, OTP, Range, Leaflet, date picker, …) start after hydration.
764
405
 
765
- ### UppyInput
406
+ ## Composables
766
407
 
767
- The most feature-rich uploader, powered by the **external package family `@uppy/core`, `@uppy/vue`, `@uppy/xhr-upload`** (the latter two are bundled; `@uppy/audio`, `@uppy/dashboard`, `@uppy/drag-drop`, and `@uppy/locales` are listed as dependencies for you to opt into via the slot below).
768
-
769
- Default usage renders Uppy's built-in `Dropzone` + `FilesList`:
408
+ Build your own fields with the same plumbing:
770
409
 
771
410
  ```vue
772
- <UppyInput
773
- name="attachments"
774
- url="/upload"
775
- multiple
776
- :config="{ restrictions: { maxFileSize: 5 * 1024 * 1024, allowedFileTypes: ['.jpg', '.png', '.pdf'] } }"
777
- />
778
- ```
779
-
780
- | Prop | Type | Default | Description |
781
- |---|---|---|---|
782
- | `name` | String | required | |
783
- | `multiple` | Boolean | `false` | |
784
- | `useXHR` | Boolean | `true` | Automatically registers `@uppy/xhr-upload` pointed at `url`. Set `false` if you want to register your own uploader plugin (e.g. AWS S3, Tus) on the exposed `uppy` instance instead. |
785
- | `XHRConfig` | Object | `{}` | Merged into `@uppy/xhr-upload`'s own config (headers, formData, fieldName, etc.). |
786
- | `url` | String | `'/upload'` | |
787
- | `config` | Object | `{}` | Merged into Uppy's core constructor options (`id`, `autoProceed`, `restrictions`, `locale`, etc.). |
788
- | `errorHandler` | Function | `null` | Custom error display/handling instead of the built-in inline error banner. |
789
- | `showRestrictionCaption` | Boolean | `true` | Shows an auto-generated Persian caption describing the configured file-size/type/count restrictions. |
790
-
791
- Events: `update:modelValue`, `file-added`, `file-removed`, `beforeUpload`, `progress`, `upload`, `upload-progress`, `upload-error`, `upload-success`, `upload-pause`, `complete`, `error`, `upload-retry`, `upload-stalled`, `retry-all`, `cancel-all`, `restriction-failed`.
792
-
793
- Slot: default — scoped with `{ uppy }`, the raw Uppy instance, letting you completely replace the default Dropzone/FilesList UI with your own, or register extra Uppy plugins:
794
-
795
- ```vue
796
- <UppyInput name="cover" url="/upload" v-slot="{ uppy }">
797
- <!-- bring your own plugins, e.g. @uppy/dashboard, @uppy/audio, @uppy/drag-drop -->
798
- </UppyInput>
799
- ```
800
-
801
- Because the file restriction caption, error banners, and the `Dropzone`/`FilesList` building blocks all come straight from Uppy, refer to [Uppy's documentation](https://uppy.io/docs/uppy/) for every available core/plugin option (restrictions, locales, custom plugins like Dashboard/Audio/Webcam/DragDrop, etc.) — anything supported there can be passed through `config`, `XHRConfig`, or registered manually on the exposed `uppy` instance.
802
-
803
- ### SubmitButton
411
+ <script setup>
412
+ import { fieldProps, useField } from 'inertia-bootstrap-forms';
804
413
 
805
- Submit button wired to the injected `form`'s `processing`/`uploading` state, showing a spinner automatically.
414
+ const props = defineProps({ ...fieldProps, color: String });
415
+ const emit = defineEmits(['update:modelValue', 'change']);
416
+ const { model, inputId, isInvalid, isDisabled, emitChange } = useField(props, emit);
417
+ </script>
806
418
 
807
- ```vue
808
- <SubmitButton />
809
- <SubmitButton>Save changes</SubmitButton>
419
+ <template>
420
+ <input type="color" v-model="model" :id="inputId" :disabled="isDisabled" :class="{ 'is-invalid': isInvalid }" @change="emitChange(model, $event)" />
421
+ </template>
810
422
  ```
811
423
 
812
- | Prop | Type | Default |
813
- |---|---|---|
814
- | `disabled` | Boolean | `false` |
815
-
816
- Slots:
817
- - default — scoped with `{ form }`. Defaults to "تایید و ثبت اطلاعات" (Persian for "Confirm and submit").
818
- - `progress` — scoped, defaults to an upload percentage (`form.uploading`%) while a file upload is in progress.
819
-
820
- Must be used inside a `<FormContainer>` (injects `form` directly).
424
+ Also exported: `useFormApi()`, `useDisposables()`, `useBsInstance()`, `configureBootstrap()`, `configureI18n()`, `toEnglishDigits()`, `countryCodes`.
821
425
 
822
426
  ---
823
427
 
824
- ## Upload Endpoint Contract (backend)
428
+ ## Migrating from v2
825
429
 
826
- `FileInput`, `SimpleUploader`, `DropzoneInput`, and `UppyInput` all expect a similar JSON contract from your backend:
827
-
828
- - **Upload (`POST` to the configured endpoint/url):** respond with JSON describing the stored file. At minimum a `path` field, e.g. `{ "path": "uploads/abc123.jpg" }`. (`DropzoneInput` also accepts the raw response if there's no `.data` wrapper; `UppyInput` reads from `response.body`.)
829
- - **Delete (`DELETE` to the same endpoint/url):** the components send the stored file's data (e.g. `{ "path": "..." }`) as the request body so the backend can remove it from storage.
830
-
831
- The original package README points to a companion Laravel package, **`novinvision/simple-uploader`**, for implementing this endpoint server-side — use it if it's available to you, or implement an equivalent controller.
832
-
833
- ---
834
-
835
- ## Styling & Theming
836
-
837
- All components are plain Bootstrap 5 markup/classes (`.form-control`, `.input-group`, `.form-check`, `.btn`, etc.) plus a handful of custom classes (`.form-check-toggle`, `.star-rating`, `.multi-quantity-input`, `.file-input-uploader`, `.uppy-input-area`, …) shipped in `dist/style.css`. Most custom styling reads Bootstrap CSS variables (`--bs-primary`, `--bs-border-radius`, `--bs-body-bg`, etc.), so if your project already themes Bootstrap via CSS variables, these components will inherit that theme automatically. Persian/Farsi numeral display uses a `.fanum` utility class throughout — make sure your global stylesheet defines the font-feature/font-family rules you want for it (it's used as a styling hook, not generated by this package).
838
-
839
- ---
840
-
841
- ## TypeScript
842
-
843
- Type declarations are provided in `index.d.ts` (most components are typed loosely as `DefineComponent<{}, {}, any>` except for the ones with richer prop/event typing: `CheckboxButtonInput`, `EditorInput`, `UppyInput`, `FormContainer`, `LocationInput`, `Select2Input`, `SubmitButton`, `RangeSliderInput`). Import as usual:
430
+ | v2 | v3 |
431
+ |---|---|
432
+ | Bootstrap 5 classes (`.form-check`, `.btn-primary`, …) | Bootstrap 6 (`.check`, `.btn-solid.theme-primary`, …). `.btn-primary` does not exist in Bootstrap 6: `SubmitButton` now renders `btn-solid theme-primary` by itself — use the `variant` / `theme` props instead of `class="btn btn-primary"` |
433
+ | `import 'inertia-bootstrap-forms/dist/style.css'` | **Removed** — no CSS is shipped. Import Bootstrap 6 + third-party CSS yourself |
434
+ | `Select2Input` (choices.js) | `ComboboxInput`; `Select2Input` remains as an alias. The choices.js `config` object is gone — use the documented props. Option field names are set with `valueKey` / `labelKey` (`label` still works as a legacy alias) |
435
+ | Select value always a string | Select value keeps the option's original type (a numeric `id` stays a number) |
436
+ | `RangeSliderInput` (dual thumb, svelte-range-slider-pips) | `RangeInput` (single thumb, Bootstrap 6). Dual range is not supported by Bootstrap 6 |
437
+ | `OTPInput` default length `5` | default `6`; pass `:length="5"` to keep the old size. Fires `complete`; use `auto-submit` instead of `data-auto-submit-length` |
438
+ | `CheckboxInput` `change` event passes the DOM event | `change(value, event)` for every field |
439
+ | `StarRatingInput` stored `"3.5"` strings | stores numbers |
440
+ | `FormContainer` rendered server messages with `v-html` | text by default, `html-messages` to opt in |
441
+ | `FormContainer` emitted extra helper methods inside the data | emits only the form data |
442
+ | `FileInput` needed `window.axios` | uses `XMLHttpRequest` + `fetch`, forwards the `XSRF-TOKEN` cookie |
443
+ | `AmountInput` / `QuantityInput` needed `maska` | built in (Persian digits supported) |
444
+ | `vue3-bootstrap-components`, `choices.js`, `svelte-range-slider-pips` bundled | removed |
445
+ | `name` was `required` | optional — omit it for standalone `v-model` usage |
446
+ | Peer: Bootstrap 5 CSS | Peer: `bootstrap@^6.0.0-alpha.1`, `@floating-ui/dom` |
447
+
448
+ ## Notes and known limitations
449
+
450
+ * Bootstrap 6 is currently in **alpha**; class names and plugin APIs may still change before the stable release.
451
+ * Bootstrap 6 Combobox creates a `Menu` on the same toggle element, which makes Bootstrap log *"Bootstrap doesn't allow more than one instance per element"* to the console. This message comes from Bootstrap itself; the component works normally.
452
+ * Bootstrap 6's own Datepicker has no Jalali calendar, so `PersianDatePickerInput` keeps `vue3-persian-datetime-picker`. Theme it with CSS variables in your app.
453
+ * `LocationInput` in RTL pages: Leaflet panes need `left: auto; right: 0` — add that rule in your own CSS if the map renders offset.
454
+ * `AmountInput` / `QuantityInput` handle integers.
455
+ * `data-bs-toggle` attributes on the Combobox / Menu toggles are required for Bootstrap's data-API click handling and are rendered by the components.
456
+
457
+ ## Development
844
458
 
845
- ```ts
846
- import { TextInput, FormContainer } from 'inertia-bootstrap-forms';
459
+ ```bash
460
+ npm install
461
+ npm test # vitest + jsdom (components, Bootstrap plugin lifecycle, SSR)
462
+ npm run build # vite library build → dist/
847
463
  ```
848
464
 
849
465
  ---
850
466
 
851
- ## Known Limitations
852
-
853
- A few quirks in the current source worth knowing about before you rely on certain props:
854
-
855
- - **`EmailInput`'s `name` prop:** the component's `props` block doesn't actually declare a `name` prop (it declares an unrelated `FileInput` prop instead), so passing a custom `name="..."` may not bind correctly to the form. If you need a non-default field name for an email field, use `<TextInput name="..." type="email" placeholder="..." />` directly.
856
- - **`TelInput`'s default `name`:** defaults to `'email'` rather than something like `'mobile'`/`'phone'` — always pass an explicit `name` prop to avoid surprises.
857
- - **`Select2Input`'s `key` prop:** intended to let you choose which object field is used as an option's value, but it can't actually be set from a template, since Vue reserves `:key` for its own list-rendering/diffing mechanism. In practice, value resolution always falls back to `id` → `name` → `label` → `value` (or the `label` prop, for the display text).
858
- - **`MultiQuantityInput`'s `totalMax`/`totalMin` props:** declared, but not currently enforced anywhere in the component's logic — treat them as reserved for future use rather than working constraints.
859
- - Several components are typed `DefineComponent<{}, {}, any>` in `index.d.ts`, i.e. without real prop typing — rely on this README (or the `.vue` source) rather than IDE autocomplete for the full prop list of those components.
467
+ Built and maintained by **[Novin Vision](https://novinvision.com)** · MIT license