createModel
The createModel
service creates a new model instance with custom validation rules and behaviors. It's primarily used for form validation and data processing in Pinstripe applications.
Interface
this.createModel(definition) -> Model
Parameters
definition
(Object): An object containing model definition with validation rules and behaviorsmeta()
(Function): Defines validation rules and model behaviors
Returns
A new Model
instance with the specified validation rules and behaviors.
Description
The createModel
service is a factory function that creates ephemeral model instances for form validation and data processing. Unlike database models, these models exist only in memory and are primarily used to:
- Define validation rules for form inputs
- Handle custom validation logic
- Provide structured error handling
- Process form data with validation
The service extends the base Model
class and includes the Hookable
mixin, allowing you to define validation rules using lifecycle hooks like on('validation', ...)
.
Built-in Validation Methods
Models created with createModel
have access to several built-in validation methods:
mustNotBeBlank(fieldName, options)
Validates that a field is not empty or whitespace-only.
this.mustNotBeBlank('email');
this.mustNotBeBlank('name', { message: 'Name is required' });
mustMatchPattern(fieldName, pattern, options)
Validates that a field matches a regular expression pattern.
this.mustMatchPattern('phone', /^\d{10}$/);
this.mustMatchPattern('code', /^[A-Z]{3}$/, { message: 'Must be 3 uppercase letters' });
mustBeAValidEmail(fieldName, options)
Validates that a field contains a valid email address.
this.mustBeAValidEmail('email');
this.mustBeAValidEmail('contactEmail', { message: 'Please enter a valid email address' });
Validation Lifecycle Hooks
on('validation', callback)
Custom validation logic that runs during the validation process.
this.on('validation', function() {
if (!this.isValidationError('password') && this.password.length < 8) {
this.setValidationError('password', 'Password must be at least 8 characters');
}
});
on('before:validation', callback)
Runs before validation starts, useful for data preprocessing.
this.on('before:validation', function() {
this.email = (this.email || '').toLowerCase().trim();
});
on('after:validation', callback)
Runs after validation completes successfully.
Error Handling Methods
setValidationError(fieldName, message)
Manually sets a validation error for a specific field.
isValidationError(fieldName)
Checks if a specific field has a validation error.
validationErrors
Returns an object containing all validation errors.
Examples
Basic Form Validation
this.createModel({
meta() {
this.mustNotBeBlank('name');
this.mustBeAValidEmail('email');
}
})
Authentication Form with Custom Validation
this.createModel({
meta() {
this.mustNotBeBlank('email');
this.mustBeAValidEmail('email');
this.on('validation', function() {
if (!this.isValidationError('legal') && this.legal != 'true') {
this.setValidationError('legal', 'You must agree to the terms of service.');
}
});
}
})
Password Verification with Async Validation
this.createModel({
meta() {
this.mustNotBeBlank('password');
this.on('validation', async function() {
if (!this.isValidationError('password')) {
const user = await that.database.users.where({ email }).first();
if (user && !await user.verifyPassword(this.password)) {
this.setValidationError('general', 'Your password is incorrect.');
}
}
});
}
})
Complex Form Validation
this.createModel({
meta() {
this.mustNotBeBlank('title');
this.mustNotBeBlank('content');
this.mustMatchPattern('slug', /^[a-z0-9-]+$/);
this.on('before:validation', function() {
// Auto-generate slug from title if not provided
if (!this.slug && this.title) {
this.slug = this.title.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
});
this.on('validation', async function() {
// Check for duplicate slugs
if (!this.isValidationError('slug')) {
const existing = await that.database.posts.where({ slug: this.slug }).first();
if (existing) {
this.setValidationError('slug', 'This slug is already taken.');
}
}
});
}
})
Conditional Validation
this.createModel({
meta() {
this.mustNotBeBlank('email');
// Only require password for new users
this.mustNotBeBlank('password', {
when: function() { return this.isNewUser; }
});
this.on('validation', function() {
if (this.subscriptionType === 'premium' && !this.paymentMethod) {
this.setValidationError('paymentMethod', 'Payment method required for premium subscription.');
}
});
}
})
Data Preprocessing
this.createModel({
meta() {
this.on('before:validation', function() {
// Normalize email
this.email = (this.email || '').toLowerCase().trim();
// Format phone number
if (this.phone) {
this.phone = this.phone.replace(/\D/g, '');
}
// Set default values
if (!this.role) {
this.role = 'user';
}
});
this.mustBeAValidEmail('email');
this.mustMatchPattern('phone', /^\d{10}$/, { message: 'Phone must be 10 digits' });
}
})
Multi-step Form Validation
this.createModel({
meta() {
// Step 1: Basic info
if (this.step === 1) {
this.mustNotBeBlank('firstName');
this.mustNotBeBlank('lastName');
this.mustBeAValidEmail('email');
}
// Step 2: Address
if (this.step === 2) {
this.mustNotBeBlank('address');
this.mustNotBeBlank('city');
this.mustMatchPattern('zipCode', /^\d{5}$/);
}
// Step 3: Payment
if (this.step === 3) {
this.mustNotBeBlank('cardNumber');
this.mustMatchPattern('cardNumber', /^\d{16}$/);
this.mustNotBeBlank('expiryDate');
}
}
})
Usage with renderForm
The createModel
service is typically used in conjunction with renderForm
to provide validation for form submissions:
return this.renderForm(
this.createModel({
meta() {
this.mustNotBeBlank('title');
this.mustBeAValidEmail('email');
}
}),
{
title: 'Contact Form',
fields: [
{ name: 'title', label: 'Subject' },
{ name: 'email', label: 'Your Email' },
{ name: 'message', type: 'textarea', label: 'Message' }
],
success: async (data) => {
// Process validated form data
await this.sendEmail(data);
return this.renderRedirect({ url: '/thank-you' });
}
}
);
The model handles all validation automatically, and the success
callback only runs if validation passes.
Best Practices
- Keep validation logic in the meta() function - This ensures all validation rules are defined in one place
- Use descriptive error messages - Provide clear feedback to users about what went wrong
- Leverage built-in validators - Use
mustNotBeBlank
,mustBeAValidEmail
, etc. before writing custom validation - Handle async validation carefully - Use async/await properly in validation hooks
- Preprocess data in before:validation - Normalize and clean data before validation runs
- Use conditional validation - Apply validation rules only when they're relevant
- Group related validations - Keep related validation logic together for maintainability
The createModel
service provides a powerful and flexible way to handle form validation and data processing in Pinstripe applications, with built-in support for common validation patterns and extensibility for custom requirements.