Skip to content

Binding

Binding synchronises a ModelRecord with a set of form components. Components that implement BindableTextField, Checkbox, ComboBox, DateField, TimeField — can be bound by field name. Any other component can be wired via explicit accessor callbacks.

Binding is standalone — it is not a layout component. You wire your own form layout and pass the input components to binding.bind().

Quick start

typescript
import { Binding } from '@jimka/typescript-ui/core';
const binding = new Binding()
    .bind('name',   nameField)
    .bind('active', activeCheckbox)
    .bind('role',   roleCombo);

// Populate all components from a record:
binding.setRecord(store.getAt(0));

// Commit or reject the user's edits:
binding.commit();
// binding.reject();

bind is chainable and returns the same binding.

Explicit accessors

Use the long form of bind for components that do not implement Bindable:

typescript
const binding = new Binding()
    .bind('name', myWidget, {
        get:    () => myWidget.getValue(),
        set:    (v) => myWidget.setValue(v),
        listen: (fn) => myWidget.addChangeListener(fn),
    });

The accessor object matches BindingAccessors. The listen callback is what tells the binding "the user just edited this field" — typically you wire it to whatever change event your component fires.

Listeners

Binding fires three event types:

typescript
binding.addChangeListener(() => console.log('field edited, dirty =', binding.getRecord()?.isDirty()));
binding.addCommitListener(() => console.log('committed'));
binding.addRejectListener(() => console.log('rejected'));

These let callers react to record mutations without polling. Use them to enable / disable a save button, show a "you have unsaved changes" indicator, etc.

Switching records

Call setRecord() again to switch the binding to a different record:

typescript
binding.setRecord(store.getAt(0));
// user edits name field…
binding.setRecord(store.getAt(1));   // discards uncommitted edits on record 0

setRecord is synchronous — there is no built-in confirmation step if the current record is dirty. If you need a "save first?" prompt, run it at the call site before setRecord.

Vetoing a record change

addBeforeRecordListener registers a guard that runs before setRecord mutates any state. The listener receives the next record (which may be null) and returns false to cancel the change:

typescript
binding.addBeforeRecordListener((next) => {
    const current = binding.getRecord();

    if (current && current !== next && current.isDirty()) {
        Notification.show('Commit or reject your changes first.', 'error');
        return false;
    }

    return true;
});

A vetoed call is a complete no-op — the previous record stays bound, field values are not repopulated, and validation decorations are preserved. Multiple listeners can be registered; the first one to return false short-circuits the rest, so adding a listener can never widen permission. Returning true or omitting return allows the change.

The veto API is intentionally synchronous and boolean. For async confirmation flows (a "Discard unsaved changes?" dialog), orchestrate the dialog at the call site and only invoke setRecord once the user has decided.

If a veto fires, any picker UI that drove the call (e.g. a record-selector combo) will still show the rejected selection while the binding remains on the previous record. The call site is responsible for reconciling — compare getRecord after the call and reset the picker if they diverge:

typescript
recordCombo.addActionListener(() => {
    const next = store.find('id', Number(recordCombo.getElement().value));
    if (!next) return;

    binding.setRecord(next);

    const active = binding.getRecord();
    if (active && active !== next) {
        recordCombo.getElement().value = String(active.get('id'));
    }
});

Listeners that only want to guard switches (and let setRecord(null) clears through) must short-circuit next === null themselves.

Unbinding

binding.unbind(fieldName) removes a component from the binding. binding.bind on an already-bound field rebinds it.

See also