mithril-vndb/docs/mithril.module.md
2015-02-26 22:51:07 -05:00

12 KiB

m.module



A module is an Object with two keys: controller and view. Each of those should point to a Javascript function. Note that the name of both properties should be lower-cased and both keys are optional.

//a valid module
{controller: function() {}, view: function() {}}

Rendering Modules

Usage

Calling m.module with a DOM element as the first argument and a module as the second argument will instantiate the module's controller, and call the module's view function with the controller instance as the first argument.

var MyModule = {}
MyModule.controller = function() {
	this.greeting = "Hello"
}
MyModule.view = function(ctrl) {
	return m("h1", ctrl.greeting)
}

m.module(document.body, MyModule)

//<body><h1>Hello</h1></body>

Using controllers as factories

When using m.module, Mithril instantiates controllers as if they were class constructors. However, if a controller returns an object, the returned object will be used as the controller instance (this is a feature in Javascript, which can be used to use a controller as a factory).

var MyModule = {}
MyModule.controller = function() {
	return {greeting: "Hello"}
}
MyModule.view = function(ctrl) {
	return m("h1", ctrl.greeting)
}

m.module(document.body, MyModule)

//<body><h1>Hello</h1></body>

Parameterized modules

Any extra parameters passed to m.module (after the DOM element and the module to be rendered) are appended to the list of arguments of both the controller and the view functions.

var MyModule = {}
MyModule.controller = function(options, extras) {
	this.greeting = "Hello"
	console.log(options.name, extras) // logs "world", "this is a test"
}
MyModule.view = function(ctrl, options, extras) {
	return m("h1", ctrl.greeting + " " + options.name + " " + extras)
}

m.module(document.body, MyModule, {name: "world"}, "this is a test")

//<body><h1>Hello world this is a test</h1></body>

Unloading modules

If a module's controller implements an instance method called onunload, this method will be called when a new m.module call updates the root DOM element tied to the module in question.

var module1 = {};
module1.controller = function() {
	this.onunload = function() {
		console.log("unloading module 1");
	};
};
module1.view = function() {};

m.module(document, module1);



var module2 = {};
module2.controller = function() {};
module1.view = function() {};

m.module(document, module2); // logs "unloading module 1"

This mechanism is useful to clear timers and unsubscribe event handlers. If you have a hierarchy of components, you can recursively call onunload on all the components in the tree or use a pubsub library to unload specific components on demand.

You can also use this event to prevent a module from being unloaded in the context of a route change (e.g. to alert a user to save their changes before navigating away from a page)

var module1 = {}
module1.controller = function() {
	this.unsaved = false
	
	this.onunload = function(e) {
		if (this.unsaved) {
			e.preventDefault()
		}
	}
}

Normally, calling m.module will return the controller instance for that module, but there's one corner case: if preventDefault is called from a controller's onunload method as a result of calling m.module, then the m.module call will not instantiate the new controller, and will return undefined.

To unload a module without loading another module, you can simply call m.module without a module parameter:

m.module(rootElement, null);

Mithril does not hook into the browser's onbeforeunload event. To prevent unloading when attempting to navigate away from a page, you can check the return value of m.module

window.onbeforeunload = function() {
	if (!m.module(rootElement, null)) {
		//onunload's preventDefault was called
		return "Are you sure you want to leave?"
	}
}

Signature

How to read signatures

Object module(DOMElement rootElement, Module module [, Object options [, any... args]])

where:
	Module :: Object { Controller, void view(Object controllerInstance) }
	Controller :: void controller() | void controller() { prototype: void unload(UnloadEvent e) }
	UnloadEvent :: Object {void preventDefault()}
  • DOMElement rootElement

    A DOM element which will contain the view's template.

  • Module module

    A module is supposed to be an Object with two keys: controller and view. Each of those should point to a Javascript class constructor function

    The controller class is instantiated immediately and a reference is returned upon calling m.module.

    Once the controller code finishes executing (and this may include waiting for AJAX requests to complete), the view class is instantiated, and the instance of the controller is passed as an argument to the view's constructor.

    Note that controllers can manually instantiate child controllers (since they are simply Javascript constructors), and likewise, views can call child views and manually pass the child controller instances down the the child view constructors. You should avoid instantiating controllers from views, since views can be rendered many times across the lifecycle of a page, and a redraw might wipe out sub-controller data, if it houses any.

    However, if hierarchical nesting of modules is desirable, it's preferable to put the module itself in the template. Mithril's rendering system can detect these modules and manage their lifecycles automatically.

  • Object options

    A key-value map of optional arguments to be passed to the controller and view functions of module

  • any... args

    Extra arguments are passed to both controller and view functions in the same fashion as the options argument

  • returns Object controllerInstance

    An instance of the controller constructor


Using modules as components

Modules can have arguments "preloaded" into them. This is useful when using modules as components

Calling m.module without a DOM element as an argument will create copy of the module with parameters already bound as arguments to the controller and view functions

var MyModule = {}
MyModule.controller = function(options, extras) {
	this.greeting = "Hello"
	console.log(options.name, extras)
}
MyModule.view = function(ctrl, options, extras) {
	return m("h1", ctrl.greeting + " " + options.name + " " + extras)
}


//note the lack of a DOM element in the list of parameters
var LoadedModule = m.module(MyModule, {name: "world"}, "this is a test")

var ctrl = new LoadedModule.controller() // logs "world", "this is a test"

m.render(document.body, LoadedModule.view(ctrl))

//<body><h1>Hello world this is a test</h1></body>

Modules can be part of a template's virtual element tree, so the "preloading" mechanism can be used to create parameterized components:

var MyApp = {
	controller: function() {},
	view: function() {
		return m("div", [
			m("h1", "My app"),
			
			//a parameterized module
			m.module(MyModule, {name: "users"}, "from component"),
		])
	}
}

m.module(document.body, MyApp)

/*
<body>
	<h1>My app</h1>
	<h1>Hello users from component</h1>
</body>
*/

Note that adding a key property in the list of attributes ({name: "users"} above) will propagate this key to the root element of the component's template even if you don't manually do so. This allows all components to be identifiable without intervention from component authors.


Unloading components

Modules declared in templates can also call onunload and its e.preventDefault() like regular modules. The onunload event is called if an instantiated module is removed from a virtual element tree via a redraw.

In the example below, clicking the button triggers the component's onunload event and logs "unloaded!".

var MyApp = {
	controller: function() {
		this.loaded = true
	},
	view: function(ctrl) {
		return [
			m("button[type=button]", {onclick: function() {ctrl.loaded = false}}),
			ctrl.loaded ? m.module(MyComponent) : ""
		])
	}
}

var MyComponent = {
	controller: function() {
		this.onunload = function() {
			console.log("unloaded!")
		}
	},
	view: function() {
		return m("h1", "My component")
	}
}

m.module(document.body, MyApp)

Asynchronous components

Since components are Mithril modules, it's possible to encapsulate asynchronous behavior. Under regular circumstances, Mithril waits for all asynchronous tasks to complete, but when using components, a component's parent view renders before the component completes its asynchronous tasks (because the existence of the component only becomes known to the diff engine at the time when the template is rendered).

When a component has asynchronous payloads and they are queued by the auto-redrawing system, its view is NOT rendered until all asynchronous operations complete. When the component's asynchronous operations complete, another redraw is triggered and the entire template tree is evaluated again. This means that the virtual dom tree may take two or more redraws (depending on how many nested asynchronous components there are) to be fully rendered.

For this reason, it's recommended to refactor code in such a way that asynchronous operations happen in the root module and avoid making AJAX calls within components.


Component limitations and caveats

There are a few caveats to using modules as components:

1 - component views must return a virtual element. Returning an array, a string, a number, boolean, falsy value, etc will result in an error. This limitation exists in order to support the correctness of unloading semantics component identity.

2 - components cannot change m.redraw.strategy from the controller constructor (but they can from event handlers).


Signature

How to read signatures

Object module(Module module [, Object options [, any... args]])

where:
	Module :: Object { Controller, void view(Object controllerInstance) }
	Controller :: void controller() | void controller() { prototype: void unload(UnloadEvent e) }
	UnloadEvent :: Object {void preventDefault()}
  • Module module

    A module is supposed to be an Object with two keys: controller and view. Each of those should point to a Javascript class constructor function

    The controller class is instantiated immediately and a reference is returned upon calling m.module.

    Once the controller code finishes executing (and this may include waiting for AJAX requests to complete), the view class is instantiated, and the instance of the controller is passed as an argument to the view's constructor.

    Note that controllers can manually instantiate child controllers (since they are simply Javascript constructors), and likewise, views can call child views and manually pass the child controller instances down the the child view constructors. You should avoid instantiating controllers from views, since views can be rendered many times across the lifecycle of a page, and a redraw might wipe out sub-controller data, if it houses any.

    However, if hierarchical nesting of modules is desirable, it's preferable to put the module itself in the template. Mithril's rendering system can detect these modules and manage their lifecycles automatically.

  • Object options

    A key-value map of optional arguments to be passed to the controller and view functions of module

  • any... args

    Extra arguments are passed to both controller and view functions in the same fashion as the options argument

  • returns Object controllerInstance

    An instance of the controller constructor