Last active
August 4, 2022 11:08
-
-
Save barneycarroll/0e3c1b9811b47c012b13 to your computer and use it in GitHub Desktop.
Modulator: a light-touch API (with heavy internals) for auto-instantiating Mithril modules. Makes Mithril lifecycle management more user-friendly.
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
var mod = ( function initModulator(){ | |
if( !Map ){ | |
// A naive shim for maps functionality | |
var Map = shim; | |
var WeakMap = shim; | |
} | |
// Registry of instantiation contexts | |
var contexts = new WeakMap(); | |
// All automated counts | |
var counts = new Map(); | |
// Prevent infinite recursion if a modulated controller calls redraw | |
var pauseRedraw = ( function(){ | |
var snapRedraw = m.redraw; | |
var redraw; | |
var forced; | |
for( var key in m.redraw ){ | |
queueRedraw[ key ] = snapRedraw[ key ] = m.redraw[ key ]; | |
} | |
return function pause(){ | |
m.redraw = queueRedraw; | |
setTimeout( function unpause(){ | |
m.redraw = snapRedraw; | |
if( redraw ) m.redraw( forced ); | |
redraw = forced = false; | |
} ); | |
} | |
function queueRedraw( force ){ | |
redraw = true; | |
if( force ) forced = true; | |
} | |
}() ); | |
var unique = {}; | |
// Clear counts at the begninning of every redraw | |
m.module( document.createElement( 'x' ), { | |
view : counts.clear.bind( counts ) | |
} ); | |
// Shorthand for a component which will always return the same instance | |
mod.unique = function( component ){ | |
return mod( component, unique, unique ); | |
}; | |
// Shorthand for a keyed component with a global context | |
mod.global = function( component, key ){ | |
return arguments.length > 2 | |
? mod( component, unique, key ) | |
? mod( component, unique ) | |
}; | |
return mod; | |
function mod( component, context, key ){ | |
var components = register( contexts, context || unique, WeakMap ); | |
var keys = register( components, component, WeakMap ); | |
return function identify( key ){ | |
var count = key === undefined && register( counts, keys, m.prop.bind( undefined, 0 ) ); | |
// eg. ctrl.mod( profile ).mapWith( users(), 'username' ); | |
apply.mapWith = function( collection, keys ){ | |
var keyed = typeof keys === 'array'; | |
var path = [].slice.call( arguments, 1 ); | |
return Object.keys( collection ).map( function getItemIdentifier( index ){ | |
var key; | |
if( keyed ){ | |
key = keys[ index ]; | |
} | |
else if( path.length ){ | |
key = path.reduce( function getKeyValue( source, segment ){ | |
var node = source[ segment ]; | |
if( node instanceof Function ) node = node.call( source ); | |
return node; | |
}, collection[ index ] ); | |
} | |
else { | |
key = index; | |
} | |
return identify( key )( collection[ index ], index, collection ); | |
} ); | |
}; | |
return apply; | |
function apply(){ | |
var args = [].slice.call( arguments ); | |
var view; | |
if( count ){ | |
key = count( count() + 1 ); | |
} | |
var ctrl = register( keys, key, function newController(){ | |
pauseRedraw(); | |
var controller = component.controller || noop; | |
var instance = new ( controller.bind.apply( controller, [ controller ].concat( args ) ) )(); | |
// Shorthand for instantiating sub-modules | |
instance.mod = function( component, key ){ | |
return mod( component, instance, key ); | |
}; | |
// Force a re-instantiation of this controller on next redraw. | |
// Returns m.redraw to allow instant re-instantiation. | |
// So, to re-initialise with the same arguments and run a forced | |
// redraw immediately: | |
// ctrl.refresh( [].slice.call( arguments, 1 ) )() | |
instance.refresh = function(){ | |
args = [].slice.call( arguments ); | |
ctrl = register( keys, key, newController, true ); | |
return m.redraw; | |
}; | |
return instance; | |
} ); | |
// Return the controller instance if the component is view-less. | |
if( component.view ){ | |
if( args.length ){ | |
view = component.view.apply( undefined, [ ctrl ].concat( args ) ); | |
} | |
else { | |
view = component.view( ctrl ); | |
} | |
if( view instanceof Object ){ | |
view.ctrl = ctrl; | |
} | |
return view; | |
} | |
return ctrl; | |
} | |
}( key ); | |
} | |
// Convenience map method: retrieve key from map. If it's not registered, set it first with Constructor. | |
function register( map, key, Constructor, force ){ | |
return !force && map.has( key ) ? map.get( key ) : map.set( key, new Constructor() ).get( key ); | |
} | |
function shim(){ | |
var keys = []; | |
var values = []; | |
var map = { | |
get : function( key ){ | |
var index = keys.indexOf( key ); | |
return values[ index ]; | |
}, | |
has : function( key ){ | |
var index = keys.indexOf( key ); | |
return index > -1; | |
}, | |
set : function( key, value ){ | |
var index = map.has( key ) ? keys.indexOf( key ) : keys.length; | |
keys[ index ] = key; | |
values[ index ] = value; | |
return map; | |
}, | |
clear : function(){ | |
keys = []; | |
values = []; | |
}, | |
delete : function( key ){ | |
var index = keys.indexOf( key ); | |
if( index > -1 ){ | |
keys.splice( index, 1 ); | |
values.splice( index, 1 ); | |
return true; | |
} | |
return false; | |
} | |
}; | |
return map; | |
} | |
function noop(){} | |
}() ); |
Update
Modulator was created prior to Mithril 0.2. With the advent of m.component
, Mithril 0.2 supposedly has its own core method for easy automatic component invocation and automatic management. Modulator provides a few things that m.components doesn't. Significantly:
- A clear separation between component instantiation logic and arguments to be passed to the component. Mithril handles this via a
key
property in the first argument passed to the component, meaning component API is compromised and liable to confusion of concerns. - Component instantiation logic is used exclusively for determining the conditions in which to initialise or retrieve a controller, and doesn't affect anything else. Mithril conflates
key
with DOM identity, meaning redraw strategy is compromised. - An infinitely greater degree of control over initialisation logic, allowing, for example:
- Perpetual controllers that can persist through route changes
- The ability to move component position anywhere in the DOM without reinitialising
- Mapping over collections as a first class API method
Then there are things that Mithril 0.2 does do, which Modulator doesn't:
- Blocking subcomponent view renders
onunload
method triggering for subcomponents
Update
Mithril's config
API for real DOM access has similar limitations to 0.2 component invocation in that it mandates a 1-to-1 relationship between virtual and real DOM in order for lifecycle hooks to behave as expected.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
mapWith overload option
mapWith( collection, keysArray )
specify the keys to be used for each item.