An opinionated library for writing Mithril.js components.
Mithril is the leader and forerunner of declarative HTML views in plain old JavaScript. However, its flourishing flexibility can leave one uneasy on the "right" way to do things. The wide array of available options all have different pros and cons that depend on the type of component you're writing.
To cure, CC compresses these options into a pleasant, one-size-fits-all approach, allowing you to trade discouraging decision fatigue for simple peace and tranquility.
In other words: Closure components are the epitome of userland Mithril, and CC brings out the best in them.
yarn add mithril-cc
# or
npm install mithril-cc
In your component files:
import {cc} from 'mithril-cc'
If you use a CDN, mithril-cc will be available via m.cc
, m.ccs
, etc.
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
<script src="https://unpkg.com/mithril-cc"></script>
For type inference, simply parameterize your cc
calls:
type Attrs = {
initialCount: number
}
const Counter = cc<Attrs>(/* ... */)
- Simple counter
- View Attrs
- Component Setup
- Reacting to Attrs Changes
- Unsubscribe
- Lifecycle Methods
addEventListener
setTimeout
andsetInterval
- React Hooks-like Abstractions
- Shorthand Components
- Island Components
State in a cc
closure component is as simple as you could ever hope for. Set some variables, return a view thunk, and you're ready to go.
import m from 'mithril'
import {cc} from 'mithril-cc'
const Counter = cc(function(){
let n = 0
return () => [
m('p', 'Count: ', n),
m('button', { onclick: () => n++ }, 'Inc')
]
})
For convenience, Mithril's vnode.attrs
are made available directly as the first parameter to your view thunk.
const Greeter = cc(function(){
return (attrs) => (
m('p', `Hello, ${attrs.name}!`)
)
})
In case you need it, vnode is provided as a second parameter.
Sometimes you need to set up state when your component gets initalized. CC provides your component with attrs
as a stream. Note how this is different from your view thunk, which receives attrs
directly (as a non-stream).
Because it's a stream, your setup callbacks always have access to the latest value of you component's attrs.
const Counter = cc(function($attrs){
let n = $attrs().initialCount
return () => [
m('p', 'Count: ', n),
m('button', { onclick: () => n++ }, 'Inc')
]
})
Because top-level attrs
is a stream, you can easily react to changes using .map
.
Note also the this.unsub =
in this example. This will clean up your stream listener when the component unmounts. You can assign it as many times as you like; CC will remember everything.
import {cc, uniques} from 'mithril-cc'
const Greeter = cc(function($attrs){
let rank = 0
let renderCount = -1
this.unsub = $attrs.map(a => a.name).map(uniques()).map(n => rank++)
this.unsub = $attrs.map(() => renderCount++)
return (attrs) => (
m('p', `Hello, ${attrs.name}! You are person #${rank} (renderCount ${renderCount})`)
)
})
Implementation detail: Because the $attrs
stream gets updated before the view thunk, your view thunk will see the latest and correct version of your closure variables.
If CC doesn't cover your cleanup use case, you can assign this.unsub =
to any function. CC will run the function when the component unmounts.
const UnmountExample = cc(function(){
this.unsub = () => console.log("unmount")
return () => m('div', 'UnmountExample')
})
Even though you're using view thunks, you still have access to all of Mithril's lifecycles via this
. You can even call oncreate
and onupdate
multiple times, which can be useful for creating React Hooks-like abstractions.
const HeightExample = cc(function(){
let height = 0
this.oncreate(vnode => {
height = vnode.dom.offsetHeight
m.redraw()
})
return () => m('p', `The height of this tag is ${ height || '...' }px`)
})
Often times you need to listen for DOM events. With this.addEventListener
, CC will automatically clean up your listener when the component unmounts. It will also call m.redraw()
for you.
const MouseCoords = cc(function(){
let x = 0, y = 0
this.addEventListener(window, 'mousemove', event => {
x = event.offsetX, y = event.offsetY
})
return () => m('p', `Mouse is at ${x}, ${y}`)
})
Just like this.addEventListener, you can use this.setTimeout
and this.setInterval
to get auto cleanup and redraw for free.
const Delayed = cc(function(){
let show = false
this.setTimeout(() => {
show = true
}, 1000)
return () => m('p', `Show? ${show}`)
})
const Ticker = cc(function(){
let tick = 0
this.setInterval(() => tick++, 1000)
return () => m('p', `Tick: ${tick}`)
})
Because CC's this
has everything you need to manage a component, you can abstract setup and teardown behavior like you would using React hooks.
For example, we can refactor the MouseEvents example into its own function:
import {cc} from 'mithril-cc'
import Stream from 'mithril/stream'
const MouseCoords = cc(function(){
let [$x, $y] = useMouseCoords(this)
return () => m('p', `Mouse is at ${$x()}, ${$y()}`)
})
function useMouseCoords(ccx) {
const $x = Stream(0), $y = Stream(0)
ccx.addEventListener(window, 'mousemove', event => {
$x(event.offsetX); $y(event.offsetY)
})
return [$x, $y]
}
If you only need attrs and nothing else, you can use ccs
.
import {ccs} from 'mithril-cc'
const Greeter = ccs(attrs => (
m('p', `Hello, ${attrs.name}!`)
)
Mithril's best feature is how it recalculates the entire app tree when it redraws. This makes your dev life easy by reducing a significant amonut of boilerplate code, and leaving less room for out-of-sync state-to-view bugs, all while having great performance 98% of the time.
However, in rare cases you may need to optimize for fewer redraws to fix a poor performance behavior. Islands are components that only redraw themselves instead of the whole app tree.
Islands are not necessary unless your app is rendering with a high redraw rate, such as (maybe) render-on-keystroke.
Logistics-wise, YOUR COMPONENT MUST ONLY HAVE A SINGLE, STABLE ROOT ELEMENT. You cannot, for example, return an array of elements from your component, or return an element sometimes and null other times.
This should be used as sparingly as possible. When used, your component should ONLY modify its own state, and not any state read by other components outside your component's descendants.
With that said, here is how to use it:
import m from 'mithril'
import {ccIsland} from 'mithril-cc'
const Counter = ccIsland(function(){
let n = 0
return () => [
m('p', 'Count: ', n),
m('button', { onclick: () => n++ }, 'Inc')
]
})
As you can see, your component behaves like any other cc, given the caveats described above.
npm run build
cd pkg
npm publish