npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mithril-cc

v0.12.1

Published

An opinionated library for writing Mithril components

Downloads

44

Readme

mithril-cc

An opinionated library for writing Mithril.js components.

Motivation

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.

Getting Started

yarn add mithril-cc
# or
npm install mithril-cc

In your component files:

import {cc} from 'mithril-cc'

Using a CDN

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>

TypeScript

For type inference, simply parameterize your cc calls:

type Attrs = {
  initialCount: number
}
const Counter = cc<Attrs>(/* ... */)

Learn by Example

Simple Counter

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')
  ]
})

Live demo

View Attrs

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}!`)
  )
})

Live demo

In case you need it, vnode is provided as a second parameter.

Component Setup

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')
  ]
})

Live demo

Reacting to Attrs Changes

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
  $attrs.map(a => a.name).map(uniques()).map(n => rank++)
  $attrs.map(() => renderCount++)

  return (attrs) => (
    m('p', `Hello, ${attrs.name}! You are person #${rank} (renderCount ${renderCount})`)
  )
})

Live demo

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.

Unsubscribe

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')
})

Live demo

Stream Scoping

NOTE: You do not need to scope streams based on $attrs, since that stream already gets cleaned up (via garbage collection) when the component dismounts.

Sometimes you will want to map an external stream. For example (don't do this):

import keyboard from './keyboard'

const Example = cc(function(){

  // Bad, don't do this
  let $shift = keyboard.listenStream('Shift') // Bad, don't do this

  $shift.map(onOrOff => { // Bad, don't do this
    // ^ Bad
    console.log('Shift:', onOrOff)
  })

  return () => m('p', `Shift key: ${$shift() ? 'on' : 'off'}`)
})

The problem with this example is that it has a memory leak. When you mount this component, the .map creates a child stream that will persist within the parent keyboard's stream. As long as keyboard exsits (likely for the entirety of the app in this case), then the child will never get garbage collected.

Mithril-cc has two conveniences to deal with this. The first is using unsubscribe:

const Example = cc(function(){
  let $shift = keyboard.listenStream('Shift')
  this.unsub = $shift // 👍

  $shift.map(onOrOff => {
    // All good
    console.log('Shift:', onOrOff)
    m.redraw()
  })

  return () => m('p', `Shift key: ${$shift() ? 'on' : 'off'}`)
})

This will clean up the stream (specifically, call $shift.end(true)) when the component unmounts.

The second way is using this.scope:

const Example = cc(function(){
  let $shift = this.scope(keyboard.keyStream('Shift')) // 👍

  $shift.map(onOrOff => {
    // All good
    console.log('Shift:', onOrOff)
    m.redraw()
  })

  return () => m('p', `Shift key: ${$shift() ? 'on' : 'off'}`)
})

Which does the same as this.sub, but in a more convenient syntax, as it allows you to chain map calls:

const Example = cc(function(){
  let $shift = this.scope(keyboard.keyStream('Shift')).map(onOrOff => {
    console.log('Shift:', onOrOff)
    m.redraw()
  })

  return () => m('p', `Shift key: ${$shift() ? 'on' : 'off'}`)
})

Note how we don't have to unsub the stream created by .map; the parent stream is already set to be cleaned up, which means any of its child streams will be cleaned up as well.

Lifecycle Methods

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`)
})

Live demo

addEventListener

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}`)
})

Live demo

setTimeout and setInterval

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}`)
})

Live demo

const Ticker = cc(function(){
  let tick = 0
  this.setInterval(() => tick++, 1000)
  return () => m('p', `Tick: ${tick}`)
})

Live demo

React Hooks-like Abstractions

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]
}

Live demo

Shorthand Components

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}!`)
)

Island Components

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.

Developing

npm run build
cd pkg
npm publish