Here is a bug you have met. A panel with a message box in it. You type half a sentence, close the
panel by accident, open it again, and your half sentence is not there.
That is not a glitch. It is the framework doing exactly what the code said, and once you can see
why, the fix is one line and you will never write the bug again.
@if puts real elements into the page and takes them out again, and anything declared inside the
branch goes out with them.
Type, close, open
Paste the file below into your own project and run three steps. Open the panel, type something into
the field, close the panel, then open it again.
Your text is gone. The draft variable that was holding it lived inside the branch, so when the
branch left the page the variable left with it. Opening the panel again did not restore anything. It
built a new field with a new, empty draft.
The important line is the one indented inside the @if:
import { state } from '@markless/core';export default function DraftLost() @{@{ · Component bodyOpens the body of a component, where ordinary statements and the markup they produce sit together. let open =statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.(false); <section> <buttononClickonClick · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={() => (open = !open)}>Toggle the panel</button>@if@if · ConditionalRenders the block that follows only while its condition is true. (open) { let draft =statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.(''); <label> Your message <input value={draft} onInputonInput · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={(event) => (draft = event.currentTarget.value)} /> </label> <p>Draft: {draft}</p> } </section>}
The fix is to move one line up
Take draft out of the branch and put it in the component body, above the @if. The branch still
comes and goes, but the value it reads does not:
import { state } from '@markless/core';export default function DraftKept() @{@{ · Component bodyOpens the body of a component, where ordinary statements and the markup they produce sit together. let open =statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.(false); let draft =statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.(''); <section> <buttononClickonClick · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={() => (open = !open)}>Toggle the panel</button>@if@if · ConditionalRenders the block that follows only while its condition is true. (open) { <label> Your message <input value={draft} onInputonInput · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={(event) => (draft = event.currentTarget.value)} /> </label> <p>Draft: {draft}</p> } </section>}
Run the same three steps on that one and your text comes back. The only difference between the two
components is which side of the @if the declaration sits on.
The rule to carry away: where you declare a value decides how long it lives. Inside a branch
means "for as long as this branch is on screen". Above it means "for as long as the component is".
Sometimes the first one is what you want. A filter panel that should open fresh every time is better
off declaring its own state inside the branch.
The rest of the shapes
@else covers the other side of the same condition:
import { state } from '@markless/core';export default functionEitherfunction Either(): Element() @{@{ · Component bodyOpens the body of a component, where ordinary statements and the markup they produce sit together. letopenlet open: boolean=statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.(false); <section(property) section: GlobalAttributes & MarklessAttributes<HTMLElement> & NativeEventAttributes<HTMLElement>> <button(property) button: GlobalAttributes & MarklessAttributes<HTMLButtonElement> & NativeEventAttributes<HTMLButtonElement> & FormAttributes & {onClickonClick · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={() => (open = !open)}>Toggle</button(property) button: GlobalAttributes & MarklessAttributes<HTMLButtonElement> & NativeEventAttributes<HTMLButtonElement> & FormAttributes & {>@if@if · ConditionalRenders the block that follows only while its condition is true. (open) { <p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>class(property) class?: string | undefined="open">Shown</p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>> } @else@else · Fallback branchRenders when the condition on the matching @if is false. { <p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>class(property) class?: string | undefined="closed">Hidden</p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>> } </section(property) section: GlobalAttributes & MarklessAttributes<HTMLElement> & NativeEventAttributes<HTMLElement>>}
Choosing between more than two reads better as @switch than as a chain. It renders the one @case
whose value matches, and @default catches everything else:
import { state } from '@markless/core';export default functionPickfunction Pick(): Element() @{@{ · Component bodyOpens the body of a component, where ordinary statements and the markup they produce sit together. letkindlet kind: string=statestate · Watched valueDeclares a variable the page follows, which you read and assign exactly like any other variable.('alpha'); <section(property) section: GlobalAttributes & MarklessAttributes<HTMLElement> & NativeEventAttributes<HTMLElement>> <button(property) button: GlobalAttributes & MarklessAttributes<HTMLButtonElement> & NativeEventAttributes<HTMLButtonElement> & FormAttributes & {onClickonClick · Event handlerRuns when this element fires the matching DOM event, and receives the native event object.={() => (kind = 'beta')}>Choose beta</button(property) button: GlobalAttributes & MarklessAttributes<HTMLButtonElement> & NativeEventAttributes<HTMLButtonElement> & FormAttributes & {>@switch@switch · Value matchRenders the one @case block whose value matches the expression it is given. (kind) {@case@case · Match branchRenders when the value handed to @switch equals this case. 'alpha': { <p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>>A</p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>> }@case@case · Match branchRenders when the value handed to @switch equals this case. 'beta': { <p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>>B</p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>> }@default@default · Match fallbackRenders when no @case matched the value.: { <p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>>Other</p(property) p: GlobalAttributes & MarklessAttributes<HTMLParagraphElement> & NativeEventAttributes<HTMLParagraphElement>> } } </section(property) section: GlobalAttributes & MarklessAttributes<HTMLElement> & NativeEventAttributes<HTMLElement>>}
These are part of the language, not a trick played with JavaScript expressions inside the markup.
That is what lets the compiler treat a branch as a real thing with a beginning and an end, rather
than as an opaque expression it has to re-evaluate.
What actually happens when the condition flips
Not much, and that is the point. Flipping a branch swaps a range of real DOM nodes in or out and
disposes the graph state that belonged to the branch. No component function runs again, not the one
holding the @if and not any component inside it.
Coming from another framework?
If you are used to writing conditions as an expression, with a ternary or a logical and inside the markup, the shape here is a statement instead, and the difference is more than cosmetic. A branch has an identity the compiler can name, so it can be given its own scope, its own disposal and its own place in the page. That is why the draft in the first file really is destroyed rather than merely hidden: there is no retained shadow copy of the tree anywhere for it to hide in.
Try it yourself
Take the second component and add a second field inside the branch, declared inside the @if this
time, so one value survives the toggle and the other does not. Type into both, close, open. Seeing
the two behaviours side by side in one panel is the fastest way to make the rule stick.