DEV Community

Andrew Chou
Andrew Chou

Posted on • Edited on

A Beginner's Attempt at Mithril.js

I started teaching myself web development about half a year ago, and one of the first "from scratch" front-end projects I created was a color button. Basically, type in a valid color name (most of which you can find here) or hex value, and after clicking the button, its color would change to the one that was inputted.

I came up with this idea as an exercise for using event listeners in JavaScript because I was having trouble with them at the time, and so I focused on using just plain JavaScript when creating it. Here's the result:

It's pretty simple and I'm sure that there are more efficient ways to do this using plain JS (NOTE: I haven't tried to change it since I finished it).

As a beginner, it's easy to be overwhelmed by all of the front-end frameworks that exist. I wanted to "level up" and try something unfamiliar but not too unfamiliar. I was looking for something that would introduce more advanced concepts, but without straying too far from what I already understood. Also, I'm a complete sucker for minimalist frameworks and libraries, so that was a consideration when looking for something to try as well.

And so I found Mithril.js. It's known to be extremely minimal (<8kb gzip!) and it has a simple API that can be used similarly to React. Its documentation definitely contains more content about how to use Mithril than it does about its actual API, and so I highly recommend it to beginners.

So for the rest of the post, I'm basically going to rewrite the color button I made earlier - using Mithril. Feel free to follow along using CodePen or whichever sandbox tool you prefer!

Step 1: Create Some Components

If you're familiar with React, then you'll understand what I mean when I say that we can view each of the elements for this application as a component. The input, the color button (AKA the big button), and the reset button are each a component that, when put together, make up the content of the page. Components in Mithril are basically just objects with a view property, which is a function that returns some markup node(s). For example, let's start by creating a component for the input:

const InputComponent = {
  view: function() {
    return m("div", "This is the input container")
  }
};

// The view function is essentially returning this HTML element:

// <div>This is the input container</div>

Enter fullscreen mode Exit fullscreen mode

What the function in view is returning is what Mithril refers to as a vnode, which is essentially an HTML element. The m() function is a hyperscript function that allows any HTML structure to be written in JavaScript syntax - so in this case, the first argument indicates the type of element it is (a div), and the second argument is the text that's contained in the element.

Right now, the input component only contains the container element that I used for styling purposes. To add the necessary elements, we can nest elements into this div like so:

const InputComponent = {
  view: function() {
    return m("div", { id: "input" }, [
    m("label", "input color: "),
    m("input", {
        id: "color-input",
        type: "text",
        onkeydown: submit,
        autofocus: "autofocus"
      })
    ]);
  }
};

// Now the view function renders the following HTML:
/*
<div id="input">
    <label>input color: </label>
    <input id="color-input" type="text" onKeyDown="submit" autofocus="autofocus">
</div>
*/

Enter fullscreen mode Exit fullscreen mode

It may look complicated at first glance, so I'll explain what I added:

  1. We notice that now the second argument of the m() function is an object containing different properties. In Mithril, we can define the attributes of the HTML tag here. So my container div element now has id="input" when rendered. The same goes for the input element that's defined.

  2. The last argument of the div element is an array of other elements. This is how we can nest elements in Mithril. So now our div element contains a label element and an input element.

  3. It's important to notice that the input element has the attribute onkeydown: submit. Right now, submit is a function that we haven't defined, but due to Mithril's autoredraw system, you don't want to set this attribute to submit() i.e. calling the function.

Now we have the whole input component done. Let's quickly create the color button and the reset button:

const ColorButtonComponent = {
  view: function(vnode) {
    return m("div", { id: "color" }, 
        m("button", {
            id: "color-btn",
            style: `background-color: ${vnode.attrs.color.background}`,
            onclick: submit
          })
        );
  }    
};


const ResetButtonComponent = {
  view: function(vnode) {
    return m("div", { id: "reset" },
        m("button", {
          id: "reset-btn",
          style: `border-color: ${vnode.attrs.color.border}`,
          onclick: submit
          },
          "reset"
          )
        );
  }
};
Enter fullscreen mode Exit fullscreen mode

A few things to note here:

  1. The view function for each component now has a vnode argument. We'll see how this is used when we render our components together.

  2. Each of these buttons contain an onclick attribute, instead of an onkeydown attribute like we saw with the input component, but they still invoke the same submit function.

  3. The style attribute references some property from the vnode argument in the view function. This is a way of accessing data. In this case, we're referencing some vnode to figure out what color the color button's background and the reset button's border should turn into.

Step 2: Add in the State Variable and Necessary Functions

So we finally created our components! But we still need to define some functions that will help us to actually change the colors:

// This acts as our global state for the component color
// Our components will access this whenever the buttons are clicked or the correct keys are pressed.
let State = {
  background: "#ffffff",
  border: "#000000",
  defaultBackground: "#ffffff",
  defaultBorder: "#000000"
};


function changeColor(val) {
  State.background = State.border = val;
}


function resetToDefault(element) {
  State.background = State.defaultBackground;
  State.border = State.defaultBorder;
  element.value = "";
}


// This is the submit function that we saw in the components before
function submit(event) {
  let inputElement = document.getElementById("color-input");
  let currentValue = inputElement.value;

  switch (event.type) {
    case "keydown":
      switch (event.keyCode) {

        // If the Enter key is pressed...
        case 13:
          changeColor(currentValue);
          break;

        // If the Escape key is pressed...
        case 27:
          resetToDefault(inputElement);
      }
      break;

    case "click":
      if (event.target.id.includes("reset")) {
        resetToDefault(inputElement);
      } else {
        changeColor(currentValue);
      }
      break;
  }
}

Enter fullscreen mode Exit fullscreen mode

Once again, looks like we did a lot. Here's the rundown:

  1. We created an object State that acts as the global state for our app. To be quite honest, I'm not sure if this is the best way to do that, but it works for something small like this. The background and border properties of State are accessed by the components, as we'll see in a little bit.

  2. We created the submit function that we saw earlier in our components. We also created two helper functions, changeColor and resetToDefault. The submit function listens for an event i.e. a mouse click or a key press, and invokes the helper functions, which change the background and border properties of State depending on the event. This is then communicated to the elements as it occurs (more on this soon).

Step 3: Put It All Together

So now we have all of the components and the necessary variables and functions, but how do we actually make it so that we have a working app on our screen? The solution to this is the m.mount method in Mithril. This takes a component and "attaches" it to some part of the DOM, whether it's an HTML element or some other part of the window. In this case, we're going to create a component that contains all of the components we made, and then attach it to document.body:

const App = {
  view: function() {
    return m("div",
      { id: "flex-container" },
      m(inputComponent),
      m(ColorButtonComponent, { color: State }),
      m(ResetButtonComponent, { color: State })
    );
  }
};

m.mount(document.body, App);
Enter fullscreen mode Exit fullscreen mode

This may be a little confusing at first. To put it simply, our App component is creating elements based on the components we defined earlier. In other words, App is a component that contains components. What's rendered from these elements depends on the view function that the input, color button, and reset button contain.

Remember that the color button and reset button each had an attribute like this:

style: `border-color: ${vnode.attrs.color.border}`
Enter fullscreen mode Exit fullscreen mode

This is actually referencing the object that's passed in as the attributes argument in the nested elements in our App component i.e. { color: State }. The attribute is accessible in the view function for our color button and reset button components as vnode.attrs.color. So this explains the view: function(vnode){...} that we saw earlier, as { color: State } is passed in as the vnode argument.

Our button components can now access our global variable State. We see that they're specifically referencing vnode.attrs.color.background (color button) and vnode.attrs.color.border (reset button), which translates to State.background and State.border, respectively. So when an event is successfully triggered, new colors (based on the input value) are assigned to the buttons. The UI is updated instantaneously when Mithril detects this change in color for the components.

Here's the final result:

Step 4: Final Thoughts

I know that this post was pretty dense, but I tried my best to make it easy for beginners to follow. To recap, my first implementation of this app didn't have that much JavaScript, but I had to write some HTML boilerplate. The rewritten version contained a lot more JavaScript but no HTML whatsoever. It's difficult to understand the tradeoff with a really small app like this one, but using Mithril and idea of components was logical and relatively simple to implement in this case, and it's definitely useful when you try to create more complex applications.

Hopefully you learned something from this or at least enjoyed reading about my process. If you have any suggestions for me (or want to point out something that I messed up on), let me know! This is actually my first technical post, so I welcome the feedback. I'll hopefully have more opportunities to write more in the near future :)

Thanks for reading!

Top comments (14)

Collapse
 
ben profile image
Ben Halpern

@maestromac can you take a look at the {%raw%} issue here?

Collapse
 
maestromac profile image
Mac Siri

Hey @botherchou ! if you change all the triple tildas(~) to a triple backticks(`) It will fix the raw/endraw issue!

Collapse
 
andrewchou profile image
Andrew Chou

Just changed the blocks using the template literals to use the backticks. Left all the other ones with the tildas. Thanks!

Collapse
 
andrewchou profile image
Andrew Chou

Ok I'll try that. I used the tildas because when I used the backticks, the markdown rendered the first two backticks as a quotations ("), so it wouldn't even create a codeblock.

Thread Thread
 
ben profile image
Ben Halpern

Mac, let's try and make this a bit more robust so it can't cause this issue.

Collapse
 
peter profile image
Peter Kim Frank

Great post! Appreciate that you took it step-by-step along with the snippets and CodePen embed.

Collapse
 
andrewchou profile image
Andrew Chou

Thanks Peter! I personally like tutorials done this way, especially for smaller things

Collapse
 
sho_carter profile image
Sho Carter-Daniel

Hey Andrew,

Are you still using Mithril? and if so, how are you finding it at the moment.

Collapse
 
andrewchou profile image
Andrew Chou

Hi Sho! Haven't used in a while but they recently reached version 2.0, which brought in some nice changes. Still definitely suggest using it for small/medium sized projects, especially since it comes with most of what you need for projects of that size.

Collapse
 
sho_carter profile image
Sho Carter-Daniel

Yeah course. :)
I have been using it already, but I wanted to see if your opinions have changed. It certainly seems like a framework that could be used in the long term (if that is even possible in the world of JavaScript, lol).

Thread Thread
 
andrewchou profile image
Andrew Chou

Nah I still love it as much as I did before. It'll always have a special place in my heart – definitely in my top five favorite UI frameworks/libraries

Thread Thread
 
sho_carter profile image
Sho Carter-Daniel

Likewise.

Collapse
 
alexismellamo profile image
Alexis Navarro

The link to Mithril is not right, this is the right url mithril.js.org/

Collapse
 
andrewchou profile image
Andrew Chou

ah thanks for the correction!