@fastkit/vue-form-control
Version:
Basic form implementation library for Vue applications.
848 lines (711 loc) ⢠18 kB
Markdown
# @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