Mithril 2.0.3

JSX


Description

JSX is a syntax extension that enables you to write HTML tags interspersed with JavaScript. It's not part of any JavaScript standards and it's not required for building applications, but it may be more pleasing to use depending on you or your team's preferences.

function MyComponent() {
    return {
        view: () =>
            m("main", [
                m("h1", "Hello world"),
            ])
    }
}

// can be written as:
function MyComponent() {
    return {
        view: () => (
            <main>
                <h1>Hello world</h1>
            </main>
        )
    }
}

When using JSX, it's possible to interpolate JavaScript expressions within JSX tags by using curly braces:

var greeting = "Hello"
var url = "https://google.com"
var link = <a href={url}>{greeting}!</a>
// yields <a href="https://google.com">Hello!</a>

Components can be used by using a convention of uppercasing the first letter of the component name:

m.render(document.body, <MyComponent />)
// equivalent to m.render(document.body, m(MyComponent))

Setup

The simplest way to use JSX is via a Babel plugin.

Babel requires npm, which is automatically installed when you install Node.js. Once npm is installed, create a project folder and run this command:

npm init -y

If you want to use Webpack and Babel together, skip to the section below.

To install Babel as a standalone tool, use this command:

npm install @babel/cli @babel/preset-env @babel/plugin-transform-react-jsx --save-dev

Create a .babelrc file:

{
    "presets": ["@babel/preset-env"],
    "plugins": [
        ["@babel/plugin-transform-react-jsx", {
            "pragma": "m",
            "pragmaFrag": "'['"
        }]
    ]
}

To run Babel as a standalone tool, run this from the command line:

babel src --out-dir bin --source-maps

Using Babel with Webpack

If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps.

npm install @babel/core babel-loader @babel/preset-env @babel/plugin-transform-react-jsx --save-dev

Create a .babelrc file:

{
    "presets": ["@babel/preset-env"],
    "plugins": [
        ["@babel/plugin-transform-react-jsx", {
            "pragma": "m",
            "pragmaFrag": "'['"
        }]
    ]
}

Next, create a file called webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './bin'),
        filename: 'app.js',
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /\/node_modules\//,
            loader: 'babel-loader'
        }]
    }
}

For those familiar with Webpack already, please note that adding the Babel options to the babel-loader section of your webpack.config.js will throw an error, so you need to include them in the separate .babelrc file.

This configuration assumes the source code file for the application entry point is in src/index.js, and this will output the bundle to bin/app.js.

To run the bundler, setup an npm script. Open package.json and add this entry under "scripts":

{
    "name": "my-project",
    "scripts": {
        "start": "webpack -d --watch"
    }
}

You can now then run the bundler by running this from the command line:

npm start

Production build

To generate a minified file, open package.json and add a new npm script called build:

{
    "name": "my-project",
    "scripts": {
        "start": "webpack -d --watch",
        "build": "webpack -p",
    }
}

You can use hooks in your production environment to run the production build script automatically. Here's an example for Heroku:

{
    "name": "my-project",
    "scripts": {
        "start": "webpack -d --watch",
        "build": "webpack -p",
        "heroku-postbuild": "webpack -p"
    }
}

JSX vs hyperscript

JSX and hyperscript are two different syntaxes you can use for specifying vnodes, and they have different tradeoffs:

You can see the tradeoffs come into play in more complex trees. For instance, consider this hyperscript tree, adapted from a real-world project by @isiahmeadows with some alterations for clarity and readability:

function SummaryView() {
    let tag, posts

    function init({attrs}) {
        Model.sendView(attrs.tag != null)
        if (attrs.tag != null) {
            tag = attrs.tag.toLowerCase()
            posts = Model.getTag(tag)
        } else {
            tag = undefined
            posts = Model.posts
        }
    }

    function feed(type, href) {
        return m(".feed", [
            type,
            m("a", {href}, m("img.feed-icon[src=./feed-icon-16.gif]")),
        ])
    }

    return {
        oninit: init,
        // To ensure the tag gets properly diffed on route change.
        onbeforeupdate: init,
        view: () =>
            m(".blog-summary", [
                m("p", "My ramblings about everything"),

                m(".feeds", [
                    feed("Atom", "blog.atom.xml"),
                    feed("RSS", "blog.rss.xml"),
                ]),

                tag != null
                    ? m(TagHeader, {len: posts.length, tag})
                    : m(".summary-header", [
                        m(".summary-title", "Posts, sorted by most recent."),
                        m(TagSearch),
                    ]),

                m(".blog-list", posts.map((post) =>
                    m(m.route.Link, {
                        class: "blog-entry",
                        href: `/posts/${post.url}`,
                    }, [
                        m(".post-date", post.date.toLocaleDateString("en-US", {
                            year: "numeric",
                            month: "long",
                            day: "numeric",
                        })),

                        m(".post-stub", [
                            m(".post-title", post.title),
                            m(".post-preview", post.preview, "..."),
                        ]),

                        m(TagList, {post, tag}),
                    ])
                )),
            ])
    }
}

Here's the exact equivalent of the above code, using JSX instead. You can see how the two syntaxes differ just in this bit, and what tradeoffs apply.

function SummaryView() {
    let tag, posts

    function init({attrs}) {
        Model.sendView(attrs.tag != null)
        if (attrs.tag != null) {
            tag = attrs.tag.toLowerCase()
            posts = Model.getTag(tag)
        } else {
            tag = undefined
            posts = Model.posts
        }
    }

    function feed(type, href) {
        return (
            <div class="feed">
                {type}
                <a href={href}><img class="feed-icon" src="./feed-icon-16.gif" /></a>
            </div>
        )
    }

    return {
        oninit: init,
        // To ensure the tag gets properly diffed on route change.
        onbeforeupdate: init,
        view: () => (
            <div class="blog-summary">
                <p>My ramblings about everything</p>

                <div class="feeds">
                    {feed("Atom", "blog.atom.xml")}
                    {feed("RSS", "blog.rss.xml")}
                </div>

                {tag != null
                    ? <TagHeader len={posts.length} tag={tag} />
                    : (
                        <div class="summary-header">
                            <div class="summary-title">Posts, sorted by most recent</div>
                            <TagSearch />
                        </div>
                    )
                }

                <div class="blog-list">
                    {posts.map((post) => (
                        <m.route.Link class="blog-entry" href={`/posts/${post.url}`}>
                            <div class="post-date">
                                {post.date.toLocaleDateString("en-US", {
                                    year: "numeric",
                                    month: "long",
                                    day: "numeric",
                                })}
                            </div>

                            <div class="post-stub">
                                <div class="post-title">{post.title}</div>
                                <div class="post-preview">{post.preview}...</div>
                            </div>

                            <TagList post={post} tag={tag} />
                        </m.route.Link>
                    ))}
                </div>
            </div>
        )
    }
}

Converting HTML

In Mithril, well-formed HTML is generally valid JSX. Little more than just pasting raw HTML is required for things to just work. About the only things you'd normally have to do are change unquoted property values like attr=value to attr="value" and change void elements like <input> to <input />, this being due to JSX being based on XML and not HTML.

When using hyperscript, you often need to translate HTML to hyperscript syntax to use it. To help speed up this process along, you can use a community-created HTML-to-Mithril-template converter to do much of it for you.


License: MIT. © Leo Horie.