Build Your Own Wrapper Components
EncolaJS provides a powerful way to create custom wrapper components that leverage the headless field functionality while giving you complete control over the UI and behavior. This guide will walk you through creating your own wrapper components.
Overview
Wrapper components are Vue components that wrap the headless field components (HeadlessField, HeadlessRepeatable, etc.) to provide a consistent UI and behavior across your application. They help reduce boilerplate code and ensure consistency in your form implementation.
Basic Wrapper Component Example
Here's a basic example of a wrapper component that wraps HeadlessField:
<template>
<div v-bind="$attrs">
<HeadlessField :name="name">
<template #default="{ value, attrs, error, events, id }">
<label v-if="label" class="block" :for="id">{{ label }}</label>
<slot
:value="value"
:attrs="attrs"
:error="error"
:events="events"
:id="id"
/>
<div v-if="error"
:id="attrs['aria-errormessage']"
class="text-red-500">
{{ error }}
</div>
</template>
</HeadlessField>
</div>
</template>
<script setup>
import { HeadlessField } from '@encolajs/enforma'
defineProps({
name: {
type: String,
required: true
},
label: {
type: String,
default: null
}
})
</script>Implementing Base Components
When implementing base components, consider these key aspects:
Field State Management: The headless components provide field state including:
value: The current field valueerror: Any validation errorsisDirty: Whether the field has been modifiedisTouched: Whether the field has been focusedisValidating: Whether validation is in progressisFocused: Whether the field is currently focused
Event Handling: The headless components provide event handlers:
input: For immediate value changeschange: For committed value changesblur: When the field loses focusfocus: When the field gains focus
Accessibility: The headless components provide accessibility attributes:
aria-invalid: For error statearia-errormessage: For error message associationid: For label association
Managing State and Validation
The headless components handle state management and validation automatically:
<template>
<HeadlessField :name="name" :validate-on="validateOn">
<template #default="{ value, validate, error }">
<input
:value="value"
@blur="validate"
/>
<div v-if="error">{{ error }}</div>
</template>
</HeadlessField>
</template>
<script setup>
import { HeadlessField } from '@encolajs/enforma'
defineProps({
name: String,
validateOn: {
type: String,
default: 'blur',
validator: (value) => ['input', 'change', 'blur', 'submit'].includes(value)
}
})
</script>Implementing Custom Behavior
You can extend the base functionality with custom behavior:
<template>
<HeadlessField :name="name">
<template #default="{ value, attrs, events }">
<div class="custom-field">
<input
v-bind="attrs"
v-on="events"
:value="value"
@input="handleCustomInput"
/>
<div class="custom-indicator" v-if="showIndicator">
Custom indicator
</div>
</div>
</template>
</HeadlessField>
</template>
<script setup>
import { ref } from 'vue'
import { HeadlessField } from '@encolajs/enforma'
const props = defineProps({
name: String
})
const showIndicator = ref(false)
function handleCustomInput(event) {
// Custom input handling
showIndicator.value = event.target.value.length > 0
}
</script>Best Practices for Component Design
Composition: Use slots to allow customization of the field content while maintaining consistent structure.
Props Design:
- Keep required props minimal
- Provide sensible defaults
- Use TypeScript for better type safety
Accessibility:
- Always bind the provided accessibility attributes
- Maintain proper label associations
- Handle error states appropriately
Styling:
- Use CSS classes for styling
- Allow style customization through props
- Support theme customization
Error Handling:
- Display validation errors clearly
- Provide clear error messages
- Handle edge cases gracefully
Advanced Usage Examples
Custom Repeatable Field Wrapper
<template>
<HeadlessRepeatable
:name="name"
:min="min"
:max="max"
:validate-on-add="validateOnAdd"
:validate-on-remove="validateOnRemove"
>
<template #default="{ value, add, remove, canAdd, moveUp, moveDown, count }">
<div class="repeatable-field">
<div v-for="(item, index) in value" :key="index" class="repeatable-item">
<slot :item="item" :index="index" />
<div class="item-actions">
<button @click="moveUp(index)" :disabled="index === 0">↑</button>
<button @click="moveDown(index)" :disabled="index === count - 1">↓</button>
<button @click="remove(index)">Remove</button>
</div>
</div>
<button v-if="canAdd" @click="add">Add Item</button>
</div>
</template>
</HeadlessRepeatable>
</template>
<script setup>
import { HeadlessRepeatable } from '@encolajs/enforma'
defineProps({
name: String,
min: {
type: Number,
default: 0
},
max: Number,
validateOnAdd: {
type: Boolean,
default: true
},
validateOnRemove: {
type: Boolean,
default: true
}
})
</script>Custom Table Field Wrapper
<template>
<HeadlessRepeatable :name="name">
<template #default="{ value, add, remove, canAdd }">
<table class="custom-table">
<thead>
<tr>
<th v-for="header in headers" :key="header">{{ header }}</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in value" :key="index">
<td v-for="header in headers" :key="header">
<slot :name="header" :value="row[header]" :row="row" :index="index" />
</td>
<td>
<button @click="remove(index)">Remove</button>
</td>
</tr>
</tbody>
</table>
<button v-if="canAdd" @click="add">Add Row</button>
</template>
</HeadlessRepeatable>
</template>
<script setup>
import { HeadlessRepeatable } from '@encolajs/enforma'
defineProps({
name: String,
headers: {
type: Array,
required: true
}
})
</script>Integration with Form Configuration
Wrapper components can integrate with the form's configuration system:
<template>
<HeadlessField :name="name">
<template #default="{ value, attrs, error }">
<div v-bind="getConfig('pt.wrapper')">
<label v-bind="getConfig('pt.label')">{{ label }}</label>
<input
v-bind="attrs"
v-bind="getConfig('pt.input')"
:class="{ 'is-invalid': error }"
/>
<div v-if="error" v-bind="getConfig('pt.error')">
{{ error }}
</div>
</div>
</template>
</HeadlessField>
</template>
<script setup>
import { HeadlessField } from '@encolajs/enforma'
import { useFormConfig } from '@encolajs/enforma'
const { getConfig } = useFormConfig()
defineProps({
name: String,
label: String
})
</script>This documentation provides a comprehensive guide to creating wrapper components that leverage EncolaJS's headless components while giving you complete control over the UI and behavior. Remember to follow the best practices and consider accessibility, state management, and error handling in your implementations.
