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-success">
<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 = ''
},
})
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
Three additional input-level attributes layer on top of 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.state.formValid(boolean): whole-form validity after the most recent pass.this.state.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>
Custom validation logic
When you need richer rules (cross-field comparisons, debounced server checks, password-strength meters), write the validation 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-success">
<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-success">
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 = ''
}
})