docs/reference/language.md
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.
[!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
Within Marko templates a few variables are automatically made available.
inputA 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.
$signalAn AbortSignal is available in all JavaScript statements, expressions, and blocks in a .marko file.
It is aborted when
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.marko<script> document.addEventListener("resize", () => { // this function will be automatically cleaned up }, { signal: $signal }) </script>
$globalGives access the "render globals" provided through the top level api.
Marko supports a few module scoped top level statements.
importJavaScript import statements are allowed at the root of the template.
import sum from "sum"
<div data-number=sum(1, 2)></div>
[!NOTE] This syntax is a shorthand for
static import. For server and client specific imports, you can useserverandclientstatements.
import shorthandCustom 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/>
exportJavaScript export statements are allowed at the root of the template.
export function getAnswer() {
return 42;
}
<div>${getAnswer()}</div>
staticStatements 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()></div>
All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.
static {
console.log("this will be logged only ONE time");
console.log("no matter how often the component is used");
console.log("or how many requests are made to the server");
}
server and clientAs 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")
All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.
server {
import { connectToDatabase } from './database';
const db = connectToDatabase();
console.log('Database connection established on server');
// Only happens ONCE, when the application loads
// and this component is used for the first time
const users = await db.query('SELECT * FROM users');
console.log(`Found ${users.length} users in the database`);
}
[!TIP] The
importstatement is really a shortcut forstatic import. This can be leveraged withserverandclientif you want a module to only be imported on one platformmarkoserver import "./init-db" client import "bootstrap"
Marko supports all native HTML/SVG/whatever tags and attributes. In addition to these, a set of useful core tags are provided. Each project may have its own custom tags, and third-party tags may be included through node_modules.
All of these types of tags use the same syntax:
<my-tag/>
.marko files are automatically discovered as custom tags (no need for import).
All tags can be self closed when there is no content. This means <div/> is valid, unlike in HTML. Additionally void tags like <input> and can be self closed.
In all closing tags, the tag name may be omitted.
<div>Hello World</>
Attribute values are JavaScript expressions:
<my-tag str="Hello"></my-tag>
<my-tag str=`Hello ${name}`></my-tag>
<my-tag num=1 + 1></my-tag>
<my-tag date=new Date()></my-tag>
<my-tag fn=function myFn(param1) { console.log("hi") }></my-tag>
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:marko<my-tag value=(1 > 2)></my-tag>
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.
HTML boolean attributes become JavaScript booleans.
<input type="checkbox" checked>
<input type="checkbox" checked=true>
[!IMPORTANT]
ARIA enumerated attributes use strings instead of booleans, so make sure to pass a string.
marko// ❌ WRONG: Don't do this <button aria-pressed=isPressed /> // outputs <button aria-pressed=""/>marko// 👍 Correct use of aria attributes <button aria-pressed=isPressed && "true" /> // outputs <button aria-pressed="true"/>
Attributes may be dynamically included with the spread syntax.
<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. This means it can be used to leverage shorthand property names:marko<my-tag ...{ property }/>
Method definitions allow for a concise way to pass functions as attributes, such as event handlers.
<button onClick(e) { console.log(e.target) }>Click Me</button>
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 }/>
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/>
The change handler shorthand may optionally include a function that transforms the value before assignment. The function name appears between colons (:refiningFunction:=).
<input type="number" value:parseFloat:=num>
// desugars to
<input type="number" value=num valueChange(newValue) { num = parseFloat(newValue) }>
For Property Accessors, the desugared handler includes a boolean expression.
<input type="number" value:parseFloat:=input.num>
// desugars to
<input type="number" value=input.num valueChange=(input.num && (newValue) => { input.numChange(parseFloat(newValue)) })>
class and idEmmet style class and id attribute shorthands are supported.
<div#foo.bar.baz/>
// same as
<div id="foo" class="bar baz"/>
[!TIP] Interpolations are supported within a dynamic class/id.
marko<div.icon-${iconName}/>
valueIt 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/>
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=function () {
console.log("Hello JavaScript!");
}/>
// Received by the child as { value() { ... } }
Attributes can be terminated with a comma. This is useful in concise mode.
<my-tag a=1, b=2/>
[!CAUTION] Sequence expressions with comma operators must be wrapped in parentheses
marko<my-tag a=(console.log(foo), foo)/>
Markup within the body of a tag is made available as the content property of its input.
<my-tag>Content</my-tag>
The implementation of <my-tag> above can write out the content by passing its input.content to a dynamic tag:
export interface Input {
content: Marko.Body;
}
<div>
<${input.content}/>
</div>
Dynamic text content can be ${interpolated} in the tag content. This uses the same syntax as template literals in JavaScript.
export interface Input {
name: string;
}
<div>
Hello ${input.name}
</div>
[!NOTE] The interpolated value is automatically escaped to avoid XSS.
Tags prefixed with an @ are not rendered, but instead passed alongside attributes in input. Attribute tags allow for passing named or repeated content as additional attributes.
<my-layout title="Welcome">
<@header class="foo">
<h1>Big things are coming!</h1>
</@header>
<p>Lorem ipsum...</p>
</my-layout>
Here, @header is available to <my-layout> as input.header. The class attribute from @header is in input.header.class and its content is in input.header.content.
The full input object provided to <my-tag> in this example would look like:
// a representation of `input` received by `my-layout.marko` (from the previous code snippet)
{
title: "Welcome",
header: {
class: "foo",
content: /* <h1>Big things are coming!</h1> */,
},
content: /* <p>Lorem ipsum...</p> */,
}
The implementation of my-layout.marko might look like
export interface Input {
title: string;
header: Marko.AttrTag<{
class: string;
content: Marko.Body;
}>;
content: Marko.Body;
}
<!doctype html>
<html>
<head>
<title>${input.title}</title>
</head>
<body>
<header
// use the class from `@header`
class=input.header.class
>
// render the content of `@header`
<${input.header.content}/>
</header>
<main>
// render the main tag content
<${input.content}/>
</main>
<footer>
Copyright ♾️
</footer>
</body>
</html>
[!NOTE] Control flow tags (
<if>and<for>) cannot contain attribute tags themselves, and instead are used for dynamically creating attribute tags.
Attribute tags may be nested in other attribute tags.
<my-tag>
<@a value=1>
<@b value=2/>
</>
</>
Would provide the following as input
{
a: {
value: 2,
b: { value: 2 }
}
}
When multiple attribute tags share a name, all instances may be consumed using the iterable protocol.
<my-menu>
<@item value="foo">
Foo Item
</@item>
<@item value="bar">
Bar Item
</@item>
</my-menu>
This example uses two <@item> tags, but <my-menu> receives only a single item attribute.
{
item: {
value: "foo",
content: /* Foo Item */,
[Symbol.iterator]() {
// Not the exact implementation, but essentially this is what the function contains
yield* [
{ value: "foo", content: /* Foo Item */ },
{ value: "bar", content: /* Bar Item */ }
];
}
}
}
The other <@item> tags are reached through the iterator. The most common way to do so is with a for tag or one of JavaScript's syntaxes for iterables.
/* my-menu.marko */
export interface Input {
item?: Marko.AttrTag<{
value: string;
content: Marko.Body;
}>;
}
<for|item| of=input.item>
Value: ${item.value}
<${item.content}/>
</for>
Attribute tags are generally singular by name, even when repeated. Prefer singular property names when consuming repeated attribute tags (for example, iterate input.item rather than input.items).
[!TIP] If you need repeated attribute tags as a list, it is a common pattern to spread into an array with a
<const>tagmarkoexport interface Input { item?: Marko.AttrTag<{}>; } <const/items=[...input.item || []]> <div>${items.length}</div>
Attribute tags are generally provided directly to their immediate parent. The exception to this is control flow tags (<if> and <for>), which are used to dynamically apply attribute tags.
<my-message>
<if=welcome>
<@title>Hello</>
</if>
<else>
<@title>Good Bye</>
</else>
</my-message>
In this case, the @title received by <my-message> depends on welcome.
<my-select>
<@option>None</@option>
<for|opt| of=["a", "b", "c"]>
<@option>${opt}</>
</for>
</my-select>
Here, <my-select> unconditionally receives the first @option, and also all of the @option tags applied by the <for> loop.
[!NOTE] You can't mix attribute tags with default content while inside a control flow tag.
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>
Native tags have an implicitly returned tag variable that contains a reference to the element.
<div/myDiv/>
<script>
myDiv().innerHTML = "Hello";
</script>
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.
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>
While rendering content, child may pass information back to its parent using tag parameters.
/* child.marko */
export interface Input {
content: Marko.Body<[{ number: number }]>;
}
<div>
<${input.content} number=1337 />
</div>
/* parent.marko */
<child|params|>
Rendered with ${params.number} as the `number=` attribute.
</child>
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.
marko<child|{ number }|> Rendered with ${number} as the `number=` attribute. </child>
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.
export interface Input {
content: Marko.Body<[number, number, number]>;
}
<${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>
[!WARNING] Tag content may use attributes or arguments, but not both at once.
marko<my-tag a=1 b=2 c=3 /> // identical to <my-tag({ a: 1, b: 2, c: 3 })/>
Tag parameters are scoped to the tag content only. This means you cannot access the tag parameters outside the body of the tag.
[!CAUTION] Tag parameters cannot be accessed by attribute tags since they are evaluated as attributes.
Both HTML and JavaScript comments are supported.
<div>
<!-- html comments -->
// JavaScript line comments
/** JavaScript block comments */
</div>
[!NOTE] Comments are ignored completely. To include a literal HTML comment in the output, use the
<html-comment>core tag.
In place of the tag name, an ${interpolation} may be used to dynamically output a native tag, custom tag, or tag content.
With a dynamic tag the closing tag should be </>, or if there is no content the tag may be self-closed.
When the value of the dynamic tag name is a string,
export interface Input {
headingSize: 1 | 2 | 3 | 4 | 5 | 6;
}
// Dynamically output a native tag.
<${"h" + input.headingSize}>Hello!</>
// Dynamically output a custom tag.
import MyTagA from "<my-tag-a>"
import MyTagB from "<my-tag-b>"
<${Math.random() > 0.5 ? MyTagA : MyTagB}/>
<!---->[!CAUTION] Strings will always render native tags. When rendering a custom tag, you must have a reference to it. The following is not equivalent to the above example, since Marko would output a native HTML element (as if you called
document.createElement("my-tag-a")).marko<${Math.random() > 0.5 ? "my-tag-a" : "my-tag-b"}/>
[!NOTE] If an object is provided with a
contentproperty, thecontentvalue will become the dynamic tag name. This is how the define tag works under the hood 🤯.marko<define/message> Hello World </define> <${message}/>Although in this case you should prefer a PascalCase
<Message>tag instead.
When a dynamic tag name is falsy it will output the tag's content only. This is useful for conditional parenting and fallback content.
export interface Input {
href: string;
}
// Only wrap the text with an anchor when we have an `input.href`.
<${input.href && "a"} href=input.href>Hello World</>
Local variable names that start with an upper case letter (PascalCase) can also be used as tag names without the explicit dynamic tag syntax. This is useful for referencing an imported custom tag or with the <define> tag.
import MyTag from "./my-tag.marko"
<MyTag/>
This is equivalent to
import MyTag from "./my-tag.marko"
<${MyTag}/>