Last active
August 4, 2022 11:08
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 forisInialized
to befalse
, the real DOM element associated with the virtual node must have been created in the current draw cycle; in order foronunload
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:
Now:
previousVdom
is the compiled vdom representation that rendered to the element in the last draw. Whereasdraw
always has a present and previous vdom,init
andexit
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.