sercrod

Event bindings (fallback for @name)

This page describes the general behavior of Sercrod’s event bindings for @name when there is no dedicated manual for that event.

Typical examples that rely on this fallback:

Events such as:

have (or may have) their own manuals (event-click.md, event-submit.md, and so on) and are not fully described here.

This page explains the common rules for all @name event bindings that do not have a dedicated entry.

Note: This document uses @ as the event prefix. If you configure config.events.prefix to something else (for example ne-), mentally replace @click with the actual prefix you use (ne-click, and so on).

Overview

You can read @name as “run this code when this DOM event occurs on this element”.

Syntax

General form:

Recognized modifiers and categories

Sercrod understands the following modifier names.
They fall into three categories:

  1. DOM listener option modifiers
    These map directly to standard addEventListener options and are not Sercrod specific:

    • .capture

      • Sets the listener option capture: true.
      • The handler runs in the capture phase.
    • .once

      • Sets the listener option once: true.
      • The browser calls the handler at most once and then automatically removes it.
    • .passive

      • Sets the listener option passive: true.
      • Signals that the handler will not call preventDefault() in normal browser semantics.
  2. DOM method helpers (Sercrod specific syntax, standard DOM behavior)
    These modifiers are Sercrod syntax that call standard methods on the event object. They are not native event options and are not passed to addEventListener:

    • .prevent

      • Calls event.preventDefault() for that event before evaluating the handler code.
    • .stop

      • Calls event.stopPropagation() for that event before evaluating the handler code.
  3. Sercrod specific re-rendering modifiers
    These modifiers control Sercrod’s update logic only. They are not standard JS event options and are not passed to addEventListener:

    • .update

      • Forces a re-render after the handler, even if the event is normally treated as non mutating.
    • .noupdate

      • Suppresses host re-rendering after the handler, even if the event type would normally trigger an update.

    These flags affect only Sercrod’s internal “should we update the host?” decision. They do not change event propagation, default behavior, or listener options.

Examples:

<div @pointerdown="startDrag($event)"></div>

<div @dragover="handleDragOver($event)"
     @drop="handleDrop($event)"></div>

<section @wheel.prevent="zoomWithWheel($event)">
  ...
</section>

<button @pointerdown.stop.update="handlePress($event)">
  Press
</button>

The code part is arbitrary JavaScript that runs inside a Sercrod evaluation context similar to:

with (scope) {
  // your expression or statements here
}

You can write either:

Event object and element access

For all event bindings:

Typical patterns:

<div
  @pointerdown="
    drag = {
      active: true,
      x: $event.clientX,
      y: $event.clientY
    }
  "
  @pointermove="drag && drag.active && updateDrag($event)"
  @pointerup="endDrag($event)"
></div>

Inside a helper function you can use the full event API:

function updateDrag($event){
  const t = $event.target;
  // Use $event.clientX, $event.clientY, t.dataset.id, and so on.
}

Notes:

Data updates and re-render timing

Event handlers often mutate data. Sercrod’s event pipeline is tuned so that:

The basic rules are:

  1. During the handler:

    • Writes like count++ or state.value = ... go into the current evaluation scope.
    • If a written key also exists in the host data object, Sercrod updates data[key] as well and marks it as dirty.
  2. After the handler, Sercrod decides how to update based on:

    • the event type (ev),
    • the events.non_mutating configuration list,
    • whether the event is treated as input like,
    • and modifiers .update, .noupdate, and .once.

    Concretely:

    • Sercrod maintains a set NON_MUTATING derived from config.events.non_mutating.
      By default it contains events like:

      • mouseover, mouseenter, mousemove, mouseout, mouseleave, mousedown
      • pointerover, pointerenter, pointermove, pointerout, pointerleave, pointerrawupdate, pointerdown
      • wheel, scroll, touchmove, touchstart
      • dragstart, drag, dragenter, dragover, dragleave, dragend
      • resize, timeupdate, selectionchange
    • It detects input like events:

      • input, change, beforeinput,
      • keydown, keyup,
      • compositionstart, compositionupdate, compositionend (any event name starting with composition),
      • click on form controls (inputs, textareas, selects, or contentEditable elements).
    • It then computes wantsUpdate:

      • Start with wantsUpdate = !NON_MUTATING.has(ev).
      • If .update is present, set wantsUpdate = true.
      • If .noupdate is present, set wantsUpdate = false.
      • If .once is present and ev is in NON_MUTATING, Sercrod forces wantsUpdate = false to avoid re-creating a one time listener on a non mutating event.
    • Finally, it applies the update:

      • If the event is input like (as defined above), or a click on a form control, Sercrod performs a lightweight children update:
        • it calls an internal _updateChildren(...) so only the host’s children are reconciled, preserving focus.
      • Otherwise, if wantsUpdate is true, Sercrod triggers a normal host update.
      • If wantsUpdate is false, no re-render is triggered by this handler.
  3. Modifiers override defaults:

    • .update forces an update even for events listed in events.non_mutating.
    • .noupdate suppresses updates even for events that would normally re-render.

Examples:

<!-- No automatic update: wheel is non mutating by default -->
<div @wheel="pan.x += $event.deltaX; pan.y += $event.deltaY"></div>

<!-- Force update after a move event -->
<div @pointermove.update="cursor = { x: $event.clientX, y: $event.clientY }"></div>

<!-- Explicitly suppress update (even if you touch data) -->
<div @mousedown.noupdate="debug.lastDown = $event.clientX"></div>

Practical guidelines:

Interaction with *input and other helpers

Input helpers such as n-input / *input, *lazy, and *eager control:

@name is orthogonal to those helpers:

Example:

<input
  n-input="form.name"
  @keydown="lastKey = $event.key"
  @change="validate(form)"
>

Interaction with *prevent-default and *prevent

Sercrod also provides structural directives for some common default behaviors:

These directives are evaluated when the element is rendered. In the current implementation:

The directives do not stop propagation, and they do not automatically cover all event types.

For fine grained control on a specific @name binding, use modifiers on the event attribute:

Typical patterns:

<!-- Prevent Enter key from submitting or triggering default behavior -->
<input *prevent-default="enter"
       @keydown="handleKey($event)">

<!-- Prevent form submission via browser default, use JavaScript instead -->
<form *prevent-default="submit"
      @submit="saveForm()">
  ...
</form>

<!-- Per event control with modifiers -->
<a href="/danger"
   @click.prevent="confirmLeave && !confirmLeave()">
  Leave page
</a>

<button @click.stop="handleClick($event)">
  Click
</button>

For complete details, see the separate manuals for *prevent-default and *prevent.

Custom events

You can also bind handlers to custom events dispatched from JavaScript.

In the template:

<div @card-activated="setActive($event.detail.id)">
  ...
</div>

In JavaScript:

element.dispatchEvent(new CustomEvent("card-activated", {
  detail: { id: 123 }
}));

Inside the handler code, $event.detail contains the payload passed by the dispatcher.

Error handling and edge cases

Relation to specific event manuals

Some events are common enough to have their own detailed manuals, for example:

Those pages:

If you bind an event with @something and there is no event-something.md, the behavior of that binding is determined by the rules on this page.