Skip to content

Instantly share code, notes, and snippets.

@barneycarroll
Last active August 4, 2022 11:08
Show Gist options
  • Save barneycarroll/c2dda75bcc94a7424d75 to your computer and use it in GitHub Desktop.
Save barneycarroll/c2dda75bcc94a7424d75 to your computer and use it in GitHub Desktop.
A factory for Mithril DOM plugins to distinguish between initialisation, subsequent draw and teardown without the config API's semantic reliance on real DOM element lifecycle
// Config plugins with reliable setup & teardown in Mithril without relying on unique real DOM element lifecycle.
// This is meant to stand in for `config` in a way that's more suited to an application that continuously diffs the DOM.
import m from 'mithril'
// A plugin consists of 1 or more of the following methods:
// * init runs once when the plugin first executes
// * draw runs on every draw loop
// * exit runs at the beginning of the config cycle when the plugin instance has disappeared from view
//
// Each method is passed the element, the element context / state object, and the virtual DOM element.
// We need a map of all elements to infer teardown requirements
const elements = new Map()
const next = ( setImmediate || setTimeout ).bind( fn, 0 )
// Mounting a component to an unattached element allows us to tap in to
// the Mithril lifecycle before application code without impacting the real document DOM.
m.module( document.createElement( 'plugins' ), {
view : () => {
// Grab a reference to all elements registered last draw.
const previous = new Map( elements )
// ...then wipe that slate. By the time the config callback executes,
// all plugins for this view will have registered.
elements.clear()
return m( 'config', {
config : () => next( () => {
// The exit check loop
previous.forEach( ( { ctrl, ctxt, exit, vdom }, el ) => {
const replacement = elements.get( el )
if( exit && ( !elements.has( el ) || !replacement || ctrl !== replacement.ctrl && ctxt !== replacement.ctxt ) )
exit( el, ctxt, replacement ? replacement.vdom : null, vdom )
} )
// The init & draw loop
elements.forEach( ( { ctrl, ctxt, init, draw, exit, vdom }, el ) => {
const substitute = previous.get( el )
if( !previous.has( el ) || !substitute || ctrl !== substitute.ctrl && ctxt !== substitute.ctxt ){
if( init )
init( el, ctxt, vdom, substitute ? substitute.vdom : null )
}
// Draw does not execute on init by default.
// If that's what you want, invoke it manually from within init.
else if( draw )
draw( el, ctxt, vdom, substitute.vdom )
} )
} )
} )
}
} )
// Rather than using DOM elements as the unique identifier for a plugin lifecycle,
// we use DOM elements in combination with controller instances & position indicators, so a ctrl must be supplied.
// We make the assumption that an element in the same relative position compared to the same controller is
// the same element.
// To write a plugin for reuse:
// export default plugin( { initFn, drawFn, exitFn } )
// // Then ...
// import myPlugin from 'somewhere/or/other'
// export default {
// view : ctrl =>
// m( 'div', {
// config : myPlugin( ctrl )
// } )
// }
// // Or as an immediately-invoked, disposable declaration:
// {
// view : ctrl =>
// m( 'div', {
// config : plugin( {
// initFn, drawFn, exitFn
// }, ctrl )
// } )
// }
export default ( plugin, ctrl ) => {
// Shorthands: a single function is assumed to be an initialiser
if( plugin instanceof Function )
var init = plugin
else
var { init, draw, exit } = plugin
const bind = ctrl =>
( el, mInit, ctxt, vdom ) =>
elements.set( el, { ctrl, ctxt, init, draw, exit, vdom } )
return ctrl ? bind( ctrl ) : bind
}
@barneycarroll
Copy link
Author

Rationale:

The config custom attribute is Mithril's API for live DOM access. Its problem is that the lifecycle hooks ( isInitialized, ctxt.onunload ) depend upon the virtual DOM having a 1-to-1 relationship to real DOM: in order for isInialized to be false, the real DOM element associated with the virtual node must have been created in the current draw cycle; in order for onunload to fire, it must be destroyed.

This is an anti-pattern – DOM virtualization is supposed to obviate such concerns, with the eventual DOM being used as efficiently as possible ('diffing', 'DOM reuse / recycling', etc). Significantly, if elements are not keyed there is no guarantee that config's lifecycle hooks will operate as desired. The plugin interface aims to change that.

Previously:

config : ( el, init, ctxt, vdom ) => {
  if( !init ){
    // init code
    ctxt.onunload = function(){
      // exit code
    }
  }
  else {
    // draw code
  }
}

Now:

import plugin from 'path/to/plugin'

config : plugin( {
  init : ( el, ctxt, vdom, previousVdom = null ) =>
    // init code
  draw : ( el, ctxt, vdom, previousVdom ) =>
    // draw code
  exit : ( el, ctxt, vdom = null, previousVdom ) =>
    // exit code
}, ctrl )

previousVdom is the compiled vdom representation that rendered to the element in the last draw. Whereas draw always has a present and previous vdom, init and exit may refer to elements which have just been created or destroyed respectively, but may refer to 'repurposed' elements, in which case more state can be provided.

@barneycarroll
Copy link
Author

Tastes great in combination with Modulator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment