Skip to content

Instantly share code, notes, and snippets.

@barneycarroll
Last active August 4, 2022 11:08
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save barneycarroll/0e3c1b9811b47c012b13 to your computer and use it in GitHub Desktop.
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.
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(){}
}() );
@barneycarroll
Copy link
Author

New API hooks:

mod( component, context? ).mapWith( collection, ...keyStrings? ) iterate over each item in object or array-like collection, using collectionItem[ keyString ] to provide unique keys. This is especially useful when iterating over a list whose order may change. You can supply multiple keyStrings for deep key identification and these can be methods (as long as methods don't expect arguments). For instance:

var users = [
  {
    names : [ 'Barney', 'Carroll' ],
    /* ... */
  },
  {
    names : [ 'Nikolai', 'Fyodorovich', 'Fyodorov' ],
    /* ... */
  }
];

mod( profile ).mapWith( users, 'names', 'toString' );

If no keyStrings are provided, it will use the key of the item under iteration. This can be useful for un-ordered objects, where keys will be unique.

mod( document.body, component )( ...args? ) brings Modulator closer in line with Mithril's component branch and allows 'turtles all the way down'. In combination with the default mod.extend = true, this means all ctrl instances will have a context-bound mod method for convenient sub-module invocation.

@barneycarroll
Copy link
Author

mapWith overload option

mapWith( collection, keysArray ) specify the keys to be used for each item.

@barneycarroll
Copy link
Author

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:

  1. 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.
  2. 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.
  3. 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
  4. Mapping over collections as a first class API method

Then there are things that Mithril 0.2 does do, which Modulator doesn't:

  1. Blocking subcomponent view renders
  2. onunload method triggering for subcomponents

@barneycarroll
Copy link
Author

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.

This interface solves that problem.

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