Skip to content

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:

typescript
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.

typescript
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).

typescript
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.

typescript
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.

typescript
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 ​

typescript
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:

typescript
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:

typescript
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 ​

typescript
import { action } from 'rangka';

// Generic — works for any action type
action('navigate', { path: '/home' });

Named helpers ​

typescript
// 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 ​

typescript
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() } }),
    ]),
  ],
});