Integration: Alpine.js
Alpine.js is a rugged, minimal framework for composing JavaScript behavior in your markup. Its declarative nature and ability to manipulate the DOM make it a perfect partner for Ctrovalidate, especially for creating dynamic forms.
The Strategy
The key to a successful integration is to use Ctrovalidate's programmatic API (addField and removeField) to keep the validator synchronized with the DOM changes made by Alpine.
- Initialize Ctrovalidate Once: Create a single validator instance for your form.
- Register Alpine Component: Use the
alpine:initevent to safely register your component data. This is the recommended pattern. - Call
addField: When your Alpine component adds a new form field to the DOM, use$nextTickto wait for the DOM to update, then callvalidator.addField()on the new element(s). - Call
removeField: Before your Alpine component removes a field, callvalidator.removeField()on the element(s) to be removed.
Example: Dynamic Invoice Form
This example shows how to build a form where users can dynamically add and remove invoice items.
1. HTML Structure with Alpine Directives
The form is controlled by an Alpine component defined in x-data="invoice". We use x-for to loop over an items array and render the fields.
<form
id="invoice-form"
x-data="invoice"
@submit.prevent="submitForm"
novalidate
>
<header>
<button type="button" @click="addItem">Add Item</button>
</header>
<template x-for="(item, index) in items" :key="item.id">
<div class="item-row">
<div>
<label :for="`description-${item.id}`">Description</label>
<input
type="text"
:id="`description-${item.id}`"
data-ctrovalidate-rules="required|minLength:5"
/>
</div>
<div>
<button type="button" @click="removeItem(index)">Remove</button>
</div>
</div>
</template>
<button type="submit">Submit Invoice</button>
</form>2. JavaScript Initialization
In our script, we initialize Ctrovalidate and then register our Alpine component, which contains the core logic.
import { Ctrovalidate } from 'ctrovalidate';
import Alpine from 'alpinejs'; // Assuming ESM import
const form = document.getElementById('invoice-form');
const validator = new Ctrovalidate(form, { realTime: true });
document.addEventListener('alpine:init', () => {
Alpine.data('invoice', () => ({
items: [{ id: 1 }],
nextId: 2,
addItem() {
this.items.push({ id: this.nextId++ });
// Use $nextTick to wait for the DOM to update
this.$nextTick(() => {
const newId = this.items[this.items.length - 1].id;
const newField = form.querySelector(`#description-${newId}`);
// Tell Ctrovalidate about the new field
validator.addField(newField);
});
},
removeItem(index) {
const itemToRemove = this.items[index];
const fieldToRemove = form.querySelector(
`#description-${itemToRemove.id}`
);
// Tell Ctrovalidate to forget the field *before* removing it
validator.removeField(fieldToRemove);
this.items.splice(index, 1);
},
async submitForm() {
const isFormValid = await validator.validate();
if (isFormValid) {
alert('Invoice is valid!');
}
},
}));
});
Alpine.start();This pattern ensures that Ctrovalidate's internal tracking of fields is always perfectly in sync with the dynamic DOM managed by Alpine.js.