Control flow
How @if, @switch, and keyed @for add, remove, and move real elements on the page.
Last page, a value decided if an attribute exists. Blocks decide if whole elements exist.
I expected @for to rebuild the whole list on every change. It does not, and the key is why.
@if, @switch, and @for are blocks inside the markup. A branch adds and removes real elements. A keyed @for ties each row to its key, so a reorder moves the rows that already exist.
Click Shuffle, then Add, then Remove first.
What happens to existing rows when a keyed list changes?
Click Shuffle, then Add, then Remove first. Watch the element number on each row.
- Milkkey Aelement 1
- Eggskey Belement 2
- Breadkey Celement 3
import { state } from '@markless/core';import { START, nextItem, shuffle } from './list.ts'; export default function Shopping() @{ let items = state(START); <section> <ul> @for (const item of items; key item.id) { (each row follows its key) <li>{item.name}</li> } @empty { <li>No items yet</li> } </ul> <button onClick={() => (items = shuffle(items))}>Shuffle</button> <button onClick={() => (items = [...items, nextItem(items)])}>Add</button> <button onClick={() => (items = items.slice(1))}>Remove first</button> </section>}- ranMade 3 row elements, one per key: A, B, C.
Simplified. The element numbers are labels for this figure, not something Markless writes on the page. Each kept key keeps its element, as packages/vitest-browser/browser/keyed-row-behaviors.test.ts checks.
Notice that the row with key A keeps element 1 until you remove it.
Branches
import { computed, state } from '@markless/core';
export default function Counter() @{
let count = state(0);
const many = computed(() => count > 3);
<section>
<button onClick={() => count++}>Count {count}</button>
@if (many) {
<p>That is a lot of clicks.</p>
} @else {
<p>Keep going.</p>
}
</section>
}
The test inside @if ( ) is a plain read: a state or a computed(). Computed values get their own page later. Write @else with the @. A plain else fails the build.
For more than two cases, use @switch:
import { state } from '@markless/core';
export default function Light() @{
let color = state('red');
<div>
<button onClick={() => (color = color === 'red' ? 'green' : 'red')}>Switch</button>
@switch (color) {
@case 'red': { <p>Stop.</p> }
@case 'green': { <p>Go.</p> }
@default: { <p>Wait.</p> }
}
</div>
}
Lists
import { state } from '@markless/core';
export default function Todos() @{
let items = state([{ id: 1, name: 'Milk' }]);
<ul>
@for (const item of items; index i; key item.id) {
<li>{i + 1}. {item.name}</li>
} @empty {
<li>No items yet</li>
}
</ul>
}
key names what makes a row unique. index i gives the row its position. @empty shows when the list has no items.
Create state at the top of the body, not inside a block. A state() inside @if fails with MARKLESS_STATE_CREATION_SITE_UNSTABLE.
Coming from another framework?
@for with key plays the part of items.map() with a key prop in JSX. @if and @switch replace && and ? : around markup.
Next: How does one component pass data to another? Props and children →
