I have build a small Todo app to use in SpaceOS on the site deta.space, it’s a pretty awesome site and I am using it to manage and organize my hobbies, I needed to create a small todo app for managing my tasks, and I have used MithrilJS to do so, it’s a pretty simple a lightweight framework that doesn’t need a build step, and it’s perfect for small apps like this one.
In addition i have used the Deta SDK to save the todos in a database so that I can access them from any device, I will probably write a post about the Deta SDK in the future, but for now let’s focus on MithrilJS.
MithrilJS has a simple syntax but in all fairness it’s a little bit daunting at first bust is based on the concept of components, so is pretty similiar to a react without jsx. A component is an object that has a view
function that returns a virtual element. Virtual elements are JavaScript objects that describe how the user interface should be, using a syntax similar to HTML. For example, this is a component that shows a welcome message:
const root = document.getElementById("root");
const Welcome = {
view: (vnode) {
return m("h1", "Hello, world!")
}
}
m.render(root, Welcome);
In this case this Welcome component create a virtual element that represents an h1
element with the text “Hello, world!”. The m.render
function takes the virtual element and renders it into the DOM, in the element with the id “root”, the render
function is called only one time and is used to initialize the application and render the app once.
The mount
function is used to render the component into the DOM, and the redraw
function is used to update the component when its state changes. For example, this is a component that shows a counter, and we have two approaches to update the counter:
var count = 0 // added a variable
var Counter = {
view: function() {
return m("main", [
m("h1", {
class: "title"
}, "My first app"),
m("button", {
onclick: () => { count++ }
}, count + " clicks")
])
}
}
m.mount(root, Counter);
It will works but the counter is global and if we have more counter it will use the same value, for this reason we can create a Closure component state
:
function ComponentWithState(initialVnode) {
// Component state variable, unique to each instance
let count = 0
// POJO component instance: any object with a
// view function which returns a vnode
return {
oninit: (vnode) => {
console.log("init a closure component")
},
view: (vnode) => {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: () => {
count += 1
}
}, "Increment count")
)
}
}
}
m.mount(root, ComponentWithState);
Now the count variable is unique for each instance of the component, and we can have more counter with different values.
<!DOCTYPE html>
<html>
<head>
<title>Todo List App</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://unpkg.com/mithril/mithril.js"></script>
<style>
.list-group-item {
transition: all 0.3s ease-in-out;
}
.list-group-item.completed {
background-color: #d9ead3;
}
.list-group-item.completed span {
text-decoration: line-through;
}
.buttons-div {
text-decoration: none !important;
display: flex;
gap: 0.5rem;
}
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
position: absolute;
top: calc(50% - 10%);
left: calc(50% - 5%);
animation: rotate 1s linear infinite
}
.loader::before {
content: "";
box-sizing: border-box;
position: absolute;
inset: 0px;
border-radius: 50%;
border: 5px solid #aeaeae;
animation: prixClipFix 2s linear infinite;
}
@keyframes rotate {
100% {transform: rotate(360deg)}
}
@keyframes prixClipFix {
0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)}
25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)}
50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)}
75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)}
100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
const root = document.getElementById("root");
import { Base } from "https://cdn.deta.space/js/deta@horizon/deta.mjs";
const db = Base("todo");
const Todo = {
view: (vnode) => {
const todo = vnode.attrs.todo;
return m(
"div",
{
class: `list-group-item justify-content-between align-items-center ${
todo.completed && "completed"
}`
},
[
m("span", todo.text),
m("div", { class: "buttons-div" }, [
!todo.completed && m(
"button",
{
onclick: (e) => vnode.attrs.oncomplete(todo),
class: "btn btn-success btn-sm complete-btn",
type: "button"
},
"Complete"
),
m(
"button",
{
onclick: (e) => vnode.attrs.onremove(todo),
class: "btn btn-danger btn-sm remove-btn",
type: "button"
},
"Remove"
)
])
]
);
}
};
const App = () => {
let todos = [];
let isFetching = false;
let newTodoText = ''
const onremove = async (todo) => {
await db.delete(todo.key)
todos = todos.filter(el => el.key != todo.key)
m.redraw();
};
const oncomplete = async (todo) => {
await db.update({completed: true}, todo.key)
todo.completed = true;
m.redraw();
};
async function getTodos() {
isFetching = true;
todos = (await db.fetch()).items;
isFetching = false;
m.redraw();
}
async function addTodo() {
const newTodo = {
text: newTodoText,
completed: false
};
await db.put(newTodo);
await getTodos();
}
return {
oninit: async (vnode) => {
await getTodos();
},
view: (vnode) => {
return m("div", { class: "container py-4" }, [
m("h1", { class: "text-center mb-4" }, "Todo List"),
m("div", { class: "input-group mb-3" }, [
m("input", {
class: "form-control",
type: "text",
id: "todo-input",
value: newTodoText,
autocomplete: "off",
onchange: (e) => {
newTodoText = e.target.value
},
onkeyup: (e) => {
e.redraw = false;
if (e.keyCode == 13) {
addTodo(newTodoText, todos);
newTodoText = ''
}
},
placeholder: "Add new todo",
"aria-label": "Add new todo",
"aria-describedby": "add-btn",
}),
m(
"div",
{ class: "input-group-append" },
m(
"button",
{
class: "btn btn-outline-secondary",
type: "button",
id: "add-btn",
onclick: (e) => {
addTodo(newTodoText, todos);
}
},
"Add"
)
)
]),
m("div", { class: "loader", id: "loader", style: { display: "none" } }),
isFetching && m(".loader"),
m("div", { class: "list-group", id: "todo-list" }, [
todos.map((el) => m(Todo, {key: el.key, todo: el, onremove, oncomplete }))
])
]);
}
};
};
m.mount(root, App);
</script>
</body>
</html>
Looking at this code, it gives me the vibe of React without the JSX, and I like it. The code is pretty clear if you have worked with JSX before, and the change detection is very good. I have used m.redraw()
to update the component when its state changes because I have made some asynchronous operations. In this case, we have to call the m.redraw()
function manually, unless we use the m.request
function, which is a wrapper around the fetch function and will call the m.redraw()
function automatically.
You can access the props of the component with the vnode.attrs
object, and you can access the state of the component with the vnode.state
object.
In conclusion, MithrilJS is a JavaScript framework that offers a simple, lightweight and flexible solution for creating interactive web applications. However, MithrilJS also has some drawbacks, such as its low popularity and a bit of a learning curve. If you are looking for a simple and lightweight framework, then MithrilJS might be the right choice for you. But if you want something more powerful or popular, then you should consider other options like React or Angular.
Explore the MithrilJS documentation to learn more about the framework and all its features (I have omitted the router, animations and some other things).