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.
My groceries
The page's HTML right now
<main>
<h1>My groceries</h1>
<button>Count 0</button>
</main>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>}- ran
Appran 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 →
