Composition¶
Three ways to avoid repeating markup: templates, for a subtree you use many times with different values; components, for markup a script decides on, usable from a script or straight from the tree; and includes, for splitting one long file into several.
Templates¶
A <template> declares a named subtree with parameters. Declare it once, then
instantiate it wherever you need it:
<root>
<template name="day">
<column class="day" id="day-{idx}" width="120px">
<label class="day-name" id="day-{idx}-name" text="{name}"/>
<image class="day-icon" src="icons/{icon}.png" width="56px" height="56px"/>
</column>
</template>
<row gap="12">
<day idx="0" name="Mon" icon="sun"/>
<day idx="1" name="Tue" icon="cloud"/>
<day idx="2" name="Wed" icon="rain"/>
</row>
</root>
Each {name} marker in the body takes the value the use site binds to that
name. Markers work in any attribute value and in text, which is how the example
above gives each instance its own id and image. A marker written where a tag
name goes is not markup, and fails the build.
A marker is substituted once, when the instance is created, and the value stays
put after that. For text that changes while the app runs, bind inside the
template body (bind-text="$status", see
reactivity) rather than passing the value through a marker; a
bind-* attribute cannot read a marker, and the $arg.<name> form is refused.
The <template> block itself never renders. It is stripped from the tree, so
where you put it in the file does not matter, and a use site may come before
the declaration.
A template and an lmn! block in a script are the same thing: both declare a
fragment, and both instantiate the same way. See
components for the script side.
Two ways to instantiate¶
<day idx="0"/> and <use template="day" idx="0"/> do the same thing. The
short form reads better for a small component; the <use> form is clearer when
the template name would collide with something else, and it is the form to
prefer for a shared page layout.
Do not name a template after a built-in tag. A template named button takes
over every <button> in the app.
Parameters and defaults¶
Every marker the body reads is a parameter. Attributes on the <template> tag,
other than name, give a parameter its default, which fills in wherever the use
site leaves that name unbound:
<template name="chip" icon="dot" tone="neutral">
<row class="chip chip-{tone}">
<image src="icons/{icon}.png"/>
<label text="{label}"/>
</row>
</template>
<chip label="Ready"/>
<chip label="Failed" tone="danger" icon="warning"/>
Bind every marker either at the use site or as a default. A marker with neither
stays in the markup as written, where it reads as a
global signal like any other {marker} in the tree, and shows
up as literal text when no signal by that name is set.
Slots¶
A <slot/> in the template body is where the use site's own content lands.
This is what makes a shared frame possible:
<template name="card">
<column class="card" gap="8">
<label id="title" class="card-title" text="{title}"/>
<slot/>
</column>
</template>
<card title="Recent">
<label text="Nothing yet."/>
<button text="Refresh"/>
</card>
When the use site is self-closing, or has no content, the slot falls back to its own:
Slots are unnamed. If a template body has more than one <slot/>, every one of
them receives the same content.
Ids inside a template¶
Give the use site an id and every id written in the template body is prefixed
with it:
The card's title label, written as id="title" in the template body, is
addressable as recent:title in that instance, and prefixes stack through
nested instances. Content you pass into a slot keeps the ids you gave it.
Without an id on the use site, the ids in the body stay exactly as the
template wrote them, which is what you want for a template used only once.
Reach for this whenever a template appears more than once and its contents need to be reachable from a script or a CSS id selector; otherwise the instances all answer to the same id.
Templates using templates¶
A template body can instantiate other templates, up to 64 levels deep. A template that instantiates itself, directly or through a chain, fails the build with the chain named.
Where templates are visible¶
A template is visible to the whole file it is declared in, and to any file that includes it.
In a multi-page app, templates are visible app-wide: a
<template> in any .lmn file in src/ can be used from any page.
Put a shared frame in layout.lmn, which contributes its templates but is not
itself a page:
<!-- layout.lmn -->
<root>
<template name="layout">
<column padding="20" gap="16" width="100%" height="100%">
<row gap="12">
<a href="index" text="Home"/>
<a href="settings" text="Settings"/>
</row>
<column gap="8">
<slot/>
</column>
</column>
</template>
</root>
Every page then wraps its content in it:
<!-- settings.lmn -->
<root>
<use template="layout">
<label text="Settings" font-size="24"/>
</use>
</root>
Two files declaring the same template name with different bodies fails the build: the set is app-wide, so either answer would change what half the use sites render.
Components¶
A component is a candela function that returns markup. Write the markup in an
lmn! block and the logic around it in candela:
import "lumen.cdl";
fn Home(name: string) {
return lmn!(<label class="home" text="home for $name"/>);
}
fn App() {
return lmn!(
<column id="app">
<Home name="bob"/>
</column>
);
}
fn on_ready() {
lumen::mount(App());
}
fn main() {}
lmn! is a markup block, not candela: tags, attributes, $name
interpolation, and elements naming another component. Everything else in the
function is ordinary candela, so a component decides what to render with if
and loops and then hands back one piece of markup.
A block is the same entity a <template> is. It compiles to a fragment when
the app is built, and the call instantiates that fragment by key, so a shipped
app carries the compiled markup and parses nothing while it runs. lumenc
check reads every block, and a malformed one fails the check with the file and
line it was written on.
A call returns a node handle, valid for the tick it was minted in. Attach it
with lumen::mount(handle) to put it at the app root, or with any of the
tree-mutating builtins in the
candela reference to put it somewhere
else.
Write components in a .cdl file. An inline <script> block is read as XML
like the rest of the markup, so a block written in one has to sit inside a
<![CDATA[ ... ]]> section for its tags to survive.
Arguments¶
$name reads the candela value of that name where the block was written, and
substitutes it once, when the instance is built. A value that changes while the
app runs belongs in a bind-* attribute inside the block, exactly as in a
template body:
{name} keeps its markup meaning inside a block: it is a
signal reference, resolved from the global scope every time
that signal changes. Write $name for something the surrounding candela knows
and {name} for something the app's signals hold.
Write $$ for a literal $.
Components inside components¶
An element whose tag starts with a capital letter names the candela function of that name. Props map to that function's parameters by name, in any order; a parameter no prop names is passed the empty string. A prop naming a parameter the function does not declare fails the compile, naming the component.
fn Row(title: string, tone: string) {
return lmn!(<row class="row row-$tone"><label text="$title"/></row>);
}
fn List() {
return lmn!(
<column>
<Row title="First" tone="warm"/>
<Row tone="cool" title="Second"/>
</column>
);
}
A prop is text. $name in a prop value reads the candela value of that name
where the element was written and renders it into the string, so Row above
receives "First" and "warm". A component that wants a number parses it.
A component element means the same thing in a block as in a .lmn file: the
build resolves it against the component it names, rather than the block
expanding to a call. List above builds Row twice at compile time and
carries the result.
Naming a component from markup¶
A .lmn file writes a component as a tag, the way it writes a template:
Props there are markup attribute values, so name="bob" reaches the block's
$name as text. Any component can be named this way; the section below is what
that costs.
Component names and <template name="..."> names share one namespace, and it
is app-wide. Two declarations claiming one name fail the build with both sites
named; rename either. A component that reaches itself, directly or through
another, fails the build with the chain named.
When the subtree appears¶
Nothing parses markup while an app runs, so the tree a use site stands for is built before the app starts. What that takes depends on the component:
- The build can stand in for the call.
Homeabove returns its block and nothing else, and every value in the block came from a parameter. The build puts the block at the use site with the arguments already substituted, which is exactly what callingHomewould have produced. The subtree is on screen in the first frame, and the function is never called. - The function has to run. It works a value out, or picks between blocks:
fn Greet(who: string) {
let loud = who + "!";
return lmn!(<label text="hello $loud"/>);
}
fn Pick(on: string) {
if on == "yes" { return lmn!(<label text="on"/>); }
return lmn!(<label text="off"/>);
}
Every block either one may return is still compiled into the app. What
stands at the use site is a marker; the runtime calls the function with the
props as arguments and puts the node it returns in the marker's place.
Pick picks its arm there, from blocks that are already built.
Both are on screen in the first frame. The fill runs on the first tick, before the tree is drawn, so a component that has to run costs the time that run takes rather than a frame the reader sees empty. A component doing real work at that moment does delay that first frame; move it behind a signal if it is slow.
A function the loaded program does not declare is reported once, naming the component, rather than leaving an empty element behind.
Annotate the parameters of a component that runs¶
Note the : string on Greet and Pick. A component that has to run is
called by name, and both the build and a shipped app call it through compiled
bytecode: candela makes a function callable by name only where it says what its
arguments are. A component written with a bare parameter compiles and ships,
and the call to it finds nothing, which leaves an empty element where its body
belongs.
Props arrive as text, so string fits every one of them; any works too.
A component the build stands in for is never called, so its parameters need no annotation. Annotating them all is the simpler rule, and it is what keeps a component usable after an edit turns it into one that has to run.
lumenc web names the ones that would come out empty, and prints the
component's own parameter list annotated, to write in place of what is there.
Pass --strict and the same warning ends the build instead.
A component on the web¶
Both kinds are in the document. A component's shape is markup, not app state,
so lumenc web resolves it while it builds the site: where the build cannot
stand in for the call it runs the function itself and writes the body it
returns into the HTML. A reader with no scripting and a crawler get the whole
tree, in every render and prerender combination. The browser adopts those
elements the way it adopts the rest of the page rather than building them
again.
A component written inside a <for> reaches the document too. It is called
once per row while the site is built, and each body is written into the row it
belongs to, so a crawler reads every card of the list. What the artifact
carries there is the row template, so the browser calls the component again as
it builds each row; it adopts the body the document holds when the call comes
out the same, and builds the subtree when the row or its arguments differ from
the ones the site was built with.
What a block may not do¶
- One root element. A block returns one node, so a body with no root or several fails the build.
- No markup children on a component element.
<Row title="x"/>is a call;<Row><label/></Row>is refused. Pass what the component renders as a prop, or give the component its own<slot/>and instantiate it as a template. lmn-is reserved. Names starting withlmn-belong to what a block generates.
Includes¶
An include splices another file's markup into this one at that exact spot:
<root>
<include src="parts/toolbar.lmn"/>
<column grow="1">
<include src="parts/editor.lmn"/>
</column>
</root>
The included file holds bare markup, not a document; it does not need its own
<root>. Paths are relative to the file doing the including, includes may
nest, and a cycle fails the build with the whole chain named.
Includes are resolved before anything else, so a <template> declared in an
included file is usable in the file that included it. This is the way to
keep a component library in its own file:
lumenc run watches included files, so editing one reloads the app.
Which to reach for¶
- The same subtree appearing several times, with different text or images: a template.
- Content that needs a frame around it: a template with a
<slot/>. - Markup a script decides on and places: a component.
- A subtree whose logic lives in candela but whose place in the page is fixed:
a component, named as a tag from the
.lmnfile. - One long page you want to read in pieces, each appearing once: includes.
- A stylesheet growing too large:
@importinmain.css. See styling.