The @Validate decorator provides form validation that runs on both client and server. It integrates seamlessly with @State and @ClientState decorators.
Usage
import { Cossack, Page, State, Validate } from '@cossackframework/core'; import { html } from '@cossackframework/renderer'; @Page({ transport: 'http' }) export class LoginForm extends Cossack { @State() @Validate({ rules: { required: true, email: true, message: 'Please enter a valid email' } }) email: string = ''; @State() @Validate({ rules: { required: true, minLength: 8, message: 'Password must be at least 8 characters' } }) password: string = ''; @State() errors: Record<string, string> = {}; render() { return html` <input .value="${this.email}" @input="${e => this.email = e.target.value}" /> ${this.hasError('email') ? html`<span>${this.getError('email')}</span>` : ''} `; } }
Validation Rules
| Rule | Type | Description |
|---|---|---|
required |
boolean |
Field cannot be empty |
minLength |
number |
Minimum string/array length |
maxLength |
number |
Maximum string/array length |
min |
number |
Minimum numeric value |
max |
number |
Maximum numeric value |
pattern |
RegExp |
Must match regex pattern |
email |
boolean |
Must be valid email format |
url |
boolean |
Must be valid URL (http/https) |
custom |
(value: any) => boolean |
Custom synchronous validator |
customAsync |
(value: any, component?: any) => Promise<boolean> |
Custom async validator |
message |
string |
Custom error message |
Validation Config
@Validate({ rules: { required: true }, config: { trigger: 'all', // 'input' | 'blur' | 'submit' | 'all' runOn: 'both', // 'client' | 'server' | 'both' errorProperty: 'errors', // Property name to store errors debounce: 0 // Debounce in ms } })
| Option | Type | Default | Description |
|---|---|---|---|
trigger |
'input' | 'blur' | 'submit' | 'all' |
'all' |
When to run validation |
runOn |
'client' | 'server' | 'both' |
'both' |
Where validation runs |
errorProperty |
string |
'errors' |
Property name to store error messages |
debounce |
number |
0 |
Debounce time in milliseconds |
Component API
this.getError(propertyName)
Returns the error message for a property, or undefined if valid.
this.getError('email') // => 'Please enter a valid email' | undefined
this.hasError(propertyName)
Returns true if the property has validation errors.
this.hasError('email') // => true | false
this.validateProperty(name)
Validates a single property. Returns a Promise that resolves to true if valid.
await this.validateProperty('email')
this.validateAll()
Validates all properties with validation rules. Returns a Promise that resolves to true if all are valid.
const isValid = await this.validateAll(); if (isValid) { // Submit form }
this.clearErrors()
Clears all validation errors.
this.clearErrors();
Async Validation with Server Methods
The customAsync rule allows async validation that can call @Server() methods via proxy. This is useful for checking unique values in a database.
@State() @Validate({ rules: { required: true, customAsync: async (value: string, component: any) => { // component gives access to @Server() methods if (!value) return true; return await component.checkUsernameAvailable(value); }, message: 'Username is already taken' } }) username: string = ''; // Server method - runs on server, can access database @Server() async checkUsernameAvailable(username: string): Promise<boolean> { const existing = await db.query('SELECT ... WHERE username = ?', [username]); return existing.length === 0; }
Note: The customAsync function receives a component parameter that gives you access to the component instance and its @Server() methods.
Complete Example
import { html } from '@cossackframework/renderer'; import { Cossack, Page, State, Validate, Client } from '@cossackframework/core'; @Page({ transport: 'http' }) export class RegistrationForm extends Cossack { @State() @Validate({ rules: { required: true, email: true, message: 'Please enter a valid email' } }) email: string = ''; @State() @Validate({ rules: { required: true, minLength: 8, message: 'Password must be at least 8 characters' } }) password: string = ''; @State() errors: Record<string, string> = {}; @State() submitted: boolean = false; @Client() handleInput(field: string, event: Event) { const target = event.target as HTMLInputElement; this.setProperty(field, target.value); this.validateProperty(field); } @Client() handleBlur(field: string, event: Event) { this.validateProperty(field); } @Client() async handleSubmit(event: Event) { event.preventDefault(); const isValid = await this.validateAll(); if (isValid) { this.submitted = true; this.requestUpdate(); } } render() { return html` <form @submit="${(e: Event) => this.handleSubmit(e)}"> <div> <input .value="${this.email}" @input="${(e: Event) => this.handleInput('email', e)}" @blur="${(e: Event) => this.handleBlur('email', e)}" /> ${this.hasError('email') ? html`<span>${this.getError('email')}</span>` : ''} </div> <div> <input type="password" .value="${this.password}" @input="${(e: Event) => this.handleInput('password', e)}" @blur="${(e: Event) => this.handleBlur('password', e)}" /> ${this.hasError('password') ? html`<span>${this.getError('password')}</span>` : ''} </div> <button type="submit">Submit</button> </form> ${this.submitted ? html`<p>Form submitted successfully!</p>` : ''} `; } }