/ithril

Templates for haxe. Compiles to HTML or Mithril views.

Primary LanguageHaxeMIT LicenseMIT

ithril

Build Status

Mithril 1.1.1 for Haxe.

Ithril uses Haxe macros to transpile Jade/Pug like templates into Mithril hyperscript.

Template Syntax

Mithril views are declared in a class that extends ithril.Component or implements ithril.View. The declaration must be inside [brackets] marked with a @m meta.

import ithril.*;

class MyComponent extends Component {
    public override function view (vnode:Vnode) @m[
    	(div.intro)
    		(ul)
    			(li > 'Some')
    			(li > 'List items')
    		(ul.another-list)
    			(vnode.attrs.list => item)
    				(li > item)
    		(form)
    			(input[type="text"] (value = "Text value", onfocus = focus))
    			(input[type="checkbox"][required]) ['Check this']
    ];
}

class Views implements View {
    public function view1(vnode:Vnode) @m[
        (div > 'view one')
            (MyComponent(list=['item one', 'item two', 'item three']))
    ]

    public function view2(vnode:Vnode) @m[
        (div > 'view two')
            (MyComponent(list=['apples', 'oranges', 'bananas']))
    ]}
}

Elements

Any html element can be expressed in parentheses:

(img)

CSS classes can be set using the . operator:

(img.my-class-name.my-other-class-name)

An element id can be set with the + operator (as # wouldn't be valid haxe syntax):

(img+my-id)

Attributes can be used inside the selector:

(img[src="img.jpg"])

Attributes can also be expressed separately:

(img (src="img.jpg", alt=""))
(img ({src: "img.jpg", alt: ""}))
(img (aFunctionCallReturningAttributes()))

Children

A shortcut for defining one child:

(h1 > 'My title')

More than one child can simply be nested by using indentation:

    (nav)
    	(ul.links)
    		(li)
    			(a[href="http://haxe.org"] > 'Haxe')
    		(li)
    			(a[href="http://github.com"] > 'Github')

Inline expressions

Any expression can be used inside brackets:

(h1)
	['A string for example']
(button)
	[this.buttonLabel]
(div)
    [{
        var blockExpression = "is Ok";
        return blockExpression;
    }]
(div)
    [{
        return true ? @m[
            (div > 'this works too')
        ] : @m [ ]
    }]

Conditionals

$if, $elseif, and $else can be used inside templates:

($if (headerSize == 1))
	(h1 > 'Big')
($elseif (headerSize == 2))
	(h2 > 'Not that big')
($else)
	(p > 'Paragraph')

For loop

(link in links)
	(a (href=link.url, target='_blank') > link.title)

// or:

($for (link in links))
	(a (href=link.url, target='_blank') > link.title)

While loop

($while (expr))
	(div > 'text')

Map

The following syntax can be used for any object (in this case links) with a map method:

(links => link)
	(a (href=link.url, target='_blank') > link.title)

Map with null check

Using the >> or << operator adds a null check prior to map execution.

// using >>:

(links >> link)
	(a (href=link.url, target='_blank') > link.title)

// or using <<:

(link << links)
	(a (href=link.url, target='_blank') > link.title)

Translates to:

if (links != null)
	[links.map(function(link) m('a', { href: link.url, target: '_blank' }, [ link.title ])];
else
	[];

Try/Catch

Try/Catch is limited to a single catch of the Dynamic type.

($try)
	(div > functionThatThrows())
($catch (err))
	(div > err)

Trusted HTML

Embedding javascript or CSS assets requires marking content as trusted so it is not automatically escaped. Use the @trust meta:

(style > @trust css)
(script)
    [@trust javascript]

Components

Custom components can be created by extending ithril.Component. A component can then be used in a view like any other element:

class Hello extends Component {
    public override function view (vnode:Vnode) @m[
    	(div.component > 'Hello ' + vnode.attrs.name)
    ];
}

class World implements View {
    public function helloView(vnode:Vnode) @m[
            (Hello(name='World'))
    ]
}

Children

Children of a Component can be accessed via vnode.children:

class List extends Component {
	public function view(vnode:Vnode) [
		(ul)
			(vnode.children => child)
				(li > child)
	];
}

Which can be used like this:

(List)
	(span > 'A')
	(span > 'B')
	(span > 'C')

And would output:

<ul>
	<li><span>A</span></li>
	<li><span>B</span></li>
	<li><span>C</span></li>
</ul>

Lifecycle

Mithril creates, reuses, and destroys components as specified by it's diffing algorithm. The lifecycle of a Component can be observed by overriding these methods:

	public function oninit(vnode:Vnode) {}
	public function oncreate(vnode:Vnode) {}
	public function onupdate(vnode:Vnode) {}
	public function onbeforeremove(vnode:Vnode) {}
	public function onremove(vnode:Vnode) {}
	public function onbeforeupdate(vnode:Vnode) {}

You can also specify these methods as attributes of both regular elements and components:

    (div(oncreate=function() trace('oncreate')))
    (MyComponent(oncreate=function() trace('oncreate')))

State

Mithril manages component state by cloning a component's fields post-constructor and storing them in vnode.state. Because component instances can be cached and reused by Mithril, accessing state must be thru vnode.state unless the initial state is being set.

class StatefulComponent extends Component {
    var someState = "my state"; // set initial state value here or in constructor
    var someMoreState:String;

    override public function new(vnode:Vnode) {
        someMoreState = "other state"; // can also set initial component state here
    }

    override public function view(vnode:Vnode) @m[
        (div > vnode.state.someState) // access it via vnode.state
            [vnode.state.someMoreState]
    ]
}

Rendering

Components can be rendered by passing a Component class to Mithril:

M.mount(js.Browser.document.body, MyComponent);

Or may be rendered in nodejs as html:

HTMLRenderer.render(Ithril.view(@m[ (div > 'view') ])).then(function(html) Sys.print(html))

Mithril routing is also supported:

M.route(js.Browser.document.body, "/", routes);

Usage

Any of your class methods can use ithril syntax if you either implement ithril.View or extend ithril.Component. Additionally you can use the static function ithril.Ithril.view to parse an ithril view.

import ithril.*;
import ithril.M.*;

class Web extends Component {
	override public function view(vnode) @m[
#if nodejs
		(!doctype)
		(meta[charset="utf-8"])
		(link[href="layout.css"][rel="stylesheet"])
		(body)
#end
			(div.intro)
				(h1 > 'Ithril example')
				(p > 'Hello world')
#if nodejs
		(script[src="https://cdnjs.cloudflare.com/ajax/libs/mithril/1.1.1/mithril.min.js"])
		(script[src="main.js"])
#end
	];

}

class Main {
	public static function main() {
		// On the server
		#if nodejs 
		HTMLRenderer.render(Web).then(function(html) Sys.print(html));
		#else
		// On the client
		M.mount(js.Browser.document.body, Web);
		#end
	}
}

You can find this usage example in examples/simple.

Output

@m[ (div(attrs)) ] is transpiled to Mithril hyperscript m('div', attrs). On the browser end, it's passed directly to Mithril. In server instances ithril.HTMLRenderer can be used to render HTML.

Sample Website

An example website can be found at examples/website. You will need node, npm, sass, and either closure or uglifyjs installed in order to build and run it.

Initial build: (this will run npm install)

cd examples/website
haxe build.hxml

Start webserver: (listens on localhost:4200, and restarts when webserver.js changes)

npm run start

Auto-build: (rebuilds on file changes)

npm run autobuild