-
-
Save barneycarroll/0e3c1b9811b47c012b13 to your computer and use it in GitHub Desktop.
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(){} | |
}() ); |
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.
mapWith overload option
mapWith( collection, keysArray )
specify the keys to be used for each item.
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.
NB: For the sake of clarity, a component is a static object containing one or both of the
controller
andview
properties; a module is an instance of such a component bound to the Mithril lifecycle.Modulator
Modulator removes a lot of boilerplate by handling controller instance management automatically, allowing you to express your module invocation entirely in the view, in one expression, without the need for specifying higher order controllers – but without losing any of the inherent functionality of Mithril's controllers either. Thanks to the affordances for implicit controllers in 0.1.29, the componentization example in the Mithril guide becomes:
Of course, the example above is simplistic and ignores the fact that controller lifecycle management fulfils a purpose: idiomatic Mithril usage initialises controllers once when the module is first invoked, and then once for every route change. In theory this is a useful property for initialising components automatically. In practice, only top-level components (bound with
m.route
&m.module
) get this for free: vanilla Mithril puts the onus on the developer to instantiate sub-component controllers manually (in their parent controller). Other componentization proposals either fail to cater for re-initialisation, or adopt the semantics of virtual DOM node identity, whereby position in the DOM tree or a unique string identifier are used to determine whether the element should be created from scratch – effectively making initialisation equivalent to a view's root node'sconfig
function, but taking place before or during the virtual DOM rendering loop.Modulator aims to cater for any complexity of controller instantiation that would be possible with vanilla Mithril without forcing the burden of expressing that complexity on people who don't want it.
Signature
Modulator is a functor. It's first function accepts a component and optional arguments to allow differentiation for automatic state management, and returns a function that corresponds to a distinct module instance. This second function accepts arguments which are passed to the module's methods and return the view:
mod( component, context?, key? )( ...args? )
component
The component definition. If a function is passed, it is assumed to be a view. If no view is present, the controller instance will be returned. As with Mithril's
m.module
andm.route
methods, modules returned by controller-less components will still get a free unique instance passed through as the first argument of the view. If a component is passed without a context or a `key, each module invocation in a view loop will return a new instance: subsequent renders will fetch the previously initialised instance based on execution order.context
Context is used to isolate multiple component instances within their parent module instance, and should correspond to the module's
ctrl
instance. Why would you want to do this? Imagine you have a page with an auto-complete dropdown component that establishes data-binding hooks based on input data. Initialising it without a context argument means it will always return the same instance, because it's always the first registered dropdown component. By providing it with a unique context you can ensure it never returns the wrong instance. When you don't provide a context, modulator assumes all instances share the same global context.key
When key isn't supplied, modulator starts a counter for each component + context, and increments it with each render. At the beginning of each redraw cycle, the counter is reset. Specifying a key instead of using the default rendering order index is useful if rendering order can vary within context. A lot of componentization APIs use keys exclusively, but this means you have to take care of unique identifiers throughout the application. In contrast, specifying a context and a key means you only have to worry about uniqueness within your current scope, which is trivial.
...args
After consuming all the component identification parameters, modulator returns a function that accepts any arguments you want to pass to the module. These are passed straight through to the controller and the view – although they will be offset by 1 in the view function to allow the view to receive a unique context (the controller instance, if the component provided one).
Extras
mod.extend
Modulator extends controllers with a couple of convenience methods. You can stop this happening by setting
mod.extended = false
.ctrl.mod( component, key? )
A partially applied call to modulator with context pre-set to the current instance. Components defined controllers will still populate their module views with instances, so this syntax is convenient if you know the component will be 'modulated':
ctrl.refresh( ...args? )
Allows a module to refresh itself with the passed in arguments. 'Refreshing' means forcing the controller to re-initialise the next time the view renders. Even if the component doesn't have a controller of its own, this can be useful for forcing all sub-modules to be reinitialised in the next redraw. Controllers and views always receive all the arguments passed in in the first place, so passing in the same thing again is trivial:
The refresh method returns
m.redraw
, so you can force an instant re-computation as follows:ctrl.refresh( ...arguments )()
.Miscelaneous API hooks
mod.extend
can be set to false if you prefer controllers not to be extended withmod
andrefresh
methods.mod.unique( component )
will initialize the module when it is first called and return the same instance thereafter. The only way to reinitialize the module is internally viactrl.refresh
.mod.cleanup
is true by default if modulator is running in an environment without support for native Javascript Maps & Weakmaps. Native Weakmaps are optimised for garbage collection. In cases where they are not present, andmod.cleanup
is true, modulator will destroy module instance registries when their context unloads by safely binding to Mithril'sonunload
hook. The binding does not replace pre-assigned functions and takes account ofpreventDefault
.