Core Tags
<if>
/ <else>
The <if>
and <else>
control flow tags are used to conditionally display content or apply attribute tags.
An <if>
is applied when its value=
attribute (shorthand used below) is truthy and may be followed by an <else>
.
The <else>
tag may have its own condition as an if=
attribute. When it has a condition, the condition is checked before the <else>
is applied and another <else>
may follow.
Expressions in the if/else chain are evaluated in order.
<if=EXPRESSION>
Body A
</if>
<else if=ANOTHER_EXPRESSION>
Body B
</else>
<else>Body C</else>
if=EXPRESSION -- Body A
else if=ANOTHER_EXPRESSION -- Body B
else -- Body C
<for>
The <for>
control flow tag allows for writing content or applying attribute tags while iterating. Its content has access to information about each iteration through the Tag Parameters.
The <for>
tag can iterate over:
Arrays and Iterables with the
of=
attribute<for|item, index| of=["a", "b", "c"]> ${index}: ${item} </for>
for|item, index| of=["a", "b", "c"] -- ${index}: ${item}
Object properties and values with the
in=
attribute<for|key, value| in={ a: 1, b: 2, c: 3 }> ${key}: ${value} </for>
for|key, value| in={ a: 1, b: 2, c: 3 } -- ${key}: ${value}
Ranges of numbers with the
to=
,from=
, andstep=
attributes<for|num| to=5> ${num} </for> // 0 1 2 3 4 5 <for|num| from=3 to=7> ${num} </for> // 3 4 5 6 7 <for|num| from=2 to=10 step=2> ${num} </for> // 2 4 6 8 10
for|num| to=5 -- ${num} // 0 1 2 3 4 5 for|num| from=3 to=7 -- ${num} // 3 4 5 6 7 for|num| from=2 to=10 step=2 -- ${num} // 2 4 6 8 10
The <for>
tag has a by=
attribute which helps preserve state while reordering content within the loop. The value should be a function (which receives the same parameters as the loop itself) that is used to give each iteration a unique key.
<for|user| of=users by=((user) => user.id)>
${user.firstName} ${user.lastName}
</for>
for|user| of=users by=((user) => user.id)
-- ${user.firstName}${" "}${user.lastName}
The by=
attribute above keys each iteration by its user.id
property.
Additionally, when using the of=
attribute, by=
may be a string. This will key the items by the corresponding property on each item.
This means the previous example can simplified to:
<for|user| of=users by="id">
${user.firstName} ${user.lastName}
</for>
for|user| of=users by="id" -- ${user.firstName}${" "}${user.lastName}
<let>
The <let>
tag introduces mutable state through its Tag Variable.
<let/x=1/>
let/x=1
The value=
attribute (usually with a shorthand) provides an initial value for its state.
When a tag variable is updated, everywhere it is used also re-runs. This is the core of Marko's reactive system.
<let/count=1/>
<button onClick() {
count++;
}>
Current count: ${count}
</button>
let/count=1
button onClick() {
count++;
}
-- Current count: ${count}
In this template, count
is incremented when the button is clicked. Since count
is a Tag Variable, it will cause any downstream expression (in this case the text in the button) to be updated every time it changes.
Note
The <let>
tag is not reactive to changes in its value=
attribute unless it is controllable. Its tag variable updates only through direct assignment or its change handler.
<let/count=input.initialCount/>
<p>Count: ${count}</p>
<p>Input Count: ${input.initialCount}</p>
let/count=input.initialCount
p -- Count: ${count}
p -- Input Count: ${input.initialCount}
Here, even if input.initialCount
changes, count
remains at its initial value.
Controllable Let
The <let>
tag can be made controllable using its valueChange=
attribute, similarly to native tag change handlers. This enables interception and transformation of state changes, or synchronization of state between parent and child components.
<let/value="HELLO"/>
<let/controlled_value=value valueChange(newValue) {
value = newValue.toUpperCase();
}/>
let/value="HELLO"
let/controlled_value=value valueChange(newValue) {
value = newValue.toUpperCase();
}
In this example:
value
holds the base state with an initial value of "HELLO"controlled_value
reflects the value ofvalue
, but itsvalueChange
handler ensures all updates are uppercase- Any changes to
controlled_value
are intercepted, transformed to uppercase, and stored invalue
A more common use case is creating state that can be optionally controlled by a parent component:
<let/count:=input.count/>
<button onClick() {
count++;
}>
Clicked ${count} times
</button>
let/count:=input.count
button onClick() {
count++;
}
-- Clicked ${count} times
This creates two possible behaviors:
Uncontrolled: If the parent only provides
count=
, the child maintains its own state:<counter count=0/>
counter count=0
Controlled: If the parent provides both
count=
andcountChange=
, the parent takes control of the state:<let/count=0/> <counter count:=count/> <button onClick() { count = 0; }> Reset </button>
let/count=0 counter count:=count button onClick() { count = 0; } -- Reset
<const>
The <const>
exposes its value=
attribute (usually with a shorthand) through its Tag Variable.
Extending the <let>
example we could derive data from the count
state like so:
<let/count=1/>
<const/doubleCount=count * 2/>
<button onClick() {
count++;
}>
Current count: ${count} And the double is ${doubleCount}
</button>
let/count=1
const/doubleCount=count * 2
button onClick() {
count++;
}
-- Current count: ${count} And the double is ${doubleCount}
Note
The <const>
tag is locally scoped and will be initialized for every instance of a component. If your goal is to expose a program wide constant, you should use static const
instead.
<return>
The <return>
tag allows any custom tag to expose a Tag Variable.
The value=
attribute (usually expressed via the shorthand) is made available as the tag variable of the template.
<return=42/>
return=42
The return value may then be used in the parent template:
<answer/value/>
<div>${value}</div>
answer/value
div -- ${value}
Assignable Return Value
By default, an exposed variable can not be assigned a value. Value assignment may be enabled with the valueChange=
attribute on the <return>
.
If a valueChange=
attribute is provided, it is called whenever the tag variable is assigned a value.
<let/value=input.value.toUpperCase()/>
<return=value valueChange(newValue) {
value = newValue.toUpperCase();
}/>
let/value=input.value.toUpperCase()
return=value valueChange(newValue) {
value = newValue.toUpperCase();
}
In the above example, the exposed tag variable is initialized to an UPPERCASE version of input.value
and when new values are assigned it will first UPPERCASE the value before storing it in state.
<uppercase/value=""/>
<input onInput(e) {
value = e.target.value;
}>
<div>${value}</div>
--
${" "}// value is always uppercased
--
uppercase/value=""
input onInput(e) {
value = e.target.value;
}
div -- ${value}
--
${" "}// value is always uppercased
--
<script>
The <script>
tag has special behavior in Marko.
The content of a <script>
tag is executed first when the template has finished rendering and is mounted in the browser. It will also be executed again after any Tag Variable or Tag Parameter it references has changed.
<let/count=1/>
<button/myButton onClick() {
count++;
}>
Current count: ${count}
</button>
<script>
// Runs in the browser for each instance of this tag.
// Also runs when either `myButton` or `count` updates
console.log("clicked", myButton(), count, "times");
</script>
let/count=1
button/myButton onClick() {
count++;
}
-- Current count: ${count}
script
--
// Runs in the browser for each instance of this tag.
// Also runs when either `myButton` or `count` updates
console.log("clicked", myButton(), count, "times");
--
Often the <script>
tag is coupled with the $signal
api to apply some side effect, and cleanup afterward.
<script>
const intervalId = setInterval(() => {
console.log("time", Date.now());
}, 1000);
$signal.onabort = () => clearInterval(intervalId);
</script>
script
--
const intervalId = setInterval(() => {
console.log("time", Date.now());
}, 1000);
$signal.onabort = () => clearInterval(intervalId);
--
Tip
There are very few cases where you should be using a real <script>
tag, but if you absolutely need it you can use the <html-script>
fallback.
<style>
The <style>
tag has special behavior in Marko. No matter how many times a component renders, its styles are only loaded once.
<style>
/* Bundled and loaded once */
body {
color: green;
}
</style>
style
--
/* Bundled and loaded once */
body {
color: green;
}
--
The <style>
may include a file extension to enable css preprocessors such as scss and less.
<style.scss>
$primary-color: green;
.fancy-scss {
color: $primary-color;
}
</style>
<div class="fancy-scss">
Hello!
</div>
<style.less>
@primary-color: blue;
.fancy-less {
color: @primary-color;
}
</style>
<div class="fancy-less">
Hello!
</div>
style.scss
--
$primary-color: green;
.fancy-scss {
color: $primary-color;
}
--
div.fancy-scss -- Hello!
style.less
--
@primary-color: blue;
.fancy-less {
color: @primary-color;
}
--
div.fancy-less -- Hello!
If the <style>
tag has a Tag Variable, it leverages CSS Modules to expose its classes as an object.
<style/styles>
.foo {
border: 1px solid red;
}
.bar {
color: green;
}
</style>
<div class=styles.foo/>
<div class=[styles.foo, styles.bar]/>
<div class={
[styles.bar]: true,
}/>
<div class=styles.foo/>
style/styles
--
.foo {
border: 1px solid red;
}
.bar {
color: green;
}
--
div class=styles.foo
div class=[styles.foo, styles.bar]
div class={
[styles.bar]: true,
}
div class=styles.foo
Tip
There are very few cases where you should be using a real inline <style>
tag but if needed you can use the fallback <html-style>
tag.
<define>
The <define>
tag is primarily used to create reusable snippets of markup that can be shared across the template.
<define/MyTag|input: { name: string }| foo=1>
<span>Hello ${input.name}</span>
</define>
<MyTag name="HTML"/>
<MyTag name="Marko"/>
<div>${MyTag.foo}</div>
define/MyTag|input: { name: string }| foo=1
span -- Hello ${input.name}
MyTag name="HTML"
MyTag name="Marko"
div -- ${MyTag.foo}
The Tag Variable reflects the attributes the <define>
tag was provided (including the content).
Tip
The implementation of the <define>
tag above is conceptually identical to <return>
ing its input
. 🤯
<return=input/>
return=input
<lifecycle>
The <lifecycle>
tag is used to synchronize side-effects from imperative client APIs.
<lifecycle
onMount() {
// Called once this tag is attached to the dom, and never again.
}
onUpdate() {
// Called every time the dependencies of the `onUpdate` function are invalidated.
}
onDestroy() {
// Called once this tag is removed from the dom.
}
/>
lifecycle [
onMount() {
// Called once this tag is attached to the dom, and never again.
}
onUpdate() {
// Called every time the dependencies of the `onUpdate` function are invalidated.
}
onDestroy() {
// Called once this tag is removed from the dom.
}
]
The this
is consistent across the lifetime of the <lifecycle>
tag and can be mutated.
client import { WorldMap } from "world-map-api";
<let/latitude=0/>
<let/longitude=0/>
<div/container/>
<lifecycle{
map: WorldMap;
}
onMount() {
this.map = new WorldMap(container(), { latitude, longitude, zoom });
}
onUpdate() {
this.map.setCoords(latitude, longitude);
this.map.setZoom(zoom);
}
onDestroy() {
this.map.destroy();
}
/>
client import { WorldMap } from "world-map-api";
let/latitude=0
let/longitude=0
div/container
lifecycle{
map: WorldMap;
} [
onMount() {
this.map = new WorldMap(container(), { latitude, longitude, zoom });
}
onUpdate() {
this.map.setCoords(latitude, longitude);
this.map.setZoom(zoom);
}
onDestroy() {
this.map.destroy();
}
]
Tip
All attributes on the <lifecycle>
tag attributes available as the this
in any of the event handler attributes.
<id>
The <id>
tag exposes a Tag Variable with a short unique id string (compatible with id=
and aria attributes).
<id/cheeseId/>
<label for=cheeseId>
Do you like cheese?
</label>
<input id=cheeseId type="checkbox" name="cheese">
id/cheeseId
label for=cheeseId -- Do you like cheese?
input id=cheeseId type="checkbox" name="cheese"
<log>
The <log>
tag performs a console.log of its value=
attribute (shown here using the shorthand).
The log is re-executed each time its tag variable updates.
<let/count=0/>
<log=`Current count: ${count}`/>
<button onClick() {
count++;
}>
Log
</button>
let/count=0
log=`Current count: ${count}`
button onClick() {
count++;
}
-- Log
This logs Current count: 0
on both server and client and again whenever count
changes.
<debug>
The <debug>
tag injects a debugger
statement within the template that will be executed once the tag renders.
<const/{ stuff }=input/>
<debug/>
--
${" "}// Can be useful to inspect render-scoped variables with a debugger.
--
const/{ stuff }=input
debug
--
${" "}// Can be useful to inspect render-scoped variables with a debugger.
--
If a value=
attribute is included, the debugger will be executed whenever it changes.
<debug=[input.firstName, input.lastName]/>
debug=[input.firstName, input.lastName]
This debugger executes on the initial render and whenever input.firstName
or input.lastName
changes.
<await>
The <await>
tag unwraps the promise in its value=
attribute and exposes it through a tag parameter.
<await|user|=getUser()>
<img src=user.avatar>
${user.name}
</await>
await|user|=getUser()
img src=user.avatar
-- ${user.name}
If this tag has a <try>
ancestor with a @placeholder
, the placeholder content is shown while the promise is pending.
<try>
<@placeholder>Loading...</@placeholder>
<@catch|err|>
${err.message}
</@catch>
<div>
<await|user|=getUser()>
${user.name}
</await>
</div>
</try>
try
@placeholder -- Loading...
@catch|err| -- ${err.message}
div
await|user|=getUser() -- ${user.name}
<try>
The <try>
tag is used for catching runtime errors and managing asynchronous boundaries. It has two optional attribute tags: @catch
and @placeholder
.
@catch
When a runtime error occurs in the content of the <try>
or its @placeholder
attribute tag, the content is replaced with the content of the @catch
attribute tag. The thrown error
is made available as the tag parameter of the @catch
.
<try>
<@catch|err|>
${err.message} // "Cannot read property `bar` of undefined"
</@catch>
<const/foo={ bar: { baz: 1 } }/>
${foo.baz.bar} // 💥 boom! 👇
</try>
try
@catch|err|
--
${err.message}${" "}// "Cannot read property `bar` of undefined"
--
const/foo={ bar: { baz: 1 } }
--
${foo.baz.bar}${" "}// 💥 boom! 👇
--
@placeholder
The content of the @placeholder
attribute tag will be displayed while an <await>
tag is pending inside of the content of the <try>
.
<html-comment>
By default, html comments are stripped from the output. The <html-comment>
tag is used to output a literal <!-- comment -->
.
<html-comment>Hello, view source</html-comment>
html-comment -- Hello, view source
This tag also exposes a tag variable which contains a getter to the reference of the comment node in the DOM.
<html-comment/commentNode/>
<return() {
return commentNode().parentNode.getBoundingClientRect();
}/>
html-comment/commentNode
return() {
return commentNode().parentNode.getBoundingClientRect();
}
<html-script>
& <html-style>
The <script>
and <style>
tags are enhanced to enable best practices and help developers avoid common footguns.
Though not typically needed, vanilla versions of these tags may be written via the <html-script>
and <html-style>
tags respectively.
// Literally written out as a `<script>` html tag.
<html-script type="importmap">
{ "imports": { "square": "./module/shapes/square.js" } }
</html-script>
// Literally written out as a `<style>` html tag.
<html-style>
@import url('https://fonts.googleapis.com/css2?family=Ubuntu&display=swap');
</html-style>
// Literally written out as a `<script>` html tag.
html-script type="importmap"
--
{ "imports": { "square": "./module/shapes/square.js" } }
--
// Literally written out as a `<style>` html tag.
html-style
--
@import url('https://fonts.googleapis.com/css2?family=Ubuntu&display=swap');
--
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.