Skip to content

Event binding

In JOPS, an event handler is a method of the view class. You connect the two in the layout: mark the element with jops-event-bind and give an on* attribute the name of the method.

src/layout/profile.html
<form jops-event-bind onsubmit="onSave">
<input name="name" jops-event-bind oninput="onNameInput">
<button type="submit">Save</button>
</form>
src/Profile.js
import { View } from "jops-core";
export default class Profile extends View {
onNameInput(event) {
this.name = event.target.value;
}
onSave(event) {
event.preventDefault();
const data = new FormData(event.target);
// ...send data
}
}
  • Any on* attribute works: onclick, onsubmit, oninput, onchange, onkeydown, and so on.
  • The handler receives the native DOM event, and this is the view.
  • The value is a method name, never code. Write onclick="onSave", not onclick="onSave()".

Binding is done by bindEvents(), which the framework calls when the view is shown, after onResume() in the lifecycle. It covers the markup of the view’s layout, across all of its top-level elements.

To pass data to a handler, keep it on the view instance (this.task) rather than in the markup. A list item that is its own view knows its own record:

src/TaskItem.js
import { View, layout } from "jops-core";
export default class TaskItem extends View {
constructor(task) {
super();
this.task = task;
this.layout = layout`
<li class="item">
<span>${task.title}</span>
<button jops-event-bind onclick="onDelete">Delete</button>
</li>`;
this.inflate();
}
onDelete() {
Tasks.remove(this.task.id);
}
}

bindEvents() only wires the layout. HTML you generate later, with Element.set() or innerHTML, is not bound. Worse, the browser treats onclick="onDelete" in that HTML as an inline script and throws a ReferenceError (or a Content-Security-Policy violation).

For generated markup you have two good options:

  1. Make each item a subview, as in the TaskItem example above.
  2. Use event delegation: one bound listener on a container from the layout, with data-* attributes on the generated elements.
src/layout/lines.html
<table>
<tbody id="lines" jops-event-bind onclick="onLineAction"></tbody>
</table>
src/Lines.js
import { View, layout } from "jops-core";
export default class Lines extends View {
showLines(lines) {
this.get("lines").set(
lines
.map((line, i) => layout`
<tr>
<td>${line.description}</td>
<td><button data-action="remove" data-index="${i}">✕</button></td>
</tr>`)
.join(""),
);
}
onLineAction(event) {
const button = event.target.closest("[data-action]");
if (button?.dataset.action === "remove") {
this.removeLine(Number(button.dataset.index));
}
}
}

The <tbody> is part of the layout, so it is bound once. Rows can be replaced as often as you like, and clicks on them still reach onLineAction().

For anything else, addEventListener() in onLayout() is fine.