UNPKG

@fastkit/vue-form-control

Version:

Basic form implementation library for Vue applications.

848 lines (711 loc) • 18 kB
# @fastkit/vue-form-control 🌐 English | [ę—„ęœ¬čŖž](https://github.com/dadajam4/fastkit/blob/main/packages/vue-form-control/README-ja.md) A fundamental form implementation library for Vue applications. Provides validation, masked input, selector functionality, and more. ## Features - **Full Composition API Support**: Leverages the latest Vue 3 features - **Type Safety**: Complete TypeScript support for type-safe form development - **Validation Integration**: Comprehensive validation through @fastkit/rules integration - **IMask Integration**: Advanced masked input functionality (imask library integration) - **Selector Functionality**: Radio button, checkbox, and select box integration - **Auto-sizing**: Dynamic textarea with VTextareaAutosize - **Real-time Validation**: Real-time validation during input - **Customizable**: Flexible control extension and customization ## Installation ```bash npm install @fastkit/vue-form-control # or pnpm add @fastkit/vue-form-control ``` ## Basic Usage ### Plugin Setup ```typescript import { createApp } from 'vue'; import { installVueFormPlugin } from '@fastkit/vue-form-control'; const app = createApp(App); // Install form plugin installVueFormPlugin(app, { // Option settings }); ``` ### Simple Form ```vue <template> <form @submit="form.submit"> <!-- Text input --> <input v-model="form.name.value" :class="{ error: form.name.invalid }" @blur="form.name.validate()" /> <div v-if="form.name.invalid">{{ form.name.errorMessage }}</div> <!-- Number input --> <input type="number" v-model.number="form.age.value" :class="{ error: form.age.invalid }" /> <!-- Select --> <select v-model="form.category.value"> <option value="">Please select</option> <option value="a">Category A</option> <option value="b">Category B</option> </select> <button type="submit" :disabled="form.invalid">Submit</button> </form> </template> <script setup lang="ts"> import { useForm } from '@fastkit/vue-form-control'; import { required, minLength, between } from '@fastkit/rules'; // Form definition const form = useForm({ name: { value: '', rules: [required(), minLength(2)] }, age: { value: 0, rules: [required(), between(0, 120)] }, category: { value: '', rules: [required()] } }, { onSubmit: async (values) => { console.log('Submit:', values); // API submission processing await api.submit(values); } }); </script> ``` ### Text Input Control ```vue <template> <div> <!-- Basic text input --> <input v-model="textControl.value" @input="textControl.handleInput" @blur="textControl.handleBlur" /> <!-- Masked input --> <input v-model="phoneControl.value" v-imask="phoneControl.maskOptions" @input="phoneControl.handleMaskInput" /> <!-- Validation display --> <div v-if="textControl.invalid" class="error"> {{ textControl.errorMessage }} </div> </div> </template> <script setup lang="ts"> import { useTextInputNodeControl } from '@fastkit/vue-form-control'; import { required, pattern } from '@fastkit/rules'; // Basic text input const textControl = useTextInputNodeControl({ value: '', rules: [required(), pattern(/^[a-zA-Z]+$/)] }); // Phone number masked input const phoneControl = useTextInputNodeControl({ value: '', mask: '000-0000-0000', rules: [required()] }); </script> ``` ### Selector Control ```vue <template> <div> <!-- Radio buttons --> <div v-for="item in radioItems" :key="item.value"> <label> <input type="radio" :value="item.value" v-model="radioControl.value" @change="radioControl.handleChange" /> {{ item.label }} </label> </div> <!-- Checkboxes (multiple selection) --> <div v-for="item in checkboxItems" :key="item.value"> <label> <input type="checkbox" :value="item.value" v-model="checkboxControl.value" @change="checkboxControl.handleChange" /> {{ item.label }} </label> </div> </div> </template> <script setup lang="ts"> import { useFormSelectorControl } from '@fastkit/vue-form-control'; import { required } from '@fastkit/rules'; // Radio button control const radioControl = useFormSelectorControl({ value: '', items: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, { value: 'option3', label: 'Option 3' } ], rules: [required()] }); // Checkbox control (multiple selection) const checkboxControl = useFormSelectorControl({ value: [] as string[], multiple: true, items: [ { value: 'item1', label: 'Item 1' }, { value: 'item2', label: 'Item 2' }, { value: 'item3', label: 'Item 3' } ] }); </script> ``` ### VTextareaAutosize Component A textarea component with auto-sizing functionality. The height dynamically adjusts based on the content. ```vue <template> <div> <!-- Basic auto-sizing textarea --> <VTextareaAutosize v-model="content" placeholder="Please enter your message..." :min-rows="3" :max-rows="10" @input="handleInput" @focus="handleFocus" @blur="handleBlur" /> <!-- Example with properties --> <VTextareaAutosize v-model="comment" name="comment" autocomplete="off" :maxlength="500" :readonly="isReadonly" :disabled="isDisabled" placeholder="Enter comment..." :min-rows="2" :max-rows="8" class="custom-textarea" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { VTextareaAutosize } from '@fastkit/vue-form-control'; const content = ref(''); const comment = ref(''); const isReadonly = ref(false); const isDisabled = ref(false); const handleInput = (event: Event) => { console.log('Input received:', (event.target as HTMLTextAreaElement).value); }; const handleFocus = (event: FocusEvent) => { console.log('Focused'); }; const handleBlur = (event: FocusEvent) => { console.log('Focus lost'); }; </script> <style scoped> .custom-textarea { width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 8px; font-family: inherit; font-size: 14px; line-height: 1.5; resize: none; transition: border-color 0.2s ease; } .custom-textarea:focus { border-color: #007bff; outline: none; } </style> ``` #### Advanced Usage Examples for VTextareaAutosize ```vue <template> <div> <!-- Chat message input --> <div class="chat-input-container"> <VTextareaAutosize ref="chatInput" v-model="message" placeholder="Enter message... (Shift+Enter for new line, Enter to send)" :min-rows="1" :max-rows="5" class="chat-input" @keydown="handleKeydown" @input="handleInput" /> <button :disabled="!message.trim()" @click="sendMessage" class="send-button" > Send </button> </div> <!-- Dynamic form field --> <div class="form-field"> <label for="description">Description</label> <VTextareaAutosize id="description" v-model="description" :min-rows="2" :max-rows="15" :maxlength="1000" placeholder="Please enter detailed product description..." class="description-input" /> <div class="char-count"> {{ description.length }}/1000 characters </div> </div> <!-- JSON editor style --> <div class="json-editor"> <label>JSON Settings</label> <VTextareaAutosize v-model="jsonConfig" :min-rows="5" :max-rows="20" placeholder="Enter settings in JSON format..." class="json-input" autocomplete="off" spellcheck="false" /> <div v-if="jsonError" class="error"> {{ jsonError }} </div> <div v-else class="success"> āœ“ Valid JSON format </div> </div> </div> </template> <script setup lang="ts"> import { ref, computed, watch } from 'vue'; import { VTextareaAutosize, type VTextareaAutosizeRef } from '@fastkit/vue-form-control'; const chatInput = ref<VTextareaAutosizeRef>(); const message = ref(''); const description = ref(''); const jsonConfig = ref('{\n "theme": "dark",\n "autoSave": true\n}'); // JSON validation const jsonError = computed(() => { if (!jsonConfig.value.trim()) return null; try { JSON.parse(jsonConfig.value); return null; } catch (error) { return `JSON syntax error: ${(error as Error).message}`; } }); // Chat input keyboard handling const handleKeydown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); } }; const handleInput = (event: Event) => { // Processing during input (e.g., updating typing indicator) console.log('Typing...'); }; const sendMessage = () => { if (!message.value.trim()) return; // Message sending process console.log('Send message:', message.value); // Clear input field message.value = ''; // Return focus chatInput.value?.focus(); }; // Auto-format JSON settings (optional) const formatJson = () => { try { const parsed = JSON.parse(jsonConfig.value); jsonConfig.value = JSON.stringify(parsed, null, 2); } catch (error) { // Do nothing on error } }; // Monitor description character count watch(description, (newValue) => { if (newValue.length > 1000) { description.value = newValue.substring(0, 1000); } }); </script> <style scoped> .chat-input-container { display: flex; gap: 8px; align-items: flex-end; padding: 16px; border: 1px solid #e1e5e9; border-radius: 8px; background: #f8f9fa; } .chat-input { flex: 1; padding: 8px 12px; border: 1px solid #ced4da; border-radius: 6px; font-family: inherit; font-size: 14px; line-height: 1.4; resize: none; } .send-button { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; } .send-button:disabled { background: #6c757d; cursor: not-allowed; } .form-field { margin-bottom: 16px; } .form-field label { display: block; margin-bottom: 4px; font-weight: 500; } .description-input { width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; font-family: inherit; font-size: 14px; line-height: 1.5; resize: none; } .char-count { text-align: right; font-size: 12px; color: #6c757d; margin-top: 4px; } .json-editor { margin-top: 24px; } .json-editor label { display: block; margin-bottom: 8px; font-weight: 500; } .json-input { width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; line-height: 1.4; resize: none; background: #f8f9fa; } .error { color: #dc3545; font-size: 12px; margin-top: 4px; } .success { color: #28a745; font-size: 12px; margin-top: 4px; } </style> ``` ## Advanced Usage Examples ### Custom Validation ```typescript import { useTextInputNodeControl } from '@fastkit/vue-form-control'; import { required } from '@fastkit/rules'; // Custom validation function const uniqueEmail = (value: string) => { return new Promise((resolve) => { // Check email uniqueness via API call api.checkEmailUnique(value).then(isUnique => { resolve(isUnique ? true : 'This email address is already in use'); }); }); }; const emailControl = useTextInputNodeControl({ value: '', rules: [ required(), uniqueEmail // Asynchronous validation ], validateTiming: ['blur', 'change'] // Validation timing }); ``` ### Masked Input with IMask ```typescript import { useTextInputNodeControl } from '@fastkit/vue-form-control'; import IMask from 'imask'; // Date mask const dateControl = useTextInputNodeControl({ value: '', mask: { mask: Date, pattern: 'YYYY-MM-DD', format: (date: Date) => date.toISOString().slice(0, 10), parse: (str: string) => new Date(str) } }); // Currency mask const currencyControl = useTextInputNodeControl({ value: '', mask: { mask: Number, scale: 2, signed: false, thousandsSeparator: ',', padFractionalZeros: true, radix: '.' } }); // Postal code mask const zipControl = useTextInputNodeControl({ value: '', mask: '000-0000' }); ``` ### Form Groups ```vue <script setup lang="ts"> import { useFormGroup } from '@fastkit/vue-form-control'; import { required, email } from '@fastkit/rules'; // Nested form structure const userForm = useFormGroup({ personal: useFormGroup({ firstName: useTextInputNodeControl({ value: '', rules: [required()] }), lastName: useTextInputNodeControl({ value: '', rules: [required()] }), email: useTextInputNodeControl({ value: '', rules: [required(), email()] }) }), preferences: useFormGroup({ newsletter: useFormSelectorControl({ value: false, items: [ { value: true, label: 'Subscribe' }, { value: false, label: 'Do not subscribe' } ] }), categories: useFormSelectorControl({ value: [] as string[], multiple: true, items: [ { value: 'tech', label: 'Technology' }, { value: 'business', label: 'Business' }, { value: 'lifestyle', label: 'Lifestyle' } ] }) }) }); // Get entire form values const formValues = computed(() => userForm.value); // Validate entire form const isFormValid = computed(() => userForm.valid); </script> ``` ## API ### useForm Composable for managing the entire form. ```typescript const form = useForm(fields, options); ``` **Parameters:** - `fields`: Field definition object - `options`: Form options **Return Value:** - Form control instance ### useTextInputNodeControl Composable for text input control. ```typescript const control = useTextInputNodeControl({ value: string, rules?: ValidationRule[], mask?: IMaskOptions, finalizers?: TextFinalizer[], validateTiming?: ValidateTiming[] }); ``` ### useFormSelectorControl Composable for selector control. ```typescript const control = useFormSelectorControl({ value: any, items: SelectorItem[], multiple?: boolean, rules?: ValidationRule[] }); ``` ### useFormGroup Composable for form group control. ```typescript const group = useFormGroup(controls); ``` ### VTextareaAutosize Textarea component with auto-sizing functionality. ```typescript interface VTextareaAutosizeRef { value: string; focus(opts?: FocusOptions): void; blur(): void; } // Properties interface VTextareaAutosizeProps { modelValue?: string; // v-model value minRows?: number | string; // Minimum rows (default: 1) maxRows?: number | string; // Maximum rows autocomplete?: string; // Autocomplete autofocus?: boolean; // Auto focus disabled?: boolean; // Disabled state readonly?: boolean; // Read-only required?: boolean; // Required field name?: string; // Field name placeholder?: string; // Placeholder maxlength?: number | string; // Maximum character count minlength?: number | string; // Minimum character count form?: string; // Associated form ID } // Events interface VTextareaAutosizeEmits { 'update:modelValue': (value: string) => void; // v-model update input: (event: Event) => void; // Input event focus: (event: FocusEvent) => void; // Focus event blur: (event: FocusEvent) => void; // Blur event } ``` **Features:** - Automatic height adjustment based on content - Minimum and maximum row limits - Responsive support with ResizeObserver - High performance with debounce processing - Infinite rendering prevention - Standard HTML textarea attribute support ## Validation ### Basic Rules (@fastkit/rules integration) ```typescript import { required, email, minLength, maxLength, pattern, between } from '@fastkit/rules'; const control = useTextInputNodeControl({ value: '', rules: [ required(), email(), minLength(5), maxLength(100), pattern(/^[a-zA-Z0-9]+$/), between(1, 999) ] }); ``` ### Custom Rules ```typescript const customRule = (value: any) => { if (value !== 'expected') { return 'Not the expected value'; } return true; }; const control = useTextInputNodeControl({ value: '', rules: [required(), customRule] }); ``` ## IMask Integration ### Basic Masks ```typescript // Phone number mask: '000-0000-0000' // Postal code mask: '000-0000' // Credit card mask: '0000 0000 0000 0000' ``` ### Advanced Masks ```typescript // Date mask: { mask: Date, pattern: 'YYYY-MM-DD', blocks: { YYYY: { mask: '0000' }, MM: { mask: '00' }, DD: { mask: '00' } } } // Number mask: { mask: Number, scale: 2, thousandsSeparator: ',', padFractionalZeros: true } ``` ## Text Finalizers ```typescript import { useTextInputNodeControl, BUILTIN_TEXT_FINALIZERS } from '@fastkit/vue-form-control'; const control = useTextInputNodeControl({ value: '', finalizers: [ BUILTIN_TEXT_FINALIZERS.trim, // Remove leading/trailing whitespace BUILTIN_TEXT_FINALIZERS.upper, // Convert to uppercase BUILTIN_TEXT_FINALIZERS.removeSpace, // Remove all whitespace // Custom finalizer (value) => value.replace(/[^\w]/g, '') ] }); ``` ## Dependencies - `vue`: ^3.5.0 (Peer Dependency) - `@fastkit/rules`: Validation rules - `@fastkit/helpers`: Helper utilities - `@fastkit/vue-utils`: Vue utilities - `imask`: Masked input library ## Documentation For detailed documentation, please see [here](https://dadajam4.github.io/fastkit/vue-form-control/). ## License MIT