diff --git a/.npmignore b/.npmignore index 48873c87..bf023d92 100644 --- a/.npmignore +++ b/.npmignore @@ -1,20 +1,8 @@ -# Configuration files +# Development-specific files .deploy.env .editorconfig .eslintrc.js .gitattributes .gitignore .travis.yml - -# Tests -test-utils/ -tests/ - -# Documentation -docs/ -examples/ CONTRIBUTING.md - -# Browser stub (use index.js w/ a bundler or mithril.js w/o one instead) -module/ -browser.js diff --git a/README.md b/README.md index 4f1d161b..b6e99098 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.48 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.59 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/api/router.js b/api/router.js index 1e1650d5..cf406737 100644 --- a/api/router.js +++ b/api/router.js @@ -8,37 +8,37 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render, component, attrs, currentPath, updatePending = false + var render, component, attrs, currentPath, lastUpdate var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") - var update = function(routeResolver, comp, params, path) { - component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, updatePending = false - render = (routeResolver.render || identity).bind(routeResolver) - run() - } var run = function() { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } var bail = function() { - routeService.setPath(defaultRoute) + routeService.setPath(defaultRoute, null, {replace: true}) } routeService.defineRoutes(routes, function(payload, params, path) { - if (payload.view) update({}, payload, params, path) + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, lastUpdate = null + render = (routeResolver.render || identity).bind(routeResolver) + run() + } + if (payload.view) update({}, payload) else { if (payload.onmatch) { - updatePending = true Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (updatePending) update(payload, resolved, params, path) + update(payload, resolved) }, bail) } - else update(payload, "div", params, path) + else update(payload, "div") } }, bail) redrawService.subscribe(root, run) } route.set = function(path, data, options) { - if (updatePending) options = {replace: true} - updatePending = false + if (lastUpdate != null) options = {replace: true} + lastUpdate = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 7ea00195..d70a7c23 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -51,7 +51,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("routed mount points can redraw synchronoulsy (#1275)", function() { + o("routed mount points can redraw synchronously (#1275)", function() { var view = o.spy() $window.location.href = prefix + "/" @@ -66,7 +66,9 @@ o.spec("route", function() { }) o("default route doesn't break back button", function(done) { - $window.location.href = "http://google.com" + $window.location.href = "http://old.com" + $window.location.href = "http://new.com" + route(root, "/a", { "/a" : { view: function() { @@ -78,9 +80,12 @@ o.spec("route", function() { callAsync(function() { o(root.firstChild.nodeName).equals("DIV") + o(route.get()).equals("/a") + $window.history.back() o($window.location.pathname).equals("/") + o($window.location.hostname).equals("old.com") done() }) @@ -574,7 +579,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(2) - + done() }) }) @@ -609,7 +614,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(2) - + done() }) }) @@ -617,7 +622,7 @@ o.spec("route", function() { o("onmatch can redirect to another route", function(done) { var redirected = false var render = o.spy() - + $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { @@ -932,6 +937,63 @@ o.spec("route", function() { }) }) + o("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { + var renderA = o.spy() + var renderB = o.spy() + var onmatchA = o.spy(function(){ + return new Promise(function(fulfill) { + setTimeout(function(){ + fulfill() + }, 10) + }) + }) + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a": { + onmatch: onmatchA, + render: renderA + }, + "/b": { + onmatch: function(){ + var p = new Promise(function(fulfill) { + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + + setTimeout(function(){ + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + + fulfill() + + p.then(function(){ + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(1) + + done() + }) + }, 20) + }) + return p + }, + render: renderB + } + }) + + callAsync(function() { + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + route.set("/b") + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + }) + }) + o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ var onmatch = o.spy() var render = o.spy(function() {return m("div")}) @@ -1133,19 +1195,19 @@ o.spec("route", function() { } }, }) - + callAsync(function() { // tick for popstate for /a callAsync(function() { // tick for promise in onmatch callAsync(function() { // tick for onpopstate for /b o(rendered).equals(false) o(resolved).equals("b") - + done() }) }) }) }) - + o("throttles", function(done, timeout) { timeout(200) diff --git a/docs/change-log.md b/docs/change-log.md index 0475c996..b67fb4c7 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -110,8 +110,8 @@ m("div", { onbeforeupdate : function(vnode, old) { /*...*/ }, // Called after the node is updated onupdate : function(vnode) { /*...*/ }, - // Called before the node is removed, call done() when ready for the node to be removed from the DOM - onbeforeremove : function(vnode, done) { /*...*/ }, + // Called before the node is removed, return a Promise that resolves when ready for the node to be removed from the DOM + onbeforeremove : function(vnode) { /*...*/ }, // Called before the node is removed, but after onbeforeremove calls done() onremove : function(vnode) { /*...*/ } }); diff --git a/docs/components.md b/docs/components.md index ee74bd5c..47f8d1c3 100644 --- a/docs/components.md +++ b/docs/components.md @@ -63,9 +63,12 @@ var ComponentWithHooks = { onupdate: function(vnode) { console.log("DOM updated") }, - onbeforeremove: function(vnode, done) { + onbeforeremove: function(vnode) { console.log("exit animation can start") - done() + return new Promise(function(resolve) { + // call after animation completes + resolve() + }) }, onremove: function(vnode) { console.log("removing DOM element") diff --git a/docs/contributing.md b/docs/contributing.md index e2e65fd6..5b4770d3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,7 +4,7 @@ ## How do I go about contributing ideas or new features? -Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. And don't worry, we're nice :) +Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities. diff --git a/docs/fragment.md b/docs/fragment.md index cd1ff53a..018dc4ab 100644 --- a/docs/fragment.md +++ b/docs/fragment.md @@ -10,6 +10,23 @@ Allows attaching lifecycle methods to a fragment [vnode](vnodes.md) +```javascript +var groupVisible = true +var log = function() { + console.log("group is now visible") +} + +m("ul", [ + m("li", "child 1"), + m("li", "child 2"), + groupVisible ? m.fragment({oninit: log}, [ + // a fragment containing two elements + m("li", "child 3"), + m("li", "child 4"), + ]) : null +]) +``` + --- ### Signature diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md new file mode 100644 index 00000000..029d0319 --- /dev/null +++ b/docs/framework-comparison.md @@ -0,0 +1,177 @@ +# Framework comparison + +- [React](#react) +- [Angular](#angular) +- [Vue](#vue) + +If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril would help you solve your problems more effectively. + +In this page, you will find common arguments about other frameworks and comments on where Mithril is similar or why it differs from them. + +--- + +## React + +React is a view library maintained by Facebook. + +React and Mithril share a lot of similarities + +- They both use virtual DOM, lifecycle methods and key-based reconciliation +- They both organize views via components +- They both use Javascript as a flow control mechanism within views + +The most obvious difference between React and Mithril is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that too much choice can lead to analysis paralysis, excessive configuration/boilerplate complexity, and [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding). At worst, it can lead to a hodge-podge of different dependencies and architectures, making it difficult for new team members to transfer knowledge from one project to the next. + +Mithril has built-in modules for common necessities such as routing and XHR. This batteries-included approach is preferable for teams that value consistency and ease of onboarding. + +### Performance + +Both React and Mithril care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using `innerHTML`). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's `shouldComponentUpdate` hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions. + +Mithril follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run. + +Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +55.8 ms | 4.5 ms + +Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. + +Since this is a micro-benchmark, you are encourage to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate. + +For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril's initialization speed is noticeably better than React. + +Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for [React](https://jsfiddle.net/bfoeay4f/) and [Mithril](https://jsfiddle.net/fft0ht7n/). Their best results are shown below: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +99.7 ms | 42.8 ms + +What these numbers show is that not only does Mithril initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use. + +##### Update performance + +Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running. + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Sample results are shown below: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +12.1 ms | 6.4 ms + +##### Development performance + +Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, [here's the 10,000 node benchmark from above using the development version of React](https://jsfiddle.net/r1jfckrd/). + +### Complexity + +Both React and Mithril have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading). + +### Learning curve + +Both React and Mithril have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril components is nearly identical. There are obviously more APIs to learn in Mithril, since Mithril also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios. + +Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel. + +### Documentation + +React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts. + +Mithril documentation also includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. + +--- + +## Angular + +Angular is a web application framework maintained by Google. + +Angular and Mithril are fairly different, but they share a few similarities: + +- Both support componentization +- Both have an array of tools for various aspects of web applications (e.g. routing, XHR) + +The most obvious difference between Angular and Mithril is in their complexity. This can be seen most easily in how views are implemented. Mithril views are plain Javascript, and flow control is done with Javascript built-in mechanisms such as ternary operators or `Array.prototype.map`. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate Javascript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in Javascript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates Javascript functions dynamically for performance, and [a slower mode](https://docs.angularjs.org/api/ng/directive/ngCsp) for dealing with Content Security Policy restrictions). + +### Performance + +Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large `$scope` structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril is often faster than Angular, due to the ease of auditing that Mithril's small codebase size affords. + +It's difficult to make a comparison of load times between Angular and Mithril for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application. + +With that being said, the smallest known Angular 2 bundle is a [29kb hello world](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/) compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril hello world - including the entire Mithril core - would not be over 8kb gzipped (a more optimized bundle could easily be half of that). + +Also, remember that frameworks like Angular and Mithril are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb. + +##### Update performance + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: + +Angular | Mithril +------- | ------- +11.5 ms | 6.4 ms + +### Complexity + +Angular is superior to Mithril in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare [Angular's API surface](https://angular.io/docs/ts/latest/api/) with [Mithril's](api.md). You can make your own judgment on which API is more self-descriptive and more relevant to your needs. + +Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what. + +### Learning curve + +If we compare apples to apples, Angular 2 and Mithril have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools. + +With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what Javascript does natively in Mithril - Angular's `ng-if`/`ngIf` is a *directive*, which uses a custom *parser* and *compiler* to evaluate an expression string and emulate lexical scoping... and so on. + +### Documentation + +Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for. + +Mithril documentation includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. + +--- + +## Vue + +Vue is a view library similar to Angular. + +Vue and Mithril have a lot of differences but they also share some similarities: + +- They both use virtual DOM and lifecycle methods +- Both organize views via components + +Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive API that overwrites native methods in a component's data (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril or React views, rather than Angular's compiled rendering functions. + +Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril is around 4kb gzipped). Both have similar performance characteristics. + +### Performance + +Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: + +Vue | Mithril +------- | ------- +21.8 ms | 4.5 ms + +Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. + +##### Update performance + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: + +Vue | Mithril +------ | ------- +9.8 ms | 6.4 ms + +### Complexity + +One could argue that Vue templates are more complex than Mithril due to the fact that they use Vue-specific syntax for logic flow, whereas Mithril view language is always just Javascript. + +As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins) so Vue codebases may be less consistent across projects and have higher onboarding costs in terms of technologies compared to idiomatic Mithril projects. + +Vue provides both bi-directional data binding and an optional Redux-like state management library, and unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach has a risk of causing architectural fragmentation in long-lived projects. + +The Mithril [tutorial](simple-application.md) implements an *idiomatic* application. This means that while it's *possible* to structure a Mithril codebase in different ways, there's a recommended way to architecture the application. + +### Documentation + +Both Vue and Mithril have thorough documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts. \ No newline at end of file diff --git a/docs/guides.md b/docs/guides.md index 4abbf045..d2bd87da 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -1,7 +1,7 @@ - Tutorials - [Installation](installation.md) - [Introduction](introduction.md) - - [Tutorial](tutorial.md) + - [Tutorial](simple-application.md) - [Testing](testing.md) - [Examples](examples.md) - Key concepts @@ -11,7 +11,9 @@ - [Keys](keys.md) - Social - [Community chat](https://gitter.im/lhorie/mithril.js) - - [Contributing](contributing.md) + - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) + - [How to contribute](contributing.md) - [Credits](credits.md) - Misc + - [Framework comparison](framework-comparison.md) - [Change log/Migration](change-log.md) diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 9d608dcd..6d27a4e6 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -249,9 +249,9 @@ Hook | Description `oninit(vnode)` | Runs before a vnode is rendered into a real DOM element `oncreate(vnode)` | Runs after a vnode is appended to the DOM `onupdate(vnode)` | Runs every time a redraw occurs while the DOM element is attached to the document -`onbeforeremove(vnode, done)` | Runs before a DOM element is removed from the document, and only triggers the actual removal of the DOM element when the `done` callback is called. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements. +`onbeforeremove(vnode)` | Runs before a DOM element is removed from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements. `onremove(vnode)` | Runs before a DOM element is removed from the document. If a `onbeforeremove` hook is defined, `onremove` is called after `done` is called. This method is triggered on the element that is detached from its parent element, and all of its children -`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children +`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md). diff --git a/docs/installation.md b/docs/installation.md index 4009bd28..6993c5d5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,5 +1,8 @@ # Installation +- [CDN](#cdn) +- [NPM](#npm) + ### CDN If you're new to Javascript or just want a very simple setup to get your feet wet, you can get Mithril from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network): @@ -12,21 +15,27 @@ If you're new to Javascript or just want a very simple setup to get your feet we ### NPM -#### Quick start +#### Quick start with Webpack ```bash # 1) install npm install mithril@rewrite --save +npm install webpack --save # 2) add this line into the scripts section in package.json # "scripts": { -# "build": "bundle index.js --output app.js --watch" +# "build": "webpack index.js app.js --watch" # } # 3) create an `index.js` file -# 4) run bundler +# 4) create an `index.html` file loading `app.js` + +# 5) run bundler npm run build + +# 6) open `index.html` in the (default) browser +open index.html ``` #### Step by step @@ -42,12 +51,14 @@ npm init --yes # creates a file called package.json ``` -Then, run `npm install mithril@rewrite --save` to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file +Then, run ```bash npm install mithril@rewrite --save ``` +to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file + You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules: ```javascript @@ -59,33 +70,36 @@ m.render(document.body, "hello world") Modularization is the practice of separating the code into files. Doing so makes it easier to find code, understand what code relies on what code, and test. -CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like Browserify and Webpack. It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler). +CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like [Browserify](http://browserify.org/) and [Webpack](https://webpack.js.org/). It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler). Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application. -The easiest way to create a bundle is to setup an NPM script for Mithril's bundler. To do so, open the `package.json` that you created earlier, and add an entry to the `scripts` section: +The easiest way to create a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: + +```bash +npm install webpack --save +``` + +Open the `package.json` that you created earlier, and add an entry to the `scripts` section: ``` { "name": "my-project", "scripts": { - "build": "bundle index.js --output app.js --watch" - }, - "dependencies": { - "mithril": "^1.0.0-rc.5" + "build": "webpack index.js app.js --watch" } } ``` Remember this is a JSON file, so object key names such as `"scripts"` and `"build"` must be inside of double quotes. -Now you can run the script via `npm run build` in your command line window. This looks up the `bundle` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. +Now you can run the script via `npm run build` in your command line window. This looks up the `webpack` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers. ``` npm run build ``` -The `--watch` flag tells the `bundle` command to watch the file system and automatically recreate `app.js` if file changes are detected. +The `--watch` flag tells webpack to watch the file system and automatically recreate `app.js` if file changes are detected. Now that you have created a bundle, you can then reference the `app.js` file from an HTML file: @@ -124,28 +138,48 @@ m.mount(document.body, MyComponent) Note that in this example, we're using `m.mount`, which wires up the component to Mithril's autoredraw system. In most applications, you will want to use `m.mount` (or `m.route` if your application has multiple screens) instead of `m.render` to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs. -#### Alternate ways to use Mithril +--- -##### Webpack +### Alternate ways to use Mithril -Webpack is a popular tool for bundling modular code. The biggest advantage of Webpack is that it has a relatively large ecosystem of plugins. The downside of that is that it can be difficult to configure correctly. +#### Live reload development environment -To use webpack, you must first install it by running `npm install webpack --save-dev`. Then you need to create a `webpack.config.js` file. Here's a basic configuration file that is equivalent to the Mithril bundler command in the first section of this page: +Live reload is a feature where code changes automatically trigger the page to reload. [Budo](https://github.com/mattdesl/budo) is one tool that enables live reloading. -```javascript -module.exports = { - entry: "./index.js", - output: {filename: "app.js"}, +```bash +# 1) install +npm install mithril@rewrite --save +npm install budo -g + +# 2) add this line into the scripts section in package.json +# "scripts": { +# "start": "budo --live --open index.js" +# } + +# 3) create an `index.js` file + +# 4) run budo +npm start +``` + +The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes. + +#### Mithril bundler + +Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. + +If you want to try it and give feedback, you can open `package.json` and change the npm script for webpack to this: + +``` +{ + "name": "my-project", + "scripts": { + "build": "bundle index.js --output app.js --watch" + } } ``` -To run webpack, use the command `webpack --watch`. - -``` -webpack --watch -``` - -##### Vanilla +#### Vanilla If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all: @@ -155,7 +189,7 @@ If you don't have the ability to run a bundler script due to company security po