Skip to content

Instantly share code, notes, and snippets.

@mlrawlings
Last active September 30, 2016 21:36
Show Gist options
  • Save mlrawlings/c11eb23a20b8fee8eebd24e8bea7e85a to your computer and use it in GitHub Desktop.
Save mlrawlings/c11eb23a20b8fee8eebd24e8bea7e85a to your computer and use it in GitHub Desktop.
Marko Component in .marko file

Overview

Given a template that displays a count passed into it:

<div>
    The current count is: ${data.count}
</div>

You could turn this into a stateful widget by simply adding a method and a click handler:

<div on('click', incrementCount)>
    The current count is: ${data.count}
</div>

method incrementCount() {
    this.set('count', data.count+1)
}

When clicked, the div will call the incrementCount method and it will set the widget's state which causes the template to rerender with the new state passed in as data. In order to simply the thinking, only data exists in this new paradigm, there's no mention of state.

This approach makes it so that adding client-side widget is a simple as adding a method right in the template. It's very intuitive and doesn't need a whole lot of additional code/boilerplate.

The downside is tooling support.We can get syntax highlighting working, no problem, but linting, setting breakpoints, and such is not going to work. The user may be able to do some of this against the compiled output, which should be very readable, but that's not ideal.

Implementation

state => data

This is actually quite easily accomplished by aliasing the setState and setStateDirty methods.

Widget.prototype.set = Widget.prototype.setState;
Widget.prototype.forceRerender = Widget.prototype.setStateDirty;

The data variable inside these methods actually refers to this.state as seen in the compile code:

function() {
    var data = this.state; // this line added
    this.set('count', data.count+1)
}

Parsing

The actual implementation of this is somewhat generic.
The method keyword is actually a custom tag that would be handled at compile-time by the marko-widgets taglib. The incrementCount() signature is an attribute argument and the body of the method/function is a new construct that requires an update to the htmljs-parser: the block.

A block is simple curly braces ({, }) with javascript statements inside.

Compiled output

Before addding widget:

var __marko = require('marko'),
    __markoHelpers = __marko.h,
    marko_escapeXml = __markoHelpers.x;

function renderer(data, out) {
    out.w("<div class=\"counter\">" +
        marko_escapeXml(data.count) +
        "</div>");
}

module.exports = __marko.t(__filename, renderer);

After adding widget:

var __marko = require('marko'),
    __markoHelpers = __marko.h,
    marko_escapeXml = __markoHelpers.x;

function renderer(data, out) {
    out.w("<div id=\"w0\" class=\"counter\" data-on-click=\"incrementCount|w0\">" +
        marko_escapeXml(data.count) +
        "</div>");
}

module.exports = __marko.w(__filename, {
  renderer: renderer,
  incrementCount: function() {
    var data = this.state;
    this.set('count', data.count+1)
  }
});

Additional Details

on()

This new attribute replaces the w-on-* attributes that are currently used in marko-widgets. This has the following benefits:

  1. the event name is distinctly separated from the on and exactly matches the emitted event: on('my-foo') vs w-on-my-foo or w-onMyFoo.
  2. Matches EventEmitter on function signature
  3. Using an argument instead of a string gives the feeling that they are referring to the method in scope (on('click', handleClick) vs w-onClick="handleClick") and we can statically check that the first argument actually refers to a method defined in the file.
  4. Using an argument also opens up the ability to pass additional parameters without inventing a weird syntax. These additional parameters would be used to curry the method.
<for(i in [1,2,3])>
     <button on('click', handleClick, i)>${i}</button>
</for>

method handleClick(i) {
    console.log(i, 'was clicked');
}

Automatic binding

No more w-bind. Once a method is detected, the root element of the template will be automatically bound to a widget.

Automatic Splitting

The split renderer/widget has benefits in that it can reduce the amount of code that gets sent down to the browser, but it also means that we have 3 different files! With this single file approach where the compiler understands the relations between different parts of the file we could concievably have two different outputs for the server and browser based on whether stateful rerendering is used (the reason you'd need the template on the client).

Given a component that doesnt use stateful rerendering:

<var getRandomName=require('random-name')/>
<var name=getRandomName()/>
<button on:click(sayHi, name)>Hello ${name}</button>

method sayHi(name) {
    alert('Hi '+name+'!');
}

The server compliation would contain the render logic:

var __marko = require('marko'),
    __markoHelpers = __marko.h,
    marko_escapeXml = __markoHelpers.x;
    marko_attr = __markoHelpers.a;

function renderer(data, out) {
    var getRandomName = require("random-name");

    var name = getRandomName();
    
    out.w("<button id=\"w0\" class=\"counter\" data-on-click=\"sayHi|w0\""+
        attr("data-on-click-args", JSON.stringify([name]))+">" +
        marko_escapeXml(name) +
        "</button>");
}

module.exports = __marko.w(__filename, {
  renderer: renderer,
  sayHi: function(name) {
    alert('Hi '+name+'!');
  }
});

But the browser compliation would not:

var __marko = require('marko');

module.exports = __marko.w(__filename, {
  sayHi: function(name) {
    alert('Hi '+name+'!');
  }
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment