Skip to content

Instantly share code, notes, and snippets.

@barneycarroll
Last active August 4, 2022 11:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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

Tastes great in combination with Modulator.

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