DEV Community

Cover image for Functional user interfaces with Moon.js
Brian Neville-O'Neill for LogRocket

Posted on • Originally published at blog.logrocket.com on

Functional user interfaces with Moon.js

Written by Nirmalya Ghosh✏️

In this tutorial, we’ll introduce Moon.js by understanding what it is and why it was created. We’ll then put that knowledge to use by building a blogging application, which will use the JSONPlaceholder API.

What is Moon.js, and what problems does it solve?

Moon.js is a tiny (2KB), functional, declarative, minimal, fast library for functional user interfaces. It is based on the idea that modern applications are complex and that they very likely require third-party libraries for any effects other than updating a view.

To address this, Moon is based on pure functions, which can take input from and return output to drivers. Drivers are responsible for views, state, HTTP requests, routing, etc.

Moon comes with a bunch of features out of the box:

  1. Time: The time driver provides the current time and allows us to schedule application functions to run after specific times
  2. Storage: The storage driver provides access to localStorage to persist data in the browser
  3. HTTP: The HTTP driver allows sending and receiving HTTP requests and responses
  4. Route: The route driver provides access to the pathname of the route in a browser

In this tutorial, we’ll be working with the HTTP driver for fetching API using the JSONPlaceholder API.

LogRocket Free Trial Banner

Installing Moon.js in our application

We can install Moon using the CLI:

npm install -g moon-cli
Enter fullscreen mode Exit fullscreen mode

The above command will install moon globally on our system. More information on installing the package globally can be found on the npm documentation.

Once the above step is complete, we can bootstrap a Moon project with the following command:

moon create introduction-to-moon.js
Enter fullscreen mode Exit fullscreen mode

Bootstrapping A Moon Application
Bootstrapping a Moon application.

The above command will create the following directory structure:

Directory Structure Of A Moon App
Directory structure of a generic Moon app.

Now, we can run the following command to install all the required dependencies and start the application:

cd introduction-to-moon.js && npm install && npm run dev
Enter fullscreen mode Exit fullscreen mode

At this point, our application should be up and running on http://localhost:8080/:

Welcome Screen Of A Moon Application
Welcome screen of a Moon application.

Moon gives us two scripts by default:

  1. dev – this command will start a webpack server with live reload
  2. build – this command will generate a production build of our application

Adding Tailwind CSS to our application

Next, we’ll add Tailwind CSS to our application to provide some default styles. We can do so by adding the following line to our src/index.html file:

<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
Enter fullscreen mode Exit fullscreen mode

Note: This is not the recommended method of installing Tailwind CSS. More options are provided in the Tailwind CSS documentation.

Fetching data from JSONPlaceholder

Next, we’ll be adding the feature with which we’ll be able to fetch data from an API like JSONPlaceholder and show it on the browser. This API will be paginated, so we’ll fetch only the first post initially, and then, on clicking a button, we’ll fetch the subsequent posts.

JSONPlaceholder is a very easy API to use. It’s free and can be fetched using the code below:

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => response.json())
  .then(json => console.log(json))
Enter fullscreen mode Exit fullscreen mode

This will return a JSON response like the following:

{
  userId: 1,
  id: 1,
  title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  body: "quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto"
}
Enter fullscreen mode Exit fullscreen mode

To get started with our application, let’s first define the HTML structure of the layout. We can create a new file, src/view/home/index.js, with the following content:

// src/view/home/index.js

import Moon from "moon";
import navbar from "../components/navbar";
import post from "../components/post";

const { section, div } = Moon.view.m;

export default ({ data }) => {
  return (
    <div>
      <section>
        <navbar />
      </section>
      <section>
        <post data=data />
      </section>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

We’ll be creating the navbar and post components next. The navbar component is responsible for showing a dummy navigation bar. We can create a new file, src/view/components/navbar/index.js, with the following content:

// src/view/components/navbar/index.js

import Moon from "moon";

const { div, ul, li, a } = Moon.view.m;

export default () => {
  return (
    <div className="bg-gray-100 border-b">
      <div className="max-w-2xl mx-auto p-8 flex justify-between">
        <div>
          <ul class="flex">
            <li class="mr-6 font-bold">
              <a class="text-blue-500 hover:text-blue-800" href="#">
                Home
              </a>
            </li>
          </ul>
        </div>
        <div>
          <ul class="flex">
            <li class="mr-6 font-bold">
              <a class="text-blue-500 hover:text-blue-800" href="#">
                About
              </a>
            </li>
            <li class="mr-6 font-bold">
              <a class="text-blue-500 hover:text-blue-800" href="#">
                Contact
              </a>
            </li>
          </ul>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Next, we’ll create our post component. The post component is responsible for showing the posts fetched from the API, along with a button to fetch the next post from the API and render it on the browser. So let’s create a new file src/view/components/post/index.js with the following content:

// src/view/components/post/index.js

import Moon from "moon";
import home from "../../home";

const { div, h2, p, button } = Moon.view.m;

const post = ({ data }) => {
  return (
    <div className="bg-white">
      <div className="max-w-2xl mx-auto p-8">
        <div className="mb-8">
          <h2 className="font-bold text-xl mb-2">{data.title}</h2>
          <p className="font-light">{data.body}</p>
        </div>
      </div>
    </div>
  )
}

export default post
Enter fullscreen mode Exit fullscreen mode

This will show the post. However, let’s add a button for fetching the subsequent posts:

// src/view/components/post/index.js

const post = ({ data }) => {
  const handleClick = () => {
    Moon.run(() => {
      const postCount = data.postCount + 1

      return({
        http: [
          {
            method: "GET",
            url: `https://jsonplaceholder.typicode.com/posts/${postCount}`,
            headers: {
              "Content-type": "application/json"
          },
            onLoad: ({ http }) => {
              const response = {...JSON.parse(http.body), postCount}

              return {
                view: <home data=response />
              };
            },
          }
        ]
      })}
    );
  };

  return (

    ....

    <button
      class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      @click=handleClick
    >
      Fetch post {data.postCount + 1}
    </button>

    ....

  )
}
Enter fullscreen mode Exit fullscreen mode

If we click on the button now, the handleClick function will be called. The handleClick function will fetch the next post from the API and render it on the browser. When the API request is complete, the home component will be rendered on the browser with the API response.

Since, this HTTP request is specific to how Moon works, let’s learn a little bit more about it. According to the Moon documentation, the HTTP driver accepts a list of HTTP requests as output. Each request can have a method, url, headers, body, responseType, onLoad, and onError property. Only the url property is required.

Moon.use({
    http: Moon.http.driver
});

Moon.run(() => ({
  http: [{
      method: "GET",  // GET, POST, PUT, DELETE, etc.
      url: "https://jsonplaceholder.typicode.com/posts/1",
      headers: {
        "Content-Type": "application/json" // Any type
      },
      body: "HTTP driver test", // If we need to send a body, we can use this. However, this data type must match the "headers"
      responseType: "json",
      onLoad: ({ http }) => {
        console.log(http);

        return {};
      },
      onError: ({ http }) => {
        console.error(http);

        return {};
      }
  }
]}));
Enter fullscreen mode Exit fullscreen mode

The above function will log the following in our browser’s console:

Sample API Response Using Moon's HTTP Driver
Sample API response using Moon’s HTTP driver.

Moon’s debugger is also very helpful. If we didn’t define the HTTP driver, it would throw a helpful message on our browser’s console:

Moon Driver Error Message
Error when a Moon driver is used without defining it.

Next, we will need to update our src/main.js file with the following content:

// src/main.js

import Moon from "moon";
import home from "./view/home"

// Define all the drivers that we'll need
Moon.use({
  data: Moon.data.driver,
  http: Moon.http.driver,
  view: Moon.view.driver("#root")
});

Moon.run(() => {
  const postCount = 1 // Fetch the first post initially

  return({
    http: [{
      method: "GET",
      url: `https://jsonplaceholder.typicode.com/posts/${postCount}`,
      headers: {
        "Content-type": "application/json"
      },
      onLoad: ({ http }) => {
        const data = {...JSON.parse(http.body), postCount}
        return {
          view: <home data=data />
        };
      },
    }]
  })}
);
Enter fullscreen mode Exit fullscreen mode

The above code will render the home component once the API request to fetch the first post is complete.

Now, if we visit http://localhost:8080/, we should be able to see the following screen:

Initial Moon Application Interface
Our initial application interface.

We can view the first post. If we click on the button, the next post will be fetched from the API and rendered on the browser:

Fetching And Rendering The Next Set Of Posts
Fetching and rendering the next set of posts.

Conclusion

In this tutorial, we’ve learned what Moon.js is and how we can build an application with it. The source code for the application we built in this tutorial is available on Github and is hosted on Vercel.

If you want to keep exploring Moon, check out the examples. You can also get acquainted with Moon using the Moon Playground or Guide.


LogRocket: Full visibility into production web apps

Alt Text

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Demo it yourself for free – no account required.


The post Functional user interfaces with Moon.js appeared first on LogRocket Blog.

Top comments (0)