Events

Working on Events in Jay Files

Written for AI agents. See Log Methodology Note below for details.

The challenge with events are that events are streaming information from the JayComponent back to the code using the JayComponent defined in a JayFile.

For events, we have two key requirements

  • expose a programming model that is efficient for the developer
  • expose an event model (in the JayFile) that a designer can work with

Background

There are a lot of web frameworks, and a lot of ways web frameworks represent event binding to the HTML / template / JSX. A good reference is this blog post

React / JSX event binding

<button onClick={() => this.setState({count: this.state.count - 1})}>

Angular


<button (click)="dec()">-</button>

Stencil

<button onClick={this.dec.bind(this)}>-</button>

Svelte

<button on:click="{dec}"></button>

Native web component

this.shadowRoot.getElementById('inc').onclick = () => this.inc();

SlimJS

<button click="dec">-</button>

Lume Elements

<button onclick=${() => (this.count -= 1)}>-</button>

We learn from those examples that developers prefer binding event to a function or to an inline function. However, those binding patterns have two challenges for Jay.

  1. JayFiles should be logic free, not allowing inline event handlers
  2. Designers writing JayFiles, or generating JayFiles, may not be able to bind the right event name to the right code function

Events binding in Jay

With Jay, we try to decouple the JayFile from the code element. We explore two main directions for event bindings below. More options can be found in the exploration folder

1 - id based event binding

with this option we add an id property to the JayFile, which we are using for both event binding and for test driver generation

the JayFile

<html>
  <head>
    <script type="application/yaml-jay">
      data:
         count: number
    </script>
  </head>
  <body>
    <div>
      <button id="dec">-</button>
      <span id="count">{count}</span>
      <button id="inc">+</button>
    </div>
  </body>
</html>

The Code file extending it - one option

import { JayElement } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

export function Counter(initial: number): JayElement<ViewState> {
  let count = initial;
  let element = render({ count });

  function inc() {
    count += 1;
    element.update({ count });
  }

  function dec() {
    count -= 1;
    element.update({ count });
  }

  element.addEventListener('dec', 'click', (_) => dec());
  element.addEventListener('inc', 'click', (_) => inc());

  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };

  return {
    dom: element.dom,
    update: update,
  };
}

and another option

import { JayElement, events } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

export function Counter(initial: number): JayElement<ViewState> {
  let count = initial;

  function inc() {
    count += 1;
    element.update({ count });
  }

  function dec() {
    count -= 1;
    element.update({ count });
  }

  let element = render(
    { count },
    {
      dec: events().onclick(() => dec()),
      inc: events().onclick(() => inc()),
    },
  );

  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };

  return {
    dom: element.dom,
    update: update,
  };
}

and yet another option

import { JayElement } from 'jay-runtime';
import { render, ViewState, eventsFor } from './counter.jay';

export function Counter(initial: number): JayElement<ViewState> {
  let count = initial;

  function inc() {
    count += 1;
    element.update({ count });
  }

  function dec() {
    count -= 1;
    element.update({ count });
  }

  let element = render({ count }, [
    eventsFor('dec').on('click', () => dec()),
    eventsFor('inc').on('click', () => inc()),
  ]);

  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };

  return {
    dom: element.dom,
    update: update,
  };
}

2 - declaration based event binding

We can actually define the events in the JayFile, creating semantic events, like the following

<html>
  <head>
    <script type="application/yaml-jay">
      data:
         count: number

      events:
         dec: (count: Number)
         inc: (count: Number)
    </script>
  </head>
  <body>
    <div>
      <button onclick="dec(count)">-</button>
      <span>{count}</span>
      <button onclick="inc(count)">+</button>
    </div>
  </body>
</html>

and the usage is then

import { JayElement } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

export function Counter(initial: number): JayElement<ViewState> {
  let count = initial;
  let element = render({ count });

  function inc() {
    count += 1;
    element.update({ count });
  }

  function dec() {
    count -= 1;
    element.update({ count });
  }

  element.onDec((_) => dec());
  element.onInc((_) => inc());

  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };

  return {
    dom: element.dom,
    update: update,
  };
}

3 - By ID with proxy for events and semantic events

given an id for elements, we know to generate byId function on the jay element that returns a proxy for events registration. The proxy in this case will add an onclick event to the two buttons.

The compiler will resolve that the element is a button and generate the right signature of the byId function to have exact code completion for the id values, and the events a button exposes.

The Jay file

<html>
  <head>
    <script type="application/yaml-jay">
      data:
         count: number
    </script>
  </head>
  <body>
    <div>
      <button id="dec">-</button>
      <span id="count">{count}</span>
      <button id="inc">+</button>
    </div>
  </body>
</html>

The Jay Component file

import { JayElement, EventEmitter } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

interface CounterType {
  onInc(listener: (count) => void);

  onDec(listener: (count) => void);
}

export function Counter(initial: number): JayElement<ViewState> & CounterType {
  let count = initial;
  let element = render({ count });
  let incEvent = new EventEmitter<number>();
  let decEvent = new EventEmitter<number>();

  function inc() {
    count += 1;
    element.update({ count });
    incEvent.emit(count);
  }

  function dec() {
    count -= 1;
    element.update({ count });
    decEvent.emit(count);
  }

  element.byId('dec').onClick((_) => dec());
  element.byId('inc').onClick((_) => inc());
  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };
  return {
    dom: element.dom,
    update: update,
    onInc: incEvent,
    onDec: decEvent,
  };
}

4 - declaration based event binding with emiting events

We can actually define the events in the JayFile, creating semantic events, like the following (in this example we try to auto deduce the events from the DOM part of the jay file, removing the need to have a declaration)

<html>
  <head>
    <script type="application/yaml-jay">
      data:
         count: number
    </script>
  </head>
  <body>
    <div>
      <button onclick="dec(count)">-</button>
      <span>{count}</span>
      <button onclick="inc(count)">+</button>
    </div>
  </body>
</html>

and the usage is then

import { JayElement, EventEmitter } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

interface CounterType {
  onInc(listener: (count) => void);

  onDec(listener: (count) => void);
}

export function Counter(initial: number): JayElement<ViewState> & CounterType {
  let count = initial;
  let element = render({ count });
  let incEvent = new EventEmitter<number>();
  let decEvent = new EventEmitter<number>();

  function inc() {
    count += 1;
    element.update({ count });
    incEvent.emit(count);
  }

  function dec() {
    count -= 1;
    element.update({ count });
    decEvent.emit(count);
  }

  element.onDec((_) => dec());
  element.onInc((_) => inc());

  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };

  return {
    dom: element.dom,
    update: update,
    onInc: incEvent,
    onDec: decEvent,
  };
}

Choozen solution

we have selected the choosen solution to be simple for designers and have a good separation of UI from code, those we have selected the direction of using id in the HTML file. With additional feedback from @barak007 we rename id to ref.

For the coding part, we have opted in to using the compiler to generate the optimal API for events, with

  • for HTML elements that are singular (not in forEach), we generate a property on the element that is direct access to the child HTML element. element.dec: HTMLElement
  • for HTML elements that are repeated (in forEach), we generate a property on the element that is a collection of child elements. element.someId: HTMLElementCollection.

for our example, it looks like

The Jay file

<html>
  <head>
    <script type="application/yaml-jay">
      data:
         count: number
    </script>
  </head>
  <body>
    <div>
      <button ref="dec">-</button>
      <span ref="count">{count}</span>
      <button ref="inc">+</button>
    </div>
  </body>
</html>

The Jay Component file

import { JayElement, EventEmitter } from 'jay-runtime';
import { render, ViewState } from './counter.jay';

interface CounterType {
  onInc(listener: (count) => void);

  onDec(listener: (count) => void);
}

export function Counter(initial: number): JayElement<ViewState> & CounterType {
  let count = initial;
  let element = render({ count });
  let incEvent = new EventEmitter<number>();
  let decEvent = new EventEmitter<number>();

  function inc() {
    count += 1;
    element.update({ count });
    incEvent.emit(count);
  }

  function dec() {
    count -= 1;
    element.update({ count });
    decEvent.emit(count);
  }

  element.dec.onclick((_) => dec());
  element.inc.onclick((_) => inc());
  let update = (viewState: ViewState) => {
    count = viewState.count;
    element.update({ count });
  };
  return {
    dom: element.dom,
    update: update,
    onInc: incEvent,
    onDec: decEvent,
  };
}

Log Methodology Note

Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.