How I use mithril
Posted on April 21, 2016
Please be aware this post was written when Mithril 0.2x was state of the art. Mithril 1.0 has built in support for streams and closure components; and enables writing in a declarative fashion. Many of the points I make here are still valid, but need to be read in the context of the mithril 0.2x API. The specific code samples and techniques used do not apply if you are using Mithril 1.0
Iâll leave this article here for posterity, and when Iâve got more experience with 1.0 Iâll post a follow up. In the interim please feel free to ask questions in the mithril chatroom.
Happy Coding - James
Mithril is a tiny library for writing interactive web applications. Itâs so small it may seem difficult to know where to start.
Mithril is used by large in production apps, but its footprint is ~8kb. Yes itâs website could use a bit of spit and polish but it truly is the best MVC framework we have in Javascript right now.
Iâve previously written about other aspects of mithril, like m.prop
and how cleanly
it solves a lot of common problems Iâve experienced in other frameworks like Backbone.
In this post, Iâd like to cover how I structure components and share data throughout the app.
Mithril gives us a lot of flexibility when defining components.
But I donât write components using the traditional Mithril API.
I am going to gradually introduce my approach so we can stay focused on each layer.
If you havenât tried mithril, or you are still finding your feet, please, do not take this article as gospel. The approach I use, works for me, and my usecases. I recommend trying mithril out and get a feel for your own style.
Never use this
This isnât just for javascript hipster reasons. Using this
introduces bugs,
data hiding issues, verbosity, and encourages the use of methods instead of functions.
Simply by using the revealing module pattern, we can see the majority of our app as data processing and reuse functions across components.
Some people may see the revealing module pattern as a hack because they are used to thinking of constructors and classes - but all a constructor is meant to do is return a state object, so we are actually making it more explicit.
//traditional mithril revealing module approach
var Example = {
controller: function(){
var count = m.prop(0)
var checked = m.prop(false)
return { count, checked }
}
view: function(state){
return m('div', [
m('input[type=number]', {
onchange: m.withAttr('value', state.count)
})
,m('p', 'The number is: '+ state.count() )
,m('input[type=checkbox]', {
checked: state.checked()
, onchange: m.withAttr('checked', state.checked)
})
])
}
}
When I started with mithril I had separate namespaces for the model and the view-model. I donât do that anymore. Dividing our data into separate namespaces for its origin and intent is noise. And often the line between view state and model state can blur over time. Though, I do think the concept of a view model is valuable.
Closure components
The traditional mithril component API is a struct with a
controller
constructor function, and a view
function
that accepts the controller
as its first argument.
I think this is a great low level API, it is flexible and it allows for a variety of component patterns to work and still feel native.
However, I write my component as a function that returns a view function. All the model state lives within the closure.
This isnât just because closures are cool, and this
is lame.
Itâs because decoupling the view
from the controller
is only useful
if you reuse base views across components. But in practice, that doesnât happen often.
Using a closure also reduces library specific machinery. I donât need to rely on the somewhat magical behaviour that mithril passes the controller instance to the view function. I can just reach the state directly using a standard javascript lanaguage behaviour.
Youâll notice our view also no longer needs to prefix each prop with state
.
So the view itself is a lot less ceremonial.
function Example(){
var count = m.prop(0)
var checked = m.prop(false)
return function view(){
return m('div', [
m('input[type=number]', {
onchange: m.withAttr('value', count)
})
,m('p', 'The number is: '+ count() )
,m('input[type=checkbox]', {
checked: checked()
, onchange: m.withAttr('checked', checked)
})
])
}
}
Of course, if this Component doesnât obey the traditional mithril API, how do we mount it, or render it?
A simple utility.
function component(f){
return { controller: f, view: function(view){ return view() } }
}
We can then plugin to the normal mithril machinery.
Here Iâm injecting our component into a parent component in 3 different ways.
function ParentComponent(){
var example = Example()
return function(){
return m('div',
// { controller: Example, view: function(view){ return view() } }
component(Example)
// view()
,example()
// Allow passing in parameterized data
,m.component(component(Example), { data: [1,2,3] })
)
}
}
The above just shows whatâs possible. I tend to export the traditional API as a module. And then other devs do not need to know that I used a closure component behind the scenes.
//file: components/example.js
// es6
export default component(Example)
// commonjs
module.exports = component(Example)
Flyd Streams everywhere
Flyd is a tiny library. It lets you pass values to subscribers and set a new value at any time.
It goes with mithril like Peanut Butter and Jam - its API is almost identical to m.prop
.
A quick introduction to flyd
In flyd we get and set and values the same way that we use props.
var a = stream(0)
a() //=> 0
a(1)
a() //=> 1
But we can also subscribe to changes. And create dependent streams using map
var a = stream(0)
var b = a.map(multiply(2))
a(2)
b() //=> 4
It comes in handy in so many ways. Letâs take a look!
Replace propâs with streams.
Everything still works
var f = require('flyd')
function Example(){
var count = f.stream(0)
var checked = f.stream(false)
return function view(){
return m('div', [
m('input[type=number]', {
onchange: m.withAttr('value', count)
})
,m('p', 'The number is: '+ count() )
,m('input[type=checkbox]', {
checked: checked()
, onchange: m.withAttr('checked', checked)
})
])
}
}
Not very exciting, but worth stating.
Subviews
Below we subscribe to changes to our state streams (count
and checked
).
We then declare view streams that will be regenerated whenever the state changes.
We are creating streams based on the value of our propâs. They will always be up to date. And when we execute our sub streams as functions, we are not rerunning the view itself, it is cached.
Itâs a shame mithril doesnât know about this caching, so it will still do a diff against the DOM. But for complex view calculations, you can ensure, you are only generating that vdom when you need to.
It would also be quite simple to return { subtree: retain}
on subsequent calls to a stream.
Might be a nice experiment to try.
var f = require('flyd')
function Example(){
var count = f.stream(0)
var checked = f.stream(false)
var p = count.map(function(n){
return m('p', 'The number is: '+ count)
})
var numberInput = count.map(function(n){
return m('input[type=number]', {
onchange: m.withAttr('value', count)
value: n
})
})
var checkbox = checked.map(function(value){
m('input[type=checkbox]', {
checked: value
, onchange: m.withAttr('checked', checked)
})
})
var view = f.combine(function(){
return m('div', [
numberInput()
,p()
,checkbox()
])
}, [ checkbox, numberInput, p])
return view
}
In a real world scenario, I probably wouldnât define those particular subviews as streams. There is no real benefit. But it is nice that our component is a declarative depedency tree. Itâs simple to see what state triggers changes to a particular view.
But I do use this for complex situations like occlusion culling, where you need to calculate how many items are on screen and their respective positions. Itâs nice to know that calculcation is only happening when a dependency triggers it, and not on every redraw.
Cross component communication
The observer pattern suffers from âCall from where?â, and is too heavy. Simply sharing props, leads to managing when to redraw, and spaghetti code.
But streams are undirectional and clearly state their dependencies and flow. They are the perfect tool for sending data through multiple levels of components in your application.
Below is an example of a Sidebar. It animates on the x axis, and its position will affect the
content pane to its right. Weâve got a SidebarManager
that sets up the streams and passes it to the
sub components Sidebar
and ContentPane
.
The thing I really like about streams, is that we are not listening to events.
We are declaring relationships. Itâs not a matter of âwhen this changes, do thatâ
but âThis is that transformed by a functionâ
function SidebarManager(){
var hidden = f.stream(false)
var offset = hidden.map(function(hidden){
return hidden ? 40 : 400
})
var sidebar = Sidebar({ hidden, offset })
var content = ContentPane({ offset })
return function(){
return m('div', [
sidebar()
content()
])
}
}
function Sidebar(world){
var style = world.offset.map(function(x){
return {
left: -440+'px'
transform: 'translateX('+x+'px)'
}
})
var view = style.map(function(style){
return m('div', { style }, ...)
})
return view
}
function ContentPane(world){
var style = world.offset.map(function(x){
return {
transform: 'translateX('+x+'px)'
}
})
var view = style.map(function(style){
return m('div', { style }, ... )
})
return view
}
Easier to debug
flyd streams are easier to debug for 2 reasons.
- You can subscribe to changes and log them
- Every stream implements the
toString
interface so the chrome dev tools will show the current value.
Notice currentTime
, pointer.scale
and other fields are all streams. But we get to see the current value rendered inline!
So I hope Iâve shown that flyd+mithril is golden. Please try it out!
Opt out of Mithril when appropriate
Mithril makes it astonishingly convenient to jump in and out of virtual dom and direct dom. So if you need to just get in there and play with the dom directly, itâs straightforward.
There is no shame in this! If you want to getComputedStyle
or access parentNode
then all power to you.
In mithril itâs as simple as using the config
function.
m('div', { config:
function(el, firstTime, context){
if(firstTime){
el.parentNode.style.backgroundColor = 'red'
} else {
//subsequent calls
}
}
})
When I first saw that API, it scared me. Often I just want access to an element.
But, In plain old mithril, we could just use a prop as the config function, and because the el
is the first argument, weâll store the element in the prop.
//controller
var container = m.prop()
//view
m('div', { config: container })
And with flyd, we can actually do some work when that container arrives.
var container = f.stream()
var computedStyle = container.map(getComputedStyle)
// log the computed style every redraw
computedStyle.map(
console.log.bind(console, 'computedStyle: ')
)
//view
m('div', config: container)
Weâve got this great way of interacting with the DOM now. But there are some things virtual doms are not good at, so itâs great its so simple to transfer back and forth between mithril and the âreal worldâ.
Avoid the Model Layer
Models are closely coupled to views. The sooner we admit this, the more trouble we avoid. So letâs define them in the same file, and in most cases, lets just define them within in the controller.
In some cases it makes sense to put a model in a separate file, but do this after youâve found you need shared state across routes.
You also, do not need object relationship management (ORM).
The whole get/set/save mentality is more complicated than just using fetch({ method: 'POST', body: json })
Here is the process:
- Fetch it
(fetch(...).then(data))
- Render it
data().map( i => m('div', i.data ))
- Save it
fetch({ method: PUT, body: JSON.stringify(data) })
This should be a util function, not an application layer and should all happen in close proximity to the view.
Avoid m.request
m.request
is the XHR utility built into mithril. I personally donât like utilities that do multiple things in multiple
ways. m.request
returns both a Promise
and a m.prop
, it also can control the redraw behaviour of your app and handle
casting the data into a custom Model type.
The Promise implementation is custom, and not A+ compliant. m.request
is like a swiss army knife, and if that appeals to you,
use it by all means. But I prefer to compose separate tools that do 1 thing well. Its easier to debug, and reason about later.
Hereâs an example of using m.request
You can initialize your props immediately and mithril will automatically redraw for you when the request completes.
var users = m.request({ method: 'GET' , url: '/users' })
users() //=> undefined
users.then(function(serverResponse){
serverResponse == users()
})
I would just use fetch
.
var users = m.prop([]) // A default value so we can draw immediately.
fetch('/users') //GET by default
.then( r => r.json() )
.then(users) // save to prop
.then(m.redraw)
I personally like that I am specifically setting the data to the prop and manually triggering a redraw. It encourages us to draw immediately with an empty dataset, instead of loading. Itâs also simple to add some logs, or other transformations within the promise chain.
Conclusion
In this post Iâve demonstrated how I like to use mithril. But the great thing about mithril is that you can choose your own way to approach these problems. Mithril has smart design assumptions built in, but it also is flexible enough to let you opt out at any time.
The upcoming version of mithril will make this even more true. Mithrilâs router, rendering, etc are all separate modules that you can opt out of at will.
If you found this article interesting, I recommend hanging out in the mithril chat.
Thank you for reading. If youâd like to reach out you can get in touch on Mastodon, BlueSky.