Widget and action builders
Typed factory functions for building WidgetNode and WidgetAction objects. These are optional helpers. You can always write raw objects instead.
widget
The widget export is both a function and a namespace of helpers.
Base function
Use widget(type, opts) for custom widgets or any widget type:
import { widget } from 'rangka';
// Custom widget
widget('kanban', { props: { columns: 'status' }, source: { model: 'sales.order' } });
// Any built-in widget (equivalent to using the named helper)
widget('input', { bind: { field: 'name' }, props: { required: true } });Named helpers
Each built-in widget type has a shortcut. The first argument is always the "identity" of the widget (field name, model name, or label depending on the type). The second argument is an optional config object. Container widgets take a children array as the last argument.
Input widgets
Bind to a field. First argument is the field name.
widget.input('name', { required: true, placeholder: 'Enter name' });
widget.textarea('notes');
widget.select('status', { options: ['Draft', 'Active', 'Closed'] });
widget.checkbox('active');
widget.datepicker('start_date');
widget.datetime('scheduled_at');
widget.money('total');
widget.link('customer', { required: true });
widget.attachment('document');
widget.attachments('files');
widget.json('config');
widget.code('template');
widget.tree('parent');
widget.sequence('reference');
widget.computed('full_name');Display widgets
First argument is the field name (for text/badge) or a value (for icon/image).
widget.text('total', { style: 'heading' });
widget.badge('status');
widget.icon('alert-circle');
widget.image('/logo.png');Layout widgets
Container widgets that organize children. First argument is config, last is children array.
widget.section('Details', [
widget.input('name'),
widget.input('email'),
])
widget.section('Advanced', { collapsible: true, defaultCollapsed: true }, [
widget.input('config'),
])
widget.group({ direction: 'row', gap: 'sm', justify: 'end' }, [
widget.button('Cancel', { variant: 'ghost' }),
widget.button('Save', { variant: 'primary' }),
])
widget.grid({ columns: 2, gap: 'md' }, [
widget.input('first_name'),
widget.input('last_name'),
])
widget.card({ title: 'Summary', description: 'Monthly totals' }, [
widget.text('revenue', { style: 'heading' }),
])
widget.split({ sizes: [60, 40], direction: 'horizontal' }, [
widget.card({ title: 'Left' }, [...]),
widget.card({ title: 'Right' }, [...]),
])
widget.modal({ size: 'lg', title: 'Confirm' }, [...])
widget.drawer({ width: 'md', title: 'Details' }, [...])
widget.scrollArea({ direction: 'vertical', maxHeight: '400px' }, [...])
widget.stack({ height: '100%' }, [...])
widget.spacer({ size: 'lg' })
widget.divider({ margin: 'md' })Data widgets
Provide data context for child widgets. First argument is the model name.
widget.table('sales.order', [
widget.column('name', { label: 'Name', sortable: true }),
widget.column('status', { label: 'Status', filterable: true }),
widget.column('total', { label: 'Total', align: 'right' }),
]);
widget.table('sales.order', { sortable: true, pageSize: 25 }, [widget.column('name')]);
widget.data('sales.customer', [widget.text('name', { style: 'heading' })]);
widget.data('sales.customer', { id: '$route.id' }, [widget.input('name')]);
widget.datagrid('items', [
widget.column('product'),
widget.column('qty', { align: 'right' }),
widget.column('amount', { align: 'right' }),
]);
widget.repeat('sales.order', [widget.text('name')]);Action widgets
widget.button('Submit', { variant: 'primary', on: { click: action.submit() } });
widget.button('Delete', { variant: 'ghost', icon: 'trash' });
widget.form([widget.input('name'), widget.button('Save', { on: { click: action.submit() } })]);Event handling
All widgets accept on in their config object:
widget.input('customer', {
on: { change: action.fetchOptions('contact', ['customer']) },
});
widget.button('Submit', {
variant: 'primary',
on: {
click: action.sequence([action.service('sales.submit'), action.navigate('/sales/orders')]),
},
});Visibility
All widgets accept visible in their config object:
widget.section(
'Payment Details',
{
visible: { field: 'payment_method', operator: 'eq', value: 'card' },
},
[widget.input('card_number')],
);action
The action export is both a function and a namespace of helpers.
Base function
import { action } from 'rangka';
// Generic — works for any action type
action('navigate', { path: '/home' });Named helpers
// Navigation
action.navigate('/sales/orders');
// Form
action.submit();
action.reset();
// Field manipulation
action.setValue('status', 'submitted');
action.clearValue('notes');
action.setValues({ status: 'submitted', reviewed: true });
// Data fetching
action.fetchOptions('contact', ['customer']);
action.refreshSource();
// Service call
action.service('sales.submitInvoice');
action.service('sales.export', { format: 'csv' });
// Model operations
action.modelCreate('sales.order', { status: 'draft' });
action.modelUpdate({ status: 'confirmed' });
action.modelUpdate({ status: 'confirmed' }, { model: 'sales.order', id: '$id' });
action.modelDelete();
action.modelDelete({ model: 'sales.order', id: '$id' });
action.modelFetch('sales.order', '$id', 'currentOrder');
action.modelList('sales.order', 'orders', { status: 'active' });
// Field focus
action.focus('email');
// Table row operations
action.addRow('items');
action.removeRow('items');
action.duplicateRow('items');
// Validation
action.validate();
action.validate(['email', 'phone']);
// Composition
action.sequence([action.service('sales.submit'), action.navigate('/sales/orders')]);
action.conditional(
{ field: 'status', operator: 'eq', value: 'Draft' },
action.modelDelete(),
action.navigate('/sales/orders'),
);Full page example
import { definePage, widget, action } from 'rangka';
export default definePage({
key: 'sales.invoice-form',
label: 'Invoice',
path: '/sales/invoices/$id',
widgets: [
widget.grid({ columns: 2 }, [
widget.card({ title: 'Invoice Details' }, [
widget.input('customer', {
required: true,
on: { change: action.fetchOptions('contact', ['customer']) },
}),
widget.sequence('invoice_number'),
widget.datepicker('date'),
widget.select('status', { options: ['Draft', 'Submitted', 'Paid'] }),
]),
widget.card({ title: 'Payment' }, [
widget.select('payment_terms', { options: ['Net 15', 'Net 30', 'Net 60'] }),
widget.datepicker('due_date'),
widget.money('total'),
]),
]),
widget.card({ title: 'Line Items' }, [
widget.datagrid('items', [
widget.column('product', { label: 'Product' }),
widget.column('qty', { label: 'Qty', align: 'right' }),
widget.column('rate', { label: 'Rate', align: 'right' }),
widget.column('amount', { label: 'Amount', align: 'right' }),
]),
]),
widget.group({ direction: 'row', justify: 'end', gap: 'sm' }, [
widget.button('Cancel', {
variant: 'ghost',
on: { click: action.navigate('/sales/invoices') },
}),
widget.button('Save', { variant: 'primary', on: { click: action.submit() } }),
]),
],
});