Utilities

Lodash-compatible utility functions with 120+ methods for arrays, objects, strings, and more.

Array Methods

Manipulate and transform arrays with powerful utility functions.

chunk()

compact()

concat()

difference()

drop()

dropRight()

dropWhile()

fill()

flatten()

flattenDeep()

flattenDepth()

fromPairs()

head()

indexOf()

initial()

intersection()

join()

last()

lastIndexOf()

nth()

pull()

pullAt()

reverse()

slice()

tail()

take()

takeRight()

union()

uniq()

uniqBy()

without()

xor()

zip()

zipObject()

// Chunk array into groups
_.chunk([1, 2, 3, 4, 5], 2)  // [[1, 2], [3, 4], [5]]

// Remove falsy values
_.compact([0, 1, false, 2, '', 3])  // [1, 2, 3]

// Array difference
_.difference([1, 2, 3], [2, 3])  // [1]

// Flatten nested arrays
_.flatten([[1, 2], [3, [4]]])  // [1, 2, 3, [4]]
_.flattenDeep([[1, [2, [3]]]])  // [1, 2, 3]

// Get unique values
_.uniq([1, 2, 2, 3, 3, 3])  // [1, 2, 3]
_.uniqBy([{x:1}, {x:1}, {x:2}], 'x')  // [{x:1}, {x:2}]

// Combine arrays
_.union([1, 2], [2, 3])  // [1, 2, 3]
_.intersection([1, 2], [2, 3])  // [2]

// Array to object
_.zipObject(['a', 'b'], [1, 2])  // {a: 1, b: 2}
_.fromPairs([['a', 1], ['b', 2]])  // {a: 1, b: 2}
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-util="chunk">chunk([1,2,3,4,5], 2)</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="compact">compact([0,1,false,2])</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="uniq">uniq([1,2,2,3,3])</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="flatten">flattenDeep([[1,[2,[3]]]])</button>
</div>
<div class="utils-demo-result" id="array-result">Click a button to see result</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
    const users = [
            {name: 'Alice', age: 25, dept: 'Engineering'},
            {name: 'Bob', age: 30, dept: 'Sales'},
            {name: 'Carol', age: 25, dept: 'Engineering'},
            {name: 'Dave', age: 35, dept: 'Marketing'}
        ];
        // Array demos
    $('[data-util]').on('click', function () {
            const util = $(this).data('util');
            let result;

            switch (util) {
                case 'chunk':
                    result = _.chunk([1, 2, 3, 4, 5], 2);
                    break;
                case 'compact':
                    result = _.compact([0, 1, false, 2, '', 3]);
                    break;
                case 'uniq':
                    result = _.uniq([1, 2, 2, 3, 3, 3]);
                    break;
                case 'flatten':
                    result = _.flattenDeep([[1, [2, [3]]]]);
                    break;
                case 'groupBy':
                    result = _.groupBy(users, 'dept');
                    break;
                case 'sortBy':
                    result = _.sortBy(users, 'age');
                    break;
                case 'filter':
                    result = _.filter(users, {dept: 'Engineering'});
                    break;
                case 'countBy':
                    result = _.countBy(users, 'dept');
                    break;
            }

            const targetId = ['chunk', 'compact', 'uniq', 'flatten'].includes(util) ? 'array-result' : 'collection-result';
            $('#' + targetId).text(JSON.stringify(result, null, 2));
        });
}

Collection Methods

Iterate, search, and transform collections (arrays and objects).

countBy()

each()

every()

filter()

find()

findIndex()

findLast()

flatMap()

forEach()

groupBy()

includes()

keyBy()

map()

orderBy()

partition()

reduce()

reduceRight()

reject()

sample()

sampleSize()

shuffle()

size()

some()

sortBy()

const users = [
    { name: 'Alice', age: 25, dept: 'Engineering' },
    { name: 'Bob', age: 30, dept: 'Sales' },
    { name: 'Carol', age: 25, dept: 'Engineering' }
];

// Group by property
_.groupBy(users, 'dept')
// { Engineering: [...], Sales: [...] }

// Sort by property
_.sortBy(users, 'age')
_.orderBy(users, ['dept', 'name'], ['asc', 'desc'])

// Filter and find
_.filter(users, { dept: 'Engineering' })
_.find(users, { name: 'Bob' })

// Key by unique property
_.keyBy(users, 'name')
// { Alice: {...}, Bob: {...}, Carol: {...} }

// Partition by condition
_.partition(users, u => u.age > 25)
// [[Bob], [Alice, Carol]]

// Count by property
_.countBy(users, 'dept')
// { Engineering: 2, Sales: 1 }
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-util="groupBy">groupBy(users, 'dept')</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="sortBy">sortBy(users, 'age')</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="filter">filter(users, {dept:'Engineering'})</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="countBy">countBy(users, 'dept')</button>
</div>
<div class="utils-demo-result" id="collection-result">Click a button to see result</div>

Object Methods

Work with objects - access, transform, and manipulate properties.

assign()

at()

defaults()

defaultsDeep()

entries()

findKey()

forIn()

forOwn()

get()

has()

invert()

keys()

mapKeys()

mapValues()

merge()

omit()

omitBy()

pick()

pickBy()

set()

toPairs()

unset()

values()

const obj = {
    user: { name: 'Alice', address: { city: 'NYC' } },
    settings: { theme: 'dark' }
};

// Safe property access
_.get(obj, 'user.name')  // 'Alice'
_.get(obj, 'user.phone', 'N/A')  // 'N/A' (default)
_.get(obj, 'user.address.city')  // 'NYC'

// Set nested property
_.set(obj, 'user.address.zip', '10001')

// Check if path exists
_.has(obj, 'user.name')  // true
_.has(obj, 'user.email')  // false

// Pick/omit properties
_.pick(obj.user, ['name'])  // { name: 'Alice' }
_.omit(obj.user, ['address'])  // { name: 'Alice' }

// Merge objects
_.merge({a: 1}, {b: 2}, {a: 3})  // {a: 3, b: 2}
_.defaults({a: 1}, {a: 2, b: 2})  // {a: 1, b: 2}

// Transform keys/values
_.mapKeys({a: 1, b: 2}, (v, k) => k.toUpperCase())  // {A: 1, B: 2}
_.mapValues({a: 1, b: 2}, v => v * 2)  // {a: 2, b: 4}

// Invert object
_.invert({a: 1, b: 2})  // {1: 'a', 2: 'b'}
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-util="get">get(obj, 'user.address.city')</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="pick">pick(user, ['name'])</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="merge">merge({a:1}, {b:2})</button>
    <button type="button" class="btn btn-outline btn-sm" data-util="invert">invert({a:1, b:2})</button>
</div>
<div class="utils-demo-result" id="object-result">Click a button to see result</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
    const obj = {
            user: {name: 'Alice', address: {city: 'NYC'}},
            settings: {theme: 'dark'}
        };
        // Object demos
    $('[data-util="get"]').on('click', function () {
            const result = _.get(obj, 'user.address.city');
            $(root).find('#object-result').text(`_.get(obj, 'user.address.city') = "${result}"`);
        });
    $('[data-util="pick"]').on('click', function () {
            const result = _.pick(obj.user, ['name']);
            $(root).find('#object-result').text(JSON.stringify(result, null, 2));
        });
    $('[data-util="merge"]').on('click', function () {
            const result = _.merge({a: 1}, {b: 2}, {c: 3});
            $(root).find('#object-result').text(JSON.stringify(result, null, 2));
        });
    $('[data-util="invert"]').on('click', function () {
            const result = _.invert({a: '1', b: '2', c: '3'});
            $(root).find('#object-result').text(JSON.stringify(result, null, 2));
        });
}

Function Methods

Control function execution with debounce, throttle, memoize, and more.

after()

ary()

before()

bind()

curry()

curryRight()

debounce()

defer()

delay()

flip()

memoize()

negate()

once()

partial()

partialRight()

throttle()

unary()

wrap()

// Debounce - wait for pause in calls
const search = _.debounce((query) => {
    console.log('Searching:', query);
}, 300);

input.addEventListener('input', e => search(e.target.value));

// Throttle - limit call frequency
const scroll = _.throttle(() => {
    console.log('Scroll position:', window.scrollY);
}, 100);

window.addEventListener('scroll', scroll);

// Memoize - cache results
const expensive = _.memoize((n) => {
    console.log('Computing...');
    return n * n;
});
expensive(5);  // Computing... 25
expensive(5);  // 25 (cached)

// Once - only call once
const init = _.once(() => console.log('Initialized!'));
init();  // Initialized!
init();  // (nothing)

// Curry - partial application
const add = _.curry((a, b, c) => a + b + c);
add(1)(2)(3);  // 6
add(1, 2)(3);  // 6
add(1)(2, 3);  // 6
Debounce
Start typing to see debounce in action...
<input type="text" id="debounce-input" class="form-input mb-2" placeholder="Type here (debounced)">
<div class="utils-demo-result" id="debounce-result">Start typing to see debounce in action...</div>
/**
 * Debounce - from public/showcase/utils.
 */
export function mount(root) {
        // Debounce demo
    const debouncedSearch = _.debounce(function (value) {
            $(root).find('#debounce-result').text(`Debounced callback fired with: "${value}"`);
        }, 500);
    $(root).find('#debounce-input').on('input', function () {
            const value = $(this).val();
            $(root).find('#debounce-result').text(`Typing... (waiting for 500ms pause)`);
            debouncedSearch(value);
        });
}

String Methods

Transform and manipulate strings.

camelCase()

capitalize()

endsWith()

escape()

kebabCase()

lowerCase()

lowerFirst()

pad()

padEnd()

padStart()

parseInt()

repeat()

replace()

snakeCase()

split()

startCase()

startsWith()

template()

toLower()

toUpper()

trim()

trimEnd()

trimStart()

truncate()

unescape()

upperCase()

upperFirst()

words()

// Case conversion
_.camelCase('hello world')    // 'helloWorld'
_.kebabCase('helloWorld')     // 'hello-world'
_.snakeCase('Hello World')    // 'hello_world'
_.startCase('helloWorld')     // 'Hello World'

// Capitalize
_.capitalize('hello')         // 'Hello'
_.upperFirst('hello')         // 'Hello'
_.lowerFirst('HELLO')         // 'hELLO'

// Padding
_.pad('abc', 8)               // '  abc   '
_.padStart('5', 3, '0')       // '005'
_.padEnd('5', 3, '0')         // '500'

// Truncate
_.truncate('Hello World', { length: 8 })  // 'Hello...'

// Template
const tpl = _.template('Hello, <%= name %>!');
tpl({ name: 'Alice' })        // 'Hello, Alice!'

// Escape HTML
_.escape('')            // '<div>'

// Words
_.words('fred, barney, & pebbles')  // ['fred', 'barney', 'pebbles']
Try It
Click a button to transform the string
<input type="text" id="string-input" class="form-input mb-3" value="hello world example">
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-string="camelCase">camelCase</button>
    <button type="button" class="btn btn-outline btn-sm" data-string="kebabCase">kebabCase</button>
    <button type="button" class="btn btn-outline btn-sm" data-string="snakeCase">snakeCase</button>
    <button type="button" class="btn btn-outline btn-sm" data-string="startCase">startCase</button>
    <button type="button" class="btn btn-outline btn-sm" data-string="capitalize">capitalize</button>
</div>
<div class="utils-demo-result" id="string-result">Click a button to transform the string</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
        // String demos
    $('[data-string]').on('click', function () {
            const method = $(this).data('string');
            const input = $(root).find('#string-input').val();
            const result = _[method](input);
            $(root).find('#string-result').text(`_.${method}("${input}") = "${result}"`);
        });
}

Template Engine

Mustache-style templates with loops, conditionals, partials, and automatic HTML escaping.

template()

render()

Basic Usage

// Compile and reuse
const tmpl = _.template('Hello {{name}}!');
tmpl({ name: 'World' });  // 'Hello World!'

// One-shot render
_.render('Welcome, {{user.name}}!', { user: { name: 'Alice' } });
// 'Welcome, Alice!'

// HTML is escaped by default
_.render('{{content}}', { content: '' });
// '<script>'

// Use triple braces for raw output
_.render('{{{html}}}', { html: 'Bold' });
// 'Bold'

Conditionals

// {{#if}}...{{/if}}
_.render('{{#if active}}Online{{/if}}', { active: true });
// 'Online'

// {{#if}}...{{else}}...{{/if}}
_.render('{{#if premium}}Pro{{else}}Free{{/if}}', { premium: false });
// 'Free'

// {{#unless}}...{{/unless}}
_.render('{{#unless verified}}Please verify{{/unless}}', { verified: false });
// 'Please verify'

Loops

// {{#each}}...{{/each}}
_.render('{{#each items}}{{name}}{{/each}}', {
    items: [{ name: 'Apple' }, { name: 'Banana' }]
});
// 'AppleBanana'

// Primitive arrays use {{.}}
_.render('{{#each tags}}#{{.}} {{/each}}', { tags: ['js', 'css', 'html'] });
// '#js #css #html '

// Access loop metadata
_.render('{{#each items}}{{@index}}: {{name}}{{/each}}', { items: [...] });
// '0: Apple1: Banana'

Context & Partials

// {{#with}}...{{/with}} - shift context
_.render('{{#with user}}{{firstName}} {{lastName}}{{/with}}', {
    user: { firstName: 'John', lastName: 'Doe' }
});
// 'John Doe'

// {{> partial}} - include named partials
const opts = { partials: { header: '{{title}}' } };
_.render('{{> header}}', { title: 'Welcome' }, opts);
// 'Welcome'
Try It
Click 'Render' to see the output
<textarea id="template-input" class="form-input font-mono mb-2" rows="3">Hello {{name}}! {{#if premium}}(Premium){{/if}}</textarea>
<input type="text" id="template-data" class="form-input mb-3" value="{&quot;name&quot;: &quot;Alice&quot;, &quot;premium&quot;: true}" placeholder="JSON data">
<button type="button" class="btn btn-primary btn-sm" id="template-run">Render</button>
<div class="utils-demo-result" id="template-result">Click 'Render' to see the output</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
        // Template demo
    $(root).find('#template-run').on('click', function () {
            try {
                const template = $(root).find('#template-input').val();
                const data = JSON.parse($(root).find('#template-data').val());
                const result = _.render(template, data);
                $(root).find('#template-result').text(result);
            } catch (e) {
                $(root).find('#template-result').text('Error: ' + e.message);
            }
        });
}

Lang (Type) Methods

Type checking, comparison, and cloning utilities.

castArray()

clone()

cloneDeep()

eq()

isArray()

isBoolean()

isDate()

isEmpty()

isEqual()

isFinite()

isFunction()

isInteger()

isMatch()

isNaN()

isNil()

isNull()

isNumber()

isObject()

isPlainObject()

isRegExp()

isString()

isSymbol()

isUndefined()

castArray()

parseInt()

toArray()

toFinite()

toInteger()

toLength()

toNumber()

toPlainObject()

toSafeInteger()

toString()

// Type checking
_.isArray([1, 2])        // true
_.isObject({})           // true
_.isPlainObject({})      // true
_.isFunction(() => {})
_.isString('hello')      // true
_.isNumber(123)          // true
_.isNil(null)            // true
_.isEmpty([])            // true
_.isEmpty({})            // true

// Deep equality
_.isEqual({a: 1}, {a: 1})  // true
_.isEqual([1, 2], [1, 2])  // true

// Cloning
const obj = { a: { b: 1 } };
const shallow = _.clone(obj);      // Shallow copy
const deep = _.cloneDeep(obj);     // Deep copy

// Object matching
_.isMatch({a: 1, b: 2}, {a: 1})  // true

// Conversions
_.parseInt('42')         // 42
_.parseInt('08', 10)     // 8
_.toNumber('3.14')       // 3.14
_.toInteger('3.7')       // 3
_.toFinite(Infinity)     // 1.7976931348623157e+308
_.toSafeInteger(3.7)     // 3
_.toArray('abc')         // ['a', 'b', 'c']
_.toString([1, 2, 3])    // '1,2,3'
_.castArray(1)           // [1]
_.castArray([1])         // [1]
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-lang="isEqual">isEqual({a:1}, {a:1})</button>
    <button type="button" class="btn btn-outline btn-sm" data-lang="isEmpty">isEmpty([])</button>
    <button type="button" class="btn btn-outline btn-sm" data-lang="cloneDeep">cloneDeep(obj)</button>
    <button type="button" class="btn btn-outline btn-sm" data-lang="isMatch">isMatch({a:1,b:2}, {a:1})</button>
    <button type="button" class="btn btn-outline btn-sm" data-lang="parseInt">parseInt('42')</button>
    <button type="button" class="btn btn-outline btn-sm" data-lang="toNumber">toNumber('3.14')</button>
</div>
<div class="utils-demo-result" id="lang-result">Click a button to see result</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
        // Lang demos
    $('[data-lang]').on('click', function () {
            const method = $(this).data('lang');
            let result, description;

            switch (method) {
                case 'isEqual':
                    result = _.isEqual({a: 1}, {a: 1});
                    description = `_.isEqual({a: 1}, {a: 1}) = ${result}`;
                    break;
                case 'isEmpty':
                    result = _.isEmpty([]);
                    description = `_.isEmpty([]) = ${result}`;
                    break;
                case 'cloneDeep':
                    const original = {a: {b: 1}};
                    const cloned = _.cloneDeep(original);
                    cloned.a.b = 999;
                    description = `Original: ${JSON.stringify(original)}
    Cloned (modified): ${JSON.stringify(cloned)}`;
                    break;
                case 'isMatch':
                    result = _.isMatch({a: 1, b: 2}, {a: 1});
                    description = `_.isMatch({a:1, b:2}, {a:1}) = ${result}`;
                    break;
                case 'parseInt':
                    result = _.parseInt('42');
                    description = `_.parseInt('42') = ${result}\n_.parseInt('08', 10) = ${_.parseInt('08', 10)}
    _.parseInt('0xff', 16) = ${_.parseInt('0xff', 16)}`;
                    break;
                case 'toNumber':
                    result = _.toNumber('3.14');
                    description = `_.toNumber('3.14') = ${result}\n_.toNumber('0b101') = ${_.toNumber('0b101')}\n_.toNumber(null) = ${_.toNumber(null)}`;
                    break;
            }

            $(root).find('#lang-result').text(description);
        });
}

Maths Methods

Mathematical operations and aggregations.

add()

ceil()

divide()

floor()

max()

maxBy()

mean()

meanBy()

min()

minBy()

multiply()

round()

subtract()

sum()

sumBy()

clamp()

inRange()

random()

// Basic maths
_.add(6, 4)              // 10
_.subtract(6, 4)         // 2
_.multiply(6, 4)         // 24
_.divide(6, 4)           // 1.5

// Rounding
_.round(4.567, 2)        // 4.57
_.floor(4.567)           // 4
_.ceil(4.123)            // 5

// Aggregations
_.sum([1, 2, 3, 4])      // 10
_.mean([1, 2, 3, 4])     // 2.5
_.max([1, 2, 3, 4])      // 4
_.min([1, 2, 3, 4])      // 1

// With iteratee
const items = [{n: 1}, {n: 2}, {n: 3}];
_.sumBy(items, 'n')      // 6
_.maxBy(items, 'n')      // {n: 3}
_.minBy(items, 'n')      // {n: 1}
_.meanBy(items, 'n')     // 2

// Number utilities
_.clamp(10, 0, 5)        // 5 (clamp to range)
_.inRange(3, 2, 4)       // true
_.random(0, 100)         // Random number 0-100
_.random(1.5, 5.5, true) // Random float
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-math="sum">sum([1,2,3,4,5])</button>
    <button type="button" class="btn btn-outline btn-sm" data-math="mean">mean([1,2,3,4,5])</button>
    <button type="button" class="btn btn-outline btn-sm" data-math="random">random(1, 100)</button>
    <button type="button" class="btn btn-outline btn-sm" data-math="clamp">clamp(150, 0, 100)</button>
</div>
<div class="utils-demo-result" id="math-result">Click a button to see result</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
        // Math demos
    $('[data-math]').on('click', function () {
            const method = $(this).data('math');
            let result, description;

            switch (method) {
                case 'sum':
                    result = _.sum([1, 2, 3, 4, 5]);
                    description = `_.sum([1, 2, 3, 4, 5]) = ${result}`;
                    break;
                case 'mean':
                    result = _.mean([1, 2, 3, 4, 5]);
                    description = `_.mean([1, 2, 3, 4, 5]) = ${result}`;
                    break;
                case 'random':
                    result = _.random(1, 100);
                    description = `_.random(1, 100) = ${result}`;
                    break;
                case 'clamp':
                    result = _.clamp(150, 0, 100);
                    description = `_.clamp(150, 0, 100) = ${result}`;
                    break;
            }

            $(root).find('#math-result').text(description);
        });
}

Real-World Patterns & Config Integration

Combine utility functions with Domma's config engine for cleaner, more maintainable code in real-world scenarios.

Data Transformation Pipeline

// Transform API data for display using utility pipeline
const transformUserData = _.flow([
    // 1. Filter active users only
    (users) => _.filter(users, { status: 'active' }),

    // 2. Sort by name
    (users) => _.sortBy(users, 'name'),

    // 3. Group by department
    (users) => _.groupBy(users, 'department'),

    // 4. Map to summary objects
    (grouped) => _.mapValues(grouped, (users, dept) => ({
        department: dept,
        count: users.length,
        users: _.map(users, u => _.pick(u, ['id', 'name', 'email']))
    }))
]);

// Usage with $.setup()
$.setup({
    '#user-dashboard': {
        events: {
            ready: async (e, $el) => {
                const rawUsers = await Domma.http.get('/api/users');
                const transformed = transformUserData(rawUsers);

                // Render grouped data
                _.forEach(transformed, (group, dept) => {
                    $el.append(`
                        
                            ${dept} (${group.count})
                            ${_.map(group.users, u => `${u.name}`).join('')}
                        
                    `);
                });
            }
        }
    }
});

Form Validation & Data Processing

// Centralized form validation with utils
const validators = {
    email: (value) => /^[^S@]+@[^S@]+\.[^S@]+$/.test(value),
    required: (value) => !_.isEmpty(_.trim(value)),
    minLength: (min) => (value) => _.trim(value).length >= min,
    alphanumeric: (value) => /^[a-zA-Z0-9]+$/.test(value)
};

$.setup({
    '#signup-form': {
        initial: {
            data: { errors: {} }
        },
        events: {
            submit: (e, $el) => {
                e.preventDefault();

                // Get form data
                const formData = {
                    username: $el.find('[name="username"]').val(),
                    email: $el.find('[name="email"]').val(),
                    password: $el.find('[name="password"]').val()
                };

                // Validate with utils
                const errors = {};

                if (!validators.required(formData.username)) {
                    errors.username = 'Username is required';
                } else if (!validators.alphanumeric(formData.username)) {
                    errors.username = 'Username must be alphanumeric';
                }

                if (!validators.required(formData.email)) {
                    errors.email = 'Email is required';
                } else if (!validators.email(formData.email)) {
                    errors.email = 'Invalid email address';
                }

                if (!validators.minLength(8)(formData.password)) {
                    errors.password = 'Password must be at least 8 characters';
                }

                // Store errors
                $el.data('errors', errors);

                if (_.isEmpty(errors)) {
                    // Clean data before submission
                    const cleanData = _.mapValues(formData, _.trim);
                    console.log('Valid:', cleanData);
                    Domma.elements.toast.success('Form submitted!');
                } else {
                    // Display errors
                    _.forEach(errors, (msg, field) => {
                        $el.find(`[name="${field}"]`)
                            .addClass('error')
                            .next('.error-msg').text(msg);
                    });
                    Domma.elements.toast.error(`${_.size(errors)} validation errors`);
                }
            },

            'blur input': _.debounce((e, $el) => {
                // Validate on blur (debounced)
                const field = $(e.target).attr('name');
                const value = $(e.target).val();

                // Field-specific validation
                let isValid = true;
                if (field === 'email') {
                    isValid = validators.email(value);
                } else if (field === 'username') {
                    isValid = validators.alphanumeric(value);
                }

                $(e.target).toggleClass('error', !isValid);
            }, 300)
        }
    }
});

Smart Search with Debounce & Filtering

// Search implementation with utils
$.setup({
    '#product-search': {
        initial: {
            data: {
                products: [],
                filteredProducts: [],
                filters: { category: null, minPrice: 0, maxPrice: Infinity }
            }
        },
        events: {
            ready: async (e, $el) => {
                // Load products
                const products = await Domma.http.get('/api/products');
                $el.data('products', products);
                $el.data('filteredProducts', products);
                renderProducts($el, products);
            },

            'input #search-box': _.debounce((e, $el) => {
                const query = $(e.target).val().toLowerCase();
                const products = $el.data('products');
                const filters = $el.data('filters');

                // Multi-criteria search with utils
                const filtered = _.flow([
                    // Text search
                    (items) => query ? _.filter(items, item =>
                        _.toLower(item.name).includes(query) ||
                        _.toLower(item.description).includes(query)
                    ) : items,

                    // Category filter
                    (items) => filters.category
                        ? _.filter(items, { category: filters.category })
                        : items,

                    // Price range
                    (items) => _.filter(items, item =>
                        item.price >= filters.minPrice &&
                        item.price <= filters.maxPrice
                    ),

                    // Sort by relevance (name match first)
                    (items) => _.sortBy(items, [
                        item => !_.toLower(item.name).includes(query),
                        'price'
                    ])
                ])(products);

                $el.data('filteredProducts', filtered);
                renderProducts($el, filtered);

                // Show count
                $('#result-count').text(`${filtered.length} results`);
            }, 300),

            'change [name="category"]': (e, $el) => {
                const category = $(e.target).val();
                const filters = $el.data('filters');
                filters.category = category || null;
                $el.find('#search-box').trigger('input');
            }
        }
    }
});

function renderProducts($container, products) {
    const html = _.map(products, product => `
        
            ${_.escape(product.name)}
            ${_.truncate(product.description, { length: 100 })}
            $${_.round(product.price, 2)}
        
    `).join('');

    $container.find('#results').html(html || 'No products found');
}

Data Aggregation Dashboard

// Analytics dashboard with util aggregations
$.setup({
    '#analytics-dashboard': {
        events: {
            ready: async (e, $el) => {
                const sales = await Domma.http.get('/api/sales');

                // Aggregate data with utils
                const stats = {
                    // Total revenue
                    totalRevenue: _.sumBy(sales, 'amount'),

                    // Average order value
                    avgOrderValue: _.meanBy(sales, 'amount'),

                    // Sales by product
                    byProduct: _.chain(sales)
                        .groupBy('product')
                        .mapValues(items => ({
                            count: items.length,
                            revenue: _.sumBy(items, 'amount')
                        }))
                        .value(),

                    // Top 5 products
                    topProducts: _.chain(sales)
                        .groupBy('product')
                        .map((items, product) => ({
                            product,
                            revenue: _.sumBy(items, 'amount')
                        }))
                        .orderBy(['revenue'], ['desc'])
                        .take(5)
                        .value(),

                    // Sales by date
                    dailySales: _.chain(sales)
                        .groupBy(sale => D(sale.date).format('YYYY-MM-DD'))
                        .mapValues(items => _.sumBy(items, 'amount'))
                        .toPairs()
                        .orderBy([0], ['desc'])
                        .value()
                };

                // Render stats
                $el.find('#total-revenue').text(`$${_.round(stats.totalRevenue, 2)}`);
                $el.find('#avg-order').text(`$${_.round(stats.avgOrderValue, 2)}`);

                // Render top products
                const productsHTML = _.map(stats.topProducts, (item, index) => `
                    #${index + 1}: ${item.product} - $${_.round(item.revenue, 2)}
                `).join('');
                $el.find('#top-products').html(productsHTML);
            }
        }
    }
});

Benefits of Utils + Config Pattern

  • Declarative - Clear intent with functional transformations
  • Composable - Chain utilities for complex operations
  • Testable - Pure functions easy to unit test
  • Performant - Debounce, memoize, and optimize expensive operations
  • Maintainable - Centralized logic with $.setup()

Imperative vs Functional Utils Pattern

Imperative (Verbose)

// Filter, sort, group manually
const active = [];
for (const user of users) {
    if (user.status === 'active') {
        active.push(user);
    }
}

active.sort((a, b) =>
    a.name.localeCompare(b.name)
);

const byDept = {};
for (const user of active) {
    if (!byDept[user.dept]) {
        byDept[user.dept] = [];
    }
    byDept[user.dept].push(user);
}

// Hard to read, error-prone
// Mutable operations

Functional with Utils (Clean)

// Pipeline with utils
const byDept = _.flow([
    users => _.filter(users, {
        status: 'active'
    }),
    users => _.sortBy(users, 'name'),
    users => _.groupBy(users, 'dept')
])(users);

// Clear transformation steps
// Immutable operations
// Chainable and composable
// Easy to test each step

Utility Methods

General utility functions.

flow()

range()

times()

uniqueId()

// Generate ranges
_.range(5)               // [0, 1, 2, 3, 4]
_.range(1, 5)            // [1, 2, 3, 4]
_.range(0, 10, 2)        // [0, 2, 4, 6, 8]

// Unique IDs
_.uniqueId()             // '1'
_.uniqueId('user_')      // 'user_2'

// Run function N times
_.times(3, i => i * 2)   // [0, 2, 4]

// Default values using supported type checks
const withDefault = (value, fallback) =>
    _.isNil(value) || _.isNaN(value) ? fallback : value;
withDefault(null, 10)    // 10
withDefault(1, 10)       // 1

// Function composition
const addOne = x => x + 1;
const double = x => x * 2;
const composed = _.flow([addOne, double]);
composed(5)              // 12 (5+1=6, 6*2=12)

// Property accessor
const getName = (value) => _.get(value, 'name');
getName({ name: 'Alice' })  // 'Alice'

// Run a callback a set number of times
_.times(3, index => index + 1)  // [1, 2, 3]
Try It
Click a button to see result
<div class="flex gap-2 flex-wrap mb-3">
    <button type="button" class="btn btn-outline btn-sm" data-utility="range">range(0, 10, 2)</button>
    <button type="button" class="btn btn-outline btn-sm" data-utility="uniqueId">uniqueId('id_')</button>
    <button type="button" class="btn btn-outline btn-sm" data-utility="times">times(5, i =&gt; i*i)</button>
</div>
<div class="utils-demo-result" id="utility-result">Click a button to see result</div>
/**
 * Try It - from public/showcase/utils.
 */
export function mount(root) {
        // Utility demos
    $('[data-utility]').on('click', function () {
            const method = $(this).data('utility');
            let result, description;

            switch (method) {
                case 'range':
                    result = _.range(0, 10, 2);
                    description = `_.range(0, 10, 2) = [${result.join(', ')}]`;
                    break;
                case 'uniqueId':
                    result = _.uniqueId('id_');
                    description = `_.uniqueId('id_') = "${result}"`;
                    break;
                case 'times':
                    result = _.times(5, i => i * i);
                    description = `_.times(5, i => i*i) = [${result.join(', ')}]`;
                    break;
            }

            $(root).find('#utility-result').text(description);
        });
}