Language Reference
Marko is a superset of well-formed HTML.
The language makes HTML more strict while extending it with control flow and reactive data bindings. It does this by meshing JavaScript syntax features with HTML and introducing a few new syntaxes of its own. Most HTML is valid Marko but there are some important deviations.
Syntax Legend
import "...";
<tag|...params|/var ...attrs>
content with ${placeholders}
<@attr-tags/>
</>
import "...";
tag|...params|/var ...attrs
-- content with ${placeholders}
@attr-tag
Note
Jump to the section for a syntax by clicking on it. The legend is not comprehensive, for more see:
<${dynamic}/>
tag- Attributes for various attribute shorthands
- Tag Arguments as an alternative to attributes
- Concise Mode
Template Variables
Within Marko templates a few variables are automatically made available.
input
A JavaScript object globally available in every template that gives access to the attributes it was provided from a custom tag or the data passed in through the top level api.
$signal
An AbortSignal
available in all JavaScript statements and expressions.
It is aborted when
- The expression is invalidated
- The template or tag content is removed from the DOM
This is primarily to handle cleaning up side effects.
Tip
Many built-in APIs like addEventListener()
include the option to pass a signal for cleanup.
<script>
document.addEventListener(
"resize",
() => {
// this function will be automatically cleaned up
},
{
signal: $signal,
},
);
</script>
script
--
document.addEventListener(
"resize",
() => {
// this function will be automatically cleaned up
},
{
signal: $signal,
},
);
--
$global
Gives access the "render globals" provided through the top level api.
Statements
Marko supports a few module scoped top level statements.
import
JavaScript import
statements are allowed at the root of the template.
import sum from "sum";
<div data-number=sum(1, 2)/>
import sum from "sum";
div data-number=sum(1, 2)
Note
This syntax is a shorthand for static import
. For server and client specific imports, you can use server
and client
statements.
Tag import
shorthand
Custom tags may be referenced using angle brackets in the from
of the import, which will use Marko's custom tag discovery logic.
import MyTag from "<my-tag>";
<MyTag/>
import MyTag from "<my-tag>";
MyTag
export
JavaScript export
statements are allowed at the root of the template.
export function getAnswer() {
return 42;
}
<div>${getAnswer()}</div>
export function getAnswer() {
return 42;
}
div -- ${getAnswer()}
static
Statements prefixed with static
allow running JavaScript expressions in module scope. The statements will run when the template loaded on the server and in the browser.
static const answer = 41;
static function getAnswer() {
return answer + 1;
}
<div data-answer=getAnswer()/>
static const answer = 41;
static function getAnswer() {
return answer + 1;
}
div data-answer=getAnswer()
All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.
static console.log("this will be logged only ONE time");
static console.log("no matter how often the component is used");
static console.log("this will be logged only ONE time");
static console.log("no matter how often the component is used");
server
and client
As an alternative to static
, statements prefixed with server
or client
allow arbitrary module scoped JavaScript expressions that are exclusively executed when the template is loaded in a specific environment (the server or the browser).
server console.log("on the server");
client console.log("in the browser");
server console.log("on the server");
client console.log("in the browser");
All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.
server import { connectToDatabase } from "./database";
server const db = connectToDatabase();
server {
console.log("Database connection established on server");
// Only happens ONCE, when the application loads
// and this component is used for the first time
}
server const users = await db.query("SELECT * FROM users");
server console.log(`Found ${users.length} users in the database`);
server import { connectToDatabase } from "./database";
server const db = connectToDatabase();
server {
console.log("Database connection established on server");
// Only happens ONCE, when the application loads
// and this component is used for the first time
}
server const users = await db.query("SELECT * FROM users");
server console.log(`Found ${users.length} users in the database`);
Tip
The import
statement is really a shortcut for static import
. This can be leveraged with server
and client
if you want a module to only be imported on one platform
server import "./init-db";
client import "bootstrap";
server import "./init-db";
client import "bootstrap";
Attributes
Attribute values are JavaScript expressions:
<my-tag str="Hello"/>
<my-tag str=`Hello ${name}`/>
<my-tag num=1 + 1/>
<my-tag date=new Date()/>
<my-tag fn=(
function myFn(param1) {
console.log("hi");
}
)/>
my-tag str="Hello"
my-tag str=`Hello ${name}`
my-tag num=1 + 1
my-tag date=new Date()
my-tag fn=(
function myFn(param1) {
console.log("hi");
}
)
Almost all valid JavaScript expressions can be written as the attribute value. Even with <my-tag str="Hello">
the "Hello"
string is a JavaScript string literal and not an html attribute string.
Attributes can be thought of as JavaScript objects in Marko which are passed to a tag.
Caution
Values cannot contain an unenclosed >
since it is ambiguous. These expressions must use parentheses:
<my-tag value=(1 > 2)/>
my-tag value=(1 > 2)
Skipped Attributes
If an attribute value is null
, undefined
or false
it will not be written to the html.
Note
Not all falsy values are skipped. 0
, NaN
, and ""
will still be written.
Boolean Attributes
HTML boolean attributes become JavaScript booleans.
<input type="checkbox" checked>
<input type="checkbox" checked>
input type="checkbox" checked
input type="checkbox" checked
Important
ARIA enumerated attributes use strings instead of booleans, so make sure to pass a string.
// ❌ WRONG: Don't do this
<button aria-pressed=isPressed/>
// outputs <button aria-pressed=""/>
// ❌ WRONG: Don't do this
button aria-pressed=isPressed
// outputs <button aria-pressed=""/>
// 👍 Correct use of aria attributes
<button aria-pressed=isPressed && "true"/>
// outputs <button aria-pressed="true"/>
// 👍 Correct use of aria attributes
button aria-pressed=isPressed && "true"
// outputs <button aria-pressed="true"/>
Spread Attributes
Attributes may be dynamically included with the spread syntax.
<my-tag ...input foo="bar"/>
my-tag ...input foo="bar"
In this case <my-tag>
would receive the attributes as an object like { ...input, foo: "bar" }
.
Attributes are merged from left to right, with later spreads overriding earlier ones if there are conflicts.
Note
The value after the ...
(like in JavaScript) can be any valid JavaScript expression.
Shorthand Methods
Method definitions allow for a concise way to pass functions as attributes, such as event handlers.
<button onClick(ev) {
console.log(ev.target);
}>
Click Me
</button>
button onClick(ev) {
console.log(ev.target);
}
-- Click Me
Shorthand Change Handlers (Two-Way Binding)
The change handler shorthand (:=
) provides both a value for an attribute and a change handler with the attribute's name suffixed by "Change".
The value must be an Identifier or a Property Accessor.
For Identifiers, the change handler desugars to a function with an assignment.
<counter value:=count/>
// desugars to
<counter
value=count
valueChange(newCount) {
count = newCount;
}
/>
counter value:=count
// desugars to
counter [
value=count
valueChange(newCount) {
count = newCount;
}
]
For Property Accessors, the change handler desugars to a member expression with a Change
suffix.
<counter value:=input.count/>
// desugars to
<counter value=input.count valueChange=input.countChange/>
counter value:=input.count
// desugars to
counter value=input.count valueChange=input.countChange
Shorthand class
and id
Emmet style class
and id
attribute shorthands are supported.
<div class="bar baz" id="foo"/>
// same as
<div id="foo" class="bar baz"/>
div.bar.baz#foo
// same as
div#foo.bar.baz
Tip
Interpolations are supported within a dynamic class/id.
<div class=`icon-${iconName}`/>
div class=`icon-${iconName}`
Shorthand value
It is common for a tag to use a single input property; therefore Marko allows a shorthand for passing an attribute named value
. If the attribute name is omitted at the beginning of a tag, it will be passed as value
.
<my-tag=1/>
// desugars to
<my-tag value=1/>
my-tag=1
// desugars to
my-tag value=1
The method shorthand can be combined with the value attribute to give us the value method shorthand.
<my-tag() {
console.log("Hello JavaScript!");
}/>
// desugars to
<my-tag value() {
console.log("Hello JavaScript!");
}/>
// Received by the child as { value() { ... } }
my-tag() {
console.log("Hello JavaScript!");
}
// desugars to
my-tag value() {
console.log("Hello JavaScript!");
}
// Received by the child as { value() { ... } }
Attribute Termination
Attributes can be terminated with a comma. This is useful in concise mode.
<my-tag a=1 b=2/>
my-tag a=1 b=2
Caution
Comma operators / sequence expressions must be wrapped in parentheses
<my-tag a=(console.log(foo), foo)/>
my-tag a=(console.log(foo), foo)
Tag Content
Markup within a tag is made available as the content
property of its input
.
<my-tag>Content</my-tag>
my-tag -- Content
The implementation of <my-tag>
above can write out the content by passing its input.content
to a dynamic tag:
<div>
<${input.content}/>
</div>
div
${input.content}
Dynamic Text
Dynamic text content can be ${interpolated}
in the tag content. This uses the same syntax as template literals in JavaScript.
<div>Hello ${input.name}</div>
div -- Hello ${input.name}
Note
The interpolated value is automatically escaped to avoid XSS.
Tag Variables
Tag variables expose a value from a tag to be used within a template (from a custom tag, the variable is taken from its <return>
). These variables are not quite like JavaScript variables, as they are used to power Marko's compiled reactivity.
Tag Variables use a /
followed by a valid JavaScript identifier or destructure assignment pattern after the tag name.
<my-tag/foo/>
<my-other-tag/{ bar, baz }/>
<div>`my-tag` returned ${foo}</div>
<div>`my-other-tag` returned an object containing ${bar} and ${baz}</div>
my-tag/foo
my-other-tag/{ bar, baz }
div -- `my-tag` returned ${foo}
div -- `my-other-tag` returned an object containing ${bar} and ${baz}
Native tags have an implicitly returned tag variable that contains a reference to the element.
<div/myDiv/>
<script>
myDiv().innerHTML = "Hello";
</script>
div/myDiv
script
-- myDiv().innerHTML = "Hello";
In this case myDiv
will be a variable which can be called to get the myDiv
element in the browser.
Using the core <return>
tag, any custom tag can return a value into it's parents scope as a tag variable.
Scope
Tag variables are automatically hoisted and can be accessed anywhere in the template except for in module statements. This means that it is possible to read tag variables from anywhere in the tree.
<form>
<input/myInput>
</form>
<script>
// still available even though it's nested in another tag.
console.log(myInput());
</script>
form
input/myInput
script
--
// still available even though it's nested in another tag.
console.log(myInput());
--
Tag Parameters
While rendering content, child may pass information back to its parent using tag parameters.
<div>
<${input.content} number=1337/>
</div>
div
${input.content} number=1337
<child|params|>
Rendered with ${params.number} as the `number=` attribute.
</child>
child|params| -- Rendered with ${params.number} as the `number=` attribute.
This example results in the following HTML:
<div>Rendered with 1337 as the `number=` attribute</div>
The |parameters|
are enclosed in pipes after a tag name, and act functionally like JavaScript function parameters within which the first parameter is an object containing all attributes passed from the child component.
Tip
Parameters include all features of the JavaScript function parameters syntax, so feel free to destructure.
<child|{ number }|>
Rendered with ${number} as the `number=` attribute.
</child>
child|{ number }| -- Rendered with ${number} as the `number=` attribute.
Tag Arguments
Multiple tag parameters may be provided to the content by using the Tag Arguments syntax, which uses the JavaScript (...args)
syntax after the tag name.
<${input.content}(1, 2, 3)/>
${input.content}(1, 2, 3)
This example passes three arguments back to its parent.
<my-tag|a, b, c|>
Sum ${a + b + c}
</my-tag>
// spreads work also!
<my-tag|...all|>
Sum ${all.reduce((a, b) => a + b, 0)}
</my-tag>
my-tag|a, b, c| -- Sum ${a + b + c}
// spreads work also!
my-tag|...all| -- Sum ${all.reduce((a, b) => a + b, 0)}
Warning
Tag content may use attributes or arguments, but not both at once.
<my-tag a=1 b=2 c=3/>
// identical to
<my-tag({ a: 1, b: 2, c: 3 })/>
my-tag a=1 b=2 c=3
// identical to
my-tag({ a: 1, b: 2, c: 3 })
Scope
Tag parameters are scoped to the tag content only. This means you cannot access the tag parameters outside the body of the tag.
Caution
Attribute tags cannot access the tag parameters of their parent since they are evaluated as attributes.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.
Comments
Both HTML and JavaScript comments are supported.
Note
Comments are ignored completely. To include a literal HTML comment in the output, use the
<html-comment>
core tag.