if:when
Renders a tag and its content only while an expression is truthy, and narrows types inside it.
if:when="expresión" makes a tag and all its content exist only while the expression is truthy (in the JavaScript sense: 0, "", null and undefined are falsy). Valid on HTML elements, component tags, <component is> and, since 1.4, <snippet>.
Since 1.2, it also narrows types, like Vue's v-if: inside the tag, meeko check and the editor check attributes, props, events and content knowing the condition holds, without !.
Usage#
<!-- app/components/UserCard.meeko -->
<script lang="ts">
const user = signal<{ name: string; email: string } | null>(null);
</script>
<template>
<article if:when="user.value">
<h2>{user.value.name}</h2>
<a bind:href="`mailto:${user.value.email}`">{user.value.email}</a>
</article>
<p if:when="!user.value">Not signed in.</p>
</template>While user.value is null, only the second paragraph exists. Inside <article>, user.value is { name: string; email: string } to TypeScript.
Type#
<p if:when="user.value">{user.value.name}</p>Attributes#
| Attribute | Type | Default | Description |
|---|---|---|---|
if:when |
any expression | required | The condition. Evaluated as a JavaScript boolean and reevaluated when the signals it reads change. |
There is no if:else or else-if: write the opposite condition on another tag (if:when="!user.value"). An unknown if: attribute produces MEEK024: Directiva de plantilla incompleta and, alongside an if:when, warning MEEK119 (since 1.7), because it would be written as a literal HTML attribute.
Result#
- Truthy condition. The tag and its content render as if they had no directive.
- Falsy condition. Nothing renders: content is never evaluated, so its expressions do not execute or fail.
- In HTML. The region is enclosed by two marker comments, even when empty, so hydration knows where it is.
- On change. Becoming falsy disposes of the region (its effects, listeners and child components, which run their cleanup); becoming truthy recreates it with fresh state.
Examples#
Loading states#
Multiple mutually exclusive conditions cover a resource's states:
<script lang="ts">
const items = useApi("/api/items");
</script>
<template>
<p if:when="items.pending.value && !items.data.value">Loading…</p>
<p if:when="items.error.value" role="alert">Could not load.</p>
<ul if:when="items.data.value">
<li each:of="items.data.value" each:as="item" each:key="item.id">{item.name}</li>
</ul>
</template>For a region with loading, error and retry states, <MeekoAwait> does the same with less code and reserves height.
A conditional component#
On a component tag, the condition also narrows its props:
<template>
<WorkspaceHeader if:when="session.data.value?.staff" bind:staff="session.data.value.staff" />
</template>bind:staff receives the type without undefined, because TypeScript knows the condition holds.
Show and hide without losing state#
if:when destroys the region when hiding it. To retain form state or a panel's scroll position, hide the element with hidden instead of removing it:
<script lang="ts">
const open = signal(false);
const draft = signal("");
</script>
<template>
<button type="button" bind:aria-expanded="open.value ? 'true' : 'false'" on:click="() => open.value = !open.value">Notes</button>
<section bind:hidden="!open.value">
<textarea model="draft"></textarea>
</section>
</template>An optional snippet#
In the child, if:when checks whether the parent passed an optional snippet before rendering its container:
<!-- app/components/AppCard.meeko -->
<script lang="ts">
const snippets = defineSnippets<{ default: []; footer?: [] }>();
</script>
<template>
<article class="card">
<render snippet="snippets.default" />
<footer if:when="snippets.footer"><render snippet="snippets.footer" /></footer>
</article>
</template>Common mistakes#
if:when and each:of on the same tag#
<li each:of="items.value" each:as="item" if:when="item.visible">{item.name}</li>MEEK021: Un nodo no puede tener if:when y each:of a la vez. Filter the list first, or put the condition on an inner element.
<li each:of="visibleItems.value" each:as="item" each:key="item.id">{item.name}</li>With const visibleItems = computed(() => items.value.filter((item) => item.visible)) in the script.
Expect an else#
<p if:when="user.value">Hello, {user.value.name}</p>
<p if:else>Not signed in.</p>MEEK024: Directiva de plantilla incompleta, on if:else.
<p if:when="user.value">Hello, {user.value.name}</p>
<p if:when="!user.value">Not signed in.</p>Lose narrowing in an arrow function#
TypeScript does not retain property narrowing inside a function that runs later.
<div if:when="user.value">
<button on:click="() => save(user.value.id)">Save</button>
</div>meeko check reports TS18047: 'user.value' is possibly 'null'. in the handler.
<div if:when="user.value">
<button on:click="saveCurrent">Save</button>
</div>With function saveCurrent(): void { if (user.value) save(user.value.id); } in the script.
An empty condition#
<p if:when>Hello</p>MEEK020: Expression expected. The directive needs an expression; to hide an element statically, remove it from the template.
<p if:when="visible.value">Hello</p>Notes#
- SSR and hydration. The server evaluates the condition and renders the corresponding branch between its markers; the browser evaluates it again during hydration and must get the same result. A browser-dependent condition (
window.innerWidth,localStorage, local time) causes a hydration error: render that part inonMountedor store the value in a signal you change after mounting. - Performance. Hiding disposes of the region and showing recreates it with its children. For a panel opened and closed often,
bind:hiddenis cheaper and preserves state. - Types. Only checking changes: at runtime, content is never evaluated with a falsy condition. Narrowing reaches nested conditions and lists and snippets in a component tag, except where
each:as,each:indexor a snippet parameter shadows the name. - Accessibility. A message appearing with
if:whenis not announced automatically: give itrole="alert"orrole="status", or put it in an always-presentaria-liveregion.