Models
Reactive data models with pub/sub events, validation, and two-way DOM binding.
Pub/Sub Events
A simple but powerful event system for decoupled communication between components.
subscribe()
publish()
unsubscribe()
once()
on()
off()
emit()
// Subscribe to an event
M.subscribe('user:login', (data) => {
console.log('User logged in:', data.username);
});
// Aliases
M.on('user:login', callback); // Same as subscribe
// Publish an event
M.publish('user:login', { username: 'alice' });
M.emit('user:login', data); // Same as publish
// Unsubscribe
M.unsubscribe('user:login', callback);
M.off('user:login', callback);
// One-time subscription
M.once('app:ready', () => {
console.log('App initialized!');
});
// Namespace events
M.on('cart:add', handler);
M.on('cart:remove', handler);
M.on('cart:clear', handler);
Subscribed to: nothing yet
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="form-label" for="event-name">Event name</label>
<input type="text" id="event-name" class="form-input mb-3" data-model="eventName.value" placeholder="my:event">
<label class="form-label" for="event-data">Data (JSON, or plain text)</label>
<input type="text" id="event-data" class="form-input mb-3" data-model="eventData.value">
<div class="flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary btn-sm" data-on-click="subscribe">Subscribe</button>
<button type="button" class="btn btn-success btn-sm" data-on-click="publish">Publish</button>
<button type="button" class="btn btn-danger btn-sm" data-on-click="unsubscribe">Unsubscribe</button>
<button type="button" class="btn btn-secondary btn-sm" data-on-click="clear">Clear log</button>
</div>
<p class="text-sm text-muted mt-2">Subscribed to: <span data-bind-text="subscribedText.value">nothing yet</span></p>
</div>
<div class="models-event-log" role="log" aria-live="polite">
<div class="text-muted" data-if="log.value.length === 0">The event log appears here.</div>
<div data-each="log.value key=id">
<div>
<span class="models-event-log-timestamp" data-bind-text="time"></span>
<span data-bind-class="tone" data-bind-text="text"></span>
</div>
</div>
</div>
</div>/**
* Event console - publish/subscribe with M.on / M.emit / M.off.
*
* The log is a list in the data, rendered by data-each with text bindings, so
* whatever you type is shown as text - never parsed as HTML. Every subscription
* the demo makes is removed on teardown, so Reset starts clean.
*/
export function mount(root) {
const eventName = M.observable('my:event');
const eventData = M.observable('{"message": "Hello!"}');
const log = M.observable([]);
const handlers = new Map(); // event name -> handler
const subscribed = M.observable([]);
let seq = 0;
const add = (text, tone = '') => {
log.value = [{id: ++seq, time: `[${D().format('HH:mm:ss')}]`, text, tone}, ...log.value].slice(0, 50);
};
const name = () => eventName.value.trim();
const parsed = () => { try { return JSON.parse(eventData.value); } catch { return eventData.value; } };
const handle = M.applyBindings({
eventName, eventData, log,
subscribedText: M.computed(() => (subscribed.value.length ? subscribed.value.join(', ') : 'nothing yet'))
}, root, {
methods: {
subscribe() {
const n = name();
if (!n) return add('Give the event a name first.', 'text-warning');
if (handlers.has(n)) return add(`Already subscribed to "${n}".`, 'text-warning');
const handler = (data) => add(`"${n}" received ${JSON.stringify(data)}`, 'models-event-log-event-name');
handlers.set(n, handler);
M.on(n, handler);
subscribed.value = [...handlers.keys()];
add(`Subscribed to "${n}".`, 'text-success');
},
publish() {
const n = name();
if (!n) return add('Give the event a name first.', 'text-warning');
M.emit(n, parsed());
add(`Published "${n}".`, 'text-info');
},
unsubscribe() {
const n = name();
if (!handlers.has(n)) return add(`Not subscribed to "${n}".`, 'text-warning');
M.off(n, handlers.get(n));
handlers.delete(n);
subscribed.value = [...handlers.keys()];
add(`Unsubscribed from "${n}".`, 'text-danger');
},
clear() { log.value = []; }
}
});
return () => {
for (const [n, h] of handlers) M.off(n, h);
handlers.clear();
handle.dispose();
};
}Reactive Models
Create reactive data models with blueprint validation and change detection.
// Create a model with blueprint
const user = M.create({
name: { type: 'string', required: true },
email: {
type: 'string',
required: true,
validate: v => v.includes('@') || 'Invalid email'
},
age: {
type: 'number',
min: 0,
max: 150,
default: 18
},
role: {
type: 'string',
enum: ['admin', 'user', 'guest'],
default: 'user'
}
}, { name: 'Alice', email: 'alice@example.com' });
// Get values
user.get('name') // 'Alice'
user.get('email') // 'alice@example.com'
user.toJSON() // { name: 'Alice', email: '...', age: 18, role: 'user' }
// Set values (triggers validation)
user.set('name', 'Bob');
user.set('age', 25);
// Set multiple values
user.set({ name: 'Carol', age: 30 });
// Listen for changes
user.onChange((field, newValue, oldValue) => {
console.log(`${field} changed from ${oldValue} to ${newValue}`);
});
// Listen for specific field
user.onChange('email', (newVal, oldVal) => {
console.log('Email updated');
});
// Validate
const result = user.validate();
// { valid: true, errors: {} }
// or { valid: false, errors: { email: 'Invalid email' } }
Model state
Change log
<div class="grid grid-cols-2 gap-4">
<div>
<div class="form-group">
<label class="form-label" for="model-name">Name (required)</label>
<input type="text" id="model-name" class="form-input" placeholder="Enter a name" data-model="draft.name.value">
</div>
<div class="form-group">
<label class="form-label" for="model-email">Email (required, must contain @)</label>
<input type="email" id="model-email" class="form-input" placeholder="Enter an email" data-model="draft.email.value">
<div class="models-validation-error" data-bind-text="errors.value.email || ''"></div>
</div>
<div class="form-group">
<label class="form-label" for="model-age">Age (0-150)</label>
<input type="number" id="model-age" class="form-input" placeholder="Enter an age" min="0" max="150" data-model="draft.age.value">
<div class="models-validation-error" data-bind-text="errors.value.age || ''"></div>
</div>
<div class="form-group">
<label class="form-label" for="model-role">Role</label>
<select id="model-role" class="form-select" data-model="draft.role.value">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="guest">Guest</option>
</select>
</div>
<button type="button" class="btn btn-primary" data-on-click="validate">Validate the model</button>
</div>
<div>
<h5 class="font-semibold mb-2">Model state</h5>
<div class="models-model-display">
<div class="models-model-field"><span>name:</span> <span data-bind-text="state.name || '-'">-</span></div>
<div class="models-model-field"><span>email:</span> <span data-bind-text="state.email || '-'">-</span></div>
<div class="models-model-field"><span>age:</span> <span data-bind-text="state.age === undefined || state.age === null ? '-' : state.age">-</span></div>
<div class="models-model-field"><span>role:</span> <span data-bind-text="state.role || '-'">-</span></div>
</div>
<h5 class="font-semibold mt-4 mb-2">Change log</h5>
<div class="models-event-log" role="log" aria-live="polite">
<div class="text-muted" data-if="log.value.length === 0">Changes appear here.</div>
<div data-each="log.value key=id">
<div><span class="models-event-log-timestamp" data-bind-text="time"></span> <span data-bind-class="tone" data-bind-text="text"></span></div>
</div>
</div>
</div>
</div>/**
* Reactive model - a blueprint with validation.
*
* Each input feeds a draft; the demo then tries user.set(field, value). A value
* the blueprint does not allow makes set() throw, and the message shows under
* its field - so the model only ever holds valid data, and says why when it
* refuses. "Model state" reads the model through its tracked() view, so it
* shows what the model accepted, not what was typed. onChange keeps a log.
*/
export function mount(root) {
const user = M.create({
name: {type: 'string', required: true},
email: {type: 'string', required: true, validate: v => !v || v.includes('@') || 'Must contain @'},
age: {type: 'number', min: 0, max: 150},
role: {type: 'string', enum: ['admin', 'user', 'guest'], default: 'user'}
});
const draft = {name: M.observable(''), email: M.observable(''), age: M.observable(''), role: M.observable('user')};
const errors = M.observable({});
const log = M.observable([]);
let seq = 0;
const add = (text, tone = '') => {
log.value = [{id: ++seq, time: `[${D().format('HH:mm:ss')}]`, text, tone}, ...log.value].slice(0, 30);
};
const setError = (field, message) => { errors.value = {...errors.value, [field]: message}; };
// Try each draft on the model as it changes (not on mount: an empty form is not an error yet).
const stops = Object.entries(draft).map(([field, obs]) => {
let first = true;
return M.effect(() => {
const raw = obs.value;
if (first) { first = false; return; }
const value = field === 'age' ? (raw === '' ? undefined : Number(raw)) : raw;
M.untracked(() => {
try {
user.set(field, value);
setError(field, '');
} catch (err) {
setError(field, String(err.message).replace(/^Validation failed for \w+:\s*/, ''));
}
});
});
});
const stopLog = user.onChange(({field, newValue, oldValue}) => add(`${field}: "${oldValue ?? ''}" → "${newValue ?? ''}"`));
const handle = M.applyBindings({draft, errors, log, state: user.tracked()}, root, {
methods: {
validate() {
const result = user.validate();
if (result.valid) add('Validation passed - the model holds valid data.', 'text-success');
else add(`Validation failed: ${Object.entries(result.errors).map(([f, m]) => `${f} (${m})`).join(', ')}`, 'text-danger');
}
}
});
return () => {
stops.forEach(stop => stop?.());
if (typeof stopLog === 'function') stopLog();
handle.dispose();
};
}Blueprint Types
Built-in type validators for common data types.
// Available types
M.types.string // String values
M.types.number // Numeric values
M.types.boolean // true/false
M.types.array // Arrays
M.types.object // Plain objects
M.types.date // Date objects
M.types.any // Any type (no validation)
// Blueprint options
const blueprint = {
// Basic type
name: { type: 'string' },
// Required field
email: { type: 'string', required: true },
// Default value
status: { type: 'string', default: 'active' },
// Number constraints
age: { type: 'number', min: 0, max: 120 },
price: { type: 'number', min: 0 },
// String constraints
username: { type: 'string', minLength: 3, maxLength: 20 },
// Enum values
role: { type: 'string', enum: ['admin', 'user', 'guest'] },
// Custom validation
password: {
type: 'string',
validate: (value) => {
if (value.length < 8) return 'Min 8 characters';
if (!/[A-Z]/.test(value)) return 'Need uppercase';
if (!/[0-9]/.test(value)) return 'Need number';
return true;
}
},
// Array of items
tags: { type: 'array' },
// Nested object
address: { type: 'object' }
};
Two-Way DOM Binding
Keep data and the page in step automatically.
The recommended way is declarative: the markup says what each element shows (data-model for two-way,
data-bind-text for one-way) and M.applyBindings(data, root) keeps it true - the live demo below works that way,
with a computed value for the greeting. M.bind() is the imperative form, wiring one model field to one element; it
is shown in the code here and still works.
Need derived values? Reactivity covers
M.computed()andM.effect()- dependency tracking that re-runs only the work affected by a change, instead of wiring uponChangeby hand.
Binding a whole template? Template Bindings covers
data-bind-*,data-model,data-on-*,data-ifand keyed{{#each}}lists - declared in the markup, so there is no render function to remember to call.
Working with Nested Data? Check out our Dot Notation with Forms guide for handling complex nested structures using
_.get()and_.set()utilities.
const user = M.create({
name: { type: 'string' },
email: { type: 'string' }
}, { name: 'Alice' });
// One-way binding (model -> DOM)
M.bind(user, 'name', '#name-display');
// Two-way binding (model <-> DOM)
M.bind(user, 'name', '#name-input', { twoWay: true });
// With formatter
M.bind(user, 'name', '#greeting', {
format: (value) => `Hello, ${value}!`
});
// Multiple bindings
M.bind(user, 'name', '#display1');
M.bind(user, 'name', '#display2');
M.bind(user, 'name', '#display3');
// All update when model changes
user.set('name', 'Bob'); // All three elements update
// Unbind
M.unbind(user, 'name', '#name-display');
The input and both displays read the same value. Change it anywhere and every part of the page that uses it follows.
<div class="grid grid-cols-2 gap-4">
<div>
<label class="form-label" for="bound-input">Type your name (two-way)</label>
<input type="text" id="bound-input" class="form-input" placeholder="Type here…" data-model="name.value">
<p class="text-sm text-muted mt-2">The input and both displays read the same value. Change it anywhere and every
part of the page that uses it follows.</p>
</div>
<div>
<span class="form-label">Live display (one-way)</span>
<div class="models-bound-display" data-bind-text="name.value">Guest</div>
<div class="mt-3 text-center">
<span class="text-sm text-muted">Formatted:</span>
<div class="text-primary font-semibold" data-bind-text="greeting.value">Hello, Guest!</div>
</div>
<button type="button" class="btn btn-sm btn-ghost mt-3" data-on-click="reset">Reset</button>
</div>
</div>/**
* Live binding - one value, three places.
*
* The markup says what each part shows (data-model, data-bind-text); the
* greeting is a computed, so it can never disagree with the name it is made
* from. M.applyBindings keeps the page true; dispose() undoes it.
*/
export function mount(root) {
const name = M.observable('Guest');
const greeting = M.computed(() => `Hello, ${name.value.trim() || 'Guest'}!`);
const handle = M.applyBindings({name, greeting}, root, {
methods: {
reset: () => { name.value = 'Guest'; }
}
});
return () => handle.dispose();
}Practical Example: Shopping Cart
A complete example using pub/sub and reactive models together.
// Cart model
const cart = M.create({
items: { type: 'array', default: [] },
total: { type: 'number', default: 0 }
});
// Subscribe to cart events
M.on('cart:add', (item) => {
const items = cart.get('items');
items.push(item);
cart.set('items', items);
cart.set('total', calculateTotal(items));
});
M.on('cart:remove', (index) => {
const items = cart.get('items');
items.splice(index, 1);
cart.set('items', items);
cart.set('total', calculateTotal(items));
});
// UI updates on cart changes
cart.onChange('total', (newTotal) => {
$('#cart-total').text('$' + newTotal.toFixed(2));
});
cart.onChange('items', (items) => {
$('#cart-count').text(items.length);
});
// Add item from anywhere in the app
$('.add-to-cart').on('click', function() {
const product = {
name: $(this).data('name'),
price: $(this).data('price')
};
M.emit('cart:add', product);
});
Shopping cart (0 items)
The cart is empty.
<div class="grid grid-cols-3 gap-4 mb-4" data-each="products key=name">
<div class="card">
<div class="card-body text-center">
<h5 class="font-semibold" data-bind-text="name"></h5>
<p class="text-muted" data-bind-text="priceText"></p>
<button type="button" class="btn btn-primary btn-sm" data-on-click="$root.add($data)">Add to cart</button>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="flex justify-between items-center mb-3">
<h5 class="font-semibold">Shopping cart (<span data-bind-text="items.value.length">0</span> items)</h5>
<button type="button" class="btn btn-outline btn-sm" data-on-click="clear" data-bind-disabled="items.value.length === 0">Clear cart</button>
</div>
<p class="text-muted" data-if="items.value.length === 0">The cart is empty.</p>
<div class="mb-3" data-each="items.value key=id">
<div class="flex justify-between items-center py-2 border-b">
<span data-bind-text="name"></span>
<span>
<span class="mr-2" data-bind-text="priceText"></span>
<button type="button" class="btn btn-danger btn-sm" data-on-click="$root.remove($data)" aria-label="Remove">×</button>
</span>
</div>
</div>
<div class="flex justify-between items-center border-t pt-3">
<span class="font-semibold">Total:</span>
<span class="text-xl font-bold text-primary" data-bind-text="totalText.value">£0.00</span>
</div>
</div>
</div>/**
* Shopping cart - events and derived values.
*
* Buttons publish cart:add / cart:remove / cart:clear; subscribers change the
* list; the total is a computed, so it can never disagree with the items.
* Products and cart rows are bound lists (data-each), so there is no render
* function. The subscriptions are removed on teardown.
*/
export function mount(root) {
const money = (n) => `£${n.toFixed(2)}`;
const products = [
{name: 'Widget A', price: 9.99},
{name: 'Widget B', price: 19.99},
{name: 'Widget C', price: 29.99}
].map(p => ({...p, priceText: money(p.price)}));
const items = M.observable([]);
const totalText = M.computed(() => money(items.value.reduce((sum, i) => sum + i.price, 0)));
let seq = 0;
const subs = [
['cart:add', (product) => { items.value = [...items.value, {...product, id: ++seq}]; }],
['cart:remove', (id) => { items.value = items.value.filter(i => i.id !== id); }],
['cart:clear', () => { items.value = []; }]
];
subs.forEach(([event, handler]) => M.on(event, handler));
// The handlers live on the data, so rows reach them as $root.add / $root.remove.
const handle = M.applyBindings({
products, items, totalText,
add: (product) => M.emit('cart:add', product),
remove: (item) => M.emit('cart:remove', item.id),
clear: () => M.emit('cart:clear')
}, root);
return () => {
subs.forEach(([event, handler]) => M.off(event, handler));
handle.dispose();
};
}Practical Example: User Management
A complete CRUD example with model-based form validation and clipboard export.
// Users collection model
const usersModel = M.create({
users: { type: 'array', default: [] }
});
// Form validation model
const formModel = M.create({
name: { type: 'string', required: true },
email: {
type: 'string',
required: true,
validate: v => v.includes('@') || 'Invalid email'
},
role: { type: 'string', enum: ['user', 'admin', 'editor'] },
status: { type: 'string', enum: ['active', 'inactive'] }
});
// Add user
formModel.set({ name: 'Alice', email: 'alice@example.com', role: 'admin', status: 'active' });
if (formModel.validate().valid) {
const users = [...usersModel.get('users'), formModel.toJSON()];
usersModel.set('users', users);
}
// Export to clipboard
navigator.clipboard.writeText(JSON.stringify(usersModel.get('users'), null, 2));
Users (0)
No users yet. Add one with the form.
<div class="grid grid-cols-2 gap-4">
<form data-on-submit="save">
<h5 class="font-semibold mb-2" data-bind-text="state.editing !== null ? 'Edit user' : 'Add a user'">Add a user</h5>
<div class="form-group">
<label class="form-label" for="um-name">Name (required)</label>
<input type="text" id="um-name" class="form-input" placeholder="Enter a name" data-model="state.name">
<div class="models-validation-error" data-bind-text="state.errors.name || ''"></div>
</div>
<div class="form-group">
<label class="form-label" for="um-email">Email (required)</label>
<input type="email" id="um-email" class="form-input" placeholder="Enter an email" data-model="state.email">
<div class="models-validation-error" data-bind-text="state.errors.email || ''"></div>
</div>
<div class="form-group">
<label class="form-label" for="um-role">Role</label>
<select id="um-role" class="form-select" data-model="state.role">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="um-status">Status</label>
<select id="um-status" class="form-select" data-model="state.status">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="flex gap-2">
<button type="submit" class="btn btn-primary" data-bind-text="state.editing !== null ? 'Update user' : 'Add user'">Add user</button>
<button type="button" class="btn btn-secondary" data-if="state.editing !== null" data-on-click="cancel">Cancel</button>
</div>
</form>
<div>
<div class="flex justify-between items-center mb-2">
<h5 class="font-semibold">Users (<span data-bind-text="state.users.length">0</span>)</h5>
<button type="button" class="btn btn-outline btn-sm" data-on-click="copy" data-bind-disabled="state.users.length === 0">Copy as JSON</button>
</div>
<div class="models-user-list">
<p class="text-muted" data-if="state.users.length === 0">No users yet. Add one with the form.</p>
<div data-each="state.users key=uid">
<div class="models-user-item">
<div class="models-user-info">
<div class="models-user-name" data-bind-text="name"></div>
<div class="models-user-meta">
<span data-bind-text="email + ' · ' + role"></span>
<span class="models-status-badge" data-bind-class="'models-status-' + status" data-bind-text="status"></span>
</div>
</div>
<div class="models-user-actions">
<button type="button" class="btn btn-outline btn-sm" data-on-click="$root.edit($data)">Edit</button>
<button type="button" class="btn btn-danger btn-sm" data-on-click="$root.remove($data)">Remove</button>
</div>
</div>
</div>
</div>
</div>
</div>/**
* User manager - add, edit and remove, all as bindings.
*
* One model holds the form fields, the list, the errors and which user is being
* edited; the bindings read and write it through its tracked() view. The heading and the save button follow the
* editing field; each row's Edit and Remove call the methods with the row.
* Names and emails are shown with text bindings, so nothing typed is ever
* parsed as HTML.
*/
export function mount(root) {
const model = M.create({
name: {type: 'string', default: ''},
email: {type: 'string', default: ''},
role: {type: 'string', default: 'user'},
status: {type: 'string', default: 'active'},
users: {type: 'array', default: []},
errors: {type: 'object', default: {}},
editing: {type: 'any', default: null} // the uid being edited, or null
});
const FORM = {name: '', email: '', role: 'user', status: 'active'};
let seq = 0;
const resetForm = () => {
Object.entries(FORM).forEach(([k, v]) => model.set(k, v));
model.set('errors', {});
model.set('editing', null);
};
// The bindings' data: the model's tracked view (reads tracked, writes through set()),
// and the handlers - on the data itself, so a list row reaches them as $root.edit.
const handle = M.applyBindings({
state: model.tracked(),
save() {
const data = {name: model.get('name').trim(), email: model.get('email').trim(), role: model.get('role'), status: model.get('status')};
const e = {};
if (!data.name) e.name = 'A name is needed.';
if (!data.email) e.email = 'An email is needed.';
else if (!data.email.includes('@')) e.email = 'That is not an email address.';
model.set('errors', e);
if (Object.keys(e).length) return false;
const editing = model.get('editing');
model.set('users', editing === null
? [...model.get('users'), {...data, uid: ++seq}]
: model.get('users').map(u => (u.uid === editing ? {...data, uid: u.uid} : u)));
resetForm();
return false; // no page submit
},
edit(user) {
Object.keys(FORM).forEach(k => model.set(k, user[k]));
model.set('errors', {});
model.set('editing', user.uid);
},
remove(user) {
model.set('users', model.get('users').filter(u => u.uid !== user.uid));
if (model.get('editing') === user.uid) resetForm();
},
cancel: resetForm,
async copy() {
try {
await _.copyToClipboard(JSON.stringify(model.get('users').map(({uid, ...u}) => u), null, 2));
E.toast('Users copied as JSON.', {type: 'success'});
} catch {
E.toast('Could not copy to the clipboard.', {type: 'error'});
}
}
}, root);
return () => handle.dispose();
}Complete Method Reference
Event system for decoupled communication between components.
Static methods on the M/Models namespace.
Methods available on model instances returned by M.create().
Built-in validators for blueprint definitions.
<div id="method-tabs" class="tabs">
<div class="tab-list">
<button type="button" class="tab-item active" data-tab="pubsub">Pub/Sub</button>
<button type="button" class="tab-item" data-tab="factory">Model Factory</button>
<button type="button" class="tab-item" data-tab="instance">Instance Methods</button>
<button type="button" class="tab-item" data-tab="types">Type Validators</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel" data-tab="pubsub">
<p class="text-muted mb-3">Event system for decoupled communication between components.</p>
<div class="models-method-grid">
<span class="models-method-item">subscribe()</span>
<span class="models-method-item">publish()</span>
<span class="models-method-item">unsubscribe()</span>
<span class="models-method-item">once()</span>
<span class="models-method-item">on()</span>
<span class="models-method-item">off()</span>
<span class="models-method-item">emit()</span>
</div>
</div>
<div class="tab-panel" role="tabpanel" data-tab="factory">
<p class="text-muted mb-3">Static methods on the M/Models namespace.</p>
<div class="models-method-grid">
<span class="models-method-item">create()</span>
<span class="models-method-item">bind()</span>
<span class="models-method-item">unbind()</span>
</div>
</div>
<div class="tab-panel" role="tabpanel" data-tab="instance">
<p class="text-muted mb-3">Methods available on model instances returned by M.create().</p>
<div class="models-method-grid">
<span class="models-method-item">get()</span>
<span class="models-method-item">set()</span>
<span class="models-method-item">toJSON()</span>
<span class="models-method-item">validate()</span>
<span class="models-method-item">onChange()</span>
<span class="models-method-item">offChange()</span>
<span class="models-method-item">reset()</span>
</div>
</div>
<div class="tab-panel" role="tabpanel" data-tab="types">
<p class="text-muted mb-3">Built-in validators for blueprint definitions.</p>
<div class="models-method-grid">
<span class="models-method-item">types.string</span>
<span class="models-method-item">types.number</span>
<span class="models-method-item">types.boolean</span>
<span class="models-method-item">types.array</span>
<span class="models-method-item">types.object</span>
<span class="models-method-item">types.date</span>
<span class="models-method-item">types.any</span>
</div>
</div>
</div>
</div>/**
* Complete Method Reference - from public/showcase/models.
*/
export function mount(root) {
// Method Reference Tabs
E.tabs(root.querySelector('#method-tabs'), {animation: true});
}