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>` : ''}
`;
}
}