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>
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>
*/
It may look complicated at first glance, so I'll explain what I added:
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 containerdiv
element now hasid="input"
when rendered. The same goes for theinput
element that's defined.The last argument of the
div
element is an array of other elements. This is how we can nest elements in Mithril. So now ourdiv
element contains alabel
element and aninput
element.It's important to notice that the
input
element has the attributeonkeydown: 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 tosubmit()
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"
)
);
}
};
A few things to note here:
The
view
function for each component now has avnode
argument. We'll see how this is used when we render our components together.Each of these buttons contain an
onclick
attribute, instead of anonkeydown
attribute like we saw with the input component, but they still invoke the samesubmit
function.The
style
attribute references some property from thevnode
argument in theview
function. This is a way of accessing data. In this case, we're referencing somevnode
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;
}
}
Once again, looks like we did a lot. Here's the rundown:
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. Thebackground
andborder
properties ofState
are accessed by the components, as we'll see in a little bit.We created the
submit
function that we saw earlier in our components. We also created two helper functions,changeColor
andresetToDefault
. Thesubmit
function listens for an event i.e. a mouse click or a key press, and invokes the helper functions, which change thebackground
andborder
properties ofState
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);
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}`
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)
@maestromac can you take a look at the
{%raw%}
issue here?Hey @botherchou ! if you change all the triple tildas(~) to a triple backticks(`) It will fix the raw/endraw issue!
Just changed the blocks using the template literals to use the backticks. Left all the other ones with the tildas. Thanks!
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.
Mac, let's try and make this a bit more robust so it can't cause this issue.
Great post! Appreciate that you took it step-by-step along with the snippets and CodePen embed.
Thanks Peter! I personally like tutorials done this way, especially for smaller things
Hey Andrew,
Are you still using Mithril? and if so, how are you finding it at the moment.
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.
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).
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
Likewise.
The link to Mithril is not right, this is the right url mithril.js.org/
ah thanks for the correction!