some docs

This commit is contained in:
Leo Horie 2015-02-18 21:59:54 -05:00
parent 2160dc13c1
commit 8fadeadb8f
4 changed files with 333 additions and 276 deletions

View file

@ -1,245 +1,135 @@
## Components
### Widgetization
Components are self-contained units of functionality that may hold state and communicate with a larger application via input parameters and events.
Components are Mithril's mechanism for [hierarchical MVC](http://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller).
In Mithril, [modules](mithril.module.md) are components. In scenarios where components don't need to cross-communicate a lot (for example, a dashboard full of unrelated widgets), it's often convenient to use fat controllers (i.e. controllers that hold state and methods).
Here's an example of a hierarchy of such components:
In Mithril, components are simply [modules](mithril.module.md). In order to use a module as a component, simply put it in a template:
```javascript
//root module
var dashboard = {};
dashboard.controller = function() {
this.userProfile = new userProfile.controller();
this.projectList = new projectList.controller();
//first declare a component (it's just a module)
var MyComponent = {
controller: function() {
this.greeting = "Hello"
},
view: function(ctrl) {
return m("p", ctrl.greeting)
}
}
dashboard.view = function(ctrl) {
return [
userProfile.view(ctrl.userProfile)
projectList.view(ctrl.projectList)
]
//now use it in an app
var MyApp = {
controller: function() {},
view: function() {
return m("div", [
m("h1", "My app"),
MyComponent
])
}
}
m.module(document.body, MyApp)
/*
<body>
<h1>My app</h1>
<p>Hello</p>
</body>
*/
```
In the snippet above, there are three modules: `dashboard`, `userProfile` and `projectList`. Each of the sub-components can reasonably be rendered as a standalone page, but here we see how we can put them together to create a bigger page.
Modules can have arguments "preloaded" into them. 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
An important point to note is that if you have fat controllers, you should never instantiate a controller class from a view (or call a function that does it). Views are re-rendered as a result of events firing, and can clobber state from sub-component controllers.
```javascript
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>
```
This way, we can create parameterized modules that look similar to regular virtual elements:
```javascript
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>
*/
```
Since m.module can take any number of arguments, components also support more complex signatures if needed.
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.
---
### Divide and conquer
### Unloading components
Another common reason why people need components is that some pages are inherently large and complex, and need to be sub-divided into smaller pieces in order to help keep code maintainable.
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 these cases, components often need to communicate with one another frequently and in often unexpected ways. Because of the requirement of interconnectedness, the pattern of using fat controllers is not a good fit.
Instead, the best way to organize these types of components is to move code out of controllers into view-models.
A view-model can be thought of a special type of model entities. You are probably familiar with the idea of model entities being ORM classes to map to database tables, but in reality, the model layer is an abstract area where you should be putting everything related to data and the business logic surround it.
View-models are, by definition, entities that hold data about the state of the application. For example, which tab is open, which filters are applied to a grid, the temporary value of a resettable input, etc. This type of data typically doesn't fit in the ORM schema because it relates to the UI, and not the canonical data.
Refactoring a fat controller into using view-models allow better accessibility of data and provides a scalable structure for organizing and scoping non-ORM state.
Here's an example that illustrates how we can migrate from a fat-controller-based codebase to thin controllers. Suppose we have a module that displays a list of users:
In the example below, clicking the button triggers the component's `onunload` event and logs "unloaded!".
```javascript
var userList = {}
userList.controller = function() {
this.users = m.request({method: "GET", url: "/users"})
}
userList.view = function(ctrl) {
return ctrl.users().map(function(user) {
return user.name
})
}
```
Here you can see that the controller holds the state for the list of users. The problem with this is that with a large hierarchy of components, it becomes cumbersome to find this particular controller instance from any given view, and therefore it's difficult to access its data and call methods on it.
```javascript
var userList = {}
userList.controller = function() {
userList.vm.init()
}
userList.vm = {}
userList.vm.init = function() {
this.users = m.request({method: "GET", url: "/users"})
}
userList.view = function() {
return userList.vm.users().map(function(user) {
return user.name
})
}
```
This pattern allows us to access the data for the userList module from anywhere in the application, as long as the view model has been initialized. Notice that refactoring from the fat controller is easy: we simply moved the controller function body into the `init` function of the view model, and changed the reference to the controller in the view.
It's then possible to extend this pattern to create view-model dependency trees:
```javascript
userList.controller = function() {
//here we specify that this component will require with a `search` view-model
userList.vm.init()
search.vm.init()
}
```
With that, we have a guarantee that all data and all methods from the required view-models will be available from anywhere within this component, even if it has multiple sub-views and view-models.
You might have noticed that we're simply sub-dividing a component into smaller pieces and not providing controllers for each of these pieces. You should be aware that these pieces aren't Mithril modules (because they don't contain both a `view` function AND a `controller` function), and therefore they are not components.
Checking whether there is a controller for a unit of functionality gives you a dead simple way to tell whether your sub-divided code is merely an organized part of a bigger component, or whether it is a truly modular and reusable component itself.
If we decide that a unit of functionality is indeed a reusable component, we can simply add a controller to it so that it follows the module interface.
```javascript
//assuming we already have a view in `search`, adding a controller lets us use `search` as an independent component
search.controller = function() {
search.vm.init()
}
userList.controller = function() {
userList.vm.init()
//the controller encapsulates the scope that it is responsible for
new search.controller()
}
```
It's strongly recommended that you consider adopting the pattern of using thin controllers and view models. Moving logic out of fat controllers into the model layer brings your code structure closer to the original MVC pattern (where controllers merely exist to tell the views what actions are possible within a given context), and can dramatically reduce the complexity of cross-communicating modules in the long run.
#### Scoping to namespaces
Sometimes you might find that organizing code into various namespaces results in repetitive declarations of the namespace.
```javascript
//repetitive namespace declarations
myApp.users.index.controller = function() {/*...*/}
myApp.users.index.vm = {/*...*/}
myApp.users.index.view = function() {
return myApp.users.index.vm.something
}
```
There's no rule for how you should organize code, and given that namespacing is often achieved with simple javascript, you can use simple javascript patterns to alias a long namespace and reduce the amount of typing:
```javascript
!function() {
var module = myApp.users.index = {}
module.vm = {/*...*/}
module.controller = function() {/*...*/}
module.view = function() {
var vm = module.vm
return vm.something
}
}()
```
---
### Librarization
Applications often require reusable UI controls that aren't provided out of the box by HTML. Let's walk through how one might implement one. In this example, we'll create a very simple autocompleter control.
We can start building it as an singleton module as we did with our components in the previous section. Here's how an implementation might look like:
```javascript
var autocompleter = {}
autocompleter.vm = {
term: m.prop(""),
filter: function(item) {
return autocompleter.vm.term() && item.name.toLowerCase().indexOf(autocompleter.vm.term().toLowerCase()) > -1
}
}
autocompleter.view = function(ctrl) {
var vm = autocompleter.vm
return [
m("div", [
m("input", {oninput: m.withAttr("value", vm.term), value: vm.term})
]),
ctrl.data().filter(vm.filter).map(function(item) {
return m("div", {onclick: ctrl.binds.bind(this, item)}, item.name);
})
];
}
```
As with our earlier examples, we put logic and UI state in a view model entity, and use it from our view. The `<input>` updates the `term` getter-setter via a binding, and the `filter` function takes care of slicing the data set to display only relevant matches as a user types.
The problem with this component, as it stands, is that the module and its view-model are singletons, so the component can only be used once in a page. Fortunately this is easy to fix: we can simply put the whole thing in a factory function.
```javascript
var autocompleter = function() {
var autocompleter = {}
autocompleter.vm = {
term: m.prop(""),
search: function(value) {
autocompleter.vm.term(value.toLowerCase())
},
filter: function(item) {
return autocompleter.vm.term() && item.name.toLowerCase().indexOf(autocompleter.vm.term()) > -1
}
}
autocompleter.view = function(ctrl) {
var MyApp = {
controller: function() {
this.loaded = true
},
view: function(ctrl) {
return [
m("div", [
m("input", {oninput: m.withAttr("value", autocompleter.vm.search)})
]),
ctrl.data().filter(autocompleter.vm.filter).map(function(item) {
return m("div", {onclick: ctrl.binds.bind(this, item)}, item.name);
})
];
m("button[type=button]", {onclick: function() {ctrl.loaded = false}}),
ctrl.loaded ? m.module(MyComponent) : ""
])
}
return autocompleter
}
var MyComponent = {
controller: function() {
this.onunload = function() {
console.log("unloaded!")
}
},
view: function() {
return m("h1", "My component")
}
}
m.module(document.body, MyApp)
```
As you can see, the code is exactly the same as before, with the exception that it is wrapped in a function that returns the module. This allows us to easily create copies of the autocompleter:
---
```javascript
//here's an example of using the autocompleter
var dashboard = {}
dashboard.controller = function() {
dashboard.vm.init()
}
dashboard.vm = {}
dashboard.vm.init = function() {
this.users = m.prop([{id: 1, name: "John"}, {id: 2, name: "Bob"}, {id: 2, name: "Mary"}]);
this.selectedUser = m.prop()
this.userAC = new autocompleter()
this.projects = m.prop([{id: 1, name: "John's project"}, {id: 2, name: "Bob's project"}, {id: 2, name: "Mary's project"}]);
this.selectedProject = m.prop()
this.projectAC = new autocompleter()
};
### Component limitations
dashboard.view = function() {
var vm = dashboard.vm
return m("div", [
vm.userAC.view({data: vm.users, binds: vm.selectedUser}),
vm.projectAC.view({data: vm.projects, binds: vm.selectedProject}),
]);
};
There are a few caveats to using modules as components:
//initialize
m.module(document.body, dashboard);
```
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).
In the usage example above, we created a `dashboard` top-level module, and instantiated two `autocompleter` modules, along with some data to populate the autocompleter, and getter-setters to bind data to.

View file

@ -416,7 +416,8 @@ VirtualElement m(String selector [, Attributes attributes] [, Children... childr
where:
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<any | void config(DOMElement element, Boolean isInitialized, Object context)>
Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array<Children children>
Children :: String text | VirtualElement virtualElement | Module | SubtreeDirective directive | Array<Children children>
Module :: Object { Function controller, Function view }
SubtreeDirective :: Object { String subtree }
```
@ -573,6 +574,8 @@ where:
If it's a VirtualElement, it will be rendered as a DOM Element.
If it's a Mithril [module](mithril.module.md), the module will be instantiated and managed internally by Mithril as a [component](components.md)
If it's a list, its contents will recursively be rendered as appropriate and appended as children of the element being created.
If it's a SubtreeDirective with the value "retain", it will retain the existing DOM tree in place, if any. See [subtree directives.md](mithril.render.md#subtree-directives) for more information.

View file

@ -2,82 +2,85 @@
---
- [Usage](#usage)
- [Rendering modules](#rendering-modules)
- [Using controllers as factories](#using-controllers-as-factories)
- [Parameterized modules](#parameterized modules)
- [Unloading modules](#unloading-modules)
- [Signature](#signature)
- [Using modules as components](#using-modules-as-components)
- [Unloading components](#unloading-components)
- [Component limitations](#component limitations)
---
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.
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.
```javascript
//a valid module
{controller: function() {}, view: function() {}}
```
When using `m.module`, Mithril instantiates controllers as if they were class constructors. However, controllers may return objects if you want to use that Javascript feature to have more fine-grained control over a controller's lifecycle.
Conceptually, the easiest way to think of a module is as a logical namespace with which to organize applications. For example, an app might have a dashboard module, a userEditForm module, an autocompleter module, a date formatting module, etc
In the context of single page applications (SPA), a module can often be thought of as the code for a single "page", i.e. a visual state that is bookmarkable. Module can, however, also represent *parts* of pages.
Note that a module might have external dependencies and that the dependencies aren't considered part of the module.
In more complex applications, modules can be nested in a [hierarchical MVC](http://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller) pattern. Nested reusable modules that have views are called **Components**.
Modules and namespaces are often used interchangeably, but namespaces that do not implement the module interface (that is, objects that do not have a property called `controller` and a property called `view`) cannot be activated with `m.module`. For example, a namespace for date formatting utilities could be labeled a "module" (in the generic sense of the word) but it would not contain a view class, and therefore attempting to initialize it via `m.module` would result in undefined behavior.
---
## Rendering Modules
### Usage
You can make anonymous modules out of existing classes
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.
```javascript
//model object
var dashboardViewModel = {};
dashboardViewModel.init = function() {
this.greeting = "Hello";
};
var MyModule = {}
MyModule.controller = function() {
this.greeting = "Hello"
}
MyModule.view = function(ctrl) {
return m("h1", ctrl.greeting)
}
//controller class
var dashboardController = function() {
dashboardViewModel.init();
};
m.module(document.body, MyModule)
//view class
var dashboardView = function() {
return m("h1", dashboardViewModel.greeting);
};
//initialize an anonymous module
m.module(document.body, {controller: dashboardController, view: dashboardView});
//<body><h1>Hello</h1></body>
```
Typically, however, modules and namespaces are used interchangeably.
---
### 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).
```javascript
//`dashboard` is both a namespace and a module
var dashboard = {}
var MyModule = {}
MyModule.controller = function() {
return {greeting: "Hello"}
}
MyModule.view = function(ctrl) {
return m("h1", ctrl.greeting)
}
//view-model
dashboard.vm = {}
m.module(document.body, MyModule)
//controller
dashboard.controller = function() {
dashboard.vm.greeting = "Hello";
};
//view
dashboard.view = function(vm) {
return m("h1", dashboard.vm.greeting);
};
//initialize it
m.module(document.body, dashboard);
//<body><h1>Hello</h1></body>
```
Modules can also be used as components in order to assemble bigger systems. You can [read more about componentization here](components.md)
---
### 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.
```javascript
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>
```
---
@ -107,13 +110,17 @@ 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](http://microjs.com/#pubsub) library to unload specific components on demand.
You can also use this event to prevent a module from being unloaded (e.g. to alert a user to save their changes before navigating away from a page)
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)
```javascript
var module1 = {}
module1.controller = function() {
this.unsaved = false
this.onunload = function(e) {
if (!confirm("are you sure you want to leave this page?")) e.preventDefault()
if (this.unsaved) {
e.preventDefault()
}
}
}
```
@ -126,6 +133,17 @@ To unload a module without loading another module, you can simply call `m.module
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`
```javascript
window.onbeforeunload = function() {
if (!m.module(rootElement, null)) {
//onunload's preventDefault was called
return "Are you sure you want to leave?"
}
}
```
---
### Signature
@ -133,7 +151,7 @@ m.module(rootElement, null);
[How to read signatures](how-to-read-signatures.md)
```clike
Object module(DOMElement rootElement, Module module)
Object module(DOMElement rootElement, Module module [, Object options [, any... args]])
where:
Module :: Object { Controller, void view(Object controllerInstance) }
@ -155,10 +173,156 @@ where:
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.
This "[turtles all the way down](https://en.wikipedia.org/wiki/Turtles_all_the_way_down)" approach is the heart of Mithril's component system.
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.
Components are nothing more than decoupled classes that can be dynamically brought together as required. This permits the swapping of implementations at a routing level (for example, if implementing widgetized versions of existing components), and class dependency hierarchies can be structurally organized to provide uniform interfaces (for unit tests, for example).
- **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
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](components.md)
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
```javascript
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:
```javascript
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!".
```javascript
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)
```
---
### Component limitations
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](how-to-read-signatures.md)
```clike
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

4
mithril.d.ts vendored
View file

@ -2,8 +2,8 @@
interface MithrilStatic {
(selector: string, attributes: MithrilAttributes, ...children: Array<string|MithrilVirtualElement>): MithrilVirtualElement;
(selector: string, ...children: Array<string|MithrilVirtualElement>): MithrilVirtualElement;
(selector: string, attributes: MithrilAttributes, ...children: Array<string|MithrilVirtualElement|MithrilModule>): MithrilVirtualElement;
(selector: string, ...children: Array<string|MithrilVirtualElement|MithrilModule>): MithrilVirtualElement;
prop<T>(promise: MithrilPromise<T>) : MithrilPromiseProperty<T>;
prop<T>(value: T): MithrilProperty<T>;