diff --git a/README.md b/README.md index b6e99098..bfe22f41 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.59 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.60 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/tests/test-router.js b/api/tests/test-router.js index d70a7c23..6a281237 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1074,43 +1074,6 @@ o.spec("route", function() { }) }) - o("calling route.set invalidates pending onmatch resolution", function(done) { - var rendered = false - var resolved - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: function() { - return new Promise(function(resolve) { - callAsync(function() { - callAsync(function() { - resolve({view: function() {}}) - }) - }) - }) - }, - render: function(vnode) { - rendered = true - resolved = "a" - } - }, - "/b": { - view: function() { - resolved = "b" - } - } - }) - - route.set("/b") - - callAsync(function() { - o(rendered).equals(false) - o(resolved).equals("b") - - done() - }) - }) - o("calling route.set invalidates pending onmatch resolution", function(done) { var rendered = false var resolved diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 60859a11..3055bf7c 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -214,14 +214,14 @@ o.spec("bundler", function() { write("c.js", `var cc = 2\nmodule.exports = cc`) bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\nvar x = {}\var bb = 1\nnx.b = bb\nvar cc = 1\nx.c = cc\n}`) + o(read("out.js")).equals(`new function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}`) remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) - o("works if assigned to property", function() { + o("works if assigned to property using bracket notation", function() { write("a.js", `var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")`) write("b.js", `var bb = 1\nmodule.exports = bb`) write("c.js", `var cc = 2\nmodule.exports = cc`) diff --git a/docs/api.md b/docs/api.md index cf25070e..872ab605 100644 --- a/docs/api.md +++ b/docs/api.md @@ -45,19 +45,19 @@ m.route(document.body, "/home", { }) ``` -#### m.route.set(path) - [docs](route.md#routeset) +#### m.route.set(path) - [docs](route.md#mrouteset) ```javascript m.route.set("/home") ``` -#### m.route.get() - [docs](route.md#routeget) +#### m.route.get() - [docs](route.md#mrouteget) ```javascript var currentRoute = m.route.get() ``` -#### m.route.prefix(prefix) - [docs](route.md#routeprefix) +#### m.route.prefix(prefix) - [docs](route.md#mrouteprefix) Call this before `m.route()` @@ -65,7 +65,7 @@ Call this before `m.route()` m.route.prefix("#!") ``` -#### m.route.link() - [docs](route.md#routelink) +#### m.route.link() - [docs](route.md#mroutelink) ```javascript m("a[href='/Home']", {oncreate: m.route.link}, "Go to home page") @@ -171,4 +171,3 @@ var Counter = { m.mount(document.body, Counter) ``` - diff --git a/docs/autoredraw.md b/docs/autoredraw.md new file mode 100644 index 00000000..97a4575d --- /dev/null +++ b/docs/autoredraw.md @@ -0,0 +1,122 @@ +# The auto-redraw system + +Mithril implements a virtual DOM diffing system for fast rendering, and in addition, it offers various mechanisms to gain granular control over the rendering of an application. + +When used idiomatically, Mithril employs an auto-redraw system that synchronizes the DOM whenever changes are made in the data layer. The auto-redraw system becomes enabled when you call `m.mount` or `m.route` (but it stays disabled if your app is bootstrapped solely via `m.render` calls). + +The auto-redraw system simply consists of triggering a re-render function behind the scenes after certain functions complete. + +### After event handlers + +Mithril automatically redraws after DOM event handlers that are defined in a Mithril view: + +```javascript +var MyComponent = { + view: function() { + return m("div", {onclick: doSomething}) + } +} + +function doSomething() { + // a redraw happens synchronously after this function runs +} + +m.mount(document.body, MyComponent) +``` + +You can disable an auto-redraw for specific events by setting `e.redraw` to `false`. + +```javascript +var MyComponent = { + view: function() { + return m("div", {onclick: doSomething}) + } +} + +function doSomething(e) { + e.redraw = false + // no longer triggers a redraw when the div is clicked +} + +m.mount(document.body, MyComponent) +``` + + +### After m.request + +Mithril automatically redraws after [`m.request`](request.md) completes: + +```javascript +m.request("/api/v1/users").then(function() { + // a redraw happens after this function runs +}) +``` + +You can disable an auto-redraw for a specific request by setting the `background` option to true: + +```javascript +m.request("/api/v1/users", {background: true}).then(function() { + // does not trigger a redraw +}) +``` + + +### After route changes + +Mithril automatically redraws after [`m.route.set()`](route.md#mrouteset) calls (or route changes via links that use [`m.route.link`](route.md#mroutelink) + +```javascript +var RoutedComponent = { + view: function() { + return [ + // a redraw happens asynchronously after the route changes + m("a", {href: "/", oncreate: m.route.link}), + m("div", { + onclick: function() { + m.route.set("/") + } + }), + ] + } +} + +m.route(document.body, "/", { + "/": RoutedComponent, +}) +``` + +--- + +### When Mithril does not redraws + +Mithril does not redraw after `setTimeout`, `setInterval`, `requestAnimationFrame` and 3rd party library event handlers (e.g. Socket.io callbacks). In those cases, you must manually call [`m.redraw()`](redraw.md). + +Mithril also does not redraw after lifecycle methods. Parts of the UI may be redrawn after an `oninit` handler, but other parts of the UI may already have been redrawn when a given `oninit` handler fires. Handlers like `oncreate` and `onupdate` fire after the UI has been redrawn. + +If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw. + +```javascript +var StableComponent = { + oncreate: function(vnode) { + vnode.state.height = vnode.dom.offsetHeight + m.redraw() + }, + view: function() { + return m("div", "This component is " + vnode.state.height + "px tall") + } +} +``` + +Mithril does not auto-redraw vnode trees that are rendered via `m.render`. This means redraws do not occur after event changes and `m.request` calls for templates that were rendered via `m.render`. Thus, if your architecture requires manual control over when rendering occurs (as can sometimes be the case when using libraries like Redux), you should use `m.render` instead of `m.mount`. + +Remember that `m.render` expects a vnode tree, and `m.mount` expects a component: + +```javascript +// wrap the component in a m() call for m.render +m.render(document.body, m(MyComponent)) + +// don't wrap the component for m.mount +m.mount(document.body, MyComponent) +``` + +Mithril may also avoid auto-redrawing if the frequency of requested redraws is higher than one animation frame (typically around 16ms). This means, for example, that when using fast-firing events like `onresize` or `onscroll`, Mithril will automatically throttle the number of redraws to avoid lag. diff --git a/docs/change-log.md b/docs/change-log.md index b67fb4c7..c97d7d12 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -69,14 +69,14 @@ In `v0.2.x` components could be created using either `m(component)` or `m.compon ```javascript // These are equivalent -m.component(component); -m(component); +m.component(component) +m(component) ``` ### `v1.x` ```javascript -m(component); +m(component) ``` --- @@ -93,7 +93,7 @@ m("div", { // runs on each redraw // isInitialized is a boolean representing if the node has been added to the DOM } -}); +}) ``` ### `v1.x` @@ -114,7 +114,7 @@ m("div", { onbeforeremove : function(vnode) { /*...*/ }, // Called before the node is removed, but after onbeforeremove calls done() onremove : function(vnode) { /*...*/ } -}); +}) ``` If available the DOM-Element of the vnode can be accessed at `vnode.dom`. @@ -138,7 +138,7 @@ In v0.2.x, Mithril allowed 'redraw locks' which temporarily prevented blocked dr ```javascript m("div", { onclick : function(e) { - m.redraw.strategy("none"); + m.redraw.strategy("none") } }) ``` @@ -148,7 +148,7 @@ m("div", { ```javascript m("div", { onclick : function(e) { - e.redraw = false; + e.redraw = false } }) ``` @@ -164,15 +164,15 @@ In `v1.x` there is no more `controller` property in components, use `oninit` ins ```javascript m.mount(document.body, { controller : function() { - var ctrl = this; + var ctrl = this - ctrl.fooga = 1; + ctrl.fooga = 1 }, view : function(ctrl) { - return m("p", ctrl.fooga); + return m("p", ctrl.fooga) } -}); +}) ``` ### `v1.x` @@ -180,29 +180,29 @@ m.mount(document.body, { ```javascript m.mount(document.body, { oninit : function(vnode) { - vnode.state.fooga = 1; + vnode.state.fooga = 1 }, view : function(vnode) { - return m("p", vnode.state.fooga); + return m("p", vnode.state.fooga) } -}); +}) // OR m.mount(document.body, { oninit : function(vnode) { - var state = this; // this is bound to vnode.state by default + var state = this // this is bound to vnode.state by default - state.fooga = 1; + state.fooga = 1 }, view : function(vnode) { - var state = this; // this is bound to vnode.state by default + var state = this // this is bound to vnode.state by default - return m("p", state.fooga); + return m("p", state.fooga) } -}); +}) ``` --- @@ -222,9 +222,9 @@ var component = { view : function(ctrl, options) { // options.fooga == 1 } -}; +} -m("div", m.component(component, { fooga : 1 })); +m("div", m.component(component, { fooga : 1 })) ``` ### `v1.x` @@ -238,9 +238,9 @@ var component = { view : function(vnode) { // vnode.attrs.fooga == 1 } -}; +} -m("div", m(component, { fooga : 1 })); +m("div", m(component, { fooga : 1 })) ``` --- @@ -258,7 +258,7 @@ m.mount(document.body, { view : function(ctrl, options) { // ... } -}); +}) ``` ### `v1.x` @@ -273,7 +273,7 @@ m.mount(document.body, { // Use vnode.state instead of ctrl // Use vnode.attrs instead of options } -}); +}) ``` --- @@ -285,13 +285,13 @@ In `v0.2.x` you could pass components as the second argument of `m()` w/o any wr ### `v0.2.x` ```javascript -m("div", component); +m("div", component) ``` ### `v1.x` ```javascript -m("div", m(component)); +m("div", m(component)) ``` --- @@ -305,8 +305,8 @@ In `v1.x`, components are required instead in both cases. ### `v0.2.x` ```javascript -m.mount(element, m('i', 'hello')); -m.mount(element, m(Component, attrs)); +m.mount(element, m('i', 'hello')) +m.mount(element, m(Component, attrs)) m.route(element, '/', { '/': m('b', 'bye') @@ -316,8 +316,8 @@ m.route(element, '/', { ### `v1.x` ```javascript -m.mount(element, {view: function () {return m('i', 'hello')}}); -m.mount(element, {view: function () {return m(Component, attrs)}}); +m.mount(element, {view: function () {return m('i', 'hello')}}) +m.mount(element, {view: function () {return m(Component, attrs)}}) m.route(element, '/', { '/': {view: function () {return m('b', 'bye')}} @@ -333,15 +333,15 @@ In `v0.2.x` the routing mode could be set by assigning a string of `"pathname"`, ### `v0.2.x` ```javascript -m.route.mode = "pathname"; -m.route.mode = "search"; +m.route.mode = "pathname" +m.route.mode = "search" ``` ### `v1.x` ```javascript -m.route.prefix(""); -m.route.prefix("?"); +m.route.prefix("") +m.route.prefix("?") ``` --- @@ -383,17 +383,17 @@ In `v0.2.x` all interaction w/ the current route happened via `m.route()`. In `v m.route() // Setting a new route -m.route("/other/route"); +m.route("/other/route") ``` ### `v1.x` ```javascript // Getting the current route -m.route.get(); +m.route.get() // Setting a new route -m.route.set("/other/route"); +m.route.set("/other/route") ``` --- @@ -408,10 +408,10 @@ In `v0.2.x` reading route params was all handled through the `m.route.param()` m m.route(document.body, "/booga", { "/:attr" : { view : function() { - m.route.param("attr"); // "booga" + m.route.param("attr") // "booga" } } -}); +}) ``` ### `v1.x` @@ -420,13 +420,13 @@ m.route(document.body, "/booga", { m.route(document.body, "/booga", { "/:attr" : { oninit : function(vnode) { - vnode.attrs.attr; // "booga" + vnode.attrs.attr // "booga" }, view : function(vnode) { - vnode.attrs.attr; // "booga" + vnode.attrs.attr // "booga" } } -}); +}) ``` --- @@ -511,16 +511,16 @@ Additionally, if the `extract` option is passed to `m.request` the return value ```javascript var greetAsync = function() { - var deferred = m.deferred(); + var deferred = m.deferred() setTimeout(function() { - deferred.resolve("hello"); - }, 1000); - return deferred.promise; -}; + deferred.resolve("hello") + }, 1000) + return deferred.promise +} greetAsync() .then(function(value) {return value + " world"}) - .then(function(value) {console.log(value)}); //logs "hello world" after 1 second + .then(function(value) {console.log(value)}) //logs "hello world" after 1 second ``` ### `v1.x` @@ -528,13 +528,13 @@ greetAsync() ```javascript var greetAsync = new Promise(function(resolve){ setTimeout(function() { - resolve("hello"); - }, 1000); -}); + resolve("hello") + }, 1000) +}) greetAsync() .then(function(value) {return value + " world"}) - .then(function(value) {console.log(value)}); //logs "hello world" after 1 second + .then(function(value) {console.log(value)}) //logs "hello world" after 1 second ``` --- @@ -551,7 +551,7 @@ m.sync([ m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), ]) .then(function (users) { - console.log("Contributors:", users[0].name, "and", users[1].name); + console.log("Contributors:", users[0].name, "and", users[1].name) }) ``` @@ -563,7 +563,7 @@ Promise.all([ m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), ]) .then(function (users) { - console.log("Contributors:", users[0].name, "and", users[1].name); + console.log("Contributors:", users[0].name, "and", users[1].name) }) ``` @@ -618,11 +618,11 @@ In v0.2.x it was possible to force mithril to redraw immediately by passing a tr ### `v0.2.x` ```javascript -m.redraw(true); // redraws immediately & synchronously +m.redraw(true) // redraws immediately & synchronously ``` ### `v1.x` ```javascript -m.redraw(); // schedules a redraw on the next requestAnimationFrame tick +m.redraw() // schedules a redraw on the next requestAnimationFrame tick ``` diff --git a/docs/components.md b/docs/components.md index 47f8d1c3..7e454519 100644 --- a/docs/components.md +++ b/docs/components.md @@ -28,13 +28,13 @@ m(Example) ### Passing data to components -Data can be passed to component instances through an `attrs` object as a parameter in the hyperscript function: +Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function: ```javascript m(Example, {name: "Floyd"}) ``` -`attrs` data can be accessed in the component's view or lifecycle methods via the `vnode`: +This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`: ```javascript var Example = { @@ -44,7 +44,7 @@ var Example = { } ``` -NOTE: Lifecycle methods can also be provided via attrs, so you should avoid using the lifecycle method names for your own callbacks as they will be invoked by Mithril. Use lifecycle methods in `attrs` only when you specifically wish to create lifecycle hooks. +NOTE: Lifecycle methods can also be provided via the `attrs` object, so you should avoid using the lifecycle method names for your own callbacks as they would also be invoked by Mithril. Use lifecycle methods in `attrs` only when you specifically wish to create lifecycle hooks. --- @@ -108,7 +108,7 @@ The state of a component can be accessed three ways: as a blueprint at initializ #### At initialization -Any property attached to the component object is deep-cloned for every instance of the component. This allows simple state initialization. +Any property attached to the component object is copied for every instance of the component. This allows simple state initialization. In the example below, `data` is a property of the `ComponentWithInitialState` component's state object. diff --git a/docs/es6.md b/docs/es6.md new file mode 100644 index 00000000..2f629223 --- /dev/null +++ b/docs/es6.md @@ -0,0 +1,133 @@ +# ES6 + +- [Setup](#setup) +- [Using Babel with Webpack](#using-babel-with-webpack) + +--- + +Mithril is written in ES5, and is fully compatible with ES6 as well. + +In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like [Babel](https://babeljs.io) is required to compile ES6 features down to ES5. + +### Setup + +The simplest way to setup an ES6 compilation toolchain is via [Babel](https://babeljs.io/). + +Babel requires NPM, which is automatically installed when you install [Node.js](https://nodejs.org/en/). Once NPM is installed, create a project folder and run this command: + +```bash +npm init -y +``` + +If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack). + +To install Babel as a standalone tool, use this command: + +```bash +npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +```json +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +To run Babel as a standalone tool, run this from the command line: + +```bash +babel src --out-dir bin --source-maps +``` + +#### Using Babel with Webpack + +If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps. + +```bash +npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +```json +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +Next, create a file called `webpack.config.js` + +```javascript +module.exports = { + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` + +This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`. + +To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`: + +```json +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch" + } +} +``` + +You can now then run the bundler by running this from the command line: + +```bash +npm start +``` + +#### Production build + +To generate a minified file, open `package.json` and add a new npm script called `build`: + +```json +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p" + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +```json +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } +} +``` diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md index 029d0319..95aa2137 100644 --- a/docs/framework-comparison.md +++ b/docs/framework-comparison.md @@ -1,12 +1,30 @@ # Framework comparison +- [Why not X?](#why-not-insert-favorite-framework-here) +- [Why use Mithril?](#why-use-mithril) - [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. +--- + +## Why not [insert favorite framework here]? + +The reality is that most modern frameworks are fast, well-suited to build complex applications, and highly maintainable if you know how to use them effectively. There are examples of highly complex applications in the wild using just about every popular framework: Udemy uses Angular, AirBnB uses React, Gitlab uses Vue, Guild Wars 2 uses Mithril (yes, inside the game!). Clearly, these are all production-quality frameworks. + +As a rule of thumb, if your team is already heavily invested in another framework/library/stack, it makes more sense to stick with it, unless your team agrees that there's a very strong reason to justify a costly rewrite. + +If you're starting something new, do consider giving Mithril a try, if nothing else, to see how much value Mithril adopters have been getting out of 8kb (gzipped) of code. + +--- + +## Why use Mithril? + +In one sentence: because **Mithril is pragmatic**. If you don't believe me, take 10 minutes to go over the [guide](introduction.md) to see how much it accomplishes, compared with official guides for other frameworks. + +Mithril is all about getting stuff done. It comes out of the box with a compact set of tools that you'll likely need for building Single Page Applications, and no distractions. The Mithril API is small and focused, and it's designed to leverage previous knowledge - e.g. view language is Javascript, HTML attributes have no syntax caveats, Promises are Promises, hyperscript selectors mirror CSS and is JSX-compatible, `m.request` option names mirror [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) - all of this so you can get up to speed fast. --- @@ -14,15 +32,15 @@ In this page, you will find common arguments about other frameworks and comments React is a view library maintained by Facebook. -React and Mithril share a lot of similarities +React and Mithril share a lot of similarities. If you already learned React, you already know almost all you need to build apps with Mithril. - 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. +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 React-based architectures can vary wildly from project to project. -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. +Mithril has built-in modules for common necessities such as routing and XHR, and the [guide](simple-application.md) demonstrates idiomatic usage. This approach is preferable for teams that value consistency and ease of onboarding. ### Performance @@ -80,6 +98,8 @@ React documentation is clear and well written, and includes a good API reference 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. +Unfortunately, since React is limited to being only a view library, its documentation does not explore how to use React in the context of a real-life application. As a result, there are many popular state management libraries and as a result, architectures using React can differ drastically from company to company (or even between projects). + --- ## Angular @@ -121,7 +141,7 @@ Angular 2 has a lot more concepts to understand: on the language level, Typescri 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. +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. Mithril tends to be a lot more transparent, and therefore easier to reason about. ### Documentation @@ -140,9 +160,9 @@ 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 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 system 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. +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, but benchmarks often suggest Mithril is slightly faster. ### Performance @@ -164,14 +184,16 @@ Vue | Mithril ### 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. +Vue is heavily inspired by Angular and has many things that Angular does (e.g. directives, filters, `v-cloak`), but also has things inspired by React (e.g. components). 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). 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 can cause architectural fragmentation in long-lived projects. -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. +Mithril has far less concepts and typically organizes applications in terms of components and a data layer. There are no different ways of defining components, and thus there's no need to install different sets of tools to make different flavors work. ### 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 +Both Vue and Mithril have good documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts. + +However, due to Vue's many-ways-to-do-one-thing approach, some things are not adequately documented. For example, there's no documentation on hyperscript syntax or usage. + +Mithril documentation typically errs on the side of being overly thorough if a topic involves things outside of the scope of Mithril. For example, when a topic involves a 3rd party library, Mithril documentation walks through the installation process for the 3rd party library. + +Mithril's tutorials also cover a lot more ground than Vue's: the [Vue tutorial](https://vuejs.org/v2/guide/#Getting-Started) finishes with a static list of foodstuff. [Mithril's 10 minute guide](introduction.md) covers the majority of its API and goes over key aspects of real-life applications, such as fetching data from a server and routing (and there's a [lomger, more thorough tutorial](simple-application.md) if that's not enough). \ No newline at end of file diff --git a/docs/generate.js b/docs/generate.js index f8763522..a9d0f759 100644 --- a/docs/generate.js +++ b/docs/generate.js @@ -37,8 +37,11 @@ function generate(pathname) { var modified = guides.match(link) ? guides.replace(link, replace) : methods.replace(link, replace) return title + modified + "\n\n" }) - .replace(/\.md/gim, ".html") // fix links + .replace(/(\]\([^\)]+)(\.md)/gim, function(match, path, extension) { + return path + (path.match(/http/) ? extension : ".html") + }) // fix links var html = layout + .replace(/\[version\]/, version) // update version .replace(/\[body\]/, marked(fixed)) .replace(/
([^<]+?)<\/h5>/gim, function(match, id, text) { // fix anchors return "
" + text + "
" diff --git a/docs/guides.md b/docs/guides.md index d2bd87da..424d7f72 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -2,6 +2,8 @@ - [Installation](installation.md) - [Introduction](introduction.md) - [Tutorial](simple-application.md) + - [JSX](jsx.md) + - [ES6](es6.md) - [Testing](testing.md) - [Examples](examples.md) - Key concepts @@ -9,8 +11,8 @@ - [Components](components.md) - [Lifecycle methods](lifecycle-methods.md) - [Keys](keys.md) + - [Autoredraw system](autoredraw.md) - Social - - [Community chat](https://gitter.im/lhorie/mithril.js) - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) - [How to contribute](contributing.md) - [Credits](credits.md) diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 6d27a4e6..6108f2d5 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -14,6 +14,7 @@ - [Keys](#keys) - [SVG and MathML](#svg-and-mathml) - [Making templates dynamic](#making-templates-dynamic) +- [Converting HTML](#converting-html) - [Avoid anti-patterns](#avoid-anti-patterns) --- @@ -345,6 +346,14 @@ You cannot use Javascript statements such as `if` or `for` within Javascript exp --- +### Converting HTML + +In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX. + +When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can [use the HTML-to-Mithril-template converter](http://arthurclemens.github.io/mithril-template-converter/index.html). + +--- + ### Avoid Anti-patterns Although Mithril is flexible, some code patterns are discouraged: diff --git a/docs/installation.md b/docs/installation.md index 2d20dd11..ddc85853 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -20,28 +20,25 @@ If you're new to Javascript or just want a very simple setup to get your feet we ```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": "webpack index.js app.js --watch" +# "start": "webpack src/index.js bin/app.js --watch" # } -# 3) create an `index.js` file +# 3) create an `src/index.js` file -# 4) create an `index.html` file loading `app.js` +# 4) create an `index.html` file containing `` # 5) run bundler -npm run build +npm start # 6) open `index.html` in the (default) browser open index.html ``` -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. - #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. @@ -55,13 +52,13 @@ npm init --yes # creates a file called package.json ``` -Then, run +Then, to install Mithril, 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 +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: @@ -78,10 +75,10 @@ CommonJS is a de-facto standard for modularizing Javascript code, and it's used 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 [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: +A popular way for creating 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 +npm install webpack --save-dev ``` Open the `package.json` that you created earlier, and add an entry to the `scripts` section: @@ -90,22 +87,24 @@ Open the `package.json` that you created earlier, and add an entry to the `scrip { "name": "my-project", "scripts": { - "build": "webpack index.js app.js --watch" + "start": "webpack src/index.js bin/app.js -d --watch" } } ``` -Remember this is a JSON file, so object key names such as `"scripts"` and `"build"` must be inside of double quotes. +Remember this is a JSON file, so object key names such as `"scripts"` and `"start"` 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 `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 `-d` flag tells webpack to use development mode, which produces source maps for a better debugging experience. 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: +Now you can run the script via `npm start` 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 start +``` + +Now that you have created a bundle, you can then reference the `bin/app.js` file from an HTML file: ```markup @@ -113,7 +112,7 @@ Now that you have created a bundle, you can then reference the `app.js` file fro Hello world - + ``` @@ -142,6 +141,33 @@ 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. +#### Production build + +If you open bin/app.js, you'll notice that the Webpack bundle is not minified, so this file is not ideal for a live application. To generate a minified file, open `package.json` and add a new npm script: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack src/index.js bin/app.js -d --watch", + "build": "webpack src/index.js bin/app.js -p", + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } +} +``` + --- ### Alternate ways to use Mithril @@ -166,9 +192,11 @@ npm install budo -g 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. +Mithril comes with a bundler tool of its own. It is sufficient for ES5-based 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: diff --git a/docs/introduction.md b/docs/introduction.md index 29c59a4e..671541e5 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -13,7 +13,7 @@ ### What is Mithril? Mithril is a client-side Javascript framework for building Single Page Applications. -It's small (< 8kb gzip), fast and batteries-included. +It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box. If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. @@ -23,9 +23,9 @@ Note: This introduction assumes you have basic level of Javacript knowledge. If ### Getting started -The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. +The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes. -Let's create an HTML file to follow along: +Let's create an HTML file to follow along: ```markup @@ -91,7 +91,7 @@ m("main", [ ]) ``` -Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](https://babeljs.io/docs/plugins/transform-react-jsx/). +Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](jsx.md). ```markup // HTML syntax via Babel's JSX plugin @@ -236,8 +236,3 @@ Clicking the button should now update the count. We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR. This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](simple-application.md), which walks you through building a realistic application. - - - - - diff --git a/docs/jsx.md b/docs/jsx.md new file mode 100644 index 00000000..b439c867 --- /dev/null +++ b/docs/jsx.md @@ -0,0 +1,248 @@ +# JSX + +- [Description](#description) +- [Setup](#setup) +- [Using Babel with Webpack](#using-babel-with-webpack) +- [JSX vs hyperscript](#jsx-vs-hyperscript) +- [Converting HTML](#converting-html) + +--- + +### Description + +JSX is a syntax extension that enables you to write HTML tags interspersed with Javascript. + +```jsx +var MyComponent = { + view: function() { + return m("main", [ + m("h1", "Hello world"), + ]) + } +} + +// can be written as: +var MyComponent = { + view: function() { + return ( +
+

Hello world

+
+ ) + } +} +``` + +When using JSX, it's possible to interpolate Javascript expressions within JSX tags by using curly braces: + +```jsx +var greeting = "Hello" +var url = "http://google.com" +var link = {greeting + "!"} +// yields Hello +``` + +Components can be used by using a convention of uppercasing the first letter of the component name: + +```jsx +m.mount(document.body, ) +// equivalent to m.mount(document.body, m(MyComponent)) +``` + +--- + +### Setup + +The simplest way to use JSX is via a [Babel](https://babeljs.io/) plugin. + +Babel requires NPM, which is automatically installed when you install [Node.js](https://nodejs.org/en/). Once NPM is installed, create a project folder and run this command: + +```bash +npm init -y +``` + +If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack). + +To install Babel as a standalone tool, use this command: + +```bash +npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +To run Babel as a standalone tool, run this from the command line: + +```bash +babel src --out-dir bin --source-maps +``` + +#### Using Babel with Webpack + +If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps. + +```bash +npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +Next, create a file called `webpack.config.js` + +```javascript +module.exports = { + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` + +This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`. + +To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch" + } +} +``` + +You can now then run the bundler by running this from the command line: + +```bash +npm start +``` + +#### Production build + +To generate a minified file, open `package.json` and add a new npm script called `build`: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } +} +``` + +--- + +### JSX vs hyperscript + +JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and can make syntax issues difficult to spot. + +Unlike HTML, JSX is case-sensitive. This means `
` is different from `
` (all lower case). The former compiles to `m("div", {className: "test"})` and the latter compiles to `m("div", {classname: "test"})`, which is not a valid way of creating a class attribute. Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: `
`. + +JSX is useful for teams where HTML is primarily written by someone without Javascript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser) + +Hyperscript is the compiled representation of JSX. It's designed to be readable and can also be used as-is, instead of JSX (as is done in most of the documentation). Hyperscript tends to be terser than JSX for a couple of reasons: + +1 - it does not require repeating the tag name in closing tags (e.g. `m("div")` vs `
`) +2 - static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs `
` + +In addition, since hyperscript is plain Javascript, it's often more natural to indent than JSX: + +```jsx +//JSX +var BigComponent = { + activate: function() {/*...*/}, + deactivate: function() {/*...*/}, + update: function() {/*...*/}, + view: function(vnode) { + return [ + {vnode.attrs.items.map(function(item) { + return
{item.name}
+ })} +
+ ] + } +} + +// hyperscript +var BigComponent = { + activate: function() {/*...*/}, + deactivate: function() {/*...*/}, + update: function() {/*...*/}, + view: function(vnode) { + return [ + vnode.attrs.items.map(function(item) { + return m("div", item.name) + }), + m("div", { + ondragover: this.activate, + ondragleave: this.deactivate, + ondragend: this.deactivate, + ondrop: this.update, + onblur: this.deactivate, + }) + ] + } +} +``` + +In non-trivial applications, it's possible for components to have more control flow and component configuration code than markup, making a Javascript-first approach more readable than an HTML-first approach. + +Needless to say, since hyperscript is pure Javascript, there's no need to run a compilation step to produce runnable code. + +--- + +### Converting HTML + +In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX. + +When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can [use the HTML-to-Mithril-template converter](http://arthurclemens.github.io/mithril-template-converter/index.html). diff --git a/docs/layout.html b/docs/layout.html index 6e96bb1b..b6e1c504 100644 --- a/docs/layout.html +++ b/docs/layout.html @@ -9,10 +9,11 @@
-

Mithril

+

Mithril [version]

diff --git a/docs/lint.js b/docs/lint.js index 576dd181..65f6c1a5 100644 --- a/docs/lint.js +++ b/docs/lint.js @@ -2,6 +2,8 @@ var fs = require("fs") var path = require("path") +var http = require("http") +var url = require("url") //lint rules function lint(file, data) { @@ -9,6 +11,7 @@ function lint(file, data) { ensureCodeIsSyntaticallyValid(file, data) ensureCodeIsRunnable(file, data) ensureCommentStyle(file, data) + ensureLinkIsValid(file, data) } function ensureCodeIsHighlightable(file, data) { @@ -45,11 +48,29 @@ function ensureCodeIsRunnable(file, data) { try { initMocks() - new Function("console,fetch,module,require", code).call(this, silentConsole, fetch, {exports: {}}, function(dep) { + var module = {exports: {}} + new Function("console,fetch,module,require", code).call(this, silentConsole, fetch, module, function(dep) { if (dep.indexOf("./mycomponent") === 0) return {view: function() {}} if (dep.indexOf("mithril/ospec/ospec") === 0) return global.o if (dep.indexOf("mithril/stream") === 0) return global.stream if (dep === "mithril") return global.m + + if (dep === "../model/User") return { + list: [], + current: {}, + loadList: function() { + return Promise.resolve({data: []}) + }, + load: function() { + return Promise.resolve({firstName: "", lastName: ""}) + }, + save: function() { + return Promise.resolve() + }, + } + if (dep === "./view/UserList") return {view: function() {}} + if (dep === "./view/UserForm") return {view: function() {}} + if (dep === "./view/Layout") return {view: function() {}} }) } catch (e) {console.log(file + " - javascript code cannot run\n\n" + e.stack + "\n\n" + code + "\n\n---\n\n")} @@ -64,6 +85,21 @@ function ensureCommentStyle(file, data) { }) } +function ensureLinkIsValid(file, data) { + var links = data.match(/\]\(([^\)]+)\)/gim) + links.forEach(function(match) { + var link = match.slice(2, -1) + var path = link.match(/[\w-]+\.md/) + if (link.match(/http/)) { + var u = url.parse(link) + http.request({method: "HEAD", host: u.host, path: u.pathname, port: 80}).on("error", function() { + console.log(file + " - broken external link: " + link) + }) + } + else if (path && !fs.existsSync("docs/" + path)) console.log(file + " - broken link: " + link) + }) +} + function initMocks() { global.window = require("../test-utils/browserMock")() global.document = window.document @@ -104,6 +140,9 @@ function initMocks() { "GET /api/v1/users/foo:bar": function(request) { return {status: 200, responseText: JSON.stringify({id: 123})} }, + "GET /files/image.svg": function(request) { + return {status: 200, responseText: ""} + }, }) } diff --git a/docs/redraw.md b/docs/redraw.md index ebc17283..be215e18 100644 --- a/docs/redraw.md +++ b/docs/redraw.md @@ -14,6 +14,8 @@ You DON'T need to call it if data is modified within the execution context of an You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries. +Typically, `m.redraw` triggers an asynchronous redraws, but it may trigger synchronously if Mithril detects it's possible to improves performance by doing so (i.e. if no redraw was requested within the last animation frame). You should write code assuming that it always redraws asynchronously. + --- ### Signature @@ -32,4 +34,4 @@ When callbacks outside of Mithril run, you need to notify Mithril's rendering en To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw. -You should not call m.redraw from a [lifecycle method](lifecycle-methods.md). Doing so will result in undefined behavior. \ No newline at end of file +You should not call m.redraw from a [lifecycle method](lifecycle-methods.md). Doing so will result in undefined behavior. diff --git a/docs/render.md b/docs/render.md index 3218c761..1a0f81d9 100644 --- a/docs/render.md +++ b/docs/render.md @@ -54,7 +54,7 @@ In contrast, traversing a javascript data structure has a much more predictable ### Differences from other API methods -`m.render()` method is internally called by [`m.mount()`](mount.md), [`m.route()`](route.md), [`m.redraw()`](redraw.md) and [`m.request()`](request.md). It is not called by [`m.prop()`](prop.md) +`m.render()` method is internally called by [`m.mount()`](mount.md), [`m.route()`](route.md), [`m.redraw()`](redraw.md) and [`m.request()`](request.md). It is not called after [stream updates](stream.md) Unlike with `m.mount()` and `m.route()`, a vnode tree rendered via `m.render()` does not auto-redraw in response to view events, `m.redraw()` calls or `m.request()` calls. It is a low level mechanism suitable for library authors who wish to manually control rendering instead of relying on Mithril's built-in auto-redrawing system. diff --git a/docs/route.md b/docs/route.md index c443d418..d8566215 100644 --- a/docs/route.md +++ b/docs/route.md @@ -3,10 +3,10 @@ - [Description](#description) - [Signature](#signature) - [Static members](#static-members) - - [route.set](#routeset) - - [route.get](#routeget) - - [route.prefix](#routeprefix) - - [route.link](#routelink) + - [m.route.set](#mrouteset) + - [m.route.get](#mrouteget) + - [m.route.prefix](#mrouteprefix) + - [m.route.link](#mroutelink) - [RouteResolver](#routeresolver) - [routeResolver.onmatch](#routeresolveronmatch) - [routeResolver.render](#routeresolverrender) @@ -62,7 +62,7 @@ Argument | Type | Required | D #### Static members -##### route.set +##### m.route.set Redirects to a matching route, or to the default route if no matching routes can be found. @@ -77,7 +77,7 @@ Argument | Type | Required | Description `options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call. **returns** | | | Returns `undefined` -##### route.get +##### m.route.get Returns the last fully resolved routing path, without the prefix. It may differ from the path displayed in the location bar while an asynchronous route is [pending resolution](#code-splitting). @@ -87,18 +87,18 @@ Argument | Type | Required | Description ----------------- | --------- | -------- | --- **returns** | String | | Returns the last fully resolved path -##### route.prefix +##### m.route.prefix -Defines a router prefix. The router prefix is a fragment of the URL that dictates the underlying [strategy](routing-strategies.md) used by the router. +Defines a router prefix. The router prefix is a fragment of the URL that dictates the underlying [strategy](#routing-strategies) used by the router. `m.route.prefix(prefix)` Argument | Type | Required | Description ----------------- | --------- | -------- | --- -`prefix` | `String` | Yes | The prefix that controls the underlying [routing strategy](#routing-strategy) used by Mithril. +`prefix` | `String` | Yes | The prefix that controls the underlying [routing strategy](#routing-strategies) used by Mithril. **returns** | | | Returns `undefined` -##### route.link +##### m.route.link `eventHandler = m.route.link(vnode)` @@ -109,7 +109,7 @@ Argument | Type | Required | Description #### RouteResolver -A RouterResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. +A RouterResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. A RouteResolver is not a component, and therefore it does NOT have lifecycle methods. As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules. `routeResolver = {onmatch, render}` @@ -140,7 +140,7 @@ The `render` method is called on every redraw for a matching route. It is simila `vnode = routeResolve.render(vnode)` Argument | Type | Description -------------------- | --------------- | ----------- +------------------- | --------------- | ----------- `vnode` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If onmatch does not return a component or a promise that resolves to a component, the vnode's `tag` field defaults to `"div"` `vnode.attrs` | `Object` | A map of URL parameter values **returns** | `Vnode` | Returns a vnode @@ -304,7 +304,7 @@ var state = { // save the state for this route // this is equivalent to `history.replaceState({term: state.term}, null, location.href)` m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}}) - + // navigate away location.href = "https://google.com/?q=" + state.term } @@ -333,7 +333,7 @@ This way, if the user searches and presses the back button to return to the appl ### Changing router prefix -The router prefix is a fragment of the URL that dictates the underlying [strategy](routing-strategies.md) used by the router. +The router prefix is a fragment of the URL that dictates the underlying [strategy](#routing-strategies) used by the router. ```javascript // set to pathname strategy @@ -545,9 +545,9 @@ m.route(document.body, "/user/list", { "/user/list": { onmatch: state.loadUsers, render: function() { - return state.users.length > 0 ? state.users.map(function(user) { + return state.users.map(function(user) { return m("div", user.id) - }) : "loading" + }) } }, }) diff --git a/docs/simple-application.md b/docs/simple-application.md new file mode 100644 index 00000000..f23ff82b --- /dev/null +++ b/docs/simple-application.md @@ -0,0 +1,612 @@ +# Simple application + +Let's develop a simple application that covers some of the major aspects of Single Page Applications + +First let's create an entry point for the application. Create a file `index.html`: + +```markup + + + + + + My Application + + + + + +``` + +The `` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the Javascript file that controls the application. + +We could create the entire application in a single Javascript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `app.js`. + +There are many ways to setup a bundler tool, but most are distributed via NPM. In fact, most modern Javascript libraries and tools are distributed that way, including Mithril. NPM stands for Node.js Package Manager. To download NPM, [install Node.js](https://nodejs.org/en/); NPM is installed automatically with it. Once you have Node.js and NPM installed, open the command line and run this command: + +```bash +npm init -y +``` + +If NPM is installed correctly, a file `package.json` will be created. This file will contain a skeleton project meta-description file. Feel free to edit the project and author information in this file. + +--- + +To install Mithril, follow the instructions in the [installation](installation.md) page. Once you have a project skeleton with Mithril installed, we are ready to create the application. + +Let's start by creating a module to store our state. Let's create a file called `src/models/User.js` + +```javascript +// src/models/User.js +var User = { + list: [] +} + +module.exports = User +``` + +Now let's add code to load some data from a server. To communicate with a server, we can use Mithril's XHR utility, `m.request`. First, we include Mithril in the module: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [] +} + +module.exports = User +``` + +Next we create a function that will trigger an XHR call. Let's call it `loadList` + +```javascript +// models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + // TODO: make XHR call + } +} + +module.exports = User +``` + +Then we can add an `m.request` call to make an XHR request. For this tutorial, we'll make XHR calls to the [REM](http://rem-rest-api.herokuapp.com/) API, a mock REST API designed for rapid prototyping. This API returns a list of users from the `GET http://rem-rest-api.herokuapp.com/api/users` endpoint. Let's use `m.request` to make an XHR request and populate our data with the response of that endpoint. + +```javascript +// models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, +} + +module.exports = User +``` + +The `method` option is an [HTTP method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). To retrieve data from the server without causing side-effects on the server, we need to use the `GET` method. The `url` is the address for the API endpoint. The `withCredentials: true` line indicates that we're using cookies (which is a requirement for the REM API). + +The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril assumes a HTTP response body are in JSON format and automatically parses it into a Javascript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `reesult.data` array to `User.list`. + +Notice we also have a `return` statement in `loadList`. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request. + +This simple model exposes two members: `User.list` (an array of user objects), and `User.loadList` (a method that populates `User.list` with server data). + +--- + +Now, let's create a view module so that we can display data from our User model module. + +Create a file called `src/views/UserList.js`. First, let's include Mithril and our model, since we'll need to use both: + +```javascript +var m = require("mithril") +var User = require("../model/User") +``` + +Next, let's create a Mithril component. A component is simply an object that has a `view` method: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + // TODO add code here + } +} +``` + +By default, Mithril views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and in addition, since its syntax is simply Javascript, it's possible to leverage a lot of Javascript tooling ecosystem: for example [Babel](es6.md), [JSX](jsx.md) (inline-HTML syntax extension), [eslint](http://eslint.org/) (linting), [uglifyjs](https://github.com/mishoo/UglifyJS2) (minification), [istanbul](https://github.com/gotwarlost/istanbul) (code coverage), [flow](https://flowtype.org/) (static type analysis), etc. + +Let's use Mithril hyperscript to create a list of items. Hyperscript is the most idiomatic way of writing Mithril views, but [JSX is another popular alternative that you could explore](jsx.md) once you're more comfortable with the basics: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + return m(".user-list") + } +} +``` + +The `".user-list"` string is a CSS selector, and as you would expect, `.user-list` represents a class. When a tag is not specified, `div` is the default. So this view is equivalent to `
`. + +Now, let's reference the list of users from the model we created earlier (`User.list`) to dynamically loop through data: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m(".user-list-item", user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Since `User.list` is a Javascript array, and since hyperscript views are just Javascript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user. + +The problem, of course, is that we never called the `User.loadList` function. Therefore, `User.list` is still an empty array, and thus this view would render a blank page. Since we want `User.loadList` to be called when we render this component, we can take advantage of component [lifecycle methods](lifecycle-methods.md): + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + oninit: User.loadList, + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m(".user-list-item", user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Notice that we added an `oninit` method to the component, which references `User.loadList`. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, `User.list` gets populated. + +Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common newbie mistake: calling the function immediately means that the XHR request will fire even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected. + +--- + +Let's render the view from the entry point file `index.js` we created earlier: + +```javascript +var m = require("mithril") + +var UserList = require("./view/UserList") + +m.mount(document.body, UserList) +``` + +The `m.mount` call renders the specified component (`UserList`) into a DOM element (`document.body`), erasing any DOM that were there previously. Opening the HTML file in a browser should now display a list of person names. + +--- + +Right now, the list looks rather plain because we have not defined any styles. + +There are many similar conventions and libraries that help organize application styles nowadays. Some, like [Bootstrap](http://getbootstrap.com/) dictate a specific set of HTML structures and semantically meaningful class names, which has the upside of providing low cognitive dissonance, but the downside of making customization more difficult. Others, like [Tachyons](http://tachyons.io/) provide a large number of self-describing, atomic class names at the cost of making the class names themselves non-semantic. "CSS-in-JS" is another type of CSS system that is growing in popularity, which basically consists of scoping CSS via transpilation tooling. CSS-in-JS libraries achieve maintainability by reducing the size of the problem space, but come at the cost of having high complexity. + +Regardless of what CSS convention/library you choose, a good rule of thumb is to avoid the cascading aspect of CSS. To keep this tutorial simple, we'll just use plain CSS with overly explicit class names, so that the styles themselves provide the atomicity of Tachyons, and class name collisions are made unlikely through the verbosity of the class names. Plain CSS can be sufficient for low-complexity projects (e.g. 3 to 6 man-months of initial implementation time and few project phases) + +To add styles, let's first create a file called `styles.css` and include it in the `index.html` file + +```markup + + + + + + My Application + + + + + + +``` + +Now we can style the `UserList` component: + +```css +.user-list {list-style:none;margin:0 0 10px;padding:0;} +.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;} +.user-list-item:hover {text-decoration:underline;} +``` + +The CSS above is written using a convention of keeping all styles for a rule in a single line, in alphabetical order. This convention is designed to take maximum advantage of screen real estate, and makes it easier to scan the CSS selectors (since they are always on the left side) and their logical grouping, and it enforces predictable and uniform placement of CSS rules for each selector. + +Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviate from the more widespread cosmetic-oriented spacing conventions. + +Reloading the browser window now should display some styled elements. + +--- + +Let's add routing to our application. + +Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via Javascript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML). + +We can add routing by changing the `m.mount` call to a `m.route` call: + +```javascript +var m = require("mithril") + +var UserList = require("./view/UserList") + +m.route(document.body, "/list", { + "/list": UserList +}) +``` + +The `m.route` call specifies that the application will be rendered into `document.body`. The `"/list"` argument is the default route. That means the user will be redirected to that route if they land in a route that does not exist. The `{"/list": UserList}` object declares a map of existing routes, and what components each route resolves to. + +Refreshing the page in the browser should now append `#!/list` to the URL to indicate that routing is working. Since that route render UserList, we should still see the list of people on screen as before. + +The `#!` snippet is known as a hashbang, and it's a commonly used string for implementing client-side routing. It's possible to configure this string it via [`m.route.prefix`](route.md#mrouteprefix). Some configurations require supporting server-side changes, so we'll just continue using the hashbang for the rest of this tutorial. + +--- + +Let's add another route to our application for editing users. First let's create a module called `views/UserForm.js` + +```javascript +// src/views/UserForm.js + +module.exports = { + view: function() { + // TODO implement view + } +} +``` + +Then we can `require` this new module from `index.js` + +```javascript +// index.js +var m = require("mithril") + +var UserList = require("./view/UserList") +var UserForm = require("./view/UserForm") + +m.route(document.body, "/list", { + "/list": UserList +}) +``` + +And finally, we can create a route that references it: + +```javascript +// index.js +var m = require("mithril") + +var UserList = require("./view/UserList") +var UserForm = require("./view/UserForm") + +m.route(document.body, "/list", { + "/list": UserList, + "/edit/:id": UserForm, +}) +``` + +Notice that the new route has a `:id` in it. This is a route parameter; you can think of it as a wild card; the route `/edit/1` would resolve to `UserForm` with an `id` of `"1"`. `/edit/2` would also resolve to `UserForm`, but with an `id` of `"2"`. And so on. + +Let's implement the `UserForm` component so that it can respond to those route parameters: + +```javascript +// src/views/UserForm.js +var m = require("mithril") + +module.exports = { + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]"), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]"), + m("button.button[type=submit]", "Save"), + ]) + } +} +``` + +And let's add some styles to `styles.css`: + +```css +/* styles.css */ +body,.input,.button {font:normal 16px Verdana;margin:0;} + +.user-list {list-style:none;margin:0 0 10px;padding:0;} +.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;} +.user-list-item:hover {text-decoration:underline;} + +.label {display:block;margin:0 0 5px;} +.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;} +.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;} +.button:hover {background:#e8e8e8;} +``` + +Right now, this component does nothing to respond to user events. Let's add some code to our `User` model in `src/model/User.js`. This is how the code is right now: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, +} + +module.exports = User +``` + +Let's add code to allow us to load a single user + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, + + current: {}, + load: function(id) { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: {id: id}, + withCredentials: true, + }) + .then(function(result) { + User.current = result + }) + } +} + +module.exports = User +``` + +Notice we added a `User.current` property, and a `User.load(id)` method which populates that property. We can now populate the `UserForm` view using this new method: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("./model/User") + +module.exports = { + oninit: function(vnode) {User.load(vnode.attrs.id)}, + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]", {value: User.current.firstName}), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]", {value: User.current.lastName}), + m("button.button[type=submit]", "Save"), + ]) + } +} +``` + +Similar to the `UserList` component, `oninit` calls `User.load()`. Remember we had a route parameter called `:id` on the `"/edit/:id": UserForm` route? The route parameter becomes an attribute of the `UserForm` component's vnode, so routing to `/edit/1` would make `vnode.attrs.id` have a value of `"1"`. + +Now, let's modify the `UserList` view so that we can navigate from there to a `UserForm`: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + oninit: User.loadList, + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m("a.user-list-item", {href: "/edit/" + user.id, oncreate: m.route.link}, user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Here we changed `.user-list-item` to `a.user-list-item`. We added an `href` that references the route we want, and finally we added `oncreate: m.route.link`. This makes the link behave like a routed link (as opposed to merely behaving like a regular link). What this means is that clicking the link would change the part of URL that comes after the hashbang `#!` (thus changing the route without unloading the current HTML page) + +If you refresh the page in the browser, you should now be able to click on a person and be taken to a form. You should also be able to press the back button in the browser to go back from the form to the list of people. + +--- + +The form itself still doesn't save when you press "Save". Let's make this form work: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("./model/User") + +module.exports = { + oninit: function(vnode) {User.load(vnode.attrs.id)}, + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]", { + oninput: m.withAttr("value", function(value) {User.current.firstName = value}), + value: User.current.firstName + }), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]", { + oninput: m.withAttr("value", function(value) {User.current.lastName = value}), + value: User.current.lastName + }), + m("button.button[type=submit]", {onclick: User.save}, "Save"), + ]) + } +} +``` + +We added `oninput` events to both inputs, that set the `User.current.firstName` and `User.current.lastName` properties when a user types. + +In addition, we declared that a `User.save` method should be called when the "Save" button is pressed. Let's implement that method: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, + + current: {}, + load: function(id) { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: {id: id}, + withCredentials: true, + }) + .then(function(result) { + User.current = result + }) + }, + + save: function() { + return m.request({ + method: "PUT", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: User.current, + withCredentials: true, + }) + } +} + +module.exports = User +``` + +In the `save` method at the bottom, we used the `PUT` HTTP method to indicate that we are upserting data to the server. + +Now try editing the name of a user in the application. Once you save a change, you should be able to see the change reflected in the list of users. + +--- + +Currently, we're only able to navigate back to the user list via the browser back button. Ideally, we would like to have a menu - or more generically, a layout where we can put global UI elements + +Let's create a file `src/views/Layout.js`: + +```javascript +var Layout = { + view: function(vnode) { + return m("main.layout", [ + m("nav.menu", [ + m("a[href='/list']", {oncreate: m.route.link}, "Users") + ]), + m("section", vnode.children) + ]) + } +} +``` + +This component is fairly straightforward, it has a `