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}
<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 }
<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'}
<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
<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']
<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'
<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="{"name": "Alice", "premium": 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]
<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
<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]
<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 => 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);
});
}