Overview // Marko Widgets

Marko Widgets extends the Marko templating engine to provide a simple and efficient mechanism for binding behavior to UI components rendered on either the server or in the browser. In addition, changing a widgets state or properties will result in the DOM automatically being updated without writing extra code. Marko Widgets has adopted many of the good design principles promoted by the React team, but aims to be much lighter and often faster (especially on the server). When updating the view for a widget, Marko Widgets uses DOM diffing to make the minimum number of changes to the DOM through the use of the morphdom module.

Try Marko Widgets Online!

Features

Design Philosophy

Sample Code

Marko Widgets allows you to declaratively bind behavior to an HTML element inside a Marko template. The widget provides the client-side behavior for your UI component.

Stateless Widget

src/components/app-hello/template.marko

<div w-bind>
    Hello ${data.name}!
</div>

src/components/app-hello/index.js

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    getTemplateData: function(state, input) {
        return {
            name: input.name
        };
    },

    init: function() {
        var el = this.el; // The root DOM element that the widget is bound to
        console.log('Initializing widget: ' + el.id);
    }
});

Congratulations, you just built a reusable UI component! Your UI component can be embedded in other Marko template files:

<div>
    <app-hello name="Frank"/>
</div>

In addition, your UI can be rendered and added to the DOM using the JavaScript API:

var widget = require('./app-hello')
    .render({
        name: 'John'
    })
    .appendTo(document.body)
    .getWidget();

// Changing the props will trigger the widget to re-render
// with the new props and for the DOM to be updated:
widget.setProps({
    name: 'Jane'
});

Stateless Widget with Behavior

src/components/app-hello/template.marko

<div w-bind
     w-onClick="handleClick">

    Hello ${data.name}!

</div>

src/components/app-hello/index.js

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    getTemplateData: function(state, input) {
        return {
            name: input.name
        };
    },

    handleClick: function() {
        this.setSelected(true);
    },

    setSelected: function(selected) {
        if (selected) {
            this.el.style.backgroundColor = 'yellow';
        } else {
            this.el.style.backgroundColor = null;
        }
    }
});

Stateful Widget

Let's create a stateful widget that changes to yellow when you click on it:

src/components/app-hello/template.marko

<div w-bind
     w-onClick="handleClick"
     style="background-color: ${data.color}">

    Hello ${data.name}!

</div>

src/components/app-hello/index.js

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    getInitialState: function(input) {
        return {
            name: input.name,
            selected: input.selected || false;
        }
    },

    getTemplateData: function(state, input) {
        var style = ;

        return {
            name: state.name,
            color: state.selected ? 'yellow' : 'transparent'
        };
    },

    handleClick: function() {
        this.setState('selected', true);
    },

    isSelected: function() {
        return this.state.selected;
    }
});

Stateful Widget with Update Handlers

If you want to avoid re-rendering a widget for a particular state property change then simply provide your own method to handle the state change as shown below:

src/components/app-hello/template.marko

<div w-bind
     w-onClick="handleClick"
     style="background-color: ${data.color}">

    Hello ${data.name}!

</div>

src/components/app-hello/index.js

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    getInitialState: function(input) {
        return {
            name: input.name,
            selected: input.selected || false;
        }
    },

    getTemplateData: function(state, input) {
        var style = ;

        return {
            name: state.name,
            color: state.selected ? 'yellow' : 'transparent'
        };
    },

    handleClick: function() {
        this.setState('selected', true);
    },

    isSelected: function() {
        return this.state.selected;
    },

    update_selected: function(newSelected) {
        // Manually update the DOM to reflect the new "selected"
        // state" to avoid re-rendering the entire widget.
        if (newSelected) {
            this.el.style.backgroundColor = 'yellow';
        } else {
            this.el.style.backgroundColor = null;
        }
    }
});

Complex Widget

<div w-bind>
    <app-overlay title="My Overlay"
        w-id="overlay"
        w-onBeforeHide="handleOverlayBeforeHide">
        Body content for overlay.
    </app-overlay>

    <button type="button"
        w-onClick="handleShowButtonClick">
        Show Overlay
    </button>

    <button type="button"
        w-onClick="handleHideButtonClick">
        Hide Overlay
    </button>
</div>

Below is the content of index.js where the widget type is defined:

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    init: function() {
        // this.el will be the raw DOM element the widget instance
        // is bound to:
        var el = this.el;
    },

    handleShowButtonClick: function(event) {
        console.log('Showing overlay...');
        this.getWidget('overlay').show();
    },

    handleHideButtonClick: function(event) {
        console.log('Hiding overlay...');
        this.getWidget('overlay').hide();
    },

    handleOverlayBeforeHide: function(event) {
        console.log('The overlay is about to be hidden!');
    }
})

Container Widget

A container widget supports nested content. When the container widget is re-rendered, the nested content is automatically preserved.

src/components/app-alert/template.marko

<div class="alert alert-${data.type}" w-bind>
    <i class="alert-icon"/>
    <span w-body></span>
</div>

src/components/app-alert/index.js

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    init: function() {
        // this.el will be the raw DOM element the widget instance
        // is bound to:
        var el = this.el;
    },

    getInitialState: function(input) {
        return {
            type: input.type || 'success'
        }
    },

    getTemplateData: function(state, input) {
        return {
            type: state.type
        };
    },

    getInitialBody: function(input) {
        return input.message || input.renderBody;
    },

    setType: function(type) {
        this.setState('type', type);
    }
})

The widget can then be used as shown below:

<app-alert message="This is a success alert"/>

<app-alert>
    This is a success alert
</app-alert>

<app-alert message="This is a failure alert" type="failure"/>

<app-alert type="failure">
    This is a failure alert
</app-alert>

Preserving DOM Nodes during Re-render

Sometimes it is important to not re-render a DOM subtree. This may due to either of the following reasons:

Marko Widgets allows DOM nodes to be preserved by putting a special w-preserve, w-preserve-if="<condition>", w-preserve-body or w-preserve-body-if="<condition>" attribute on the HTML tags that should be preserved. Preserved DOM nodes will be reused and re-inserted into a widget's newly rendered DOM automatically.

<div w-bind>

    <span w-preserve>
        <p>
            The root span and all its children will never
            be re-rendered.
        </p>
        <p>
            Rendered at ${Date.now()}.
        </p>
    </span>
    <div w-preserve-body>
        Only the children of the div will preserved and
        the outer HTML div tag will be re-rendered.
    </div>

    Don't rerender the search results if no search results
    are provided.
    <app-search-results items="data.searchResults"
        w-preserve-if="data.searchResults == null"/>
</div>

Preserving DOM Attributes during Re-render

Similar to preserving DOM nodes, Marko Widgets also makes it possible to preserve specific attributes on a DOM node. This can be helpful if a separately library is modifying DOM attributes and those changes should be preserved during a rerender. This is mostly the case with class and style attributes when using a animation/tweening engines such as Velocity.js or GSAP.

The w-preserve-attrs attribute can be applied to any DOM element and it expects a comma-separated list of attribute names as shown below:

<div w-preserve-attrs="class,style">
    ...
</div>