Models

Reactive data models with pub/sub events, validation, and two-way DOM binding.

M or Domma.models

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);

Try It - Event Console

Event log will appear here...

Reactive Models

Create reactive data models with schema validation and change detection.

// Create a model with schema
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' } }

Try It - User Model

Model State
name: -
email: -
age: -
role: -
Change Log
Changes will appear here...

Schema 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)

// Schema options
const schema = {
    // 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

Automatically sync model data with DOM elements.

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');

Try It - Live Binding

The input and display are both bound to the same model field. Changes in either direction are synced.

Type something...
Formatted:
Hello, Guest!

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);
});

Try It - Mini Cart

Widget A

$9.99

Widget B

$19.99

Widget C

$29.99

Shopping Cart (0 items)

Cart is empty

Total: $0.00

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));

Try It - User Manager

Add New User
Users (0)

No users yet. Add one using the form.

Complete Method Reference

Event system for decoupled communication between components.

subscribe() publish() unsubscribe() once() on() off() emit()

Static methods on the M/Models namespace.

create() bind() unbind()

Methods available on model instances returned by M.create().

get() set() toJSON() validate() onChange() offChange() reset()

Built-in validators for schema definitions.

types.string types.number types.boolean types.array types.object types.date types.any