@conexusnuclear/lvform
v0.1.2
Published
Modern Vue 3 form library for Laravel backends - drop-in replacement for vform
Maintainers
Readme
lvform
Modern Vue 3 form library for Laravel backends - drop-in replacement for vform
lvform is a lightweight library designed specifically for Vue 3 applications to handle forms and validation when using Laravel as a back-end. It provides proper Vue 3 reactivity, Composition API support, and zero console warnings.
Why lvform?
If you're using vform with Vue 3, you may have noticed console warnings related to:
- ❌ Reactivity issues when using Composition API
- ❌ Element-Plus and other UI library integration warnings
- ❌ Prop mutation warnings with vform components
- ❌ Type check failures with modern Vue 3 components
lvform solves all these issues by being built from the ground up for Vue 3:
- ✅ Zero console warnings - Clean reactive implementation
- ✅ Composition API first - Built with
reactive()andtoRefs() - ✅ Options API compatible - Works with traditional Vue syntax
- ✅ TypeScript native - Full type safety with generics
- ✅ Modern components - Designed for
<script setup> - ✅ Drop-in replacement - Compatible with vform API
Installation
npm install @conexusnuclear/lvformThe package has peer dependencies that you'll also need:
npm install @conexusnuclear/lvform axios vue@^3.0.0Or add to your package.json:
{
"dependencies": {
"@conexusnuclear/lvform": "^0.1.0",
"axios": "^1.7.0",
"vue": "^3.0.0"
}
}Quick Start
Options API (Traditional Vue)
<template>
<form @submit.prevent="login" @keydown="form.onKeydown($event)">
<input v-model="form.username" type="text" name="username" placeholder="Username">
<div v-if="form.errors.has('username')" v-html="form.errors.get('username')" />
<input v-model="form.password" type="password" name="password" placeholder="Password">
<div v-if="form.errors.has('password')" v-html="form.errors.get('password')" />
<button type="submit" :disabled="form.busy">
Log In
</button>
</form>
</template>
<script>
import { Form } from '@conexusnuclear/lvform'
export default {
data() {
return {
form: new Form({
username: '',
password: ''
})
}
},
methods: {
async login() {
const response = await this.form.post('/api/login')
// Handle successful login
console.log(response.data)
}
}
}
</script>Composition API (Modern Vue 3)
<template>
<form @submit.prevent="login">
<input v-model="username" type="text" placeholder="Username">
<div v-if="errors.has('username')" v-text="errors.get('username')" />
<input v-model="password" type="password" placeholder="Password">
<div v-if="errors.has('password')" v-text="errors.get('password')" />
<button type="submit" :disabled="busy">
Log In
</button>
</form>
</template>
<script setup>
import { useForm } from '@conexusnuclear/lvform'
const { username, password, busy, errors, post } = useForm({
username: '',
password: ''
})
async function login() {
const response = await post('/api/login')
// Handle successful login
console.log(response.data)
}
</script>Laravel Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
public function login(Request $request)
{
$this->validate($request, [
'username' => 'required|string',
'password' => 'required|string|min:6',
]);
// Your authentication logic here
// ...
return response()->json([
'message' => 'Login successful',
'user' => auth()->user()
]);
}
}API Reference
Form Class
Creating an Instance
import { Form } from '@conexusnuclear/lvform'
const form = new Form({
email: '',
name: '',
age: null
})
// Or with Form.make()
const form = Form.make({
email: '',
name: ''
})Instance Properties
/**
* Indicates if the form is being submitted to the server.
*/
form.busy: boolean
/**
* Indicates if the last submission was successful.
*/
form.successful: boolean
/**
* Indicates if the form was recently successful (for 2 seconds).
*/
form.recentlySuccessful: boolean
/**
* The validation errors from the server.
*/
form.errors: Errors
/**
* The upload progress information.
*/
form.progress: { total: number, loaded: number, percentage: number } | undefined
/**
* Computed property indicating if there are any validation errors.
*/
form.hasErrors: boolean
/**
* Access to form data
*/
form.data: T // Your form data typeInstance Methods
/**
* Submit the form via HTTP request
*/
form.submit(method: string, url: string, config?: AxiosRequestConfig)
form.post(url: string, config?: AxiosRequestConfig)
form.put(url: string, config?: AxiosRequestConfig)
form.patch(url: string, config?: AxiosRequestConfig)
form.delete(url: string, config?: AxiosRequestConfig)
form.get(url: string, config?: AxiosRequestConfig)
/**
* Clear all form errors
*/
form.clear()
/**
* Reset form data to original values
*/
form.reset()
/**
* Update form data (partial update)
*/
form.update(data: Partial<T>)
/**
* Fill form data (complete replacement)
*/
form.fill(data: T)
/**
* Clear error for a field on keydown
*/
form.onKeydown(event: KeyboardEvent)Setting a Custom Axios Instance
import axios from 'axios'
import { Form } from '@conexusnuclear/lvform'
const instance = axios.create({
baseURL: 'https://api.example.com',
headers: {
'Authorization': `Bearer ${token}`
}
})
Form.axios = instanceErrors Class
const form = new Form({})
const errors = form.errorsInstance Methods
/**
* Get all errors as an object
*/
errors.all(): Record<string, string[]>
/**
* Check if a field has an error
*/
errors.has(field: string): boolean
/**
* Check if any of the given fields have errors
*/
errors.hasAny(...fields: string[]): boolean
/**
* Check if there are any errors
*/
errors.any(): boolean
/**
* Get the first error message for a field
*/
errors.get(field: string): string | undefined
/**
* Get all error messages for a field
*/
errors.getAll(field: string): string[]
/**
* Get errors only for specific fields
*/
errors.only(...fields: string[]): string[]
/**
* Get all errors in a flat array
*/
errors.flatten(): string[]
/**
* Clear error(s) - clears all if no field specified
*/
errors.clear(field?: string)
/**
* Set errors from server response
*/
errors.set(errors: Record<string, string | string[]>)
/**
* Set a specific field error
*/
errors.set(field: string, message: string)useForm Composable
The useForm composable provides a modern Composition API interface:
<script setup>
import { useForm } from '@conexusnuclear/lvform'
const form = useForm({
email: '',
name: '',
age: null
})
// Destructure for convenience
const { email, name, age, busy, errors, successful, post, reset } = form
async function submit() {
const response = await post('/api/users')
// Handle response
}
</script>
<template>
<form @submit.prevent="submit">
<!-- Direct v-model binding with .value -->
<input v-model="email.value" type="email">
<!-- Or use the form object -->
<input v-model="form.email.value" type="text">
<!-- Access errors -->
<div v-if="errors.has('email')">{{ errors.get('email') }}</div>
<!-- Check busy state -->
<button :disabled="busy.value">Submit</button>
</form>
</template>Important: When using the destructured properties, remember to use .value for reactive values:
const { email, busy } = useForm({ email: '' })
// Correct
console.log(email.value)
if (busy.value) { }
// Incorrect
console.log(email) // This is a Ref object, not the valueAdvanced Usage
File Uploads
<template>
<form @submit.prevent="submit" @keydown="form.onKeydown($event)">
<input v-model="form.name" type="text" name="name">
<div v-if="form.errors.has('name')" v-text="form.errors.get('name')" />
<input type="file" name="file" @change="handleFile">
<div v-if="form.errors.has('file')" v-text="form.errors.get('file')" />
<div v-if="form.progress">
Progress: {{ form.progress.percentage }}%
</div>
<button type="submit" :disabled="form.busy">Upload</button>
</form>
</template>
<script setup>
import { Form } from '@conexusnuclear/lvform'
import { ref } from 'vue'
const form = ref(new Form({
name: '',
file: null
}))
function handleFile(event) {
const file = event.target.files[0]
form.value.file = file
}
async function submit() {
const response = await form.value.post('/api/upload')
// File uploaded successfully
}
</script>With Element-Plus
<template>
<el-form @submit.prevent="submit">
<el-form-item label="Email">
<el-input v-model="email.value" type="email" />
<div v-if="errors.has('email')" class="error">
{{ errors.get('email') }}
</div>
</el-form-item>
<el-form-item label="Date">
<el-date-picker v-model="eventDate.value" />
<div v-if="errors.has('event_date')" class="error">
{{ errors.get('event_date') }}
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit" :loading="busy.value">
Submit
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { useForm } from '@conexusnuclear/lvform'
const { email, eventDate, busy, errors, post } = useForm({
email: '',
eventDate: new Date()
})
async function submit() {
await post('/api/events')
}
</script>TypeScript Support
lvform is written in TypeScript and provides full type safety:
interface UserForm {
email: string
name: string
age: number | null
avatar: File | null
}
const form = new Form<UserForm>({
email: '',
name: '',
age: null,
avatar: null
})
// TypeScript knows the shape of form.data
form.data.email // string
form.data.age // number | null
// With useForm
const { email, name } = useForm<UserForm>({
email: '',
name: '',
age: null,
avatar: null
})
email.value = '[email protected]' // ✅ Type-safe
email.value = 123 // ❌ TypeScript errorError Handling
<script setup>
import { useForm } from '@conexusnuclear/lvform'
const form = useForm({
email: '',
password: ''
})
async function login() {
try {
const response = await form.post('/api/login')
// Success - redirect or show message
router.push('/dashboard')
} catch (error) {
// Laravel validation errors are automatically set on form.errors
if (error.response?.status === 422) {
// Validation failed - errors are already available
console.log(form.errors.all())
} else {
// Other errors (500, network, etc.)
console.error('Login failed:', error)
}
}
}
</script>Multiple File Upload
const form = new Form({
title: '',
files: [] as File[]
})
function handleFiles(event: Event) {
const target = event.target as HTMLInputElement
if (target.files) {
form.files = Array.from(target.files)
}
}
await form.post('/api/documents')Custom Axios Configuration
const form = new Form({ email: '' })
// Per-request configuration
await form.post('/api/users', {
headers: {
'X-Custom-Header': 'value'
},
timeout: 5000
})
// Upload progress tracking
await form.post('/api/upload', {
onUploadProgress: (progressEvent) => {
console.log('Upload progress:', form.progress)
}
})Migration from vform
If you're migrating from vform to lvform, the process is straightforward:
1. Update imports
- import Form from '@conexusnuclear/lvform'
+ import { Form } from '@conexusnuclear/lvform'
- import { useForm } from '@conexusnuclear/lvform' // if vform had this
+ import { useForm } from '@conexusnuclear/lvform'2. Update component imports (if using)
- import { Button, HasError } from 'vform/src/components/bootstrap5'
+ import { Button, HasError } from 'lvform/components/bootstrap5'3. Composition API changes (if applicable)
With vform, you might have wrapped the Form in reactive():
// ❌ Old vform pattern (causes warnings)
const form = reactive(new Form({ ... }))With lvform, use the Form directly or use the useForm composable:
// ✅ Option 1: Use Form directly in a ref
const form = ref(new Form({ ... }))
// ✅ Option 2: Use useForm composable (recommended)
const form = useForm({ ... })Breaking Changes
There are minimal breaking changes:
Named export:
Formis now a named export instead of default- import Form from 'lvform' + import { Form } from '@conexusnuclear/lvform'useForm returns refs: In Composition API, form fields are returned as refs
const { email } = useForm({ email: '' }) console.log(email.value) // Use .value to access
Everything else remains API-compatible with vform!
Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Vue 3.0+
- No IE11 support (use vform if you need Vue 2/IE11)
Development
# Install dependencies
npm install
# Run tests
npm test
# Build the library
npm run build
# Type check
npm run type-checkLicense
MIT
Credits
lvform is inspired by and maintains API compatibility with vform by @cretueusebiu.
Built specifically for Vue 3 with modern reactive patterns and TypeScript support.
