From 4184da27c8855eed08ddda2ef8e43774968224e2 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 26 Sep 2014 21:42:36 -0400 Subject: [PATCH] update docs about components --- docs/components.md | 209 +++++++++++++++++++++++++----------- docs/getting-started.md | 232 +++++++++++++++++++--------------------- docs/mithril.md | 6 +- docs/mithril.module.md | 61 ++--------- docs/mithril.redraw.md | 2 +- docs/mithril.route.md | 2 +- 6 files changed, 270 insertions(+), 242 deletions(-) diff --git a/docs/components.md b/docs/components.md index c186d75e..5c4df252 100644 --- a/docs/components.md +++ b/docs/components.md @@ -1,8 +1,10 @@ ## Components +### Widgetization + Components are Mithril's mechanism for [hierarchical MVC](http://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller). -[Mithril modules](mithril.module.md) can usually be used as components, in scenarios where components don't need to cross-communicate a lot (for example, a dashboard full of unrelated widgets). +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: @@ -25,17 +27,25 @@ dashboard.view = function(ctrl) { 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. -An important point to note is that 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. +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. --- -### Organizing components +### Divide and conquer 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. -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 importing independent modules to build bigger modules is not a good fit. +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. Here's an example: Suppose we have a module that displays a list of users. +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: ```javascript var userList = {} @@ -51,9 +61,7 @@ userList.view = function(ctrl) { } ``` -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 difficult to find this particular controller instance from any given view. - -The solution is to refactor the code into a globally available entity: a view-model +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 = {} @@ -79,88 +87,159 @@ This pattern allows us to access the data for the userList module from anywhere It's then possible to extend this pattern to create view-model dependency trees: ```javascript -userList.vm.init = function() { - //here we specify that this view will interact with a `search`, a `filters` and a `grid` modules +userList.controller = function() { + //here we specify that this component will require with a `search` view-model + userList.vm.init() search.vm.init() - filters.vm.init() - grid.vm.init() } ``` -With that, we have a guarantee that all data and all methods from the specified view-models will be available from anywhere within this group of modules. +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. -We can also optionally backfill controllers for each module so that they follow the same pattern as the `userList.controller`, if we need to use those modules as top-level modules in independent pages. +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. + +``` +//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 +new function() { + var module = myApp.users.index = {} + + module.vm = {/*...*/} + + module.controller = function() {/*...*/} + + module.view = function() { + var vm = module.vm + + return vm.something + } +} +``` --- ### Librarization -Applications often reuse rich UI controls that aren't provided out of the box by HTML. Below is a basic example of a component of that type: a minimalist autocompleter component. +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. -*Note: Be mindful that, for the sake of code clarity and brevity, the example below does not support keyboard navigation and other real world features.* +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.controller = function(data, getter) { - //binding for the text input - this.value = m.prop(""); - //store for the list of items - this.data = m.prop([]); - - //method to determine what property of a list item to compare the text input's value to - this.getter = getter; - - //this method changes the relevance list depending on what's currently in the text input - this.change = function(value) { - this.value(value); - - var list = value === "" ? [] : data().filter(function(item) { - return this.getter(item).toLowerCase().indexOf(value.toLowerCase()) > -1; - }, this); - this.data(list); - }; - - //this method is called when an option is selected. It triggers an `onchange` event - this.select = function(value) { - this.value(value); - this.data([]); - if (this.onchange) this.onchange({currentTarget: {value: value}}); - }; +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, options) { - if (options) ctrl.onchange = options.onchange; - return [ - m("input", {oninput: m.withAttr("value", ctrl.change.bind(ctrl)), value: ctrl.value()}), - ctrl.data().map(function(item) { - return m("div", {data: ctrl.getter(item), onclick: m.withAttr("data", ctrl.select.bind(ctrl))}, ctrl.getter(item)); - }) - ]; +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 `` 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) { + 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); + }) + ]; + } + return autocompleter +} +``` + +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: + +``` //here's an example of using the autocompleter var dashboard = {} - dashboard.controller = function() { - this.names = m.prop([{id: 1, name: "John"}, {id: 2, name: "Bob"}, {id: 2, name: "Mary"}]); - this.autocompleter = new autocompleter.controller(this.names, function(item) { - return item.name; - }); + 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() }; -dashboard.view = function(ctrl) { - return m("#example", [ - new autocompleter.view(ctrl.autocompleter, {onchange: m.withAttr("value", log)}), - ]); +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}), + ]); }; -//an FP-friendly console.log -var log = function(value) {console.log(value)} - - //initialize m.module(document.body, dashboard); ``` + +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. + diff --git a/docs/getting-started.md b/docs/getting-started.md index c8af6a1b..9816a024 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -33,9 +33,9 @@ Yes, this is valid HTML 5! According to the specs, the ``, `` and `< ### Model -In Mithril, an application typically lives in a namespace and contains modules. Modules are merely structures that represent a viewable "page" or component. +In Mithril, an application typically lives in a namespace and contains modules. Modules are merely structures that represent a viewable "page" or a part of a page. In addition, an application can be organizationally divided into three major layers: Model, Controller and View. -For simplicity, our application will have only one module, and we're going to use it as the namespace for our application: +For simplicity, our application will have only one module, and we're going to use it as the namespace for our application. ```markup