Skip to content

Event Handlers

Event Handlers bind to DOM events and update field/form state automatically. They accept a Proxy-like event object or a direct value.

Lifecycle: User InputEvent Handler / ActionMutate StoreEvent Hook



Sync Field Value

onChange(e), onToggle(e) & sync(e)

HandlerAffected PropertyExecuted HookUse Case
sync(e)valueUpdate value without triggering hooks (silent update)
onChange(e)valueonChangeStandard value update with side effects
onToggle(e)valueonToggleToggle/checkbox value update (same as onChange)

Use onChange(e) or onToggle(e) to update the field's value and trigger the corresponding onChange/onToggle hook.

Use sync(e) if you want to update the value without triggering the onChange or onToggle hook — useful for batch updates or programmatic changes where you don't want side effects.

How sync(e) works

The sync() method handles multiple input types automatically:

Input typeBehavior
Event with e.target (standard input)Reads e.target.value (or e.target.checked for checkbox)
Direct value (string, number)Sets the value directly
Two arguments: sync(e, value)Uses the second argument value
null or undefinedTries the second argument v, then uses $try(e, v)
html
<input {...field.bind()} />  <!-- bind() handles onChange automatically -->

<!-- Or use onChange explicitly -->
<input onChange={form.$('username').onChange} />

<!-- For custom components that pass values directly -->
<CustomInput onChange={form.$('username').sync} />

<!-- Silent update: no onChange hook triggered -->
<CustomInput onChange={form.$('username').sync} />

Key difference: sync(e) → no hook fires; onChange(e)onChange hook fires; onToggle(e)onToggle hook fires.

onChange is an alias of onSync, which calls sync internally.

If you are using a custom component which doesn't work with the package's built-in sync handler, open an Issue.


Focused & Touched State

onFocus(e) & onBlur(e)

HandlerAffected PropertyExecuted Hook
onFocus(e)focusedonFocus
onBlur(e)touched, blurredonBlur

If you need to track touched or focused state, use onFocus(e) or onBlur(e) handlers:

html
<input
  {...field.bind()}
  onFocus={form.$('username').onFocus}
  onBlur={form.$('username').onBlur}
/>

Key Events

onKeyDown(e) & onKeyUp(e)

HandlerAffected PropertyExecuted Hook
onKeyDown(e)onKeyDown
onKeyUp(e)onKeyUp

Use onKeyDown(e) or onKeyUp(e) to listen for keyboard events. These handlers are pass-through — they do not modify any field property but fire the corresponding hook for you to handle:

html
<input
  {...field.bind()}
  onKeyDown={form.$('search').onKeyDown}
  onKeyUp={form.$('search').onKeyUp}
/>

Example: keyboard shortcuts with hooks

Hooks receive both the field instance and the native DOM event when triggered by an event handler:

javascript
const fields = {
  search: {
    hooks: {
      onKeyDown(field, e) {
        if (e.key === 'Enter') performSearch(field.value);
        if (e.key === 'Escape') field.clear();
      },
    },
  },
};

Example: autocomplete with onKeyUp

javascript
const fields = {
  search: {
    hooks: {
      onKeyUp(field, e) {
        if (field.value.length >= 3) fetchSuggestions(field.value);
      },
    },
  },
};

Unlike onChange which fires on every value mutation, onKeyDown/onKeyUp fire on every keyboard event — including arrow keys, modifier keys, etc. Use them when you need raw keyboard access.

Note: Hooks receive the field instance as the first parameter and the native DOM event as the second parameter (onKeyDown(field, e)) when triggered by an event handler. For hooks called directly from actions (e.g. onInit, onClear, onReset), only the field instance is passed.


Clear & Reset

onClear(e) & onReset(e)

HandlerActionAffected PropertyExecuted HookResult
onClear(e)clear()valueonClearEmpty values
onReset(e)reset()valueonResetDefault values

On the form instance:

html
<button type="button" onClick={form.onClear}>Clear</button>
<button type="button" onClick={form.onReset}>Reset</button>

On a specific field or nested fields:

html
<button type="button" onClick={form.$('members').onClear}>Clear Members</button>
<button type="button" onClick={form.$('members').onReset}>Reset Members</button>

Nested Array Elements

onAdd(e) & onDel(e)

HandlerActionAffected PropertyExecuted HookResult
onAdd(e)add()fieldsonAddAdd a field
onDel(e)del()fieldsonDelRemove a field

Adding a Field

html
<button type="button" onClick={form.$('hobbies').onAdd}>Add Hobby</button>

Or specify the field value as second argument:

html
<button type="button" onClick={e => form.$('hobbies').onAdd(e, 'soccer')}>Add Hobby</button>

Or specify a custom key and value as an object:

html
<button type="button" onClick={e => form.$('hobbies').onAdd(e, {
  key: 'favorite',
  value: 'soccer',
})}>Add Favorite</button>

Deleting a Field

html
<button type="button" onClick={field.onDel}>Delete Hobby</button>

Or using the field selector with a field key:

html
<button type="button" onClick={form.$('hobbies').$(3).onDel}>Delete Hobby 3</button>

Or specify the field path as second argument:

html
<button type="button" onClick={e => form.onDel(e, 'hobbies[3]')}>Delete Hobby 3</button>

Note: These are Event Handlers, not actions. For programmatic add/delete without hooks, use add() and del() actions instead.


Submitting the Form

onSubmit(e)

HandlerActionAffected PropertyExecuted HookFormField
onSubmit(e)submit() → validate()submitting, validatingonSubmit

The onSubmit(e) will validate the form and call onSuccess(form) or onError(form) Validation Hooks if they are implemented.

The onSuccess(form) and onError(form) methods take the form object in input, so you can perform more actions after validation occurs.

html
<button type="submit" onClick={form.onSubmit}>Submit</button>

Handle Files

onDrop(e)

HandlerAffected PropertyExecuted HookResult
onDrop(e)filesonDropRetrieve the files

The onDrop(e) Event Handler retrieves files into the files field property and executes the onDrop Hook function. When type: 'file' is set, the field's onSync/onChange automatically delegates to onDrop.

How it works

javascript
// Field.ts source logic:
onDrop = (...args) =>
  this.execHandler('onDrop', args, action(() => {
    const e = args[0];
    let files = null;
    if (isEvent(e) && hasFiles(e)) {
      files = Array.from(e.target.files);
    }
    this.files = [...(this.files || []), ...(files || args)];
  }));

The handler:

  1. Extracts files from e.target.files (standard file input) or uses the raw arguments (drag-and-drop)
  2. Appends files to the existing field.files array (preserving previous uploads)
  3. Fires the onDrop hook

Define the field type property as file and use bind() on your input — onChange will automatically delegate to onDrop:

javascript
const fields = {
  avatar: {
    type: 'file',
    hooks: {
      onDrop(field) {
        const file = field.files?.[0];
        if (file) console.log('Uploaded:', file.name);
      },
    },
  },
};
html
<input type="file" multiple {...field.bind()} />

Method 2: Custom binding with onDrop override

Without type: 'file', override the onChange handler in bind() to use onDrop:

html
<input type="file" multiple {...field.bind({ onChange: field.onDrop })} />

Method 3: Drag-and-drop zone

Create a custom drop zone and call onDrop manually:

jsx
<div
  onDragOver={(e) => e.preventDefault()}
  onDrop={(e) => {
    e.preventDefault();
    field.onDrop(e);
  }}
>
  Drop files here
</div>

Accessing uploaded files

After a drop or file selection:

javascript
field.files; // Array of File objects

// Example: read first file as data URL
const file = field.files?.[0];
if (file) {
  const reader = new FileReader();
  reader.onload = (e) => field.set('extra', {
    previewUrl: e.target.result,
  });
  reader.readAsDataURL(file);
}

Next: Custom Event Handlers — define handlers on initialization or by extending the class.

Released under the MIT License.