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