The easiest way to get started with Marko is to use the Try Online feature. You can just open it in another tab and follow along. If you'd rather develop locally, check out the Installation page.
Marko makes it easy to represent your UI using a syntax that is like HTML:
hello.marko
<h1>Hello World</h1>
In fact, Marko is so much like HTML, that you can use it as a replacement for a templating language like handlebars, mustache, or pug:
template.marko
<!doctype html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
However, Marko is much more than a templating language. It's a language that allows you to declaratively build an application by describing how the application view changes over time and in response to user actions.
In the browser, when the data representing your UI changes, Marko will automatically and efficiently update the DOM to reflect the changes.
Let's say we want to perform an action once a <button>
is clicked:
button.marko
<button>Click me!</button>
Marko makes this really easy, allowing you to define a class
for a component right in the .marko
view and call methods of that class with on-
attributes:
button.marko
class {
sayHi() {
alert("Hi!");
}
}
<button on-click("sayHi")>Click me!</button>
Alerting when a button is clicked is great, but what about updating your UI in response to an action? Marko's stateful components make this easy. All you need to do is set this.state
from inside your component's class. This makes a new state
variable available to your view. When a value in this.state
is changed, the view will automatically re-render and only update the part of the DOM that changed.
counter.marko
class {
onCreate() {
this.state = {
count: 0
};
}
increment() {
this.state.count++;
}
}
<div>The current count is ${state.count}</div>
<button on-click("increment")>Click me!</button>
While HTML itself does not support conditionally displaying elements or repeating elements, it is a critical part of building any web application. In Marko, this functionality is provided by the <if>
and <for>
tags.
The <if>
tag receives an argument which is used to determine if its body content should be present.
<if(user.loggedOut)>
<a href="/login">Log in</a>
</if>
As you might expect, there are also <else>
and <else-if>
tags as well:
<if(user.loggedOut)>
<a href="/login">Log in</a>
</if>
<else-if(!user.trappedForever)>
<a href="/logout">Log out</a>
</else-if>
<else>
Hey ${user.name}!
</else>
If you have a list of data and need to represent it in the UI, the <for>
tag is probably what you're looking for. The <for>
tag passes each item and its index to its body as parameters.
<ul>
<for|color, index| of=colors>
<li>${index}: ${color}</li>
</for>
</ul>
The <for>
tag actually support 3 different flavors:
<for|item, index, array| of=array>
renders its body for each item of an array. It's similar to the JavaScriptfor...of
loop.<for|key, value| in=object>
renders its body for each property in an object. It's similar to the JavaScriptfor...in
loop.<for|value| from=first to=last step=increment>
renders its body for each value in between and includingfrom
andto
.
Marko automatically keeps your UI in sync with the state behind it, but one place where it needs a little extra help is repeated content. Specifying keys gives Marko a way to identify items in a list and keep track of which items have been changed, added, or removed.
A key should be a string or number that uniquely identifies an item in the list and differentiates it from its siblings. The same key value should never be used twice! Often, you will use something like an id
property.
<for|user| of=users>
<user-card key=user.id data=user/>
</for>
ProTip: If you have multiple tags underneath
<for>
, you can key only the first tag and that is enough to properly identify its siblings as well<dl> <for|entry| of=entries> <!-- only the first tag needs a key --> <dt key=entry.id>${entry.word}</dt> <!-- This key can be omitted --> <dd>${entry.definition}</dd> </for> </dl>
Note: If a key is not set, Marko will use the index of an item as its key. However this only works perfectly if items are only ever added or removed at the end of a list. Here's an example where things break down: if we have a list of
["A", "B", "C"]
and reverse the order, index keys would cause "A" to be transformed into "C" (and "C" into "A"), rather than just swapping them. Additionally if these components contained state, the new "C" would contain the state from the old "A" (and vice-versa). Be aware, stateful components include tags like the native<input>
element. For this reason it is always recommended to set akey
on tags in a<for>
.
Custom tags allow you to break up your application UI into encapsulated, reusable components.
Let's say we have a page with the following content:
page.marko
<!doctype html>
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
However, this page is getting pretty complex and unmaintainable. Let's split out the content into a separate component. To do this, we'll create a components/
folder and inside it a hello.marko
file:
components/hello.marko
<h1>Hello World!</h1>
Marko automatically discovers .marko
files under a components/
directory, so we can now use the <hello>
tag in our page:
page.marko
<!doctype html>
<html>
<body>
<hello/>
</body>
</html>
Now this <hello>
tag can be used multiple times, and even on multiple pages. But what if we don't only want to say hello to the world? Let's pass some attributes.
page.marko
<!doctype html>
<html>
<body>
<hello name="World"/>
</body>
</html>
The component will receive these attributes as input
:
components/hello.marko
<h1>Hello ${input.name}!</h1>
Nice.
Marko discovers components relative to the .marko
file where a custom tag is used. From this file, Marko walks up directories until it finds a components/
folder which contains a component matching the name of the custom tag. If it reaches the project root without finding anything, it will then check installed packages for the component.
Let's take a look at an example directory structure to better understand this:
components/
app-header.marko
app-footer.marko
pages/
about/
components/
team-members.marko
page.marko
home/
components/
home-banner.marko
page.marko
The file pages/home/page.marko
can use the following tags:
<app-header>
<app-footer>
<home-banner>
And the file pages/about/page.marko
can use the following tags:
<app-header>
<app-footer>
<team-members>
The home page can't see <team-members>
and the about page can't see <home-banner>
. By using nested component/
directories, we've scoped our page-specific components to their respective pages.
In addition to a Marko template, the children of components/
can be a directory with an index.marko
template:
components/
app-header/
index.marko
logo.png
style.css
app-footer/
index.marko
Or a directory with a template whose name matches its parent directory:
components/
app-header/
app-header.marko
app-header.style.css
logo.png
app-footer/
app-footer.marko
This allows you to create components that have other files associated with them and keep those files together in the directory structure.
ProTip: You can take advantage of nested
components/
directories to create "subcomponents" that are only available to the component that contains them.components/ app-header/ components/ navigation.marko user-info.marko app-header.marko app-footer/ app-footer.marko
To use tags from npm, ensure that the package is installed and listed in your package.json
dependencies:
npm install --save @marko-tags/match-media
Marko discover tags from packages defined in your package.json
, so you can start using them right away:
<div>
<match-media|{ mobile }| mobile="max-width:30em">
<!-- nice -->
</match-media>
</div>
We saw above that tags from npm are automatically discovered. In order to make this work, your package must include a marko.json
at the root.
marko.json
{
"tags-dir": "./dist/components"
}
This example file tells Marko to expose all components directly under the dist/components/
directory to the application using your package.
We recommend adding the marko
and components
keywords to your package.json
so others can find your components. Then npm publish
!
The <macro>
tag allows you to create custom tags in the same file that they are used in.
<macro|{ name }| name="welcome-message">
<h1>Hello ${name}!</h1>
</macro>
<welcome-message name="Patrick"/>
<welcome-message name="Austin"/>
If no other tag would be discovered Marko will check for an in scope variable that matches the tag name.
import SomeTag from "./somewhere.marko"
$ const { renderBody } = input;
$ const MyTag = input.href ? "a" : "button";
<SomeTag/>
<MyTag/>
<renderBody/>
The output of a component is based on input properties passed from its parent as attributes. However, a component may also maintain internal state that it uses to control its view. If Marko detects a change to either input or to the internal state, the view will automatically be updated.
ProTip: Only data that is owned and modified by the component should go into its
state
. State should be exclusively used for data that triggers rerenders. Parents controlinput
, and the component controls its ownstate
.
To use state
in Marko, you must first create a class component and initialize the state within the onCreate
method. In class methods, this.state
may be used and within the template section, a state
variable is available.
class {
onCreate() {
this.state = { count: 0 };
}
}
<div>The count is ${state.count}</div>
Note: Only properties that exist when
this.state
is first defined will be watched for changes. If you don't need a property initially, you can set it tonull
.
You can update state
in response to DOM events, browser events, ajax calls, etc. When a property on the state changes, the view will be updated to match.
class {
onCreate() {
this.state = { count: 0 };
}
increment() {
this.state.count++;
}
}
<div>The count is ${state.count}</div>
<button on-click('increment')>Increment</button>
We've extended our example above to add a button with an event handler, so that, when clicked, the state.count
value is incremented.
Note: When browsing existing code, you may see
this.setState('name', value)
being used. This is equivalent tothis.state.name = value
.
When a property on state
is set, the component will be scheduled for an update if the property has changed. All updates are batched together for performance. This means you can update multiple state properties at the same time without causing multiple updates.
ProTip: If you need to know when the update has been applied, you can use
this.once('update', fn)
within a component method.
Note: The state object only watches its properties one level deep. This means updates to nested properties on the state (e.g.
this.state.object.something = newValue
) will not be detected.Using immutable data structures is recommended, but if you want to mutate a state property (perhaps push a new item into an array) you can let Marko know it changed using
setStateDirty
.this.state.numbers.push(num); // mark numbers as dirty, because a `push` // won't be automatically detected by Marko this.setStateDirty("numbers");
There are various tools available to manage state outside of a single component. Here are some basic guidelines.
Typically we recommend using attributes
to pass data in to a child component, and children can emit events to communicate back up to their parents. In some cases this can become cumbersome with deeply nested data dependencies or global state.
For passing state throughout a component tree without explicit attribute setting throughout the entire app, you can leverage the <context>
tag. This tag can be installed from npm.
This tag allows you to pull state from any level above in the tree and can also be used to pass global state throughout your app. Context providers can register event handlers that any child in the tree can trigger similar to the events API.
fancy-form.marko
<context coupon=input.coupon on-buy(handleBuy)>
<!-- Somewhere nested in the container will be the buy button -->
<fancy-container/>
</context>
fancy-save-button.marko
<context|{ coupon }, emit| from="fancy-form">
Coupon: ${coupon}.
<button on-click(emit, "buy")>Buy</button>
</context>
Note: Context couples tags together and can limit reuse of components.
Often the above two approaches are enough, and many people jump to this part far too quickly. Like <context>
, often anything stored in redux is global
. This means that it can (if abused) create components that are hard to reuse, reason about and test. However it is important to understand when a tool like redux
is useful in any UI library.
Redux provides indirection to updating any state that it controls. This is useful if you need the following:
- Single state update, multiple actions (eg: logging, computed data, etc).
- Time travel debugging and other redux-specific tooling.
Both HTML and Marko provide support for <style>
tags. However, Marko also provides a special syntax (called a style block) which adds support for CSS preprocessors and acts as a hint to bundlers to extract this static css from your templates into a common bundle.
style {
div {
color: green;
}
}
<div>Hello World</div>
These blocks add global css to the page. The above example will not style just the <div>
in the component, but all divs on the page. Because of this we recommend following a naming convention such as BEM. Marko will likely provide a way to automatically scope these styles to the current component in the future.
Note: Style blocks (unlike
<style>
tags) do not support${placeholders}
and must be static.
If you use a css preprocessor, you can add the extension right on style
. This will cause your bundler of choice to run the contents of the style block through the appropriate processor.
style.less {
button.primary {
background-color: @primaryColor;
}
}
Marko’s event API supports:
- Browser events on native tags
- Custom events from custom tags
Note that you can’t mix event targets and event types: custom tags can only listen for custom events, and native tags can only listen for native events.
Both kinds of events are received with an on-*
attribute and the attribute arguments syntax:
<input type="checkbox"
on-change(event => console.info(`Checked? ${event.target.checked}`))
/>
The first argument for the attribute can be a function, or a string matching a method name on the component’s class
declaration.
If you provide a function as the first argument of the on-*
attribute, the function is called whenever the event fires, like standard event listeners.
Below we use the static
prefix to define a function, then use it as a click
handler:
static function handleClick(event) {
event.preventDefault();
console.log("Clicked!");
}
<button on-click(handleClick)>
Log click
</button>
In the above example, any time the <button>
is clicked the handleClick
function is called.
You can also use an inline arrow function:
<button on-click(() => alert("Clicked! 🎉"))>
Celebrate click
</button>
…or anything that evaluates to a function:
$ const handler = (
input.dontBreakMyApp ?
() => console.error("Clicked!") :
() => { throw Error("Clicked!") }
);
<button on-click(handler)>
Do not click
</button>
When a string is the first argument, Marko calls a matching method on the component's class
.
class {
logChange(newTab) {
console.log(`changed to: ${newTab}`);
}
}
<my-tabs on-switch-tab("logChange")>
…
</my-tabs>
When <my-tabs>
emits the switch-tab
event, it will call its logChange
method.
Within the handler you can access the current component instance, read data, emit events, update state, etc.
Arguments after the handler are prepended when the handler is called:
static function removeFriend(friendId, event) {
event.preventDefault();
window.myAPI.unfriend(friendId);
}
<for|friend| of=input.friends>
<button on-click(removeFriend, friend.id)>
Unfriend ${friend.name}
</button>
</for>
Here we share the logic for removeFriend()
with each friend
in the friends
array. When the <button>
is clicked, the id
of the removed friend
is passed to the removeFriend()
, handler followed by the DOM click
event.
The recommended way for a custom tag to communicate with its parent is through custom events.
All components implement a Node.js-style event emitter to send events to parent components.
email-input.marko
class {
handleChange(event) {
if (event.target.validity.valid) {
// Only emit email-changes if they are valid.
this.emit("email-change", { email: event.target.value });
}
}
}
<input type="email" name=input.name on-change("handleChange")/>
The above code listens to native change
events from the <input>
element, and then emits its own email-change
event if the change was valid.
<form>
<email-input name="email" on-email-change("...")/>
</form>
Note: Events are not received as
input
; you cannot accessinput.onEmailChange
. Instead, they set up subscriptions.
We're used to passing body content to HTML tags. When you do this, the tag has control over where and when this content is rendered. A good example of this is the HTML <details>
element:
<details>
<summary>Hello <strong>World</strong></summary>
This is some <em>content</em> that can be toggled.
</details>
This is what it renders (try clicking it):
Hello World
This is some content that can be toggled.Custom tags can also receive content in the same way. This allows a component to give its user full control over how some section of the content is rendered, but control where, when, and with what data it is rendered. This feature is necessary to build composable components like overlays, layouts, dropdowns, etc. Imagine a <table>
that didn't give you control over how its cells were rendered. That would be pretty limited!
When a custom tag is passed body content, it is received as a special renderBody
property on the component's input
. You can include this content anywhere in your component by using the <${dynamic}>
syntax.
components/fancy-container.marko:
<div class="container fancy">
<${input.renderBody}/>
</div>
If we were to use this tag like this:
Marko Source:
<fancy-container>
<p>Content goes here...</p>
</fancy-container>
The rendered output would be:
HTML Output:
<div class="container fancy"><p>Content goes here...</p></div>
This is a pretty basic example, but you can imagine how this could be incorporated into a more advanced component to render passed content where/when needed.
ProTip: Body content can be rendered multiple times. Or not at all.
When rendering body content with <${dynamic}>
, attributes may also be passed:
components/random-value.marko:
<!-- heh, it's not actually random -->
<${input.renderBody} number=1337 />
These attribute values can be received as a tag parameter:
<random-value|{ number }|>
The number is ${number}
</random-value>
ProTip: Some tags (like the above tag) may not render anything except their body content with some data. This can be quite useful, just look at the
<for>
and<await>
tags!
You can also pass named content sections to a tag using attribute tags which are denoted by the @
prefix.
<layout>
<@heading>
<h1>Hello Marko</h1>
</@heading>
<@content>
<p>...</p>
</@content>
</layout>
Like attributes, these attribute tags are received as input.heading
and input.content
, but they each have a renderBody
property which we can now use:
components/layout.marko
<!doctype html>
<html>
<body>
<${input.heading.renderBody}/>
<hr/>
<${input.content.renderBody}/>
</body>
</html>
ProTip: The
renderBody
property can be omitted. You could use<${input.heading}/>
, for example.
Attribute tags can be repeated. Rendering the same attribute tag name multiple times will cause the input value for that attribute to become an array instead of an single object.
This allows us to, for example, build a custom table component which allows its user to specify any number of columns, while still giving the user control over how each column is rendered.
Marko Source:
<fancy-table data=people>
<@column|person|>
Name: ${person.name}
</@column>
<@column|person|>
Age: ${person.age}
</@column>
</fancy-table>
Note Attribute tags are repeatable.
- Zero: if you don't pass any
@column
tags, thefancy-table
receivesundefined
.- One: if you pass a single
@column
tag, thefancy-table
receives a single attribute tag object. (For convenience this object is iterable meaning it can be directly passed to the<for>
tag.)- Many: if you pass multiple
@column
tags, thefancy-table
receives an array of attribute tags. For TypeScript theMarko.AttrTag
orMarko.RepeatableAttrTag
helpers should be used here.
Protip To
.map
,.filter
or otherwise work with attribute tags as an array:$ const columns = [...input.column || []];
We can then use the <for>
tag to render the body content into table, passing the row data to each column's body.
components/fancy-table/index.marko:
<table class="fancy">
<for|row| of=input.data>
<tr>
<for|column| of=input.column>
<td>
<${column.renderBody} ...row/>
</td>
</for>
</tr>
</for>
</table>
We now have a working <fancy-table>
. Let's see what it renders:
Example Data:
[
{
name: "Patrick",
age: 63,
},
{
name: "Austin",
age: 12,
},
];
HTML Output:
<table class="fancy">
<tr>
<td>Name: Patrick</td>
<td>Age: 63</td>
</tr>
<tr>
<td>Name: Austin</td>
<td>Age: 12</td>
</tr>
</table>
If you look at our previous example, we had to prefix each cell with the column label. It would be better if we could give a name to each column instead and only render that once.
Marko Source:
<fancy-table>
<@column|person| heading="Name">
${person.name}
</@column>
<@column|person| heading="Age">
${person.age}
</@column>
</fancy-table>
Now, each object in the input.column
array will contain a heading
property in addition to its renderBody
. We can use another <for>
and render the headings in <th>
tags:
components/fancy-table/index.marko:
<table class="fancy">
<tr>
<for|column| of=input.column>
<th>${column.heading}</th>
</for>
</tr>
<for|row| of=input.data>
<tr>
<for|column| of=input.column>
<td>
<${column.renderBody} ...row/>
</td>
</for>
</tr>
</for>
</table>
We'll now get a row of headings when we render our <fancy-table>
HTML Output:
<table class="fancy">
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Patrick</td>
<td>63</td>
</tr>
<tr>
<td>Austin</td>
<td>12</td>
</tr>
</table>
Note You may also specify that the attribute tag can be repeated in a
marko-tag.json
file. This will cause an array to always be passed if there are any items, rather than working up fromundefined
, single object and then an array.components/fancy-table/marko-tag.json:
{ "@data": "array", "<column>": { "is-repeated": true } }
Continuing to build on our example, what if we want to add some custom content or even components into the column headings? In this case, we can extend our <fancy-table>
to use nested attribute tags. We'll now have <@heading>
and <@cell>
tags nested under <@column>
. This gives users of our tag full control over how to render both column headings and the cells within the column!
Marko Source:
<fancy-table>
<@column>
<@heading>
<app-icon type="profile"/> Name
</@heading>
<@cell|person|>
${person.name}
</@cell>
</@column>
<@column>
<@heading>
<app-icon type="calendar"/> Age
</@heading>
<@cell|person|>
${person.age}
</@cell>
</@column>
</fancy-table>
Now instead of rendering the heading as text, we'll render the heading's body content.
components/fancy-table/index.marko:
<table class="fancy">
<tr>
<for|column| of=input.column>
<th>
<${column.heading.renderBody}/>
</th>
</for>
</tr>
<for|row| of=input.data>
<tr>
<for|column| of=input.column>
<td>
<${column.cell.renderBody} ...row/>
</td>
</for>
</tr>
</for>
</table>
Our headings can now include icons (and anything else)!
HTML Output:
<table class="fancy">
<tr>
<th><img class="icon" src="profile.svg" /> Name</th>
<th><img class="icon" src="calendar.svg" /> Age</th>
</tr>
<tr>
<td>Patrick</td>
<td>63</td>
</tr>
<tr>
<td>Austin</td>
<td>12</td>
</tr>
</table>
The flexibility of the <fancy-table>
is great if you want to render columns differently or have columns that display the data in a special way (such as displaying an age derived from a date of birth). However, if all columns are basically the same, the user might feel they're repeating themselves. As you might expect, you can use <for>
(and <if>
) to dynamically render attribute tags.
$ const columns = [{
property: "name",
title: "Name",
icon: "profile"
}, {
property: "age",
title: "Age",
icon: "calendar"
}]
<fancy-table>
<for|{ property, title, icon }|>
<@column>
<@heading>
<app-icon type=icon/> ${title}
</@heading>
<@cell|person|>
${person[property]}
</@cell>
</@column>
</for>
</fancy-table>
Note: Types are supported in Marko v5.22.7+ and Marko v4.24.6+
Marko’s TypeScript support offers in-editor error checking, makes refactoring less scary, verifies that data matches expectations, and even helps with API design.
Or maybe you just want more autocomplete in VSCode. That works too.
There are two (non-exclusive) ways to add TypeScript to a Marko project:
- For sites and web apps, you can place a
tsconfig.json
file at the project root:📁 components/ 📁 node_modules/ index.marko 📦 package.json tsconfig.json
- If you’re publishing packages of Marko tags, add the following to your
marko.json
:This will automatically expose type-checking and autocomplete for the published tags."script-lang": "ts"
ProTip: You can also use the
script-lang
method for sites and apps.
A .marko
file will use any exported Input
type for that file’s input
object.
This can be export type Input
or export interface Input
.
PriceField.marko
export interface Input {
currency: string;
amount: number;
}
<label>
Price in ${input.currency}:
<input type="number" value=input.amount min=0 step=0.01>
</label>
You can also import, reuse, and extend Input
interfaces from other .marko
or .ts
files:
import { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";
export type Input = PriceInput & ExtraTypes;
import { Input as PriceInput } from "<PriceField>";
export interface Input extends PriceInput {
discounted: boolean;
expiresAt: Date;
};
Generic Types and Type Parameters on Input
are recognized throughout the entire .marko
template (excluding static statements).
For example, if you set up a component like this:
components/my-select.marko
export interface Input<T> {
options: T[];
onSelect: (newVal: T) => unknown;
}
static function staticFn() {
// can NOT use `T` here
}
$ const instanceFn = (val: T) => {
// can use `T` here
}
// can use `as T` here
<select on-input(evt => input.onSelect(options[evt.target.value] as T))>
<for|value, i| of=input.options>
<option value=i>${value}</option>
</for>
</select>
…then your editor will figure out the types of inputs to that component:
<my-select options=[1,2,3] onSelect=val => {}/>
// ^^^ number
<my-select options=["M","K","O"] onSelect=val => {}/>
// ^^^ string
Marko exposes type definitions you can reuse in a TypeScript namespace called Marko
:
Marko.Template<Input, Return>
- The type of a
.marko
file typeof import("./template.marko")
- The type of a
Marko.TemplateInput<Input>
- The object accepted by the render methods of a template. It includes the template's
Input
as well as$global
values.
- The object accepted by the render methods of a template. It includes the template's
Marko.Body<Params, Return>
- The type of the body content of a tag (
renderBody
)
- The type of the body content of a tag (
Marko.Component<Input, State>
- The base class for a class component
Marko.Renderable
- Values accepted by the
<${dynamic}/>
tag string | Marko.Template | Marko.Body | { renderBody: Marko.Body}
- Values accepted by the
Marko.Out
- The render context with methods like
write
,beginAsync
, etc. ReturnType<template.render>
- The render context with methods like
Marko.Global
- The type of the object in
$global
andout.global
that can be passed to a template's render methods as the$global
property.
- The type of the object in
Marko.RenderResult
- The result of rendering a Marko template
ReturnType<template.renderSync>
Awaited<ReturnType<template.render>>
Marko.Emitter
EventEmitter
from@types/node
Marko.NativeTags
Marko.NativeTags
: An object containing all native tags and their types
Marko.Input<TagName>
andMarko.Return<TagName>
- Helpers to extract the input and return types native tags (when a string is passed) or a custom tag.
Marko.BodyParameters<Body>
andMarko.BodyReturnType<Body>
- Helpers to extract the parameters and return types from the specified
Marko.Body
- Helpers to extract the parameters and return types from the specified
Marko.AttrTag<T>
andMarko.RepeatableAttrTag<T>
- Used to represent types for attributes tags
Marko.AttrTag<T>
: A single attribute tagMarko.RepeatableAttrTag<T>
: One or more attribute tags
The most commonly used type from the Marko
namespace is Marko.Body
which can be used to type input.renderBody
:
child.marko
export interface Input {
renderBody?: Marko.Body;
}
Here, the following will be acceptable values:
index.marko
<child/>
<child>Text in render body</child>
<child>
<div>Any combination of components</div>
</child>
Passing other values (including components) will cause a type error:
index.marko
import OtherTag from "<other-tag>";
<child renderBody=OtherTag/>
Tag parameters are passed to the renderBody
by the child tag. For this reason, Marko.Body
also allows typing of its parameters:
for-by-two.marko
export interface Input {
to: number;
renderBody: Marko.Body<[number]>
}
<for|i| from=0 to=input.to by=2>
<${input.renderBody}(i)/>
</for>
index.marko
<for-by-two|i| to=10>
<div>${i}</div>
</for-by-two>
The types for native tags are accessed via the global Marko.Input
type. Here's an example of a component that extends the button
html tag:
color-button.marko
export interface Input extends Marko.Input<"button"> {
color: string;
renderBody?: Marko.Body;
}
$ const { color, renderBody, ...restOfInput } = input;
<button style=`color: ${color}` ...restOfInput>
<${renderBody}/>
</button>
interface MyCustomElementAttributes {
// ...
}
declare global {
namespace Marko {
namespace NativeTags {
// By adding this entry, you can now use `my-custom-element` as a native html tag.
"my-custom-element": MyCustomElementAttributes
}
}
}
declare global {
namespace Marko {
interface HTMLAttributes {
"my-non-standard-attribute"?: string; // Adds this attribute as available on all HTML tags.
}
}
}
declare global {
namespace Marko {
namespace CSS {
interface Properties {
"--foo"?: string; // adds a support for a custom `--foo` css property.
}
}
}
}
Any JavaScript expression in Marko can also be written as a TypeScript expression.
<child <T>|value: T|>
...
</child>
components/child.marko
export interface Input<T> {
value: T;
}
index.marko
// number would be inferred in this case, but we can be explicit
<child<number> value=1 />
<child process<T>() { /* ... */ } />
The types of attribute values can usually be inferred. When needed, you can assert values to be more specific with TypeScript’s as
keyword:
<some-component
number=1 as const
names=[] as string[]
/>
For existing projects that want to incrementally add type safety, adding full TypeScript support is a big leap. This is why Marko also includes full support for incremental typing via JSDoc.
You can enable type checking in an existing .marko
file by adding a // @ts-check
comment at the top:
// @ts-check
If you want to enable type checking for all Marko & JavaScript files in a JavaScript project, you can switch to using a jsconfig.json
. You can skip checking some files by adding a // @ts-nocheck
comment to files.
Once that has been enabled, you can start by typing the input with JSDoc. Here's an example component with typed input
:
// @ts-check
/**
* @typedef {{
* firstName: string,
* lastName: string,
* }} Input
*/
<div>${firstName} ${lastName}</div>
Many components in existing projects adhere to the following structure:
📁 components/ 📁 color-rotate-button/ index.marko component.js
The color-rotate-button
takes a list of colors and moves to the next one each time the button is clicked:
<color-rotate-button colors=["red", "blue", "yellow"]>
Next Color
</color-rotate-button>
Here is an example of how this color-rotate-button
component could be typed:
components/color-rotate-button/component.js
// @ts-check
/**
* @typedef {{
* colors: string[],
* renderBody: Marko.Renderable
* }} Input
* @typedef {{
* colorIndex: number
* }} State
* @extends {Marko.Component<Input, State>}
*/
export default class extends Marko.Component {
onCreate() {
this.state = {
colorIndex: 0,
};
}
rotateColor() {
this.state.colorIndex =
(this.state.colorIndex + 1) % this.input.colors.length;
}
}
components/color-rotate-button/index.marko
// @ts-check
/* Input will be automatically imported from `component.js`! */
<button
onClick('rotateColor')
style=`color: ${input.colors[state.colorIndex]}`>
<${input.renderBody}/>
</button>
For type checking Marko files outside of your editor there is the "@marko/type-check" cli. Check out the CLI documentation for more information.
The way Marko streams HTML is old and well-supported, but default configurations and assumptions by other software can foil it. This page describes some known culprits that may buffer your Node server’s output HTTP streams.
-
Turn off proxy buffering, or if you can’t, set the proxy buffer sizes to be reasonably small.
-
Make sure the “upstream” HTTP version is 1.1 or higher; HTTP/1.0 and lower do not support streaming.
-
Some software doesn’t support HTTP/2 or higher “upstream” connections at all or very well — if your Node server uses HTTP/2, you may need to downgrade.
-
Check if “upstream” connections are
keep-alive
: overhead from closing and reopening connections may delay responses. -
For typical modern webpage filesizes, the following bullet points probably won’t matter. But if you want to stream small chunks of data with the lowest latency, investigate these sources of buffering:
-
Automatic gzip/brotli compression may have their buffer sizes set too high; you can tune their buffers to be smaller for faster streaming in exchange for slightly worse compression.
-
You can tune HTTPS record sizes for lower latency, as described in High Performance Browser Networking.
-
Turning off MIME sniffing with the
X-Content-Type-Options
header eliminates browser buffering at the very beginning of HTTP responses
-
Most of NGiNX’s relevant parameters are inside its builtin http_proxy
module:
proxy_http_version 1.1; # 1.0 by default
proxy_buffering off; # on by default
Apache’s default configuration works fine with streaming, but your host may have it configured differently. The relevant Apache configuration is inside its mod_proxy
and mod_proxy_*
modules and their associated environment variables.
Content Delivery Networks (CDNs) consider efficient streaming one of their best features, but it may be off by default or if certain features are enabled.
-
For Fastly or another provider that uses VCL configuration, check if backend responses have
beresp.do_stream = true
set. -
Some Akamai features designed to mitigate slow backends can ironically slow down fast chunked responses. Try toggling off Adaptive Acceleration, Ion, mPulse, Prefetch, and/or similar performance features. Also check for the following in the configuration:
<network:http.buffer-response-v2>off</network:http.buffer-response-v2>
For extreme cases where Node streams very small HTML chunks with its built-in compression modules, you may need to tweak the compressor stream settings. Here’s an example with createGzip
and its Z_PARTIAL_FLUSH
flag:
import http from "http";
import zlib from "zlib";
import MarkoTemplate from "./something.marko";
http
.createServer(function (request, response) {
response.writeHead(200, { "content-type": "text/html;charset=utf-8" });
const templateStream = MarkoTemplate.stream({});
const gzipStream = zlib.createGzip({
flush: zlib.constants.Z_PARTIAL_FLUSH,
});
templateStream.pipe(outputStream).pipe(response);
})
.listen(80);