marko Documentation

A declarative, HTML-based language that makes building web apps fun

RAW Doc

File: README.md

Intro

Marko is HTML reimagined as a language for building dynamic and reactive user interfaces. Almost any valid HTML is valid Marko, and Marko extends HTML for building modern applications more declaratively. Among these extensions are components, conditionals and loops, and a full reactivity system.

Example Component

The following renders a button and a counter of how many times the button has been pressed:

click-count.marko

marko
// HTML + JavaScript = πŸ’œ
<let/count=0>

  Clicked ${count} times

Getting Started

  1. npm init marko
  2. Read the docs

Community & Support

Contributors

Marko would not be what it is without all those who have contributed ✨

Get Involved!


File: packages/runtime-class/docs/10-awesome-marko-features.md

This article was published in August 2017. You can find the original "10 Awesome Marko Features" article here!

10 Awesome Marko Features

Marko is a friendly and super fast UI library that makes
building web apps fun! In celebration of rapidly approaching 5,000 stars on
GitHub
(the ultimate open source vanity
metric), here are 10 features that will make you more productive in no
particular order...

1. Shorthand Attributes

Tired of constantly typing out class and id attributes? No need with Marko.
Simply utilize the shorthand based on CSS selectors:

marko
style {
  .count {
    color:#09c;
  }
}

// Equivalent to 


// Equivalent to 


// Combined

2. All attribute values are Just JavaScriptβ„’

Unlike with HTML, you are not limited to string attribute values when using
Marko. Attributes can have types, which makes it really easy to pass data to
custom tags and it works for standard HTML tags too:

marko
<awesome-component myString="Hello"/>
<awesome-component myNumber=1/>
<awesome-component myTemplateString=`Hello ${name}`/>
<awesome-component myBoolean=true/>
<awesome-component myArray=[1, 2, 3]/>
<awesome-component myObject={hello: 'world'}/>
<awesome-component myVariable=name/>
<awesome-component myFunctionCall=input.foo()/>

3. Isomorphic UI components made easy

Tired of boilerplate code and trouble managing component input and state? Marko
makes it a breeze to develop self-contained and individually testable
components. Changing state is completely synchronous, so there won’t be any
headaches. You can also use inline styles making it very easy to develop small
components quickly.

marko
class {
  onInput(input) {
    this.state = {
      count: input.count || 0
    };
  }
  increment() {
    this.state.count++;
  }
}

style {
  .count {
    color:#09c;
  }
}

${state.count}

  Click me!

Do you see references to β€œMarko” in the snippet above? Yeah, me neither.

Is your component becoming too large? Do you prefer separating your CSS,
JavaScript, and markup code? No problem. You can easily rip out your code into
multiple files
:

text
components/
  click-counter/
    component.js
    index.marko
    style.css

4. Concise syntax

The DOM is just a tree structure. Indentation is a great way to describe a DOM
tree without having to worry about matching up beginning and ending tags. Marko
lets you choose between a concise, indentation-based syntax, and a familiar HTML
syntax:

marko
Count: ${state.count}


  Click me!

Here’s the same thing with the concise syntax:

marko
// Count our clicks!
div.count
  p -- Count: ${state.count}
button.example-button on-click('increment') -- Click me!

Can’t make up your mind or just want to paste in that code snippet from
StackOverflow? HTML syntax can be used within in the concise syntax. You’ll come
back and make it consistent…one day.

5. Import JavaScript modules

Do you have some helper JavaScript functions that you need to use in your views?
Marko lets you import any JavaScript module into your template using the same
syntax as the JavaScript import statement without using Babel or any other
build tool. No need for problematic globals (you could do that too, but please
don’t or your coworkers will hate you).

marko
import sum from './utils/sum';

The sum of 2 + 3 is ${sum(2, 3)}

6. No need to import custom tags (it’s a good thing, trust me)

Marko uses your directory structure as a method for automatically registering
custom tags. This means that Marko can implicitly import tags based on where the
template is located on disk. Marko will search up the directory looking for
custom tags in components/directories similar to how Node.js discovers modules
in node_modules/ directories.

Given the following directory structure:

text
components/
  fancy-button/
    index.marko
  fancy-container/
    index.marko

If fancy-button is used inside of fancy-container, it will be implicitly
imported:

marko
<fancy-button color=input.buttonColor/>

7. Use JavaScript to set CSS classes and styles

Setting CSS classes and styles is made easy using JavaScript! Marko will happily
accept simple strings, JavaScript objects and arrays (falsy values will be
ignored).

marko
$ const fontColor = input.color || 'blue';
$ const isActive = input.active === true;

8. Inline JavaScript Statements

Marko takes HTML and makes it more like JavaScript. You can exit out of HTML
mode to embed a JavaScript statement by starting the line with a $. You can
use this feature to embed JavaScript variables, functions, etc. where they are
needed (take that, β€œseparation of concerns”).

marko
$ const randomNumber = Math.random();
$ const person = {
  name: 'Frank',
  age: 32
};

Random number: ${randomNumber}
${person.name} is ${person.age} years old

If you want to combine multiple JavaScript statements you can do that too:

marko
$ {
  const randomNumber = Math.random();
  const person = {
    name: 'Frank',
    age: 32
  };
}

Random number: ${randomNumber}
${person.name} is ${person.age} years old

9. Async rendering with the `` tag

Node.js is asynchronous. Browsers are asynchronous. Why should rendering be
synchronous? Pass your promise along to your template and Marko will
asynchronously render parts of your view. Turns out, this is good for
performance
.

marko
$ const searchResultsPromise = searchService.performSearch(keywords);

<await(searchResultsPromise)>
  <@then|person|>
    Hello ${person.name}!
  </@then>
  <@catch|err|>
    The error was: ${err.message}.
  </@catch>
</await>

10. Server side rendering is easy

Can’t decide if you want to do server-side rendering or client-side rendering?
Why are we even talking about this in 2017? It doesn’t matter. Seriously, just
do both. Marko makes this a no-brainer since you can render a Marko template
directly to a stream (oh, and Marko will automatically mount UI
components rendered on the server when the page loads in the browser):

js
require("@marko/compiler/register"); // require .marko files!

const http = require("http");
const template = require("./template");

http
  .createServer()
  .on("request", (req, res) => {
    template.render(
      {
        name: "Frank",
        count: 30,
        colors: ["red", "green", "blue"],
      },
      res,
    );
  })
  .listen(8080);

Bonus: Friendly compile-time errors

We all make mistakes every now and then. Typo in your custom tag? Forgot an
ending tag? No worries! Marko will give you a friendly error message and point
you right to the problematic code.

marko
<fancy-buttn/>

You may have missed it, but it was obvious to Marko:

text
Unrecognized tag: fancy-buttn β€” More details: https://github.com/marko-js/marko/wiki/Error:-Unrecognized-Tag at line 2 col 1

Coming soon: auto correction and autonomous coding


Cover image from Wikipedia


File: packages/runtime-class/docs/class-components.md

Class Components

Marko makes it easy to create user interface components to use as building blocks for web pages and applications of any complexity.

Marko promotes self-contained components that:

  • Are independently testable
  • Encapsulate the view, client-side behavior (like event handling) and styling
  • Can easily be combined to create composite UI components.

Marko components compile into small, efficient JavaScript modules that hide implementation details from consumers. Components can be published to npm for reuse across applications.

UI component diagram

In Marko, the DOM output of a UI component is based on input properties and optional internal state used to control the view.

If Marko detects changes to input or the internal state, then the view (that is, the DOM) will automatically update to reflect the new input and state. Internally, Marko uses virtual DOM diffing/patching to update the view, but that’s an implementation detail that could change at any time.

Component structure

Marko makes it easy to keep your component’s class and styles next to the HTML view that they correspond to. The following are the key parts of any UI component:

  • View - The HTML template for your UI component. Receives input properties and states, and renders to either server-side HTML or browser-side virtual DOM nodes.
  • Client-side behavior - A JavaScript class with methods and properties for initialization, event handling (including DOM events, custom events and lifecycle events), and state management.
  • Styles - Cascading StyleSheets, including support for CSS preprocessors like Less or Sass.

Server-side rendering

A UI component can be rendered on the server or in the browser, but stateful component instances will be automatically mounted to the DOM in the browser for both. If a UI component tree is rendered on the server, then Marko will recreate the UI component tree in the browser with no extra code required. For more details, please see Rendering.

Single-file components

Marko lets you define a class for a component right in the .marko file, and call that class’s methods with on-* attributes:

marko
class {
    onCreate() {
        this.state = {
            count: 0
        };
    }
    increment() {
        this.state.count++;
    }
}

<label>The current count is <output>${state.count}</output></label>
+1

Styles

Adding styles in your view is also made easy:

marko
style {
    .primary {
        background: #09c;
    }
}

<label>The current count is <output>${state.count}</output></label>
+1

These styles aren’t output in a <style> tag as inline styles usually are, but are externalized to deduplicate them across multiple component instances on a page.

If you use a CSS preprocessor, you can add its file extension on style:

marko
style.less {
    .primary {
        background: @primaryColor;
    }
}

Note: The code in the style section is processed in a context separate from the rest of the template, so you can’t use JavaScript variables inside it. If you need variables in your CSS, use a CSS preprocessor that supports them.

Multi-file components

You might prefer to keep your component’s class and styles in separate files from the view β€” the classical separation of HTML, CSS, and JavaScript. Marko makes this possible with a filename-based convention.

ProTip: If your’re moving the component’s class and styles to separate files is because the code is getting too large, consider splitting the component into smaller, more manageable components.

Supporting files

Marko discovers supporting files in the same directory as a Marko view. For example, if you have a view named counter.marko, Marko will automatically look for counter.component.js and counter.style.css.

text
counter.marko
counter.component.js
counter.style.css

Marko also handles views named index.marko specially. It will look for component.js and style.css in addition to index.component.js and index.style.css. This allows easily grouping component files into a directory:

text
counter/
    index.marko
    component.js
    style.css

In your component.js file, export the component’s class:

js
export default class {
  onCreate() {
    this.state = {
      count: 0,
    };
  }
  increment() {
    this.state.count++;
  }
}

In your index.marko file, you can reference methods from that class with on-* attributes:

marko
<label>The current count is <output>${state.count}</output></label>
+1

And in your style.css, define the styles:

css
.primary {
  background: #09c;
}

ProTip: Marko actually looks any filenames with the pattern [name].style.*, so it will pick up any CSS preprocessor file extensions you use: .less, .stylus, .scss, etc.

Components with plain objects

If you target browsers that does not support classes, a plain object of methods can be exported:

js
export default {
  onCreate: function () {
    this.state = {
      count: 0,
    };
  },
  increment: function () {
    this.state.count++;
  },
};

Split components

Split components optimize for when a component renders on the server, and doesn’t need to dynamically rerender in the browser. As a result, its template and logic aren’t sent to the browser, reducing load time and download size.

Note: If a split component is the child of a stateful component, its full rendering logic will still be sent because the parent may pass new input to the split component and rerender it.

Additionally, if all components rendered on a page are split components, Marko’s VDOM and rendering runtime is unnecessary, and therefore not sent to the browser.

ProTip: Don’t over-optimize. If your component really doesn’t need rerendering, go ahead and split, but don’t forgo stateful rerendering when it would make your code more maintainable.

Usage

Marko discovers split components similarly to how it discovers an external component class. For example, if you have a view named button.marko, it will automatically look for button.component-browser.js. If your view is named index.marko, it will look for component-browser.js in addition to index.component-browser.js.

text
counter/
    index.marko
    component-browser.js

A split component might need to do some setup as part of its initial render. In this case, the component may define a second component class to use the onCreate, onInput, and onRender lifecycle methods.

This class can be exported from component.js, or defined right in the template as a single-file components. In this case, your component folder may contain a component.js file, and must contain a component-browser.js. The following lifecycle methods can go inside the component.js file:

text
class {
  onCreate(input, out) { }
  onInput(input, out) { }
  onRender(out) { }
  onDestroy() { }
}

And the following lifecycle methods can go inside the component-browser.js file:

text
class {
  onMount() { }
  onUpdate() { }
}

Any JavaScript code related to the DOM or browser should also be inside component-browser.js.

Example

index.marko

marko
class {
    onCreate() {
        this.number = 123;
    }
}

What’s my favorite number?

component-browser.js

js
export default {
  shout() {
    alert(`My favorite number is ${this.number}!`);
  },
};

Event handling

The on-[event](methodName|function, ...args) attributes allow event listeners to be attached for either:

  • A native DOM event, when used on a native DOM element such as a ``
  • Or a UI component event, when used on a custom tag for a UI component such as <my-component>

The on-* attributes are used to associate event handler methods with an event name. Event handlers may be specified by 'methodName' β€” a string that matches a method on the component instance, or they may be a function. Attaching listeners for native DOM events and UI component custom events is explained in more detail in the sections below.

You may also use the once-[event](methodName|function, ...args) syntax, which will listen for only the first event, and then remove the listener.

Attaching DOM event listeners

The code below illustrates how to attach an event listener for native DOM events:

marko
class {
  onButtonClick(name, event, el) {
    alert(`Hello ${name}!`);
  }
}

static function fadeIn(event, el) {
  el.hidden = false;
  el.style.opacity = 0;
  el.style.transition = 'opacity 1s';
  setTimeout(() => el.style.opacity = 1);
}


  Say Hello to Frank



  Say Hello to John

The following arguments are passed to the event handler when the event occurs:

  1. ...args - Any extra bound arguments are prepended to the arguments passed to the component’s handler method.
    For example: on-click('onButtonClick', arg1, arg2) β†’ onButtonClick(arg1, arg2, event, el)
  2. event - The native DOM event object.
  3. el - The DOM element that the event listener was attached to.

When using the on-* or once-* attributes to attach event listeners, Marko uses event delegation that is more efficient than direct attachment of el.addEventListener(). Please see Why is Marko Fast? Β§ Event delegation for more details.

Attaching custom event listeners

The code below illustrates how to attach an event listener for a UI component’s custom event:

marko
class {
  onCounterChange(newValue, el) {
    alert(`New value: ${newValue}!`);
  }
  onCounterMax(max) {
    alert(`It reached the max: ${max}!`);
  }
}

<counter on-change('onCounterChange') once-max('onCounterMax') />

The following arguments are passed to the event handler when the event occurs:

  1. ...args - Any extra bound arguments are prepended to the arguments passed to the component’s handler method.
  2. ...eventArgs - The arguments passed to this.emit() by the target UI component.
  3. component - The component instance that the event listener was attached to.

The following code illustrates how the UI component for <counter> might emit its change event:

counter/index.marko

marko
class {
  onCreate() {
    this.max = 50;
    this.state = { count: 0 };
  }
  increment() {
    if (this.state.count < this.max) {
        this.emit('change', ++this.state.count);
    }
    if (this.state.count === this.max) {
        this.emit('max', this.state.count);
    }
  }
}



  Increment

ProTip: Unlike native DOM events, UI component custom events may be emitted with multiple arguments. For example:

js
this.emit("foo", "bar", "baz");

Attributes

`on-[event](methodName|function, ...args)`

The on-* attribute syntax attaches an event listener to either a native DOM event or a UI component event. The on-* attribute associates an event handler method with an event name. Please see the Event handling section above for details.

`once-[event](methodName|function, ...args)`

The same as the on-* attribute, except that its listener is only invoked for the first event, and then removed from memory. Please see the Event handling section above for more details.

`key`

The key property does 2 things in Marko:

  • Obtains references to nested HTML elements and nested UI components.
  • Matches corresponding elements together when DOM diffing/patching after a rerender. When updating the DOM, keyed elements/components are matched up and reused rather than discarded and recreated.

Internally, Marko assigns a unique key to all HTML elements and UI components in a .marko file, based on the order they appear in the file. If you have repeated elements or elements that move between locations in the DOM, then you likely want to assign a custom key by adding a key attribute. The key attribute can be applied to both HTML elements and custom tags.

Referencing nested HTML elements and components

marko
class {
  onMount() {
    const headerElement = this.getEl('header');
    const colorListItems = this.getEls('colors');
    const myFancyButton = this.getComponent('myFancyButton');
  }
}

<h1 key="header">Hello</h1>


  <for|color| of=['red', 'green', 'blue']>
      <li key="colors[]">${color}</li>
  </for>


<fancy-button key="myFancyButton"/>

Note: The [] suffix (e.g. key="colors[]") lets Marko know that the element will be repeated multiple times with the same key.

Keyed matching

The key attribute can pair an HTML element or UI component that moves to a new location in the DOM. For example:

marko
class {
  onCreate() {
    this.state = {
      swapped: false
    }
  }
}


  B
  A

<else>
  A
  B
</else>

The key attribute can be used to pair HTML elements or UI components that are repeated:

marko
<for|user| of=input.users>
      <li key=user.id>${user.name}</li>
  </for>

This way, if the order of input.users changes, the DOM will be rerendered more efficiently.

`*:scoped`

The :scoped attribute modifier results in the attribute value getting prefixed with a unique ID associated with the current UI component. :scoped attribute modifiers can be used to assign a globally unique attribute value from a value that only needs to be unique to the current UI component.

Here’s a use-case: certain HTML attributes reference the id of other elements on the page. For example, the HTML <label> for attribute takes an id as its value. Many ARIA attributes like aria-describedby also take an id as their value.

The :scoped modifier on an attribute allows you to reference another element without fear of duplicate ids, as shown in the following examples:

for:scoped

marko
<label for:scoped="name">Name</label>

The above code will output HTML similar to the following:

html
<label for="c0-name">Name</label>

aria-describedby:scoped

marko
Close


   Closing this window will discard any entered information and return you to the main page.
html
Close


  Closing this window will discard any entered information and return you to the
  main page.

href:scoped

marko
<a href:scoped="#anchor">Jump to section</a>
<section id:scoped="anchor"></section>
html
[Jump to section](#c0-anchor)
<section id="c0-anchor"></section>

`no-update`

Preserves the DOM subtree associated with the element or component, so it won’t be modified when rerendering.

marko
marko
<app-map no-update/>

This is most useful when other JavaScript modifies the DOM tree of an element, like for embeds.

`no-update-if`

Similar to no-update, except that the DOM subtree is conditionally preserved:

marko

`no-update-body`

Similar to no-update, except that only the descendant DOM nodes are preserved:

marko
…

`no-update-body-if`

Similar to no-update-body, except that its descendant DOM nodes are conditionally preserved:

marko

`:no-update`

Prevents certain attributes from being modified during a rerender. The attribute(s) that should not be modified should have a :no-update modifier:

marko
…

Properties

`this.id`

A string identifier for the root HTML element that the component is bound to. (Not the id attribute.)

`this.state`

The current state for the component. Changing this.state or its direct properties will cause the component to rerender.

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 its value to null:

marko
class {
    onCreate() {
        this.state = {
            data: null,
            error: null
        }
    }
    getData() {
        fetch('/endpoint')
            .then(data => this.state.data = data)
            .catch(error => this.state.error = error);
    }
}

Beware: setting a state property only nominates the component for a possible rerender, and properties are only watched one level deep. Thus, the component is only rerendered if at least one of the component state properties changed (oldValue !== newValue).

If none of the properties changed (because the new value is identical, or no difference is detected by a shallow comparison), the assignment is considered a no-operation (great for performance).

We recommend using immutable data structures, but if you want to mutate a state property (perhaps push a new item into an array), you can mark it as dirty with setStateDirty:

js
this.state.numbers.push(num);

// Mark numbers as dirty, because a `push`
// won’t be automatically detected by Marko
this.setStateDirty("numbers");

`this.input`

The current input for the component. Setting this.input will rerender the component which can be useful for testing, but generally avoided (prefer state instead).

Variables

When a Marko component is compiled, some additional variables are available to the rendering function. These variables are described below.

`component`

The component variable refers to the instance of the currently rendering UI component. This variable can be used to call methods on the UI component instance:

marko
class {
    getFullName() {
        const { person } = this.input;
        return `${person.firstName} ${person.lastName}`;
    }
}

<h1>Hello, ${component.getFullName()}</h1>

`input`

The input variable refers to the input object, and is equivalent to component.input|this.input.

marko
<h1>Hello, ${input.name}</h1>

`state`

The state variable refers to the UI component’s state object, and is the unwatched equivalent of component.state|this.state.

marko
<h1>Hello ${state.name}</h1>

Methods

`destroy([options])`

Option Type Default Description
removeNode Boolean true false will keep the component in the DOM while unsubscribing all events from it
recursive Boolean true false will prevent child components from being destroyed

Destroys the component by unsubscribing from all listeners made using the subscribeTo method, and then detaching the component’s root element from the DOM. All nested components (discovered by querying the DOM) are also destroyed.

js
component.destroy({
  removeNode: false, // true by default
  recursive: false, // true by default
});

`forceUpdate()`

Queue the component to re-render and skip all checks to see if it actually needs it.

When using forceUpdate() the updating of the DOM will be queued up. If you want to immediately update the DOM
then call this.update() after calling this.forceUpdate().

`getEl([key, index])`

Signature Type Description
key String optional β€” the scoped identifier for the element
index Number optional β€” the index of the component, if key references a repeated component
return value HTMLElement The element matching the key, or this.el if no key is provided

Returns a nested DOM element by prefixing the provided key with the component’s ID. For Marko, nested DOM elements should be assigned an ID with the key attribute.

`getEls(key)`

Signature Type Description
key String The scoped identifier for the element
return value Array<HTMLElement> An array of repeated DOM elements for the given key

Repeated DOM elements must have a value for the key attribute that ends with []. For example, key="items[]".

`getElId([key, index])`

Signature Type Description
key String optional β€” The scoped identifier for the element
index Number optional β€” The index of the component, if key references a repeated component
return value String The element ID matching the key, or this.el.id if key is undefined

Similar to getEl, but only returns the String ID of the nested DOM element instead of the actual DOM element.

`getComponent(key[, index])`

Signature Type Description
key String The scoped identifier for the element
index Number optional β€” The index of the component, if key references a repeated component
return value Component A reference to a nested Component for the given key. If an index is provided and the target component is a repeated component (i.e. key="items[]"), then the component at the given index will be returned.

For example, given the following component,

marko
<app-main>
  <app-child key="child"/>
</app-main>

The following code can be used to get the <app-child/> component:

js
const childComponent = this.getComponent("child");

`getComponents(key, [, index])`

Signature Type Description
key String The scoped identifier for the element
index Number optional β€” The index of the component, if key references a repeated component
return value Array<Component> An array of repeated Component instances for the given key

Repeated components must have a value for the key attribute that ends with [], like key="items[]".

`isDestroyed()`

Returns true if a component has been destroyed using component.destroy(), otherwise false.

`isDirty()`

Returns true if the component needs a bath.

`replaceState(newState)`

Signature Type Description
newState Object A new state object to replace the previous state

Replaces the state with an entirely new state. Equivalent to this.state = newState.

Note: While setState() is additive and will not remove properties that are in the old state but not in the new state, replaceState() will add the new state and remove the old state properties that are not found in the new state. Thus, if replaceState() is used, consider possible side effects if the new state contains less or other properties than the replaced state.

`rerender([input])`

Signature Type Description
input Object optional β€” New input data to use when rerendering

Rerenders the component using its renderer, and either supplied input or internal input and state.

`setState(name, value)`

Signature Type Description
name String The name of the state property to update
value Any The new value for the state property

Changes the value of a single state property. Equivalent to this.state[name] = value, except it will also work for adding new properties to the component state.

js
this.setState("disabled", true);

`setState(newState)`

Signature Type Description
newState Object A new state object to merge into the previous state

Changes the value of multiple state properties:

js
this.setState({
  disabled: true,
  size: "large",
});

`setStateDirty(name[, value])`

Signature Type Description
name String The name of the state property to mark as dirty
value Any optional β€” A new value for the state property

Forces a state property change, even if the value is equal to the old value. This is helpful in cases where a change occurs to a complex object that would not be detected by a shallow compare. Invoking this function completely circumvents all property equality checks (shallow compares) and always rerenders the component.

More details

The first parameter, name, is used to allow update handlers (e.g. update_foo(newValue)) to handle the state transition for the specific state property that was marked dirty.

The second parameter, value, is used as the new value that is given to update handlers. Because setStateDirty() always bypasses all property equality checks, this parameter is optional. If not given or equal to the old value, the old value will be used for the update handler.

Important: the given parameters do not affect how or if setStateDirty() rerenders a component; they are only considered as additional information to update handlers.

js
// Because this does not create a new array, the change
// would not be detected by a shallow property comparison
this.state.colors.push("red");

// Force that particular state property to be considered dirty so
// that it will trigger the component's view to be updated
this.setStateDirty("colors");

`subscribeTo(emitter)`

Signature Description
emitter A Node.js EventEmitter or DOM object that emits events (window, document, etc.)
return value A tracked subscription

When a component is destroyed, it is necessary to remove any listeners that were attached by the component to prevent memory leaks. By using subscribeTo, Marko will automatically track and remove any listeners you attach when the component is destroyed.

Marko uses listener-tracker to provide this feature.

js
this.subscribeTo(window).on("scroll", () =>
  console.log("The user scrolled the window!"),
);

`update()`

Immediately executes any pending updates to the DOM, rather than following the normal queued update mechanism for rendering.

js
this.setState("foo", "bar");
this.update(); // Force the DOM to update
this.setState("hello", "world");
this.update(); // Force the DOM to update

Event methods

Marko components inherit from EventEmitter. Below are a few commonly used methods β€” view the Node.js docs for the full list.

`emit(eventName, ...args)`

Signature Type Description
eventName String Name of the event
...args Any All subsequent parameters are passed to the listeners

Emits a UI component custom event. If a UI component attached a listener with the matching eventName, then the corresponding event listener method will be invoked. Event listeners can be attached using either the on-[event](methodName|function, ...args) attribute syntax, or targetComponent.on().

`on(eventName, handler)`

Signature Type Description
eventName String Name of the event to listen for
handler Function The function to call when the event fires

Adds the listener function to the end of the listeners array for the eventName event. Does not check to see if the listener has already been added. Multiple calls passing the same combination of eventName and handler will result in the listener being added and called multiple times.

`once(eventName, handler)`

Signature Type Description
eventName String Name of the event to listen for
handler Function The function to call when the event fires

Adds a one-time listener function for the eventName event. The next time eventName triggers, this listener is removed and then invoked.

Lifecycle events

Marko defines six lifecycle events:

  • create
  • input
  • render
  • mount
  • update
  • destroy

These events are emitted at specific points over the lifecycle of a component, as shown below:

First render

js
emit('create')Β β†’Β emit('input')Β β†’Β emit('render')Β β†’Β emit('mount')

New input

js
emit('input')Β β†’Β emit('render')Β β†’Β emit('update')

Internal state change

js
emit('render')Β β†’Β emit('update')

Destroy

js
emit("destroy");

Lifecycle event methods

Each lifecycle event has a corresponding component lifecycle method that can listen for the event:

js
class {
  onCreate(input, out) { }
  onInput(input, out) { }
  onRender(out) { }
  onMount() { }
  onUpdate() { }
  onDestroy() { }
}

ProTip: When a lifecycle event occurs in the browser, the corresponding event is emitted on the component instance. A parent component, or other code that has access to the component instance, can listen for these events. For example:

js
component.on("input", function (input, out) {
  // The component received an input
});

`onCreate(input, out)`

Signature Description
input The input data used to render the component for the first time
out The async out used to render the component for the first time

The create event is emitted (and onCreate is called) when the component is first created.

onCreate is typically used to set the initial state for stateful components:

marko
class {
    onCreate(input) {
        this.state = { count: input.initialCount };
    }
}

`onInput(input, out)`

Signature Description
input The new input data

The input event is emitted (and onInput is called) when the component receives input: both the initial input, and for any subsequent updates to its input.

`onRender(out)`

Signature Description
out The async out for the current render

The render event is emitted (and onRender is called) when the component is about to render or rerender.

`onMount()`

The mount event is emitted (and onMount is called) when the component is first mounted to the DOM. For server-rendered components, this is the first event that is emitted only in the browser.

This is the first point at which this.el and this.els are defined. onMount is commonly used to attach third-party JavaScript to the newly-mounted DOM.

For example, attaching a library that monitors if the component is in the viewport:

marko
import scrollmonitor from 'scrollmonitor';

class {
    onMount() {
        this.watcher = scrollmonitor.create(this.el);
        this.watcher.enterViewport(() => console.log('I have entered the viewport'));
        this.watcher.exitViewport(() => console.log('I have left the viewport'));
    }
}

`onUpdate()`

The update event is emitted (and onUpdate is called) when the component is called after a component rerenders and the DOM has been updated. If a rerender does not update the DOM (nothing changed), this event will not fire.

`onDestroy()`

The destroy event is emitted (and onDestroy is called) when the component is about to unmount from the DOM and cleaned up. onDestroy should be used to do any additional cleanup beyond what Marko handles itself.

For example, cleaning up from our scrollmonitor example in onMount:

marko
import scrollmonitor from 'scrollmonitor';

class {
    onMount() {
        this.watcher = scrollmonitor.create(this.el);
        this.watcher.enterViewport(() => console.log('Entered the viewport'));
        this.watcher.exitViewport(() => console.log('Left the viewport'));
    }
    onDestroy() {
        this.watcher.destroy();
    }
}

DOM manipulation methods

The following methods move the component’s root DOM node(s) from the current parent element to a new parent element (or out of the DOM in the case of detach).

`appendTo(targetEl)`

Moves the UI component’s DOM elements into the position after the target element’s last child.

js
this.appendTo(document.body);

`insertAfter(targetEl)`

Moves the UI component’s DOM elements into the position after the target DOM element.

`insertBefore(targetEl)`

Moves the UI component’s DOM elements into the position before the target DOM element.

`prependTo(targetEl)`

Moves the UI component’s DOM elements into the position before the target element’s first child.

`replace(targetEl)`

Replaces the target element with the UI component’s DOM elements.

`replaceChildrenOf(targetEl)`

Replaces the target element’s children with the UI component’s DOM elements.


File: packages/runtime-class/docs/cloudflare-workers.md

Marko + Cloudflare Workers

See the the cloudflare sample
project for a working example.

Usage

When using Marko with Cloudflare Workers, make sure that Marko is loaded with a worker export condition. Most bundlers support defining export conditions.

After that point, imported .marko files will export a .stream method that returns a worker compatible ReadableStream. You can then respond with that returned stream:

js
import Template from "./index.marko";

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  return new Response(Template.stream(), {
    headers: {
      status: 200,
      headers: { "content-type": "text/html;charset=UTF-8" },
    },
  });
}

BYOB (Bring your own bundler)

For the large portion of Marko's API a bundler is required. The example code above assumes that Marko templates can be loaded in your environment.
Marko supports a number of bundlers, take a look through our supported bundlers and pick what works best for you.


File: packages/runtime-class/docs/compiler.md

Compiler

Warning:
The compiler API and hooks are not terribly stable. They’re intended for advanced integrations or userland experimentation with new language features.

Prefer existing official plugins and the standard tag library when possible.

Compile API

Warning:
The Compile API is intended for advanced integration with build tools, like Webpack and Rollup. Unless you’re doing that, you probably instead want build/serve in the Marko CLI, or one of Marko’s bundler integrations.

Compile Functions

Compile functions take two arguments:

  1. A source Marko template
  2. CompileOptions

Then, they return a CompileResult:

ts
type CompileResult = {
  code: string;
  map?: SourceMap;
  meta: Record<string, unknown>;
};
  • code: The compiled output of executable JavaScript code.
  • map: A source map, used for debugging.
  • meta: Metadata gathered while compiling β€” nothing terribly useful, probably going to get deprecated.
    • Data about child dependencies. Was useful back when it was primarily the bundlers that handled tree-shaking out server components. Now that happens in the compiler, so we don’t use this anymore and therefore might become inaccurate in the future.
    • A list of watchFiles: files that were used to compile the template (e.g. marko.json). Used to tell bundlers which files should be watched in dev mode.

`compileFile()` and `compileFileSync`

ts
compiler.compileFile(filename: string, options?: CompileOptions): Promise<CompileResult>
compiler.compileFileSync(filename: string, options?: CompileOptions): CompileResult

compileFile and compileFileSync load filename from disk to use as a source template, then translate it into JavaScript.

js
import * as compiler from "@marko/compiler";

const asyncResult = await compiler.compileFile("./src/index.marko", {
  modules: "cjs",
});
const syncResult = compiler.compileFileSync("./src/index.marko", {
  modules: "cjs",
});

`compile()` and `compileSync()`

ts
compiler.compile(src: string, filename: string, options?: CompileOptions): Promise<CompileResult>
compiler.compileSync(src: string, filename: string, options?: CompileOptions): CompileResult

compile and compileSync accept source templates as a string, rather than loading from disk.

The filename location is used for resolving taglibs and imports, but does not have to be an actually existing file.

js
import * as compiler from "@marko/compiler";

const asyncResult = await compiler.compile(
  "<h1>Hello!</>",
  "./src/index.marko",
  { modules: "cjs" },
);
const syncResult = compiler.compileSync("<h1>Hello!</>", "./src/index.marko", {
  modules: "cjs",
});

Options

The compiler may be configured globally to change its default options:

js
import * as compiler from "@marko/compiler";
compiler.configure({ output: "dom" });

Or you can pass options objects when calling compile functions. Each property will override individual properties set by configure():

js
import * as compiler from "@marko/compiler";
compiler.configure({
  output: "dom",
  sourceMaps: true,
});
const result = compiler.compileFileSync("./example.marko", {
  output: "html",
});

In the above example, result would be compiled with the options of { output: "html", sourceMaps: true }.

`output`

Type: string
Default: "html"

  • "html": compiles templates to JavaScript that generates HTML strings for server HTTP responses, writing .html files, or maybe even constructing Responses in Web Workers.
  • "dom": compiles templates to JavaScript that generates DOM nodes for client-side rendering in browsers.
  • "hydrate": like "dom", but only includes assets & components needed in-browser, assuming the page was rendered on the server.
  • "migrate": only runs migrations (no transforms or translation) and returns the migrated template code.
  • "source": parses templates without running any migrations or transforms. (Useful with ast: true)

Note:
For dom or hydrate outputs, you should also specify a resolveVirtualDependency function.

`code`

Type: boolean
Default: true

If false, will not generate or return the compiled code string.

`ast`

Type: boolean
Default: false

If true, the compiler will provide the ast in its output.

`stripTypes`

Type: boolean|undefined
Default: undefined

If true, removes all TypeScript types from the output.
If the value is undefined (the default), the compiler will remove types if the output option is not source or migrate.

For example, to run migrations and strip types, you can set both output: "migrate" and stripTypes: true.

`runtimeId`

Type: string
Default: undefined

Optionally use to override the runtime ID used to differentiate multiple copies of Marko on the same page, which is passed to marko/components.init(runtimeId) when compiling in the hydrate output.

`writeVersionComment`

Type: boolean
Default: true

Whether the Marko version should be written to the template in a comment, like so:

js
// Compiled using [email protected] - DO NOT EDIT

`ignoreUnrecognizedTags`

Type: boolean
Default: false

Whether unrecognized tags should be silently ignored or throw a compile error. Ignored tags will be output as native elements.

ProTip:
Some test setups use this alongside @marko/compiler/taglib's excludeDir and excludePackage to simulate "shallow" rendering.

`sourceMaps`

Type: boolean or string
Default: false

Whether source maps should be output with the compiled templates.

  • When true a map property will be available on the compile result.
  • When "inline" the sourcemap will be inlined as a comment in the output code.
  • When "both" both of the above will be used.

`meta`

Type: boolean
Default: false

Deprecated. This option inlines the metadata in the output Javascript code. Metadata should be accessed instead from the CompileResult.

`fileSystem`

Type: typeof fs (specifically read APIs)
Default: Cached fs

Use a different file system object (eg. webpack's CachedInputFileSystem or arc-fs)

`modules`

Type: string ("esm" or "cjs")
Default: "esm"

By default Marko outputs ES Modules. You can optionally specify "cjs" for CommonJS/require().

`optimize`

Type: boolean
Default: environment based (false in development, true in production)

Enables production mode optimizations.

`optimizeKnownTemplates`

Type: string[]
Default: undefined

If optimize is enabled you can provide an array of template paths which the compiler will use to generate shorter registry/template ids using incrementing ids. This can only be used if the same optimizeKnownTemplates are used for both server and client compilations.

`resolveVirtualDependency`

Type:

ts
(
  filename: string,
  dep: {
    code: string;
    virtualPath: string;
    map?: SourceMap;
  },
) => string;

Default: undefined

This option should be set for dom or hydrate outputs. Since Marko templates can represent multiple output files (eg. JS renderer and CSS styles), a single source .marko file must be treated as potentially multiple virtual files.

Different build tools have different mechanisms for handling virtual files. You should pass a function that returns a virtual path that can be handled by your build tool.

Example based on `@marko/webpack/loader`:

js
// lookup is shared between resolveVirtualDependency and markoLoader
const virtualSources = new Map();

function resolveVirtualDependency(filename, { virtualPath, code, map }) {
  const virtualFilename = `${filename}?virtual=${virtualPath}`;

  // Add virtual source to the lookup to be later accessed by the loader
  virtualSources.set(virtualFilename, { code, map });

  // Generate the webpack path, from right to left...
  // 1. Pass the virtualFilename so webpack can find the real file
  //    located at sourceFilename, but the virtualPath is also present
  //    (eg. "./index.marko?virtual=./index.marko.css")
  // 2. Use an inline loader to run this file through @marko/webpack/loader
  //    https://webpack.js.org/concepts/loaders/#inline
  // 3. Use an inline matchResource to redefine this as the virtualPath
  //    which allows the appropriate loaders to match the virtual dependency
  //    https://webpack.js.org/api/loaders/#inline-matchresource
  return `${virtualPath}!=!@marko/webpack/loader!${virtualFilename}`;
}

export default function markoLoader(source) {
  let code, map;

  if (virtualSources.has(this.resource)) {
    // If the resource has a ?virtual query param, we should
    // find it in the lookup and then return the virtual code
    // rather than performing the normal compilation
    { code, map } = virtualSources.get(this.resource);
    virtualSources.delete(this.resource);
  } else {
    // The default behavior is to compile the template in dom output mode
    { code, map } = markoCompiler.compileSync(source, this.resourcePath, {
      output: "dom",
      resolveVirtualDependency
    });
  }

  return this.callback(null, code, map);
}

`hydrateIncludeImports`

This option is only used for output: "hydrate". By default, imports in server-only files are not included in the hydrate output. However, for some assets, like stylesheets, it is useful to have them included in hydrate mode.

The hydrateIncludeImports option allows you to provide a function which receives an import path, or a regexp to match against that path which tells Marko to include that import in the hydrate mode output.

The default regexp includes a list of common known asset file extensions, and is as follows:

js
/\.(css|less|s[ac]ss|styl|png|jpe?g|gif|svg|ico|webp|avif|mp4|webm|ogg|mp3|wav|flac|aac|woff2?|eot|[ot]tf)$/;

Looking at a partial Marko file such as:

marko
import "./bar"
import "./foo.css";
import "./baz.wasm";

For hydrate output, with the default hydrateIncludeImports, would only cause ./foo.css to be loaded in the browser.

`hydrateInit`

This option is only used for output: "hydrate". It defaults to true and causes the hydrate output to include code which tells the Marko runtime to begin hydrating any registered components.

Setting to false will disable that init call and allow you to generate code which just imports any hydrate dependencies for a template.

`cache`

Type: typeof Map (specifically, .get() is required)
Default: new Map()

Compiling a Marko template may require other (used) Marko templates to compile. To prevent compiling templates more than once, most of the compilation is cached.

The default cache strategy is to clear the cache each macrotask. If the default cache is overwritten, it is up to the user to determine when the cache is cleared.

`babelConfig`

Type: see babel options
Default: babel defaults, plus

js
{
  filename,
  sourceType: "module",
  sourceMaps: config.sourceMaps
}

`translator`

Type: { analyze: Visitor, transform:Visitor }
Default: autodiscovers a translator package starting with @marko/translator- or marko-translator-

The translator is a collection of transforms that translates the Marko AST into a valid JavaScript AST based on the output option. There is a default translator in Marko, but this option may be used to switch to experimental translators for alternate runtimes.

The translator is an object with analyze and transform Babel Visitors:

  • The result of the analyze visitor is cached and may be requested by other templates.
  • The transform visitor transforms the AST to its final JavaScript AST.

See @marko/translator-default for a reference implementation.

Hooks

Note:
These compiler hooks aren’t terribly stable either. Using hooks for one-time migrations, like a codemod, is the best-supported way to use the compiler hooks, since you won’t have to worry about the code changing underneath you in the future.

The Marko compiler runs a series of stages to produce its final JavaScript output. These stages handle different steps of template processing, and can be tweaked, extended, and hooked into with marko.json configuration files.

Here is an example hook:

js
export default (tag, types) => {
  if (types.isStringLiteral(tag.node.name)) {
    console.log(`Found a tag called ${tag.node.name.value}`);
    tag.remove();
  }
};

Parse

Marko compilation starts by converting the raw text of your Marko template into an AST (Abstract Syntax Tree) β€” an object representation of your code.

marko
<h1>Hello!</h1>

…will roughly become:

json
{
  "type": "MarkoTag",
  "name": {
    "type": "StringLiteral",
    "value": "h1"
  },
  "body": {
    "type": "MarkoTagBody",
    "body": [
      {
        "type": "MarkoText",
        "value": "Hello!"
      }
    ]
  }
}

This might look a bit verbose, but ASTs aim for completeness, not terseness.

Marko parses in two steps to stay flexible with the ever-changing syntax of JavaScript:

  1. The first parsing pass happens in our htmljs-parser, which understands the HTML and HTML-like parts of your template.

  2. For JavaScript expressions, Marko defers to @babel/parser. The resulting Marko AST is a superset of what @babel/parser would return.

To hook into the parse stage, use the parse option in the marko.json file.

Note: The parse hook deviates from the other compiler hooks:

  • It does not support the enter & exit API.
  • You must return a replacement AST node.

Migrate

That’s right, Marko has first-class support for migrations. The migration stage can translate outdated APIs into modern counterparts, leaving the rest of the compilation none the wiser.

Migrations run automatically in the background, and can be written to disk when users are ready by running the @marko/migrate CLI command.

To hook into the migrate stage, use the migrate option in the marko.json file.

Note:
To make the compiler stop at this point and output the migrated template, rather than continuing to produce the JavaScript output, set output: "migrate" in the compilation options.

Transform

The transform stage is meant for userland transformations of Marko code into different Marko code. Think of it like babel.transform for Marko templates.
At this stage, you are given a fully parsed and migrated AST to do what you will with.

To hook into the transform stage, use the transform option in the marko.json file.

Analyze

Next is the analyze stage, intended for non-mutative analysis of the entire AST in a way that can be cached in RAM.

Note:
β€œNon-mutative analysis” means that if you modify the AST during this stage, you’ll probably regret it someday.

Metadata should be stored on nodes’ .extra property. These .extra properties are typically read in the translate stage, or when using the child template analysis helpers.

To hook into the analyze stage, use the analyze option in the marko.json file.

Translate

Finally, we have the translation stage. This stage is Marko’s β€œRosetta Stone”, and is responsible for turning your beautiful .marko code into different versions of optimized JavaScript you’d rather not write yourself.

To hook into the translate stage, use the translate option in the marko.json file.

Utilities

The @marko/babel-utils package exposes a handful of utilities for various tasks on Marko ASTs.

Marko AST

Marko extends Babel’s AST types to add nodes for MarkoTag, MarkoAttribute, etc.

For AST creation and assertion utilities, you can import Marko’s @babel/types superset from the compiler package:

js
import { types } from "@marko/compiler";

The @babel/types documentation shows all utility methods available for Babel AST nodes. When importing types from @marko/compiler, you also get the same types of utilities for Marko nodes: types.markoTag, types.isMarkoTag, types.assertMarkoTag, and so on.

For full definitions, view the source code for Babel and Marko:


File: packages/runtime-class/docs/concise.md

Concise syntax

Marko's concise syntax is very similar to the HTML syntax, except it's more... concise. Essentially, you take an HTML tag, remove the angle brackets (<>) and use indentation rather than a closing tag:

input.marko

marko
div class="thumbnail"
    img src="https://example.com/thumb.png"

output.html

html

Shorthand attributes

Marko provides a shorthand for declaring classes and ids on an element:

input.marko

marko
div.my-class
span#my-id
button#submit.primary.large

Yields this HTML:

output.html

html

ProTip: These shorthand attributes are available within the HTML syntax as well

Attributes on multiple lines

If a component has lots of attributes, you can spread them across multiple lines by surrounding them with square brackets

input.marko

marko
div [
    id="hello"
    class=["class1", "class2", "class3"]
    style={ border: "1px solid red" }
] -- hello

output.html

html
hello

Text

Text in concise mode is denoted by two or more dashes (--).

If there is text on the same line following --, it is single-line text:

single-line-text.marko

marko
-- Hello world

The dashes can also follow an element to give it a single text node as a child

single-line-text.marko

marko
div -- Hello world

If there is a line break immediately following --, everything following the -- at the current indentation is parsed as multi-line line text.

multi-line-text.marko

marko
div
    --
    Hello world
    this text
    is multi-line

div
    --
    this is more
    text

A multi-line text block can be ended by the same number of dashes that opened it. This allows it to have siblings:

multi-line-text.marko

marko
div
    img src="https://example.com/photo.png"
    --
    Hello world
    this text
    is multi-line
    --
    span -- text after

Root level text

There is one "gotcha" that you need to be aware of. The Marko parser starts out in the concise mode. Therefore, given the following template:

input.marko

marko
Hello World
Welcome to Marko

The output would be the following:

output.html

html
<Hello World></Hello> <Welcome to Marko></Welcome>

Instead, prefix the lines with -- so they are parsed as text:

input.marko

marko
-- Hello World
-- Welcome to Marko

File: packages/runtime-class/docs/conditionals-and-lists.md

Conditionals and Lists

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 `` and <for> tags.

Conditionals

The `` tag receives an argument which is used to determine if its body content should be present.

marko
[Log in](/login)

As you might expect, there are also <else> and <else-if> tags as well:

marko
[Log in](/login)

<else-if(!user.trappedForever)>
    [Log out](/logout)
</else-if>
<else>
    Hey ${user.name}!
</else>

Lists

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.

marko
<for|color, index| of=colors>
        <li>${index}: ${color}</li>
    </for>

The <for> tag actually support 3 different flavors:

Always set a `key`

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.

marko
<for|user| of=users>
    
</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

marko
<dl>
    <for|entry| of=entries>
        
        <dt key=entry.id>${entry.word}</dt>
        
        <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 `` element. For this reason it is always recommended to set a key on tags in a <for>.