Skip to content
These are temporary community docs. The official docs are in progress.Help improve them
Markless
Esc
↑↓navigate↵open⌘Jpreview
On this page

The same counter on a phone

Two hand-written proofs drive a counter on iOS UIKit and macOS AppKit through JavaScriptCore, from a graph-shaped artifact instead of the DOM.

Last page, the graph and its symbols turned out to be data plus small functions. Does a browser have to run them?

The Markless README states the goal: one web-like component model for web and native apps. Today, two small proofs test that idea. Neither one is a product.

The problem

The web build ends in DOM operations, such as setText. A phone has no DOM. A native button needs its own control, its own event, and its own way to set a title.

So the question is narrow. Can a graph-shaped description of a counter drive real native controls, with the handler still written in JavaScript?

The mental model

Think of sheet music. That is an analogy. The score says which notes play and when. Each instrument plays the same score in its own way.

Here the score is the artifact. It lists state cells, host nodes, event records, text bindings, and symbol bodies. A small Swift runtime is the instrument. It builds UIKit or AppKit controls from the host nodes.

Press Count 0. Then pick another host and press it again.

What stays the same on a native host, and what changes?

Press Count 0. Then pick another host and press it again.

Host
The pageTry it
CounterMarkless build

The event record for this host

{ hostNodeId: 'h0', eventName: 'click', (changes) symbolIds: ['symbol:click'] }

Event record in the view payload. Shape from packages/web/test/render.test.ts.

Your code: Counter.tsrx
import { state } from '@markless/core'; export function Counter() @{ let count = state(0); <main> <button onClick={() => count++}>Count {count}</button> </main>}
What the host did

Same in every hostsame

State
state:count, starts at 0
Event record
The button's onClick runs the handler
Text binding
Button text Count {count}
Symbol
The handler: add one to count

Changes with the host

Control
<button>
Event name
click
Listens with
One capture listener on the root: root.addEventListener(eventName, listener, { capture: true })
Sets the title
Journal entry setText sets textContent
Records written by
The Markless compiler
JavaScript runs in
The page
  1. noteReady. Press the button in the window.

Simplified. The browser record has the shape that @markless/web reads. This one comes from packages/web/test/render.test.ts. The iOS and macOS hosts are hand-written proofs in poc/fixtures/proofs/. A person wrote their artifact.json, and no Markless package emits it. The proofs add an <h1> heading that is not shown here.

Notice that state:count, the handler, and the text binding stay the same. Only the control, its event name, and the title write change.

The proof

The iOS proof starts from this component. The macOS proof differs only in its heading.

import { state } from '@markless/core';

export function Counter() @{
  let count = state(0);

  <main>
    <h1>Markless iOS Proof</h1>
    <button onClick={() => count++}>Count {count}</button>
  </main>
}

The hand-written artifact maps the authored onClick to a semantic activate event. Then it names the host event. On iOS that is touchUpInside. On macOS it is action.

{
  "node": "host:button",
  "authoredEvent": "onClick",
  "semanticEvent": "activate",
  "nativeEvent": "touchUpInside",
  "symbolId": "symbol:counter.increment"
}

The symbol body is also hand-written JavaScript:

graph["state:count"] = graph["state:count"] + 1;

What the Swift runtime does

  1. It creates a JSContext from JavaScriptCore, and a plain graph object with state:count set to 0.
  2. It turns each host node into a control. On iOS, main becomes a UIStackView and button becomes a UIButton.
  3. It connects the native event to the symbol ID.
  4. On a tap, it runs the symbol in JavaScriptCore.
  5. Then it re-reads every text binding and sets the button title, such as Count 1.

The macOS proof does the same with an NSButton and its action event. In both proofs, an XCTest checks that the title changes from Count 0 to Count 1.

What the proofs do not show

The proofs are narrow on purpose. Keep these limits in mind:

  • They do not use @markless/runtime. The graph is a plain JavaScript object.
  • They update every text binding after each tap. They do not use path subscriptions.
  • They do not cover styling, Android, Windows, Linux, or production packaging.
  • A check script in each proof makes sure that the files name no web view or cross-platform shell.

Misconception: Markless ships native apps today. It does not. The proofs show that the artifact shape can drive native controls. The compiler does not produce that artifact yet.

Next: That is the whole engine, from compiler to native proof. Where does each part live in the source? Repo tour →

Was this page helpful?