@template-form/wc-formbuilder
v1.0.1
Published
A lightweight, template-driven form builder using Final Form and Lit Element with dynamic field visibility, validation, and colspan support
Maintainers
Readme
Final Form Lit Template Builder
A lightweight, template-driven form builder using Final Form and Lit Element with dynamic field visibility, validation, and colspan support.
Features
- 📋 Template-driven: Define forms as pure JSON
- 🎯 Type-safe: Full TypeScript support with comprehensive types
- 🔄 Reactive state: Built on Final Form for reliable state management
- 👁️ Conditional visibility: Show/hide fields based on form values
- ✅ Flexible validation: Custom validators with string or function references
- 🎨 Responsive layout: 12-column CSS Grid with colspan support
- 🔌 Web Components: Use as standard HTML custom element
- 🏗️ Modular: Clean separation of concerns with reusable utilities
- 🚀 Performance: Lazy rendering of hidden fields
- 🔄 Auto-reset: Hidden fields automatically reset to default values
Installation
npm install @template-form/wc-formbuilderPeer Dependencies
This package requires final-form and lit:
npm install final-form litQuick Start
1. Basic Usage
import { componentMap } from "@template-form/wc-formbuilder/components";<template-form-builder
.template="${template}"
.componentMap="${componentMap}"
.validation="${validation}"
.conditions="${conditions}"
@form-submit="${handleSubmit}"
></template-form-builder>2. Define Template
import { FormTemplate } from "@template-form/wc-formbuilder";
const template: FormTemplate = [
{
title: "Personal Info",
component: "card",
fields: [
{
name: "firstName",
component: "text",
label: "First Name",
placeholder: "John",
required: true,
colSpan: 6,
},
{
name: "email",
component: "email",
label: "Email",
placeholder: "[email protected]",
required: "Email is required",
validate: "email",
colSpan: 6,
},
],
},
];3. Setup Component Map
import { ComponentMap } from "@template-form/wc-formbuilder";
import * as components from "@template-form/wc-formbuilder/components";
import { html } from "lit";
const componentMap: ComponentMap = {
component: {
text: components.textField,
email: components.emailField,
select: components.selectField,
textarea: components.textareaField,
checkbox: components.checkboxField,
},
section: {
card: (context) => html`<div class="card">${context.fields}</div>`,
},
};4. Setup Validators
import { ValidationMap } from "@template-form/wc-formbuilder";
const validation: ValidationMap = {
required: (value) => (value ? undefined : "This field is required"),
email: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value ?? ""))
? undefined
: "Invalid email",
};5. Setup Conditions
import { ConditionMap } from "@template-form/wc-formbuilder";
const conditions: ConditionMap = {
isAdmin: (values) => values.role === "admin",
hasEmail: (values) => Boolean(values.email),
};Template Structure
Field Template
type FieldTemplate = {
name: string; // Field identifier
component: string; // Component type
label?: string; // Display label
placeholder?: string; // Input placeholder
defaultValue?: FieldValue; // Initial value
required?: boolean | string; // Required validation
validate?: FieldValidation; // Custom validators
visible?: boolean | string; // Visibility condition
readOnly?: boolean | string; // Read-only condition
colSpan?: number; // Grid columns (1-12, default: 12)
events?: FieldEvents; // Event handlers
props?: Record<string, unknown>; // Component-specific props
helperText?: string; // Helper text
};Section Template
type SectionTemplate = {
title?: string; // Section title
description?: string; // Description text
component?: string; // Section component type
visible?: boolean | string; // Visibility condition
fields: FieldTemplate[]; // Contained fields
props?: Record<string, unknown>; // Section-specific props
};Advanced Features
Responsive Layout with colspan
const template: FormTemplate = [
{
title: "Contact Info",
component: "card",
fields: [
{ name: "firstName", colSpan: 4 }, // 1/3 width
{ name: "lastName", colSpan: 4 }, // 1/3 width
{ name: "phone", colSpan: 4 }, // 1/3 width
{ name: "address", colSpan: 12 }, // Full width
],
},
];Conditional Fields
const template: FormTemplate = [
{
name: "needsShipping",
component: "checkbox",
label: "Need shipping?",
},
{
name: "shippingAddress",
component: "textarea",
label: "Shipping Address",
visible: "needsShipping", // String reference to condition
},
];
const conditions: ConditionMap = {
needsShipping: (values) => values.needsShipping === true,
};Field Events
const template: FormTemplate = [
{
name: "email",
component: "email",
events: {
change: "onEmailChange",
blur: "validateEmail",
},
},
];
const eventMap: EventMap = {
onEmailChange: ({ value, values, field }) => {
console.log("Email changed to:", value);
},
validateEmail: ({ name, builder }) => {
builder.form?.validateFields([name]);
},
};Development
Setup
npm install
npm run devBuild
# Build library
npm run buildVersion Management
# Patch release (1.0.0 -> 1.0.1)
npm version patch
# Minor release (1.0.0 -> 1.1.0)
npm version minor
# Major release (1.0.0 -> 2.0.0)
npm version majorPublishing to npm
# Ensure everything is built and tested
npm run build
npm run typecheck
# Login to npm
npm login
# Publish
npm publishFor scoped packages:
npm publish --access publicAPI Reference
TemplateFormBuilder Component
// Properties
@property componentMap: ComponentMap // Field and section renderers
@property template: FormTemplate // Form template
@property data: FormValues // Initial form data
@property validation: ValidationMap // Validators
@property conditions: ConditionMap // Conditions
@property eventMap: EventMap // Event handlers
@property validateOnBlur: boolean // Enable blur validation
// Methods
submit(): Promise<void> // Submit the form
reset(): void // Reset form to initial state
// Properties (readonly)
get values(): FormValues // Current form values
get submitting(): boolean // Is form submitting
// Events
@event form-submit // Form submitted
@event field-change // Field value changedFile Structure
@template-form/wc-formbuilder/
├── src/
│ ├── index.ts # Main entry point
│ ├── types.ts # Type definitions
│ ├── constants.ts # Default values
│ ├── utils.ts # Utility functions
│ ├── validators.ts # Validation logic
│ ├── renderers.ts # Rendering functions
│ ├── styles.ts # CSS styles
│ ├── template-form-builder.ts # Main component
│ ├── components/ # Field components
│ │ ├── text-field.ts
│ │ ├── email-field.ts
│ │ ├── select-field.ts
│ │ ├── checkbox-field.ts
│ │ ├── textarea-field.ts
│ │ └── component-map.ts
│ └── sections/ # Section components
│ ├── card-section.ts
│ └── accordion-section.ts
├── dist/ # Compiled output
├── package.json
├── tsconfig.json
├── tsconfig.lib.json
├── vite.config.js
└── index.html # DemoPerformance Considerations
- Hidden fields are not rendered, reducing DOM overhead
- Field values are automatically reset when hidden
- Validation only runs on visible fields
- Efficient state updates using Lit's reactivity
Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Modern browsers with ES2020 support
Contributing
We welcome contributions! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © 2024 @template-form/wc-formbuilder contributors
See LICENSE file for details.
Changelog
See CHANGELOG.md for version history and updates.
Related Projects
- Final Form - Form state management
- Lit - Web components library
- Web Components - Web Standards
Support
For issues, questions, or suggestions:
- Open an issue
- Start a discussion
- Check documentation required: 'Email is required.', validate: 'email', events: { change: ['logChange', 'syncEmail'] } } ] }, { name: 'accountId', component: 'text', label: 'Account ID', readOnly: true }, { title: 'Preferences', component: 'accordion', props: { open: true }, visible: values => values.role !== 'guest', fields: [ { name: 'notes', component: 'textarea', label: 'Notes', visible: values => Boolean(values.newsletter), readOnly: values => values.role === 'designer' } ] } ];
Field options:
- `required`: `true` for the default message, or a string for a custom required message.
- `visible`: boolean or `(values) => boolean`; hidden fields are not rendered or validated.
- `readOnly`: boolean or `(values) => boolean`; renderers receive this flag and builder updates are ignored.
- `validate`: named validators or inline validator functions for custom validation.
Section options:
- `component`: named section renderer, such as `card` or `accordion`.
- `props`: renderer-specific section options, such as `{ open: true }` for accordions.
- `visible`: boolean or `(values) => boolean`; hidden sections are not rendered or validated.
### Component Renderer Example
```ts
// src/components/text-field.ts
export const textField = ({ field, value, error, required, readOnly, update, blur }) => html`
<input
id=${field.name}
.value=${String(value ?? '')}
aria-invalid=${Boolean(error)}
?required=${required}
?readonly=${readOnly}
@input=${event => update(event.target.value, event)}
@blur=${blur}
/>
`;
// src/components/component-map.ts
export const componentMap = {
component: {
text: textField,
email: emailField,
select: selectField,
checkbox: checkboxField,
textarea: textareaField
},
section: {
accordion: accordionSection,
card: cardSection
}
};Section Renderer Example
// src/sections/accordion-section.ts
export const accordionSection = ({ section, fields }) => html`
<details class="section-accordion" ?open=${section.props?.open !== false}>
<summary>
${section.title ? html`<h2>${section.title}</h2>` : ""}
${section.description
? html`<p class="description">${section.description}</p>`
: ""}
</summary>
<div class="section-content">${fields}</div>
</details>
`;
// Register this renderer in componentMap.section.Validation Example
const validation = {
required: (value) => (value ? undefined : "This field is required."),
email: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? undefined
: "Enter a valid email.",
};Events
Every configured field can trigger named or inline handlers:
const eventMap = {
logChange: ({ name, value, values }) => {
console.log(name, value, values);
},
};The builder also emits DOM events:
field-change: fired after every field update.form-submit: fired with validated form values.
