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

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.

The pageTry it
Shopping
  • Milkkey Aelement 1
  • Eggskey Belement 2
  • Breadkey Celement 3
Your code: Shopping.tsrx
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>}
What Markless did
Row elements made3one per new key
Row elements removed0one per key gone
  1. 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 →

Was this page helpful?