Guide

This guide will introduce you to the basics of Moon while creating a simple todo application. Get started by installing Moon and adding the following to the <body> of an HTML file. You can also follow along and load the examples in the playground.

<div id="root"></div>

This is a root element where Moon's view driver will mount the view. For the following guide, along with the rest of the documentation, the above HTML is assumed.

To get started with a basic todo view, add the following contents to a JavaScript file linked to the HTML page or a <script> tag.

const Todos = ({ data }) => (
    <for={todo} of={data.todos} name="ul">
        <li>{todo}</li>
    </for>
);

Moon.use({
    data: Moon.data.driver({
        todo: "",
        todos: [
            "Learn Moon",
            "Take a nap",
            "Go Shopping"
        ]
    }),
    view: Moon.view.driver("#root")
});

Moon.run(({ data }) => ({
    view: (<Todos data={data}/>)
}));

Try it!

First of all, the Todos function returns a view using the Moon view language. It uses an HTML-like syntax for creating views based on components. In this case, <for> is a component. You'll notice how we pass name="ul", which tells the <for> component to wrap the loop in a ul tag. Inside the loop, we render an li element. The curly braces {} are for interpolating JavaScript expressions.

Everything in the view language is extra syntax for function calls. In the end, <for> and <li> boil down to a function call, like this:

for({
    name: "ul"
    of: todos,
    children: (todo) => li({ children: [ text({ "": todo }) ] })
});

However, Moon compiles it to a normal for loop for performance.

Next, Moon.use is called with an object. This is how Moon configures drivers, programs that give input from the real world and handle output to the real world. In our case, we call it with two properties: data and view.

The data input and output is handled by the Moon data driver, which gives data as input and can accept new data as output. It is responsible for storing data. We pass it with default data as configuration, storing the current new todo along with a list of all of the todos.

The view input and output is handled by the Moon view driver, which gives event information as input and can accept a new view as output. It is responsible for providing a functional interface to the DOM, and uses a virtual DOM under the hood. We pass it the #root element to tell it where to mount.

Now, an input can be added to the view to allow the user to create todos.

const Todos = ({ data }) => (
    <div>
        <input type="text" value={data.todo} @input={updateTodo}/>
        <button @click={createTodo}>Create</button>

        <for={todo} of={data.todos} name="ul">
            <li>{todo}</li>
        </for>
    </div>
);

The @ syntax in front of attribute names is for DOM events. In this case, we are using the @input and @click events. The @input event runs a handler when the value of an input changes. The @click event runs a handler whenever there is a click.

Now we can add the event handlers.

const updateTodo = ({ data, view }) => {
    const dataNew = { ...data, todo: view.target.value };

    return {
        data: dataNew,
        view: (<Todos data={dataNew}/>)
    };
};

const createTodo = ({ data }) => {
    const dataNew = {
        todo: "",
        todos: [...data.todos, data.todo]
    };

    return {
        data: dataNew,
        view: (<Todos data={dataNew}/>)
    };
};

The event handlers have the same structure as the function we passed to Moon.run. Every time Moon runs a function, it gives it inputs from drivers and expects outputs for drivers. Event handlers are no different.

For the updateTodo event handler, we get the event data using driver input from view. Using this, we make a copy of the data with the updated todo, and return new data along with a new view based on that data. These are handled by drivers to store the data and update the DOM for us.

For the createTodo event handler, we don't need the event data, only the current state data. Using this, we create a new data object with an empty todo. This will empty the input so that it doesn't retain the old value. We also create new todos using the current value of data.todo. We then return the new data along with a new view based on that data for drivers to handle.

Try it!

This guide resulted in an extremely basic todo application, but can be extended to support more features. Try the following exercises to test your knowledge:

  • Add support for removing/completing todos using events and if.
  • Add support for editing todos using the @dblclick event.
  • Add support for filtering todos using data functions.
  • Make the application more modular using components.

By default, the playground has a fully functioning todo application that has a few more advanced features. Try extending it with what you've learned!