Basic Form Handling
WildflowerJS provides two-way data binding between form inputs and component state through data-model attributes, with automatic type conversion for numbers, booleans, and dates.
Basic Form Binding
Use data-model for two-way binding between form inputs and component state:
<div data-component="basic-form">
<p class="text-muted">Demonstrates two-way data binding with various form input types.</p>
<div class="row">
<div class="col-md-6">
<h5>Form Inputs</h5>
<form data-action="handleSubmit">
<div class="mb-3">
<label class="form-label">Name: *</label>
<input type="text" data-model="form.name" class="form-control"
placeholder="Enter your full name" required>
</div>
<div class="mb-3">
<label class="form-label">Email: *</label>
<input type="email" data-model="form.email" class="form-control"
placeholder="your.email@example.com" required>
</div>
<div class="mb-3">
<label class="form-label">Age:</label>
<input type="number" data-model="form.age" class="form-control"
min="1" max="120" placeholder="Your age">
</div>
<div class="mb-3">
<label class="form-label">Profession:</label>
<select data-model="form.profession" class="form-select">
<option value="">Select your profession</option>
<option value="developer">Software Developer</option>
<option value="designer">Designer</option>
<option value="manager">Manager</option>
<option value="student">Student</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Bio:</label>
<textarea data-model="form.bio" class="form-control" rows="3"
placeholder="Tell us a little about yourself..."></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" data-model="form.subscribe" id="subscribe" class="form-check-input">
<label for="subscribe" class="form-check-label">
Subscribe to our newsletter
</label>
</div>
<div class="form-check">
<input type="checkbox" data-model="form.notifications" id="notifications" class="form-check-input">
<label for="notifications" class="form-check-label">
Allow push notifications
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
Submit Form
</button>
<button type="button" data-action="resetForm" class="btn btn-secondary">
Reset
</button>
</div>
</form>
</div>
<div class="col-md-6">
<h5>Live Data Preview</h5>
<div class="card h-100">
<div class="card-body">
<div class="mb-2">
<strong>Name:</strong>
<span data-bind="form.name || 'Not provided'"
data-bind-class="form.name ? 'text-success' : 'text-muted'"></span>
</div>
<div class="mb-2">
<strong>Email:</strong>
<span data-bind="form.email || 'Not provided'"
data-bind-class="form.email ? 'text-success' : 'text-muted'"></span>
</div>
<div class="mb-2">
<strong>Age:</strong>
<span data-bind="form.age || 'Not provided'"
data-bind-class="form.age ? 'text-success' : 'text-muted'"></span>
<span data-show="form.age" class="text-muted small"> years old</span>
</div>
<div class="mb-2">
<strong>Profession:</strong>
<span data-bind="form.profession || 'Not selected'"
data-bind-class="form.profession ? 'text-success' : 'text-muted'"></span>
</div>
<div class="mb-2">
<strong>Bio:</strong>
<div data-bind="form.bio || 'No bio provided'"
data-bind-class="form.bio ? 'text-success' : 'text-muted'"
style="white-space: pre-wrap; font-size: 0.9em;"></div>
</div>
<div class="mb-2">
<strong>Newsletter:</strong>
<span data-bind="form.subscribe ? '✅ Subscribed' : '❌ Not subscribed'"
data-bind-class="form.subscribe ? 'text-success' : 'text-muted'"></span>
</div>
<div class="mb-2">
<strong>Notifications:</strong>
<span data-bind="form.notifications ? '✅ Enabled' : '❌ Disabled'"
data-bind-class="form.notifications ? 'text-success' : 'text-muted'"></span>
</div>
<hr>
<div class="small text-muted">
<strong>Form Completeness:</strong>
<span data-bind="completionPercentage"></span>%
<div class="progress mt-1" style="height: 12px;">
<div class="progress-bar" data-bind-class="progressColorClass"
data-bind-style="{ width: completionPercentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-3" data-show="submissionResult">
<div class="alert alert-info">
<h6>✅ Form Submitted Successfully!</h6>
<p>Here's the data that was captured:</p>
<pre class="small bg-light p-2 rounded"><code data-bind="submissionResult"></code></pre>
</div>
</div>
</div>
wildflower.component('basic-form', {
state: {
form: {
name: '',
email: '',
age: '',
profession: '',
bio: '',
subscribe: false,
notifications: false
},
submissionResult: ''
},
computed: {
// Calculate form completion percentage
completionPercentage() {
const fields = ['name', 'email', 'age', 'profession', 'bio']
const completed = fields.filter(field => this.form[field]).length
return Math.round((completed / fields.length) * 100)
},
// Dynamic progress bar color based on completion
progressColorClass() {
const percentage = this.completionPercentage
if (percentage >= 80) return 'bg-success'
if (percentage >= 50) return 'bg-warning'
return 'bg-danger'
},
},
// Handle form submission
handleSubmit(event, element) {
event.preventDefault()
// Create a clean copy of form data for submission
const submissionData = {
...this.form,
submittedAt: new Date().toISOString(),
completeness: `${this.completionPercentage}%`
}
// Simulate form submission
this.submissionResult = JSON.stringify(submissionData, null, 2)
// Clear result after 8 seconds
setTimeout(() => {
this.submissionResult = ''
}, 8000)
},
// Reset form to initial state
resetForm() {
this.form = {
name: '',
email: '',
age: '',
profession: '',
bio: '',
subscribe: false,
notifications: false
}
this.submissionResult = ''
},
})
Store-Backed Inputs
A data-model path whose first segment names a registered store binds that store's state instead of the component's, in both directions: typing writes the store, and a programmatic store write repaints the input. This is how a form spread across several components edits one shared record:
<input data-model="checkout.email">
wildflower.store('checkout', {
state: { email: '' }
});
Which one a root binds to follows the same rule bare names follow everywhere else: the component comes first. If the component's own state declares the root (a state key named checkout in the example), the binding is component state and the same-named store is never touched, reading or writing. The store route is the fallback for roots the component does not own, which is exactly the shape the pattern is used in, since a component editing a shared store record has no reason to declare a local key by the same name. Development builds note the shadowing (WF-512) when a root names both, so a collision is never a silent surprise. The $store.path spelling is not accepted in data-model (WF-501); the bare root is the store spelling here.
Form Input Types
WildflowerJS supports all HTML form input types with automatic type conversion and proper data binding:
<div data-component="input-types-demo">
<p class="text-muted">Experience how WildflowerJS handles various HTML input types with automatic data binding and type conversion.</p>
<div class="row">
<div class="col-md-6">
<h5>Text-Based Inputs</h5>
<div class="mb-3">
<label class="form-label">Text:</label>
<input type="text" data-model="inputs.text" class="form-control" placeholder="Enter any text">
</div>
<div class="mb-3">
<label class="form-label">Password:</label>
<input type="password" data-model="inputs.password" class="form-control" placeholder="Enter password">
</div>
<div class="mb-3">
<label class="form-label">Email:</label>
<input type="email" data-model="inputs.email" class="form-control" placeholder="user@example.com">
</div>
<div class="mb-3">
<label class="form-label">URL:</label>
<input type="url" data-model="inputs.url" class="form-control" placeholder="https://example.com">
</div>
<div class="mb-3">
<label class="form-label">Search:</label>
<input type="search" data-model="inputs.search" class="form-control" placeholder="Search terms...">
</div>
<div class="mb-3">
<label class="form-label">Tel:</label>
<input type="tel" data-model="inputs.tel" class="form-control" placeholder="(555) 123-4567">
</div>
</div>
<div class="col-md-6">
<h5>Numeric & Date Inputs</h5>
<div class="mb-3">
<label class="form-label">Number:</label>
<input type="number" data-model="inputs.number" class="form-control" min="0" max="100" step="1">
</div>
<div class="mb-3">
<label class="form-label">Range (<span data-bind="inputs.range"></span>):</label>
<input type="range" data-model="inputs.range" class="form-range" min="0" max="100" step="5">
</div>
<div class="mb-3">
<label class="form-label">Date:</label>
<input type="date" data-model="inputs.date" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Time:</label>
<input type="time" data-model="inputs.time" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Date/Time:</label>
<input type="datetime-local" data-model="inputs.datetime" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Color:</label>
<input type="color" data-model="inputs.color" class="form-control form-control-color">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h5>Selection Inputs</h5>
<div class="mb-3">
<label class="form-label">Select:</label>
<select data-model="inputs.select" class="form-select">
<option value="">Choose category...</option>
<option value="frontend">Frontend Development</option>
<option value="backend">Backend Development</option>
<option value="fullstack">Full Stack Development</option>
<option value="mobile">Mobile Development</option>
<option value="devops">DevOps</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Experience Level:</label>
<div class="form-check">
<input type="radio" data-model="inputs.experience" value="beginner" id="beginner" name="experience" class="form-check-input">
<label for="beginner" class="form-check-label">Beginner (0-2 years)</label>
</div>
<div class="form-check">
<input type="radio" data-model="inputs.experience" value="intermediate" id="intermediate" name="experience" class="form-check-input">
<label for="intermediate" class="form-check-label">Intermediate (3-5 years)</label>
</div>
<div class="form-check">
<input type="radio" data-model="inputs.experience" value="advanced" id="advanced" name="experience" class="form-check-input">
<label for="advanced" class="form-check-label">Advanced (5+ years)</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Technologies:</label>
<div class="form-check">
<input type="checkbox" data-model="inputs.technologies.javascript" id="js" class="form-check-input">
<label for="js" class="form-check-label">JavaScript</label>
</div>
<div class="form-check">
<input type="checkbox" data-model="inputs.technologies.python" id="py" class="form-check-input">
<label for="py" class="form-check-label">Python</label>
</div>
<div class="form-check">
<input type="checkbox" data-model="inputs.technologies.java" id="java" class="form-check-input">
<label for="java" class="form-check-label">Java</label>
</div>
<div class="form-check">
<input type="checkbox" data-model="inputs.technologies.react" id="react" class="form-check-input">
<label for="react" class="form-check-label">React</label>
</div>
</div>
</div>
<div class="col-md-6">
<h5>Live Data Preview</h5>
<div class="card h-100">
<div class="card-body">
<div class="small" style="max-height: 400px; overflow-y: auto;">
<pre><code data-bind="inputsJson"></code></pre>
</div>
<hr>
<div class="small text-muted">
<strong>Data Types:</strong><br>
Number: <span data-bind="numberType"></span><br>
Range: <span data-bind="rangeType"></span><br>
Date: <span data-bind="dateType"></span><br>
Color: <span data-bind="colorType"></span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-3">
<button type="button" data-action="clearAll" class="btn btn-secondary">
Clear All
</button>
</div>
</div>
wildflower.component('input-types-demo', {
state: {
inputs: {
text: '',
password: '',
email: '',
url: '',
search: '',
tel: '',
number: 0,
range: 50,
date: '',
time: '',
datetime: '',
color: '#3498db',
select: '',
experience: '',
technologies: {
javascript: false,
python: false,
java: false,
react: false
}
},
typeInfo: false
},
computed: {
inputsJson() {
return JSON.stringify(this.inputs, null, 2)
},
numberType() {
return typeof this.inputs.number + ' (' + this.inputs.number + ')'
},
rangeType() {
return typeof this.inputs.range + ' (' + this.inputs.range + ')'
},
dateType() {
return typeof this.inputs.date + ' (' + (this.inputs.date || 'empty') + ')'
},
colorType() {
return typeof this.inputs.color + ' (' + this.inputs.color + ')'
}
},
clearAll() {
this.inputs = {
text: '',
password: '',
email: '',
url: '',
search: '',
tel: '',
number: 0,
range: 50,
date: '',
time: '',
datetime: '',
color: '#3498db',
select: '',
experience: '',
technologies: {
javascript: false,
python: false,
java: false,
react: false
}
}
}
})
Input Type Handling
WildflowerJS automatically converts input values to appropriate JavaScript types:
- number, range: Converted to JavaScript numbers
- checkbox: Converted to boolean values
- date, time, datetime-local: Remain as strings in ISO format
- text, email, url, etc.: Remain as strings
Form Validation
WildflowerJS uses the browser's native HTML5 Constraint Validation API for standard rules (required, minlength, pattern, type="email", etc.) and adds a small set of framework attributes for triggers, custom rules, and inline error rendering.
Built-in validation with data-validate-on
Add data-validate-on to the <form> element along with novalidate. The novalidate attribute is required. Without it, the browser's native tooltip popups fire first and prevent the framework's inline error messages from rendering.
Trigger values are a comma-separated list:
data-validate-on="submit": validate the entire form on submit. The configureddata-actionon the form runs only when validation passes.data-validate-on="blur": validate each input as it loses focus. Updates the matchingdata-error-forelement and toggles the.invalidclass on the input. No manualfocusoutlistener needed.data-validate-on="submit,blur": both. Per-field feedback as the user tabs through, plus a whole-form pass on submit.
Template pattern
<form data-validate-on="submit,blur" data-action="handleSubmit" novalidate>
<label>Email
<input type="email" required data-model="form.email">
</label>
<span class="error-message" data-error-for="form.email"></span>
<label>Name
<input type="text" required minlength="2" data-model="form.name">
</label>
<span class="error-message" data-error-for="form.name"></span>
<button type="submit">Submit</button>
</form>
Notes on the form-level data-action: when data-action is on a <form>, the framework handles the submit event automatically; do not use the submit: prefix. The action method receives (event, formElement) and is called only after validation passes.
Native validation rules
Standard HTML5 input attributes work out of the box; messages come from input.validationMessage (browser-default text, localized):
requiredminlength="N",maxlength="N"type="email",type="url",type="number"pattern="regex"min="N",max="N",step="N"
Framework extensions
These input-level attributes add to native validation:
data-validate="/regex/": custom regex check (slash-delimited). Runs after native validation passes.data-validate="number": accepts any value that parses as a float.data-validate="integer": accepts only-?\d+.data-validate-message="...": overrides the default error message for thedata-validaterule on that input.
<input data-model="user.code"
data-validate="/^[A-Z]{3}-\d{4}$/"
data-validate-message="Code must look like ABC-1234">
<span class="error-message" data-error-for="user.code"></span>
State the framework sets
After every validation pass, the framework writes:
- The CSS class
invalidon each failing input (removed when it passes). this.formValid(boolean): whole-form validity after the most recent pass.this.validationErrors:{ [modelPath]: errorMessage }for currently-failing fields.
You can bind to that state declaratively. For example, disable a submit button until the form is valid:
<button type="submit" data-bind-attr="{ disabled: !formValid }">Submit</button>
Or show a single summary line when any error is present:
<div class="form-summary-error" data-show="!formValid">Please fix the highlighted fields.</div>
Cross-field rules
Per-input validation cannot say "the return date must be after departure" or "provide a pickup point or an address." Those are facts about the form as a whole. Declare them in a rules: block on the component and they run on the same validation pass, block submit the same way, and surface through the same error plumbing:
wildflower.component('booking-form', {
state: { depart: '', ret: '', pickup: '', address: '' },
rules: {
'Return must be on or after departure':
'ret == "" || depart == "" || ret >= depart',
'destination': {
check: 'pickup != "" || address != ""',
message: 'Provide a pickup point or an address',
fields: ['pickup', 'address']
}
}
});
The rule's name is its user-facing message, so name rules in plain language; message: overrides that when you want a short key. A failing rule adds the invalid class to the inputs it involves and writes its message to any matching data-error-for element, either per field or under the rule's own name:
<input type="date" data-model="depart">
<input type="date" data-model="ret">
<span class="error-message" data-error-for="Return must be on or after departure"></span>
With data-validate-on="blur" or on any change, rules involving the touched field re-evaluate live, so the user sees the cross-field message as soon as the second field makes it true, without waiting for submit. String checks may reference state and computed names; a typo in a variable name rejects the rule at setup with a dev warning (WF-228) rather than silently gating your form. Rule expressions run through the CSP-safe interpreter that ships in every build, never through eval(), so they work unchanged under a strict Content-Security-Policy.
A rule checking that a text field has a value compares exactly what was typed, so a lone space would satisfy pickup != "". Put data-model-trim on the input and whitespace-only entries never reach state in the first place; the check stays a clean string and the rule can't be fooled.
<div data-component="booking-demo">
<form data-validate-on="submit,blur,change" data-action="save" novalidate>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="bk-depart">Departure *</label>
<input id="bk-depart" type="date" data-model="depart" class="form-control" required>
<div class="text-danger small" data-error-for="depart"></div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="bk-ret">Return</label>
<input id="bk-ret" type="date" data-model="ret" class="form-control">
<div class="text-danger small" data-error-for="Return must be on or after departure"></div>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="bk-pickup">Pickup point</label>
<input id="bk-pickup" type="text" data-model="pickup" data-model-trim class="form-control"
placeholder="e.g. Main St depot">
</div>
<div class="mb-3">
<label class="form-label" for="bk-address">Or an address</label>
<input id="bk-address" type="text" data-model="address" data-model-trim class="form-control"
placeholder="e.g. 42 Meadow Lane">
<div class="text-danger small" data-error-for="destination"></div>
</div>
<button type="submit" class="btn btn-primary">Book trip</button>
<div class="alert alert-info mt-3" data-show="booked">
Booked. Safe travels.
</div>
</form>
</div>
wildflower.component('booking-demo', {
state: { depart: '', ret: '', pickup: '', address: '', booked: false },
rules: {
// The rule name is its message. Empty fields pass; the date
// comparison only gates once both dates are present.
'Return must be on or after departure':
'ret == "" || depart == "" || ret >= depart',
// Object form: message and the marked inputs stated explicitly.
// The inputs use data-model-trim, so a lone space never reaches
// state and can't satisfy this check.
'destination': {
check: 'pickup != "" || address != ""',
message: 'Provide a pickup point or an address',
fields: ['pickup', 'address']
}
},
save() {
// Rules gate submit; this only runs when every rule holds.
this.booked = true
},
watch: {
// Editing anything after booking clears the stale confirmation.
depart() { this.booked = false },
ret() { this.booked = false },
pickup() { this.booked = false },
address() { this.booked = false }
}
})
Custom validation logic
When you need validation beyond declared rules (debounced server checks, password-strength meters, anything asynchronous), write it yourself in action handlers. The example below uses blur: and input: action modifiers to run validators as the user interacts:
<div data-component="form-validation">
<p class="text-muted">Experience real-time validation with custom rules, password strength analysis, and comprehensive error handling.</p>
<form data-action="handleSubmit" class="needs-validation" novalidate>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Username: *</label>
<input type="text"
data-model="form.username"
data-action="blur:validateUsername input:clearUsernameError"
class="form-control"
data-bind-class="getUsernameClass"
placeholder="Enter username"
required>
<div class="invalid-feedback" data-bind="errors.username"></div>
<div class="form-text">Username must be 3-20 characters, alphanumeric only</div>
</div>
<div class="mb-3">
<label class="form-label">Email: *</label>
<input type="email"
data-model="form.email"
data-action="blur:validateEmail"
class="form-control"
data-bind-class="getEmailClass"
placeholder="your.email@example.com"
required>
<div class="invalid-feedback" data-bind="errors.email"></div>
<div class="valid-feedback" data-show="touched.email && form.email && !errors.email">
Valid email address!
</div>
</div>
<div class="mb-3">
<label class="form-label">Phone: *</label>
<input type="tel"
data-model="form.phone"
data-action="blur:validatePhone input:formatPhone"
class="form-control"
data-bind-class="getPhoneClass"
placeholder="(555) 123-4567"
required>
<div class="invalid-feedback" data-bind="errors.phone"></div>
<div class="form-text">US phone number format</div>
</div>
<div class="mb-3">
<label class="form-label">Age: *</label>
<input type="number"
data-model="form.age"
data-action="blur:validateAge"
class="form-control"
data-bind-class="getAgeClass"
min="13" max="120"
placeholder="Enter your age"
required>
<div class="invalid-feedback" data-bind="errors.age"></div>
<div class="form-text">Must be between 13 and 120 years old</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Password: *</label>
<div class="position-relative">
<input type="password"
id="password-input"
data-model="form.password"
data-action="input:validatePassword"
class="form-control"
data-bind-class="getPasswordClass"
placeholder="Create a strong password"
required>
<button type="button"
data-action="togglePasswordVisibility"
class="btn btn-secondary position-absolute end-0 top-0 h-100 px-2"
style="border-left: none; border-radius: 0 0.375rem 0.375rem 0;">
<span data-bind="showPassword ? '🙈' : '👁️'"></span>
</button>
</div>
<div class="invalid-feedback" data-bind="errors.password"></div>
<div class="mt-2">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Password strength:</small>
<span class="badge" data-bind="passwordStrength" data-bind-class="passwordStrengthClass"></span>
</div>
<div class="progress mt-1" style="height: 6px;">
<div class="progress-bar"
data-bind-class="passwordStrengthColor"
data-bind-style="{ width: passwordStrengthPercent + '%' }"></div>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Confirm Password: *</label>
<input type="password"
data-model="form.confirmPassword"
data-action="input:validateConfirmPassword"
class="form-control"
data-bind-class="getConfirmPasswordClass"
placeholder="Confirm your password"
required>
<div class="invalid-feedback" data-bind="errors.confirmPassword"></div>
<div class="valid-feedback" data-show="touched.confirmPassword && form.confirmPassword && !errors.confirmPassword">
Passwords match!
</div>
</div>
<div class="mb-3">
<label class="form-label">Account Type: *</label>
<select data-model="form.accountType"
data-action="change:validateAccountType"
class="form-select"
data-bind-class="getAccountTypeClass"
required>
<option value="">Select account type...</option>
<option value="personal">Personal</option>
<option value="business">Business</option>
<option value="premium">Premium</option>
</select>
<div class="invalid-feedback" data-bind="errors.accountType"></div>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox"
data-model="form.termsAccepted"
data-action="change:validateTerms"
id="terms"
class="form-check-input"
data-bind-class="getTermsClass"
required>
<label for="terms" class="form-check-label">
I accept the <a href="#" data-action="showTerms">terms and conditions</a> *
</label>
</div>
<div class="invalid-feedback" data-bind="errors.terms"></div>
</div>
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2 mb-3">
<button type="submit"
class="btn"
data-bind-class="submitButtonClass"
data-bind-attr="{ disabled: !isFormValid || isSubmitting }">
<span data-show="isSubmitting">
<span class="spinner-border spinner-border-sm me-2"></span>
Creating Account...
</span>
<span data-show="!isSubmitting">Create Account</span>
</button>
<div class="text-end">
<small class="text-muted">
Form Progress: <span data-bind="validationProgress"></span>%
</small>
<div class="progress mt-1" style="width: 100px; height: 4px;">
<div class="progress-bar bg-success"
data-bind-style="{ width: validationProgress + '%' }"></div>
</div>
</div>
</div>
</form>
<div class="mt-3" data-show="submissionSuccess">
<div class="alert alert-info">
<h5>✅ Account Created Successfully!</h5>
<p>Welcome to our platform, <strong><span data-bind="form.username"></span></strong>! A confirmation email has been sent to <strong><span data-bind="form.email"></span></strong>.</p>
<button type="button" data-action="resetForm" class="btn btn-success">
Create Another Account
</button>
</div>
</div>
<div class="mt-3" data-show="showTermsModal">
<div class="modal" style="display: block; background: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Terms and Conditions</h5>
<button type="button" data-action="hideTerms" class="btn-close"></button>
</div>
<div class="modal-body">
<p>By creating an account, you agree to our terms of service and privacy policy.</p>
<p>This is a demo form for educational purposes.</p>
</div>
<div class="modal-footer">
<button type="button" data-action="acceptTerms" class="btn btn-primary">
Accept Terms
</button>
<button type="button" data-action="hideTerms" class="btn btn-secondary">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
wildflower.component('form-validation', {
state: {
form: {
username: '',
email: '',
phone: '',
age: '',
password: '',
confirmPassword: '',
accountType: '',
termsAccepted: false
},
errors: {
username: '',
email: '',
phone: '',
age: '',
password: '',
confirmPassword: '',
accountType: '',
terms: ''
},
touched: {
username: false,
email: false,
phone: false,
age: false,
password: false,
confirmPassword: false,
accountType: false,
terms: false
},
isSubmitting: false,
submissionSuccess: false,
showPassword: false,
showTermsModal: false
},
computed: {
isFormValid() {
const form = this.form
const errors = this.errors
// Check all required fields are filled
const requiredFields = ['username', 'email', 'phone', 'age', 'password', 'confirmPassword', 'accountType']
const hasAllFields = requiredFields.every(field => form[field] && form[field].toString().trim())
// Check no validation errors
const hasNoErrors = Object.values(errors).every(error => !error)
return hasAllFields && hasNoErrors && form.termsAccepted
},
validationProgress() {
const form = this.form
const errors = this.errors
const totalFields = 8 // including termsAccepted
let validFields = 0
if (form.username && !errors.username) validFields++
if (form.email && !errors.email) validFields++
if (form.phone && !errors.phone) validFields++
if (form.age && !errors.age) validFields++
if (form.password && !errors.password) validFields++
if (form.confirmPassword && !errors.confirmPassword) validFields++
if (form.accountType && !errors.accountType) validFields++
if (form.termsAccepted && !errors.terms) validFields++
return Math.round((validFields / totalFields) * 100)
},
passwordStrengthScore() {
const password = this.form.password
if (!password) return 0
let score = 0
if (password.length >= 8) score++
if (password.length >= 12) score++
if (/[a-z]/.test(password)) score++
if (/[A-Z]/.test(password)) score++
if (/[0-9]/.test(password)) score++
if (/[^A-Za-z0-9]/.test(password)) score++
return score
},
passwordStrength() {
const score = this.passwordStrengthScore
if (score <= 1) return 'Very Weak'
if (score <= 2) return 'Weak'
if (score <= 3) return 'Fair'
if (score <= 4) return 'Good'
if (score <= 5) return 'Strong'
return 'Very Strong'
},
passwordStrengthClass() {
const strength = this.passwordStrength
const baseClass = 'badge '
switch (strength) {
case 'Very Weak': return baseClass + 'bg-danger'
case 'Weak': return baseClass + 'bg-warning'
case 'Fair': return baseClass + 'bg-info'
case 'Good': return baseClass + 'bg-primary'
case 'Strong': return baseClass + 'bg-success'
case 'Very Strong': return baseClass + 'bg-success'
default: return baseClass + 'bg-secondary'
}
},
passwordStrengthColor() {
const strength = this.passwordStrength
switch (strength) {
case 'Very Weak': return 'bg-danger'
case 'Weak': return 'bg-warning'
case 'Fair': return 'bg-info'
case 'Good': return 'bg-primary'
case 'Strong': case 'Very Strong': return 'bg-success'
default: return 'bg-secondary'
}
},
passwordStrengthPercent() {
return Math.min(Math.round((this.passwordStrengthScore / 6) * 100), 100)
},
submitButtonClass() {
const isValid = this.isFormValid
const isSubmitting = this.isSubmitting
if (isSubmitting) return 'btn btn-primary'
return isValid ? 'btn btn-primary' : 'btn btn-outline-primary'
},
// Field-specific validation classes
getUsernameClass() {
const { username } = this.form
const error = this.errors.username
if (this.touched.username && error) return 'form-control is-invalid'
if (this.touched.username && username && !error) return 'form-control is-valid'
return 'form-control'
},
getEmailClass() {
const { email } = this.form
const error = this.errors.email
if (this.touched.email && error) return 'form-control is-invalid'
if (this.touched.email && email && !error) return 'form-control is-valid'
return 'form-control'
},
getPhoneClass() {
const { phone } = this.form
const error = this.errors.phone
if (this.touched.phone && error) return 'form-control is-invalid'
if (this.touched.phone && phone && !error) return 'form-control is-valid'
return 'form-control'
},
getAgeClass() {
const { age } = this.form
const error = this.errors.age
if (this.touched.age && error) return 'form-control is-invalid'
if (this.touched.age && age && !error) return 'form-control is-valid'
return 'form-control'
},
getPasswordClass() {
const { password } = this.form
const error = this.errors.password
if (this.touched.password && error) return 'form-control is-invalid'
if (this.touched.password && password && !error) return 'form-control is-valid'
return 'form-control'
},
getConfirmPasswordClass() {
const { confirmPassword } = this.form
const error = this.errors.confirmPassword
if (this.touched.confirmPassword && error) return 'form-control is-invalid'
if (this.touched.confirmPassword && confirmPassword && !error) return 'form-control is-valid'
return 'form-control'
},
getAccountTypeClass() {
const { accountType } = this.form
const error = this.errors.accountType
if (this.touched.accountType && error) return 'form-select is-invalid'
if (this.touched.accountType && accountType && !error) return 'form-select is-valid'
return 'form-select'
},
getTermsClass() {
const error = this.errors.terms
if (this.touched.terms && error) return 'form-check-input is-invalid'
if (this.touched.terms && this.form.termsAccepted) return 'form-check-input is-valid'
return 'form-check-input'
}
},
// Validation methods
validateUsername() {
this.touched.username = true
const username = this.form.username.trim()
if (!username) {
this.errors.username = 'Username is required'
} else if (username.length < 3) {
this.errors.username = 'Username must be at least 3 characters'
} else if (username.length > 20) {
this.errors.username = 'Username must be no more than 20 characters'
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
this.errors.username = 'Username can only contain letters, numbers, and underscores'
} else {
this.errors.username = ''
}
},
clearUsernameError() {
if (this.errors.username) {
this.errors.username = ''
}
},
validateEmail() {
this.touched.email = true
const email = this.form.email.trim()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email) {
this.errors.email = 'Email is required'
} else if (!emailRegex.test(email)) {
this.errors.email = 'Please enter a valid email address'
} else if (email.length > 254) {
this.errors.email = 'Email address is too long'
} else {
this.errors.email = ''
}
},
validatePhone() {
this.touched.phone = true
const phone = this.form.phone.replace(/\D/g, '')
if (!phone) {
this.errors.phone = 'Phone number is required'
} else if (phone.length !== 10) {
this.errors.phone = 'Please enter a valid 10-digit phone number'
} else {
this.errors.phone = ''
}
},
formatPhone() {
const phone = this.form.phone.replace(/\D/g, '')
if (phone.length >= 6) {
this.form.phone = `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6, 10)}`
} else if (phone.length >= 3) {
this.form.phone = `(${phone.slice(0, 3)}) ${phone.slice(3)}`
}
},
validateAge() {
this.touched.age = true
const age = parseInt(this.form.age)
if (!this.form.age) {
this.errors.age = 'Age is required'
} else if (isNaN(age) || age < 13) {
this.errors.age = 'You must be at least 13 years old'
} else if (age > 120) {
this.errors.age = 'Please enter a valid age'
} else {
this.errors.age = ''
}
},
validatePassword() {
this.touched.password = true
const password = this.form.password
if (!password) {
this.errors.password = 'Password is required'
} else if (password.length < 8) {
this.errors.password = 'Password must be at least 8 characters'
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/.test(password)) {
this.errors.password = 'Password must contain lowercase, uppercase, and number'
} else {
this.errors.password = ''
}
// Re-validate confirm password if it exists
if (this.form.confirmPassword) {
this.validateConfirmPassword()
}
},
validateConfirmPassword() {
this.touched.confirmPassword = true
const password = this.form.password
const confirmPassword = this.form.confirmPassword
if (!confirmPassword) {
this.errors.confirmPassword = 'Please confirm your password'
} else if (password !== confirmPassword) {
this.errors.confirmPassword = 'Passwords do not match'
} else {
this.errors.confirmPassword = ''
}
},
validateAccountType() {
this.touched.accountType = true
if (!this.form.accountType) {
this.errors.accountType = 'Please select an account type'
} else {
this.errors.accountType = ''
}
},
validateTerms() {
this.touched.terms = true
if (!this.form.termsAccepted) {
this.errors.terms = 'You must accept the terms and conditions'
} else {
this.errors.terms = ''
}
},
// UI interaction methods
togglePasswordVisibility() {
this.showPassword = !this.showPassword
const passwordInput = this.element.querySelector('#password-input')
if (passwordInput) {
passwordInput.type = this.showPassword ? 'text' : 'password'
}
},
showTerms() {
this.showTermsModal = true
},
hideTerms() {
this.showTermsModal = false
},
acceptTerms() {
this.form.termsAccepted = true
this.validateTerms()
this.hideTerms()
},
// Form submission
handleSubmit(event) {
event.preventDefault()
// Validate all fields
this.validateUsername()
this.validateEmail()
this.validatePhone()
this.validateAge()
this.validatePassword()
this.validateConfirmPassword()
this.validateAccountType()
this.validateTerms()
if (this.isFormValid) {
this.isSubmitting = true
// Simulate API call
setTimeout(() => {
this.isSubmitting = false
this.submissionSuccess = true
// Hide success message after 8 seconds
setTimeout(() => {
this.submissionSuccess = false
}, 8000)
}, 2000)
}
},
resetForm() {
this.form = {
username: '',
email: '',
phone: '',
age: '',
password: '',
confirmPassword: '',
accountType: '',
termsAccepted: false
}
this.errors = {
username: '',
email: '',
phone: '',
age: '',
password: '',
confirmPassword: '',
accountType: '',
terms: ''
}
this.touched = {
username: false, email: false, phone: false, age: false,
password: false, confirmPassword: false, accountType: false, terms: false
}
this.submissionSuccess = false
this.showPassword = false
this.showTermsModal = false
}
})
Dynamic Forms
Create forms that dynamically change structure based on user input with conditional fields and sections:
<div data-component="dynamic-form">
<form data-action="handleSubmit">
<div class="mb-3">
<label class="form-label">Account Type:</label>
<select data-model="accountType" class="form-select">
<option value="">Select type...</option>
<option value="personal">Personal</option>
<option value="business">Business</option>
</select>
</div>
<div data-show="accountType === 'personal'" class="mb-3 p-3 border rounded">
<h6>Personal Details</h6>
<div class="mb-2">
<label class="form-label">Full Name:</label>
<input type="text" data-model="personal.name" class="form-control" placeholder="Your name">
</div>
<div class="mb-2">
<label class="form-label">Email:</label>
<input type="email" data-model="personal.email" class="form-control" placeholder="you@example.com">
</div>
</div>
<div data-show="accountType === 'business'" class="mb-3 p-3 border rounded">
<h6>Business Details</h6>
<div class="mb-2">
<label class="form-label">Company Name:</label>
<input type="text" data-model="business.company" class="form-control" placeholder="Acme Inc.">
</div>
<div class="mb-2">
<label class="form-label">Your Role:</label>
<input type="text" data-model="business.role" class="form-control" placeholder="CTO">
</div>
</div>
<div data-show="accountType" class="mt-3">
<button type="submit" class="btn btn-primary me-2">Submit</button>
<button type="button" data-action="resetForm" class="btn btn-secondary">Reset</button>
</div>
</form>
<div class="mt-3" data-show="result">
<div class="alert alert-info">
Submitted: <code data-bind="result"></code>
</div>
</div>
</div>
wildflower.component('dynamic-form', {
state: {
accountType: 'personal',
personal: { name: '', email: '' },
business: { company: '', role: '' },
result: ''
},
handleSubmit(event) {
event.preventDefault()
const data = this.accountType === 'personal'
? this.personal
: this.business
this.result = JSON.stringify(data)
},
resetForm() {
this.accountType = ''
this.personal = { name: '', email: '' }
this.business = { company: '', role: '' }
this.result = ''
}
})