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

Your first component

Build a small grocery list in four steps, from plain markup to state, a list, and an input.

You have a counter running. Now build a small grocery list from scratch.

Write every step in one file. In the browser-only app, that file is src/App.tsrx. In a starter app, use pages/index.tsrx. Save after each step and look at the page.

A component is a function whose body is @{ ... }. You write markup as a statement inside it, so you never return it.

Step 1: Some markup

export default function App() @{
	<main>
		<h1>My groceries</h1>
		<p>Nothing here yet.</p>
	</main>
}

Step 2: Add state

state(0) makes a value that the page follows. You change it with plain code, like count++.

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

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

	<main>
		<h1>My groceries</h1>
		<button onClick={() => count++}>Count {count}</button>
	</main>
}

Press Count a few times.

What changes when I click?

Press Count a few times. Watch what lights up.

The pageTry it
My groceries

My groceries

The page's HTML right now

<main> <h1>My groceries</h1> <button>Count 0</button> </main>
Your code: src/App.tsrx
import { state } from '@markless/core'; export default function App() @{ (ran once) let count = state(0); <main> <h1>My groceries</h1> <button onClick={() => count++}>Count {count}</button> (click code) (shows count) </main>}
What Markless did
Times App ran1Clicks never run it again
Texts updated0Only the number
  1. ranApp ran once, to set up the page.

Simplified. The HTML view shows the elements App made. Yellow marks the only text that changed.

Notice the counts: each click changes one piece of text, and App does not run again.

Step 3: A list

@for repeats markup for each item. The key gives each row a stable identity. @empty shows when the list has no items.

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

export default function App() @{
	let items = state([
		{ id: 1, name: 'Milk' },
		{ id: 2, name: 'Bread' },
	]);

	<main>
		<h1>My groceries</h1>
		<ul>
			@for (const item of items; key item.id) {
				<li>{item.name}</li>
			} @empty {
				<li>Nothing yet</li>
			}
		</ul>
		<button onClick={() => (items = [])}>Clear</button>
	</main>
}

Click Clear. The two rows go away and “Nothing yet” shows.

Step 4: An input

onInput gets the browser’s own event. Write the typed text back into state. The Add button pushes a new row, moves the id on, and clears the input.

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

export default function App() @{
	let items = state([{ id: 1, name: 'Milk' }]);
	let draft = state('');
	let nextId = state(2);

	<main>
		<h1>My groceries</h1>
		<input value={draft} onInput={(event) => (draft = event.currentTarget.value)} />
		<button
			onClick={() => {
				items.push({ id: nextId, name: draft });
				nextId++;
				draft = '';
			}}
		>
			Add
		</button>
		<ul>
			@for (const item of items; key item.id) {
				<li>{item.name}</li>
			} @empty {
				<li>Nothing yet</li>
			}
		</ul>
		<p>{items.length} items</p>
	</main>
}

Type “Eggs” and click Add. A new row shows, the count says “2 items”, and the input clears.

Next: What are all the other files in a starter app? Project tour →

Was this page helpful?