From 94804356fe1ce1feec268e8c0a47447e7a1ffcb4 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Thu, 24 Nov 2016 20:46:17 -0500 Subject: [PATCH 01/60] bundler: throw error correctly when there's syntax error --- bundler/bundle.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bundler/bundle.js b/bundler/bundle.js index 84574a6d..dff20bfe 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -15,6 +15,7 @@ function parse(file) { try {return JSON.parse(json)} catch (e) {throw new Error("invalid JSON: " + json)} } +var error function run(input, output) { try { var modules = {} @@ -56,8 +57,11 @@ function run(input, output) { var code = read(filepath) // if there's a syntax error, report w/ proper stack trace try {new Function(code)} catch (e) { - proc.exec("node " + filename, function(error) { - if (error !== null) console.log("\x1b[31m" + error.message) + proc.exec("node " + filepath, function(e) { + if (e !== null && e.message !== error) { + error = e.message + console.log("\x1b[31m" + e.message + "\x1b[0m") + } }) } @@ -111,7 +115,11 @@ function run(input, output) { code = "new function() {\n" + code + "\n}" - if (!isFile(output) || code !== read(output)) fs.writeFileSync(output, code, "utf8") + if (!isFile(output) || code !== read(output)) { + try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} + error = null + fs.writeFileSync(output, code, "utf8") + } } catch (e) { console.error(e.message) From f7c187eec98f3e58a9c6959490d0cd8c58d932cf Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Thu, 24 Nov 2016 20:46:49 -0500 Subject: [PATCH 02/60] improve installation docs --- docs/installation.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 51c0848e..07e69cae 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,32 +1,52 @@ # Installation +### 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): + +``` + +``` + +--- + ### NPM #### Quick start ``` -#install +# 1) install npm install mithril@rewrite --save -# add this line into the scripts section in package.json +# 2) add this line into the scripts section in package.json # "scripts": { # "build": "bundle index.js --output app.js --watch" # } -# create an `index.js` file +# 3) create an `index.js` file -# run bundler +# 4) run bundler npm run build ``` #### Step by step +For production-level projects, the recommended way of installing Mithril is to use NPM. + NPM (Node package manager) is the default package manager that is bundled w/ Node.js. It is widely used as the package manager for both client-side and server-side libraries in the Javascript ecosystem. Download and install [Node.js](https://nodejs.org); NPM will be automatically installed as well. -To use Mithril via NPM: +To use Mithril via NPM, go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`. -- go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`. -- run `npm install mithril@rewrite --save`. 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 +``` +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 + +``` +npm install mithril@rewrite --save +``` You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules: From a835e6ac6afabd58415c728eb5078e3ae90c81ca Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 12:17:00 +0000 Subject: [PATCH 03/60] Rephrase 'restrictive interfaces' & 'magic indices' to reflect common concerns --- docs/components.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/components.md b/docs/components.md index bf61a6ee..b7588379 100644 --- a/docs/components.md +++ b/docs/components.md @@ -169,11 +169,11 @@ Be aware that when using ES5 functions, the value of `this` in nested anonymous ### Avoid anti-patterns -Although Mithril is flexible, some code patterns are discouraged: +Try to keep component interfaces generic by default - use custom interfaces when implementation isn't generic. -#### Avoid restrictive interfaces +#### Avoiding restrictive interfaces -A component has a restrictive interface when it exposes only specific properties, under the assumption that other properties will not be needed, or that they can be added at a later time. +A component has a restrictive interface when it is unnecessarily specific in its use of attributes. Custom attributes should be used when a component has a very specific purpose or requires special internal logic, but this often isn't the case - attributes and children should be used where possible. In the example below, the `button` configuration is severely limited: it does not support any events other than `onclick`, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML). @@ -188,7 +188,7 @@ var RestrictiveComponent = { } ``` -It's preferable to allow passing through parameters to a component's root node, if it makes sense to do so: +If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node, if it makes sense to do so. ```javascript // PREFER @@ -201,7 +201,9 @@ var FlexibleComponent = { } ``` -#### Avoid magic indexes +#### Don't manipulate `children` + +However, if a component is opinionated in how it applies attributes or children, you should switch to using custom attributes. Often it's desirable to define multiple sets of children, for example, if a component has a configurable title and body. @@ -233,7 +235,7 @@ m(Header, [ ]) ``` -The component above makes different children look different based on where they appear in the array. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content: +The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content: ```javascript // PREFER From 2f5fe86cafde5a4a47b834874fd1d333e81bfc48 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 12:49:38 +0000 Subject: [PATCH 04/60] Revert & fine-tune wording --- docs/components.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/components.md b/docs/components.md index b7588379..1c6aa8c1 100644 --- a/docs/components.md +++ b/docs/components.md @@ -169,11 +169,11 @@ Be aware that when using ES5 functions, the value of `this` in nested anonymous ### Avoid anti-patterns -Try to keep component interfaces generic by default - use custom interfaces when implementation isn't generic. +Although Mithril is flexible, some code patterns are discouraged: #### Avoiding restrictive interfaces -A component has a restrictive interface when it is unnecessarily specific in its use of attributes. Custom attributes should be used when a component has a very specific purpose or requires special internal logic, but this often isn't the case - attributes and children should be used where possible. +Try to keep component interfaces generic - using `attrs` and `children` directly - unless the component requires special logic to operate on input. In the example below, the `button` configuration is severely limited: it does not support any events other than `onclick`, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML). @@ -188,7 +188,7 @@ var RestrictiveComponent = { } ``` -If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node, if it makes sense to do so. +If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node. ```javascript // PREFER From 1dd341fff5851ec3605189bdac006fa02e4fde69 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 12:50:45 +0000 Subject: [PATCH 05/60] Typo --- docs/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components.md b/docs/components.md index 1c6aa8c1..49360017 100644 --- a/docs/components.md +++ b/docs/components.md @@ -171,7 +171,7 @@ Be aware that when using ES5 functions, the value of `this` in nested anonymous Although Mithril is flexible, some code patterns are discouraged: -#### Avoiding restrictive interfaces +#### Avoid restrictive interfaces Try to keep component interfaces generic - using `attrs` and `children` directly - unless the component requires special logic to operate on input. From b399af1073282f7f89b2dc990aeed4dec7e705eb Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 15:04:03 +0000 Subject: [PATCH 06/60] Add vnode mutation warning under components documentation --- docs/components.md | 66 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/components.md b/docs/components.md index bf61a6ee..524575f3 100644 --- a/docs/components.md +++ b/docs/components.md @@ -261,7 +261,9 @@ m(BetterHeader, { }) ``` -#### Avoid component factories +#### Define components statically, call them dynamically + +##### Avoid creating component definitions inside views If you create a component from within a `view` method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch. @@ -291,3 +293,65 @@ m.render(document.body, m(Component, {greeting: "hello"})) // calling a second time does not modify DOM m.render(document.body, m(Component, {greeting: "hello"})) ``` + +##### Avoid creating component instances outside views + +Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views: + +``` +// AVOID +var Counter = { + count: 0, + view: function(vnode) { + return m("div", + m("p", "Count: " + vnode.state.count ), + + m("button", { + onclick: function() { + vnode.state.count++ + } + }, "Increase count") + ) + } +} + +var counter = m(Counter) + +m.mount(document.body, { + view: function(vnode) { + return [ + m("h1", "My app"), + counter + ] + } +}) +``` + +In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created: + +``` +// Prefer +var Counter = { + count: 0, + view: function(vnode) { + return m("div", + m("p", "Count: " + vnode.state.count ), + + m("button", { + onclick: function() { + vnode.state.count++ + } + }, "Increase count") + ) + } +} + +m.mount(document.body, { + view: function(vnode) { + return [ + m("h1", "My app"), + m(Counter) + ] + } +}) +``` From 2e43bf4f7b81551d00256edc5ccbfd3fa1567894 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 15:16:55 +0000 Subject: [PATCH 07/60] Add vnode caching warning under hyperscript documentation --- docs/hyperscript.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/hyperscript.md b/docs/hyperscript.md index af1067e9..e28781d1 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -408,3 +408,9 @@ var BetterListComponent = { } } ``` + +#### Avoid creating vnodes outside views + +When a redraw encounters a vnode which is strictly equal to the one in the previous render, it will be skipped and its contents will not be updated. While this may seem like an opportunity for performance optimisation, it should be avoided because it prevents dynamic changes in that node's tree - this leads to side-effects such as downstream lifecycle methods failing to trigger on redraw. In this sense, Mithril vnodes are immutable: new vnodes are compared to old ones; mutations to vnodes are not persisted. + +The component documentation contains [more detail and an example of this anti-pattern](components.md#avoid-creating-component-instances-outside-views). From f040bfe47023a4fbdc98ea23e8cd8540aeba529a Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Sun, 27 Nov 2016 15:24:34 +0000 Subject: [PATCH 08/60] Typos --- docs/components.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/components.md b/docs/components.md index 524575f3..116cd38e 100644 --- a/docs/components.md +++ b/docs/components.md @@ -298,7 +298,7 @@ m.render(document.body, m(Component, {greeting: "hello"})) Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views: -``` +```javascript // AVOID var Counter = { count: 0, @@ -329,8 +329,8 @@ m.mount(document.body, { In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created: -``` -// Prefer +```javascript +// PREFER var Counter = { count: 0, view: function(vnode) { From c4072b82225d52567eca733226c7180ee7e906ed Mon Sep 17 00:00:00 2001 From: Eugene Marcotte Date: Sun, 27 Nov 2016 16:28:07 -0500 Subject: [PATCH 09/60] Fix tiny spelling error (#1430) --- docs/vnodes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/vnodes.md b/docs/vnodes.md index 97b8d364..9fb23cde 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -16,7 +16,7 @@ The first time a virtual DOM tree is rendered, it is used as a blueprint to crea Typically, Virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes. -It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundres of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes. +It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes. For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well. From e76bb9678773a4cd77b64fdfbc9e62b7fe0b1629 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 28 Nov 2016 14:43:29 -0500 Subject: [PATCH 10/60] Add link to response detail section (#1434) --- docs/request.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/request.md b/docs/request.md index 173233c1..cbda6b9c 100644 --- a/docs/request.md +++ b/docs/request.md @@ -10,6 +10,7 @@ - [Monitoring progress](#monitoring-progress) - [Casting response to a type](#casting-response-to-a-type) - [Non-JSON responses](#non-json-responses) +- [Retrieving response details](#retrieving-response-details) - [Why JSON instead of HTML](#why-json-instead-of-html) - [Why XMLHttpRequest instead of fetch](#why-xmlhttprequest-instead-of-fetch) From 216b03c4d71c4e32195042c60fb1a9658f164231 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Tue, 29 Nov 2016 01:17:54 +0000 Subject: [PATCH 11/60] Document array semantics and vnode equality logic in migration guide --- docs/v1.x-migration.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index f23111b1..9fbd3e5a 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -17,6 +17,8 @@ - [Accessing route params](#accessing-route-params) - [`m.request`](#mrequest) - [`xlink` namespace required](#xlink-namespace-required) +- [Nested arrays in views](#nested-arrays-in-views) +- [Vnode immutability](#nested-arrays-in-views) --- @@ -243,7 +245,7 @@ m.mount(document.body, { oninit : function(vnode) { // ... }, - + view : function(vnode) { // Use vnode.state instead of ctrl // Use vnode.attrs instead of options @@ -492,3 +494,15 @@ m("svg", m("image[xlink:href='image.gif']") ) ``` + +*** + +## Nested arrays in views + +Arrays now represent [fragments](fragments.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays. + +*** + +## Vnode immutability + +If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views). From 32e7518b6f8b177e096c2791a4ac558fd85b3f11 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Tue, 29 Nov 2016 01:21:32 +0000 Subject: [PATCH 12/60] Typo / better heading --- docs/v1.x-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index 9fbd3e5a..8a24e2d1 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -18,7 +18,7 @@ - [`m.request`](#mrequest) - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) -- [Vnode immutability](#nested-arrays-in-views) +- [Vnode equality checks](#vnode-equality-checks) --- @@ -503,6 +503,6 @@ Arrays now represent [fragments](fragments.md), which are structurally significa *** -## Vnode immutability +## Vnode equality checks If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views). From bd38286330014129eff738a7dceb757dc8f57fdc Mon Sep 17 00:00:00 2001 From: Alexander Travov Date: Tue, 29 Nov 2016 17:12:33 +0300 Subject: [PATCH 13/60] Fix broken link in migration docs --- docs/v1.x-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index 8a24e2d1..a4d78053 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -499,7 +499,7 @@ m("svg", ## Nested arrays in views -Arrays now represent [fragments](fragments.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays. +Arrays now represent [fragments](fragment.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays. *** From 5f08c278ec9bc0faf0477b41c4b85522cba36cc5 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 29 Nov 2016 14:38:31 -0800 Subject: [PATCH 14/60] Consistency --- docs/v1.x-migration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index a4d78053..6f4a415c 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -18,7 +18,7 @@ - [`m.request`](#mrequest) - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) -- [Vnode equality checks](#vnode-equality-checks) +- [`vnode` equality checks](#vnode-equality-checks) --- @@ -495,14 +495,14 @@ m("svg", ) ``` -*** +--- ## Nested arrays in views Arrays now represent [fragments](fragment.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays. -*** +--- -## Vnode equality checks +## `vnode` equality checks If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views). From 9406c905c0f41e9b064a9b6ba7c47abd474e3821 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Wed, 30 Nov 2016 11:51:10 -0800 Subject: [PATCH 15/60] Travis should lint & bundle before every build (#1441) Linting doesn't break builds though, because @lhorie doesn't want that. --- .travis.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5cac2607..4d118c54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,12 @@ install: - npm install - npm install @alrra/travis-scripts@^3.0.1 +# Lint (but don't fail build) before running tests +before_script: npm run lint || true + +# Run more than just npm test +script: npm run build && npm test + # After a successful build create bundles & commit back to the repo after_success: - | @@ -29,8 +35,7 @@ after_success: --path-encrypted-key "./.deploy.enc" # Build & commit changes - $(npm bin)/commit-changes --commands "npm run build" \ - --commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \ + $(npm bin)/commit-changes --commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \ --branch "$BRANCH" env: From 713c25c9c0490a7f50368209148737bf76647dd7 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Thu, 1 Dec 2016 01:45:07 -0500 Subject: [PATCH 16/60] fix #1404 --- README.md | 2 +- api/autoredraw.js | 19 -- api/mount.js | 37 +++- api/pubsub.js | 15 -- api/redraw.js | 26 +++ api/router.js | 79 ++++---- api/tests/index.html | 10 +- api/tests/test-autoredraw.js | 95 ---------- api/tests/test-mount.js | 16 +- api/tests/test-pubsub.js | 75 -------- api/tests/test-redraw.js | 82 ++++++++ api/tests/test-router.js | 24 ++- api/tests/test-throttle.js | 90 --------- api/throttle.js | 22 --- bundler/bundle.js | 9 +- bundler/tests/test-bundler.js | 24 +++ index.js | 6 +- mithril.js | 298 +++++++++++++++--------------- mithril.min.js | 80 ++++---- mount.js | 3 +- redraw.js | 2 +- route.js | 4 +- router/router.js | 14 +- router/tests/test-defineRoutes.js | 34 ++-- 24 files changed, 437 insertions(+), 629 deletions(-) delete mode 100644 api/autoredraw.js delete mode 100644 api/pubsub.js create mode 100644 api/redraw.js delete mode 100644 api/tests/test-autoredraw.js delete mode 100644 api/tests/test-pubsub.js create mode 100644 api/tests/test-redraw.js delete mode 100644 api/tests/test-throttle.js delete mode 100644 api/throttle.js diff --git a/README.md b/README.md index e1be8c2b..71c22280 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.42 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.41 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/autoredraw.js b/api/autoredraw.js deleted file mode 100644 index f3b88c03..00000000 --- a/api/autoredraw.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict" - -var throttle = require("../api/throttle") - -module.exports = function(root, renderer, pubsub, callback) { - var run = throttle(callback) - if (renderer != null) { - renderer.setEventCallback(function(e) { - if (e.redraw !== false) pubsub.publish() - }) - } - - if (pubsub != null) { - if (root.redraw) pubsub.unsubscribe(root.redraw) - pubsub.subscribe(run) - } - - return root.redraw = run -} diff --git a/api/mount.js b/api/mount.js index 9ff6c81b..2afadc57 100644 --- a/api/mount.js +++ b/api/mount.js @@ -1,23 +1,42 @@ "use strict" var Vnode = require("../render/vnode") -var autoredraw = require("../api/autoredraw") -module.exports = function(renderer, pubsub) { +module.exports = function(redrawService) { + function throttle(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, time - (now - last)) + } + } + } + return function(root, component) { if (component === null) { - renderer.render(root, []) - pubsub.unsubscribe(root.redraw) - delete root.redraw + redrawService.render(root, []) + redrawService.unsubscribe(root) return } if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - - var run = autoredraw(root, renderer, pubsub, function() { - renderer.render(root, Vnode(component, undefined, undefined, undefined, undefined, undefined)) + + var run = throttle(function() { + redrawService.render(root, Vnode(component)) }) - + redrawService.subscribe(root, run) run() } } diff --git a/api/pubsub.js b/api/pubsub.js deleted file mode 100644 index 7d9fbb57..00000000 --- a/api/pubsub.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -module.exports = function() { - var callbacks = [] - function unsubscribe(callback) { - var index = callbacks.indexOf(callback) - if (index > -1) callbacks.splice(index, 1) - } - function publish() { - for (var i = 0; i < callbacks.length; i++) { - callbacks[i].apply(this, arguments) - } - } - return {subscribe: callbacks.push.bind(callbacks), unsubscribe: unsubscribe, publish: publish} -} diff --git a/api/redraw.js b/api/redraw.js new file mode 100644 index 00000000..d2d20c52 --- /dev/null +++ b/api/redraw.js @@ -0,0 +1,26 @@ +"use strict" + +var coreRenderer = require("../render/render") + +module.exports = function($window) { + var renderService = coreRenderer($window) + renderService.setEventCallback(function(e) { + if (e.redraw !== false) redraw() + }) + + var callbacks = [] + function subscribe(key, callback) { + unsubscribe(key) + callbacks.push(key, callback) + } + function unsubscribe(key) { + var index = callbacks.indexOf(key) + if (index > -1) callbacks.splice(index, 2) + } + function redraw() { + for (var i = 1; i < callbacks.length; i += 2) { + callbacks[i]() + } + } + return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} +} diff --git a/api/router.js b/api/router.js index 459f68c8..553a6751 100644 --- a/api/router.js +++ b/api/router.js @@ -3,55 +3,44 @@ var Vnode = require("../render/vnode") var coreRouter = require("../router/router") -module.exports = function($window, mount) { - var router = coreRouter($window) - var currentResolve, currentComponent, currentRender, currentArgs, currentPath - - var RouteComponent = {view: function() { - return [currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined))] - }} - function defaultRender(vnode) { - return vnode - } +module.exports = function($window, redrawService) { + var routeService = coreRouter($window) + + var identity = function(v) {return v} + var current = {render: identity, component: null, path: null, resolve: null} var route = function(root, defaultRoute, routes) { - currentComponent = "div" - currentRender = defaultRender - currentArgs = null - - mount(root, RouteComponent) - - router.defineRoutes(routes, function(payload, args, path) { - var isResolver = typeof payload.view !== "function" - var render = defaultRender - - var resolve = currentResolve = function (component) { - if (resolve !== currentResolve) return - currentResolve = null - - currentComponent = component != null ? component : isResolver ? "div" : payload - currentRender = render - currentArgs = args - currentPath = path - - root.redraw(true) + if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") + var render = function(resolver, component, params, path) { + current.render = resolver.render || identity + current.component = component + current.path = path + current.resolve = null + redrawService.render(root, current.render(Vnode(component, undefined, params))) + } + var run = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + if (component.view) render({}, component, params, path) + else { + if (component.onmatch) { + if (isRouteChange === false && current.path === path || current.resolve != null) render(current, current.component, params) + else { + current.resolve = function(resolved) { + render(component, resolved, params, path) + } + component.onmatch(function(resolved) { + if (current.path !== path && current.resolve != null) current.resolve(resolved) + }, params, path) + } + } + else render(component, "div", params, path) } - var onmatch = function() { - resolve() - } - if (isResolver) { - if (typeof payload.render === "function") render = payload.render.bind(payload) - if (typeof payload.onmatch === "function") onmatch = payload.onmatch - } - - onmatch.call(payload, resolve, args, path) }, function() { - router.setPath(defaultRoute, null, {replace: true}) + routeService.setPath(defaultRoute) }) + redrawService.subscribe(root, run) } - route.link = router.link - route.prefix = router.setPrefix - route.set = router.setPath - route.get = function() {return currentPath} - + route.set = routeService.setPath + route.get = function() {return current.path} + route.prefix = routeService.setPrefix + route.link = routeService.link return route } diff --git a/api/tests/index.html b/api/tests/index.html index 75d15bea..fd2557d5 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -14,7 +14,7 @@ - + @@ -24,15 +24,11 @@ - - - + - - - + diff --git a/api/tests/test-autoredraw.js b/api/tests/test-autoredraw.js deleted file mode 100644 index 191d2815..00000000 --- a/api/tests/test-autoredraw.js +++ /dev/null @@ -1,95 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var domMock = require("../../test-utils/domMock") - -var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") -var autoredraw = require("../../api/autoredraw") - -o.spec("autoredraw", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, renderer, pubsub, spy - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - renderer = coreRenderer($window) - pubsub = apiPubSub() - spy = o.spy() - }) - - o("returns self-trigger", function() { - var run = autoredraw(root, renderer, pubsub, spy) - - run() - - o(spy.callCount).equals(1) - }) - - o("null renderer doesn't throw", function(done) { - autoredraw(root, null, pubsub, spy) - done() - }) - - o("null pubsub doesn't throw", function(done) { - autoredraw(root, renderer, null, spy) - done() - }) - - o("registers onevent", function() { - autoredraw(root, renderer, pubsub, spy) - - renderer.render(root, {tag: "div", attrs: {onclick: function() {}}}) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - o(spy.callCount).equals(1) - }) - - o("registers pubsub", function() { - autoredraw(root, renderer, pubsub, spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) - - o("re-registering pubsub works", function() { - autoredraw(root, renderer, pubsub, spy) - autoredraw(root, renderer, pubsub, spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) - - o("throttles", function(done) { - var run = autoredraw(root, renderer, pubsub, spy) - - run() - run() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }) - - o("does not redraw if e.redraw is false", function() { - autoredraw(root, renderer, pubsub, spy) - - renderer.render(root, {tag: "div", attrs: {onclick: function(e) {e.redraw = false}}}) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - o(spy.callCount).equals(0) - }) - -}) diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index b65daf4a..f3b1b431 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -5,20 +5,20 @@ var domMock = require("../../test-utils/domMock") var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") +var apiRedraw = require("../../api/redraw") var apiMounter = require("../../api/mount") o.spec("mount", function() { var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redraw, mount, render + var $window, root, redrawService, mount, render o.beforeEach(function() { $window = domMock() root = $window.document.body - redraw = apiPubSub() - mount = apiMounter(coreRenderer($window), redraw) + redrawService = apiRedraw($window) + mount = apiMounter(redrawService) render = coreRenderer($window).render }) @@ -42,18 +42,16 @@ o.spec("mount", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("mounting null deletes `redraw` from `root`", function() { + o("mounting null unmounts", function() { mount(root, { view : function() { return m("div") } }) - o(typeof root.redraw).equals('function') - mount(root, null) - o(typeof root.redraw).equals('undefined') + o(root.childNodes.length).equals(0) }) o("redraws on events", function(done) { @@ -206,7 +204,7 @@ o.spec("mount", function() { o(oninit.callCount).equals(1) o(onupdate.callCount).equals(0) - redraw.publish() + redrawService.redraw() // Wrapped to give time for the rate-limited redraw to fire setTimeout(function() { diff --git a/api/tests/test-pubsub.js b/api/tests/test-pubsub.js deleted file mode 100644 index 270b7c0e..00000000 --- a/api/tests/test-pubsub.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var apiPubSub = require("../../api/pubsub") - -o.spec("pubsub", function() { - var pubsub - o.beforeEach(function() { - pubsub = apiPubSub() - }) - - o("shouldn't error if there are no renderers", function() { - pubsub.publish() - }) - - o("should run a single renderer entry", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - - pubsub.publish() - pubsub.publish() - pubsub.publish() - - o(spy.callCount).equals(4) - }) - - o("should run all renderer entries", function() { - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - pubsub.subscribe(spy1) - pubsub.subscribe(spy2) - pubsub.subscribe(spy3) - - pubsub.publish() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - pubsub.publish() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - }) - - o("should stop running after unsubscribe", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - pubsub.unsubscribe(spy) - - pubsub.publish() - - o(spy.callCount).equals(0) - }) - - o("does nothing on invalid unsubscribe", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - pubsub.unsubscribe(null) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) -}) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js new file mode 100644 index 00000000..5f002e5f --- /dev/null +++ b/api/tests/test-redraw.js @@ -0,0 +1,82 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var apiRedraw = require("../../api/redraw") + +o.spec("redrawService", function() { + var root, redrawService, $document + o.beforeEach(function() { + var $window = domMock() + root = $window.document.body + redrawService = apiRedraw($window) + $document = $window.document + }) + + o("shouldn't error if there are no renderers", function() { + redrawService.redraw() + }) + + o("should run a single renderer entry", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + + redrawService.redraw() + + o(spy.callCount).equals(1) + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + + o(spy.callCount).equals(4) + }) + + o("should run all renderer entries", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + redrawService.subscribe(el1, spy1) + redrawService.subscribe(el2, spy2) + redrawService.subscribe(el3, spy3) + + redrawService.redraw() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + redrawService.redraw() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + }) + + o("should stop running after unsubscribe", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + redrawService.unsubscribe(root, spy) + + redrawService.redraw() + + o(spy.callCount).equals(0) + }) + + o("does nothing on invalid unsubscribe", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + redrawService.unsubscribe(null) + + redrawService.redraw() + + o(spy.callCount).equals(1) + }) +}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 721d6b28..0bd394dc 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -6,25 +6,23 @@ var browserMock = require("../../test-utils/browserMock") var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") +var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") -var apiMounter = require("../../api/mount") o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redraw, mount, route + var $window, root, redrawService, route o.beforeEach(function() { $window = browserMock(env) root = $window.document.body - redraw = apiPubSub() - mount = apiMounter(coreRenderer($window), redraw) - route = apiRouter($window, mount) + redrawService = apiRedraw($window) + route = apiRouter($window, redrawService) route.prefix(prefix) }) @@ -59,7 +57,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(view.callCount).equals(2) @@ -122,15 +120,15 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(onupdate.callCount).equals(1) }) o("redraws on events", function(done) { var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() + var oninit = o.spy() + var onclick = o.spy() var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -403,7 +401,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(matchCount).equals(1) o(renderCount).equals(2) @@ -505,7 +503,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(view.callCount).equals(2) o(onmatch.callCount).equals(1) @@ -515,7 +513,7 @@ o.spec("route", function() { }) o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ - var onmatch = o.spy(function(resolve){resolve()}) + var onmatch = o.spy(function(resolve) {resolve()}) $window.location.href = prefix + "/" route(root, '/', { diff --git a/api/tests/test-throttle.js b/api/tests/test-throttle.js deleted file mode 100644 index 4f7c7ee5..00000000 --- a/api/tests/test-throttle.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var throttle = require("../../api/throttle") - -o.spec("throttle", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var spy, throttled - o.beforeEach(function() { - spy = o.spy() - throttled = throttle(spy) - }) - - o("runs first call synchronously", function() { - throttled() - - o(spy.callCount).equals(1) - }) - - o("throttles subsequent synchronous calls", function(done) { - throttled() - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) //this delay is much higher than 16.6ms due to setTimeout clamp and other runtime costs - }) - - o("calls after threshold", function(done) { - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function(t) { - throttled() - - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - - }) - - o("throttles before threshold", function(done) { - throttled() - - o(spy.callCount).equals(1) - - callAsync(function(t) { - throttled() - - o(spy.callCount).equals(1) - - done() - }) - }) - - o("it only runs once per tick", function(done) { - throttled() - throttled() - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }) - - o("it supports forcing a synchronous redraw", function(done) { - throttled() - throttled() - throttled(true) - - o(spy.callCount).equals(2) - - setTimeout(function() { - o(spy.callCount).equals(3) - - done() - }, FRAME_BUDGET) - }) -}) diff --git a/api/throttle.js b/api/throttle.js deleted file mode 100644 index 6655b07c..00000000 --- a/api/throttle.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict" - -module.exports = function(callback) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function(synchronous) { - var now = Date.now() - if (synchronous === true || last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback() - last = Date.now() - }, time - (now - last)) - } - } -} diff --git a/bundler/bundle.js b/bundler/bundle.js index dff20bfe..4ac98300 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -32,8 +32,9 @@ function run(input, output) { def = def || "", variable = variable || "", eq = eq || "", rest = rest || "" if (def[0] === ",") def = "\nvar ", pre = "\n" var dependency = resolve(filepath, filename) - var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, uuid) : def + variable + eq + modules[dependency])) - modules[dependency] = rest ? "_" + uuid : variable + var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption + var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency])) + modules[dependency] = rest ? "_" + localUUID : variable uuid++ return code + rest }) @@ -116,7 +117,7 @@ function run(input, output) { code = "new function() {\n" + code + "\n}" if (!isFile(output) || code !== read(output)) { - try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} + //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} error = null fs.writeFileSync(output, code, "utf8") } @@ -129,7 +130,7 @@ function run(input, output) { module.exports = function(input, output, options) { run(input, output) if (options && options.watch) { - fs.watch(process.cwd(), {recursive: true}, function(file) { + fs.watch(process.cwd(), {recursive: true}, function(file, type) { if (typeof file === "string" && path.resolve(output) !== path.resolve(file)) run(input, output) }) } diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 80763c00..60859a11 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -273,6 +273,30 @@ o.spec("bundler", function() { remove("d.js") remove("out.js") }) + o("works if included multiple times", function() { + write("a.js", `module.exports = 123`) + write("b.js", `var a = require("./a").toString()\nmodule.exports = a`) + write("c.js", `var a = require("./a").toString()\nvar b = require("./b")`) + bundle(ns + "c.js", ns + "out.js") + + o(read("out.js")).equals(`new function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}`) + + remove("a.js") + remove("b.js") + remove("c.js") + }) + o("works if included multiple times reverse", function() { + write("a.js", `module.exports = 123`) + write("b.js", `var a = require("./a").toString()\nmodule.exports = a`) + write("c.js", `var b = require("./b")\nvar a = require("./a").toString()`) + bundle(ns + "c.js", ns + "out.js") + + o(read("out.js")).equals(`new function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}`) + + remove("a.js") + remove("b.js") + remove("c.js") + }) o("reuses binding if possible", function() { write("a.js", `var b = require("./b")\nvar c = require("./c")`) write("b.js", `var d = require("./d")\nmodule.exports = function() {return d + 1}`) diff --git a/index.js b/index.js index 8ae9df25..f50049c4 100644 --- a/index.js +++ b/index.js @@ -4,13 +4,13 @@ var m = require("./hyperscript") var requestService = require("./request") var redrawService = require("./redraw") -requestService.setCompletionCallback(redrawService.publish) +requestService.setCompletionCallback(redrawService.redraw) m.mount = require("./mount") m.route = require("./route") m.withAttr = require("./util/withAttr") -m.render = require("./render").render -m.redraw = redrawService.publish +m.render = redrawService.render +m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = require("./querystring/parse") diff --git a/mithril.js b/mithril.js index 7fdbec3a..4c29abab 100644 --- a/mithril.js +++ b/mithril.js @@ -328,22 +328,7 @@ var _8 = function($window, Promise) { return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback} } var requestService = _8(window, PromisePolyfill) -var _11 = function() { - var callbacks = [] - function unsubscribe(callback) { - var index = callbacks.indexOf(callback) - if (index > -1) callbacks.splice(index, 1) - } - function publish() { - for (var i = 0; i < callbacks.length; i++) { - callbacks[i].apply(this, arguments) - } - } - return {subscribe: callbacks.push.bind(callbacks), unsubscribe: unsubscribe, publish: publish} -} -var redrawService = _11() -requestService.setCompletionCallback(redrawService.publish) -var _13 = function($window) { +var coreRenderer = function($window) { var $doc = $window.document var $emptyFragment = $doc.createDocumentFragment() var onevent @@ -642,8 +627,8 @@ var _13 = function($window) { for (var i = 0; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - var key1 = vnode.key - if (key1 != null) map[key1] = i + var key2 = vnode.key + if (key2 != null) map[key2] = i } } return map @@ -749,34 +734,34 @@ var _13 = function($window) { } //attrs2 function setAttrs(vnode, attrs2, ns) { - for (var key1 in attrs2) { - setAttr(vnode, key1, null, attrs2[key1], ns) + for (var key2 in attrs2) { + setAttr(vnode, key2, null, attrs2[key2], ns) } } - function setAttr(vnode, key1, old, value, ns) { + function setAttr(vnode, key2, old, value, ns) { var element = vnode.dom - if (key1 === "key" || (old === value && !isFormAttribute(vnode, key1)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key1)) return - var nsLastIndex = key1.indexOf(":") - if (nsLastIndex > -1 && key1.substr(0, nsLastIndex) === "xlink") { - element.setAttributeNS("http://www.w3.org/1999/xlink", key1.slice(nsLastIndex + 1), value) + if (key2 === "key" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return + var nsLastIndex = key2.indexOf(":") + if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") { + element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value) } - else if (key1[0] === "o" && key1[1] === "n" && typeof value === "function") updateEvent(vnode, key1, value) - else if (key1 === "style") updateStyle(element, old, value) - else if (key1 in element && !isAttribute(key1) && ns === undefined) { + else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value) + else if (key2 === "style") updateStyle(element, old, value) + else if (key2 in element && !isAttribute(key2) && ns === undefined) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key1 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "input" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key1 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "select" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key1 === "value" && vnode.dom.value === value) return - element[key1] = value + if (vnode.tag === "option" && key2 === "value" && vnode.dom.value === value) return + element[key2] = value } else { if (typeof value === "boolean") { - if (value) element.setAttribute(key1, "") - else element.removeAttribute(key1) + if (value) element.setAttribute(key2, "") + else element.removeAttribute(key2) } - else element.setAttribute(key1 === "className" ? "class" : key1, value) + else element.setAttribute(key2 === "className" ? "class" : key2, value) } } function setLateAttrs(vnode) { @@ -788,16 +773,16 @@ var _13 = function($window) { } function updateAttrs(vnode, old, attrs2, ns) { if (attrs2 != null) { - for (var key1 in attrs2) { - setAttr(vnode, key1, old && old[key1], attrs2[key1], ns) + for (var key2 in attrs2) { + setAttr(vnode, key2, old && old[key2], attrs2[key2], ns) } } if (old != null) { - for (var key1 in old) { - if (attrs2 == null || !(key1 in attrs2)) { - if (key1 === "className") key1 = "class" - if (key1[0] === "o" && key1[1] === "n" && !isLifecycleMethod(key1)) updateEvent(vnode, key1, undefined) - else if (key1 !== "key") vnode.dom.removeAttribute(key1) + for (var key2 in old) { + if (attrs2 == null || !(key2 in attrs2)) { + if (key2 === "className") key2 = "class" + if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined) + else if (key2 !== "key") vnode.dom.removeAttribute(key2) } } } @@ -821,32 +806,32 @@ var _13 = function($window) { else if (typeof style === "string") element.style.cssText = style else { if (typeof old === "string") element.style.cssText = "" - for (var key1 in style) { - element.style[key1] = style[key1] + for (var key2 in style) { + element.style[key2] = style[key2] } if (old != null && typeof old !== "string") { - for (var key1 in old) { - if (!(key1 in style)) element.style[key1] = "" + for (var key2 in old) { + if (!(key2 in style)) element.style[key2] = "" } } } } //event - function updateEvent(vnode, key1, value) { + function updateEvent(vnode, key2, value) { var element = vnode.dom var callback = function(e) { var result = value.call(element, e) if (typeof onevent === "function") onevent.call(element, e) return result } - if (key1 in element) element[key1] = typeof value === "function" ? callback : null + if (key2 in element) element[key2] = typeof value === "function" ? callback : null else { - var eventName = key1.slice(2) + var eventName = key2.slice(2) if (vnode.events === undefined) vnode.events = {} - if (vnode.events[key1] != null) element.removeEventListener(eventName, vnode.events[key1], false) + if (vnode.events[key2] != null) element.removeEventListener(eventName, vnode.events[key2], false) if (typeof value === "function") { - vnode.events[key1] = callback - element.addEventListener(eventName, vnode.events[key1], false) + vnode.events[key2] = callback + element.addEventListener(eventName, vnode.events[key2], false) } } } @@ -888,79 +873,90 @@ var _13 = function($window) { } return {render: render, setEventCallback: setEventCallback} } -var renderService = _13(window) -var throttle = function(callback1) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function(synchronous) { - var now = Date.now() - if (synchronous === true || last === 0 || now - last >= time) { - last = now - callback1() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback1() - last = Date.now() - }, time - (now - last)) +var _11 = function($window) { + var renderService = coreRenderer($window) + renderService.setEventCallback(function(e) { + if (e.redraw !== false) redraw() + }) + + var callbacks = [] + function subscribe(key1, callback) { + unsubscribe(key1) + callbacks.push(key1, callback) + } + function unsubscribe(key1) { + var index = callbacks.indexOf(key1) + if (index > -1) callbacks.splice(index, 2) + } + function redraw() { + for (var i = 1; i < callbacks.length; i += 2) { + callbacks[i]() + } + } + return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} +} +var redrawService = _11(window) +requestService.setCompletionCallback(redrawService.redraw) +var _16 = function(redrawService0) { + function throttle(callback0) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback0() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback0() + last = Date.now() + }, time - (now - last)) + } } } -} -var autoredraw = function(root, renderer, pubsub, callback0) { - var run1 = throttle(callback0) - if (renderer != null) { - renderer.setEventCallback(function(e) { - if (e.redraw !== false) pubsub.publish() - }) - } - if (pubsub != null) { - if (root.redraw) pubsub.unsubscribe(root.redraw) - pubsub.subscribe(run1) - } - return root.redraw = run1 -} -var _17 = function(renderer, pubsub) { + return function(root, component) { if (component === null) { - renderer.render(root, []) - pubsub.unsubscribe(root.redraw) - delete root.redraw + redrawService0.render(root, []) + redrawService0.unsubscribe(root) return } if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - var run0 = autoredraw(root, renderer, pubsub, function() { - renderer.render(root, Vnode(component, undefined, undefined, undefined, undefined, undefined)) + + var run0 = throttle(function() { + redrawService0.render(root, Vnode(component)) }) + redrawService0.subscribe(root, run0) run0() } } -m.mount = _17(renderService, redrawService) -var mount = m.mount +m.mount = _16(redrawService) var parseQueryString = function(string) { if (string === "" || string == null) return {} if (string.charAt(0) === "?") string = string.slice(1) var entries = string.split("&"), data0 = {}, counters = {} for (var i = 0; i < entries.length; i++) { var entry = entries[i].split("=") - var key3 = decodeURIComponent(entry[0]) + var key4 = decodeURIComponent(entry[0]) var value = entry.length === 2 ? decodeURIComponent(entry[1]) : "" if (value === "true") value = true else if (value === "false") value = false - var levels = key3.split(/\]\[?|\[/) + var levels = key4.split(/\]\[?|\[/) var cursor = data0 - if (key3.indexOf("[") > -1) levels.pop() + if (key4.indexOf("[") > -1) levels.pop() for (var j = 0; j < levels.length; j++) { var level = levels[j], nextLevel = levels[j + 1] var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) var isValue = j === levels.length - 1 if (level === "") { - var key3 = levels.slice(0, j).join() - if (counters[key3] == null) counters[key3] = 0 - level = counters[key3]++ + var key4 = levels.slice(0, j).join() + if (counters[key4] == null) counters[key4] = 0 + level = counters[key4]++ } if (cursor[level] == null) { cursor[level] = isValue ? value : isNumber ? [] : {} @@ -982,11 +978,11 @@ var coreRouter = function($window) { } var asyncId function debounceAsync(f) { - return function() { + return function(e) { if (asyncId != null) return asyncId = callAsync0(function() { asyncId = null - f() + f(e) }) } } @@ -997,11 +993,11 @@ var coreRouter = function($window) { if (queryIndex > -1) { var queryEnd = hashIndex > -1 ? hashIndex : path.length var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd)) - for (var key2 in queryParams) queryData[key2] = queryParams[key2] + for (var key3 in queryParams) queryData[key3] = queryParams[key3] } if (hashIndex > -1) { var hashParams = parseQueryString(path.slice(hashIndex + 1)) - for (var key2 in hashParams) hashData[key2] = hashParams[key2] + for (var key3 in hashParams) hashData[key3] = hashParams[key3] } return path.slice(0, pathEnd) } @@ -1017,7 +1013,7 @@ var coreRouter = function($window) { var queryData = {}, hashData = {} path = parsePath(path, queryData, hashData) if (data != null) { - for (var key2 in data) queryData[key2] = data[key2] + for (var key3 in data) queryData[key3] = data[key3] path = path.replace(/:([^\/]+)/g, function(match2, token) { delete queryData[token] return data[token] @@ -1030,16 +1026,16 @@ var coreRouter = function($window) { if (supportsPushState) { if (options && options.replace) $window.history.replaceState(null, null, prefix1 + path) else $window.history.pushState(null, null, prefix1 + path) - $window.onpopstate() + $window.onpopstate(true) } else $window.location.href = prefix1 + path } - function defineRoutes(routes, resolve0, reject) { + function defineRoutes(routes, resolve, reject) { if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) else if (prefix1.charAt(0) === "#") $window.onhashchange = resolveRoute - resolveRoute() + resolveRoute(true) - function resolveRoute() { + function resolveRoute(isRouteChange) { var path = getPath() var params = {} var pathname = parsePath(path, params, params) @@ -1053,14 +1049,14 @@ var coreRouter = function($window) { for (var i = 0; i < keys.length; i++) { params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) } - resolve0(routes[route0], params, path, route0) + resolve(routes[route0], params, path, route0, !!isRouteChange) }) return } } reject(path, params) } - return resolveRoute + return function() {resolveRoute(false)} } function link(vnode2) { vnode2.dom.setAttribute("href", prefix1 + vnode2.attrs.href) @@ -1075,64 +1071,60 @@ var coreRouter = function($window) { } return {setPrefix: setPrefix, getPath: getPath, setPath: setPath, defineRoutes: defineRoutes, link: link} } -var _23 = function($window, mount0) { - var router = coreRouter($window) - var currentResolve, currentComponent, currentRender, currentArgs, currentPath - var RouteComponent = {view: function() { - return [currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined))] - }} - function defaultRender(vnode1) { - return vnode1 - } +var _20 = function($window, redrawService0) { + var routeService = coreRouter($window) + + var identity = function(v) {return v} + var current = {render: identity, component: null, path: null, resolve: null} var route = function(root, defaultRoute, routes) { - currentComponent = "div" - currentRender = defaultRender - currentArgs = null - mount0(root, RouteComponent) - router.defineRoutes(routes, function(payload, args0, path) { - var isResolver = typeof payload.view !== "function" - var render1 = defaultRender - var resolve = currentResolve = function (component) { - if (resolve !== currentResolve) return - currentResolve = null - currentComponent = component != null ? component : isResolver ? "div" : payload - currentRender = render1 - currentArgs = args0 - currentPath = path - root.redraw(true) + if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") + var render1 = function(resolver, component, params, path) { + current.render = resolver.render || identity + current.component = component + current.path = path + current.resolve = null + redrawService0.render(root, current.render(Vnode(component, undefined, params))) + } + var run1 = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + if (component.view) render1({}, component, params, path) + else { + if (component.onmatch) { + if (isRouteChange === false && current.path === path || current.resolve != null) render1(current, current.component, params) + else { + current.resolve = function(resolved) { + render1(component, resolved, params, path) + } + component.onmatch(function(resolved) { + if (current.path !== path && current.resolve != null) current.resolve(resolved) + }, params, path) + } + } + else render1(component, "div", params, path) } - var onmatch = function() { - resolve() - } - if (isResolver) { - if (typeof payload.render === "function") render1 = payload.render.bind(payload) - if (typeof payload.onmatch === "function") onmatch = payload.onmatch - } - - onmatch.call(payload, resolve, args0, path) }, function() { - router.setPath(defaultRoute, null, {replace: true}) + routeService.setPath(defaultRoute) }) + redrawService0.subscribe(root, run1) } - route.link = router.link - route.prefix = router.setPrefix - route.set = router.setPath - route.get = function() {return currentPath} + route.set = routeService.setPath + route.get = function() {return current.path} + route.prefix = routeService.setPrefix + route.link = routeService.link return route } -m.route = _23(window, mount) -m.withAttr = function(attrName, callback2, context) { +m.route = _20(window, redrawService) +m.withAttr = function(attrName, callback1, context) { return function(e) { - return callback2.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } -m.render = renderService.render -m.redraw = redrawService.publish +m.render = redrawService.render +m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0-rc.5" +m.version = "1.0.0-rc.6" if (typeof module !== "undefined") module["exports"] = m else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 41590c4a..3e9ac712 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,40 +1,40 @@ -new function(){function m(a,b,k,e,l,h){return{tag:a,key:b,attrs:k,children:e,text:l,dom:h,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function t(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===H[a]){for(var b,k,e=[],l={};b=O.exec(a);){var h=b[1],v=b[2];""===h&&""!==v?k=v:"#"===h?l.id=v:"."===h?e.push(v):"["===b[3][0]&&((h=b[6])&&(h=h.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===b[4]?e.push(h):l[b[4]]=h||!0)}0a.indexOf("?")?"?":"&";a+=e+f}return a}function v(a){try{return""!== -a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function r(a,b){if("function"===typeof a)if(b instanceof Array)for(var e=0;en.status||304===n.status)b(r(f.type,a));else{var h=Error(n.responseText),k;for(k in a)h[k]=a[k];e(h)}}catch(G){e(G)}}; -u&&null!=f.data?n.send(f.data):n.send()}))},jsonp:function(f){return e(new b(function(b,e){var n=f.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[n]=function(e){k.parentNode.removeChild(k);b(r(f.type,e));delete a[n]};k.onerror=function(){k.parentNode.removeChild(k);e(Error("JSONP request failed"));delete a[n]};null==f.data&&(f.data={});f.url=l(f.url,f.data);f.data[f.callbackKey||"callback"]=n;k.src=h(f.url,f.data);a.document.documentElement.appendChild(k)}))}, -setCompletionCallback:function(a){q=a}}}(window,"undefined"!==typeof Promise?Promise:y),J=function(){var a=[];return{subscribe:a.push.bind(a),unsubscribe:function(b){b=a.indexOf(b);-1=A&&B>=l;){var x=a[A],m=d[l];if(x!==m||g)if(null==x)A++;else if(null==m)l++;else if(x.key===m.key)A++,l++,h(c,x,m,f,p(a,A,e),g,n),g&&x.tag===m.tag&&r(c,v(x),e); -else if(x=a[q],x!==m||g)if(null==x)q--;else if(null==m)l++;else if(x.key===m.key)h(c,x,m,f,p(a,q+1,e),g,n),(g||l=A&&B>=l;){x=a[q];m=d[B];if(x!==m||g)if(null==x)q--;else{if(null!=m)if(x.key===m.key)h(c,x,m,f,p(a,q+1,e),g,n),g&&x.tag===m.tag&&r(c,v(x),e),null!=x.dom&&(e=x.dom),q--;else{if(!w){w=a;var x=q,D={},z;for(z=0;za.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== +a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function q(a,b){if("function"===typeof a)if(b instanceof Array)for(var d=0;dm.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(G){h(G)}}; +r&&null!=d.data?m.send(d.data):m.send()}))},jsonp:function(d){return b(new h(function(b,h){var m=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+n++,r=a.document.createElement("script");a[m]=function(f){r.parentNode.removeChild(r);b(q(d.type,f));delete a[m]};r.onerror=function(){r.parentNode.removeChild(r);h(Error("JSONP request failed"));delete a[m]};null==d.data&&(d.data={});d.url=f(d.url,d.data);d.data[d.callbackKey||"callback"]=m;r.src=k(d.url,d.data);a.document.documentElement.appendChild(r)}))}, +setCompletionCallback:function(a){m=a}}}(window,"undefined"!==typeof Promise?Promise:t),N=function(a){function h(g,c,a,b,d,f,h){for(;a=m&&u>=y;){var x=c[m],n=a[y];if(x!==n||e)if(null==x)m++;else if(null==n)y++;else if(x.key===n.key)m++,y++,k(g,x,n,d,p(c,m,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b);else if(x=c[z],x!==n||e)if(null==x)z--;else if(null==n)y++;else if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),(e||y= +m&&u>=y;){x=c[z];n=a[u];if(x!==n||e)if(null==x)z--;else{if(null!=n)if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b),null!=x.dom&&(b=x.dom),z--;else{if(!w){w=c;var x=z,D={},t;for(t=0;t Date: Thu, 1 Dec 2016 01:46:05 -0500 Subject: [PATCH 17/60] rebuild --- mithril.min.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/mithril.min.js b/mithril.min.js index 3e9ac712..eec96f1a 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,20 +1,20 @@ -new function(){function n(a,h,l,b,f,k){return{tag:a,key:h,attrs:l,children:b,text:f,dom:k,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var h,l,b=[],f={};h=O.exec(a);){var k=h[1],v=h[2];""===k&&""!==v?l=v:"#"===k?f.id=v:"."===k?b.push(v):"["===h[3][0]&&((k=h[6])&&(k=k.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), +new function(){function n(a,h,l,b,f,k){return{tag:a,key:h,attrs:l,children:b,text:f,dom:k,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var h,l,b=[],f={};h=N.exec(a);){var k=h[1],v=h[2];""===k&&""!==v?l=v:"#"===k?f.id=v:"."===k?b.push(v):"["===h[3][0]&&((k=h[6])&&(k=k.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), "class"===h[4]?b.push(k):f[h[4]]=k||!0)}0a.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== +l)})};"undefined"===typeof Promise&&("undefined"!==typeof window?window.Promise=t:"undefined"!==typeof global&&(global.Promise=t));var G=function(a){function h(a,b){if(b instanceof Array)for(var f=0;fa.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function q(a,b){if("function"===typeof a)if(b instanceof Array)for(var d=0;dm.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(G){h(G)}}; +r&&m.setRequestHeader("Content-Type","application/json; charset=utf-8");d.deserialize===v&&m.setRequestHeader("Accept","application/json, text/*");d.withCredentials&&(m.withCredentials=d.withCredentials);"function"===typeof d.config&&(m=d.config(m,d)||m);m.onreadystatechange=function(){if(4===m.readyState)try{var a=d.extract!==p?d.extract(m,d):d.deserialize(d.extract(m,d));if(200<=m.status&&300>m.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(H){h(H)}}; r&&null!=d.data?m.send(d.data):m.send()}))},jsonp:function(d){return b(new h(function(b,h){var m=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+n++,r=a.document.createElement("script");a[m]=function(f){r.parentNode.removeChild(r);b(q(d.type,f));delete a[m]};r.onerror=function(){r.parentNode.removeChild(r);h(Error("JSONP request failed"));delete a[m]};null==d.data&&(d.data={});d.url=f(d.url,d.data);d.data[d.callbackKey||"callback"]=m;r.src=k(d.url,d.data);a.document.documentElement.appendChild(r)}))}, -setCompletionCallback:function(a){m=a}}}(window,"undefined"!==typeof Promise?Promise:t),N=function(a){function h(g,c,a,b,d,f,h){for(;a=m&&u>=y;){var x=c[m],n=a[y];if(x!==n||e)if(null==x)m++;else if(null==n)y++;else if(x.key===n.key)m++,y++,k(g,x,n,d,p(c,m,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b);else if(x=c[z],x!==n||e)if(null==x)z--;else if(null==n)y++;else if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),(e||y= m&&u>=y;){x=c[z];n=a[u];if(x!==n||e)if(null==x)z--;else{if(null!=n)if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b),null!=x.dom&&(b=x.dom),z--;else{if(!w){w=c;var x=z,D={},t;for(t=0;t Date: Thu, 1 Dec 2016 01:49:19 -0500 Subject: [PATCH 18/60] small tweaks to docs --- docs/buildQueryString.md | 4 +- docs/fragment.md | 8 +- docs/hyperscript.md | 6 +- docs/installation.md | 22 ++-- docs/introduction.md | 226 +++++++++++++++++++++++++++++++++++++++ docs/jsonp.md | 6 +- docs/lint.js | 3 + docs/mount.md | 4 +- docs/parseQueryString.md | 4 +- docs/promise.md | 22 ++-- docs/redraw.md | 6 +- docs/render.md | 6 +- docs/request.md | 4 +- docs/route.md | 20 ++-- docs/stream.md | 26 ++--- docs/testing.md | 4 +- docs/trust.md | 4 +- docs/version.md | 4 +- docs/withAttr.md | 4 +- 19 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 docs/introduction.md diff --git a/docs/buildQueryString.md b/docs/buildQueryString.md index 23d855a5..0588da00 100644 --- a/docs/buildQueryString.md +++ b/docs/buildQueryString.md @@ -1,11 +1,11 @@ # buildQueryString(object) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature `querystring = m.buildQueryString(object)` diff --git a/docs/fragment.md b/docs/fragment.md index fa04799b..4b180a91 100644 --- a/docs/fragment.md +++ b/docs/fragment.md @@ -1,13 +1,13 @@ -# fragment(html) +# fragment(attrs, children) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature -Generates a trusted HTML [vnode](vnodes.md) +Generates a fragment [vnode](vnodes.md) `vnode = m.fragment(attrs, children)` diff --git a/docs/hyperscript.md b/docs/hyperscript.md index af1067e9..13ad4a9a 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -1,6 +1,6 @@ # m(selector, attributes, children) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Flexibility](#flexibility) - [CSS selectors](#css-selectors) @@ -17,13 +17,13 @@ --- -### API +### Signature `vnode = m(selector, attributes, children)` Argument | Type | Required | Description ------------ | ------------------------------------------ | -------- | --- -`selector` | `String|Object` | Yes | A CSS selector or a [component](https://github.com/lhorie/mithril.js/blob/rewrite/docs/components.md) +`selector` | `String|Object` | Yes | A CSS selector or a [component](components.md) `attributes` | `Object` | No | HTML attributes or element properties `children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats) **returns** | `Vnode` | | A [vnode](vnodes.md#structure) diff --git a/docs/installation.md b/docs/installation.md index 07e69cae..4009bd28 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,7 +4,7 @@ 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): -``` +```markup ``` @@ -14,7 +14,7 @@ If you're new to Javascript or just want a very simple setup to get your feet we #### Quick start -``` +```bash # 1) install npm install mithril@rewrite --save @@ -37,14 +37,14 @@ NPM (Node package manager) is the default package manager that is bundled w/ Nod To use Mithril via NPM, go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`. -``` +```bash 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 -``` +```bash npm install mithril@rewrite --save ``` @@ -149,13 +149,6 @@ webpack --watch 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: -```javascript -// index.js - -// if a CommonJS environment is not detected, Mithril will be created in the global scope -m.render(document.body, "hello world") -``` - ```markup @@ -167,3 +160,10 @@ m.render(document.body, "hello world") ``` + +```javascript +// index.js + +// if a CommonJS environment is not detected, Mithril will be created in the global scope +m.render(document.body, "hello world") +``` diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 00000000..88e5e2a8 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,226 @@ +# Introduction + +- [What is Mithril?](#what-is-mithril) +- [Getting started](#getting-started) +- [Hello world](#hello-world) +- [DOM elements](#dom-elements) +- [Components](#components) +- [Routing](#routing) +- [XHR](#xhr) + +--- + +### What is Mithril? + +Mithril is a framework for developing Javascript-based Single Page Applications. It's designed to be fast, small and economical. + +--- + +### Getting started + +The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll only take 10 minutes. + +Let's create an HTML file to follow along: + +```markup +
+ + +``` + +--- + +### Hello world + +Let's start as small as well can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better) + +```javascript +m.render(root, "Hello world") +``` + +Now, let's change the text to something else. Add this line of code under the previous one: + +```javascript +m.render(root, "My first app") +``` + +As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch. + +--- + +### DOM elements + +Let's wrap our text in an `

` tag. + +```javascript +m.render(root, m("h1", "My first app")) +``` + +The `m()` function can be used to describe any HTML structure you want. So if you to add a class to the `

`: + +```javascript +m("h1", {class: "title"}, "My first app") +``` + +If you want to have multiple elements: + +```javascript +[ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), +] +``` + +And so on: + +```javascript +m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), +]) +``` + +--- + +### Components + +A Mithril component is just an object with a `view` function. Here's the code above as a component: + +```javascript +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), + ]) + } +} +``` + +To activate the component, we use `m.mount`. + +```javascript +m.mount(root, Hello) +``` + +As you would expect, doing so creates this markup: + +```markup +
+

My first app

+ +
+``` + +The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events: + +```javascript +var count = 0 // added a variable + +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", {onclick: function() {count++}}, count + " clicks"), // changed this line + ]) + } +} + +m.mount(root, Hello) +``` + +We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label. + +You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril does it for you. + +If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates. + +--- + +### Routing + +Routing just means going from one screen to another in an application with several screens. + +Let's add a splash page that appears before our click counter. First we create a component for it: + +```javascript +var Splash = { + view: function() { + return m("a", {href: "#!/hello"}, "Enter!") + } +} +``` + +As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path. + +Now that we going to have more than one screen, we use `m.route` instead of `m.mount`. + +```javascript +m.route(root, "/splash", { + "/splash": Splash, + "/hello": Hello, +}) +``` + +The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a `#!` in the URL. + +The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is `http://localhost`, then you get redirected to `http://localhost/#!/splash`. + +Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `http://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button. + +--- + +### XHR + +Basically, XHR is just a way to talk to a server. + +Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial. + +First we create a function that calls `m.request`. + +```javascript +var count = 0 +var increment = function() { + m.request({ + method: "PUT", + url: "http://rem-rest-api.herokuapp.com/api/tutorial/1", + data: {count: count + 1}, + useCredentials: true, + }) + .then(function(data) { + count = parseInt(data.count) + }) +} +``` + +Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now. + +Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly: + +```javascript +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", {onclick: increment}, count + " clicks"), + ]) + } +} +``` + +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/jsonp.md b/docs/jsonp.md index 185212a0..cf760338 100644 --- a/docs/jsonp.md +++ b/docs/jsonp.md @@ -1,19 +1,19 @@ # jsonp(options) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) --- -### API +### Signature `promise = m.jsonp(options)` Argument | Type | Required | Description ---------------------- | --------------------------------- | -------- | --- `options.url` | `String` | Yes | The URL to send the request to. The URL may be either absolute or relative, and it may contain [interpolations](#dynamic-urls). -`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring (for GET requests) or body (for other types of requests). +`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring. `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.callbackName` | `String` | No | The name of the function that will be called as the callback. Defaults to a randomized string (e.g. `_mithril_6888197422121285_0({a: 1})` `options.callbackKey` | `String` | No | The name of the querystring parameter name that specifies the callback name. Defaults to `callback` (e.g. `/someapi?callback=_mithril_6888197422121285_0`) diff --git a/docs/lint.js b/docs/lint.js index 240eb368..db4dd5dc 100644 --- a/docs/lint.js +++ b/docs/lint.js @@ -86,6 +86,9 @@ function initMocks() { "GET /api/v1/todos": function(request) { return {status: 200, responseText: JSON.stringify([])} }, + "PUT /api/v1/users/1": function(request) { + return {status: 200, responseText: request.query.callback ? request.query.callback + "([])" : "[]"} + }, "POST /api/v1/upload": function(request) { return {status: 200, responseText: JSON.stringify([])} }, diff --git a/docs/mount.md b/docs/mount.md index a402e1e6..857c0018 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -1,13 +1,13 @@ # mount(root, component) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Performance considerations](#performance-considerations) - [Differences from m.render](#differences-from-m-render) --- -### API +### Signature `m.mount(element, component)` diff --git a/docs/parseQueryString.md b/docs/parseQueryString.md index 9d159c35..c27f01ed 100644 --- a/docs/parseQueryString.md +++ b/docs/parseQueryString.md @@ -1,11 +1,11 @@ # parseQueryString(string) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature `object = m.parseQueryString(string)` diff --git a/docs/promise.md b/docs/promise.md index 4cca9b48..7a8a71d5 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -1,14 +1,14 @@ # Promise(executor) -- [API](#api) -- [Static members](#static-members) - - [Promise.resolve](#promiseresolve) - - [Promise.reject](#promisereject) - - [Promise.all](#promiseall) - - [Promise.race](#promiserace) -- [Instance members](#static-members) - - [promise.then](#promisethen) - - [promise.catch](#promisecatch) +- [Signature](#signature) + - [Static members](#static-members) + - [Promise.resolve](#promiseresolve) + - [Promise.reject](#promisereject) + - [Promise.all](#promiseall) + - [Promise.race](#promiserace) + - [Instance members](#instance-members) + - [promise.then](#promisethen) + - [promise.catch](#promisecatch) - [How it works](#how-it-works) - [Promise chaining](#promise-chaining) - [Promise absorption](#promise-absorption) @@ -18,7 +18,7 @@ --- -### API +### Signature `promise = new Promise(executor)` @@ -146,7 +146,7 @@ promise.then(function(value) { }) ``` -Promises are useful for working with [asynchronous](https://en.wikipedia.org/wiki/Asynchrony_(computer_programming)) APIs, such as [`m.request`](request.md) +Promises are useful for working with asynchronous APIs, such as [`m.request`](request.md) Asynchronous APIs are those which typically take a long time to run, and therefore would take too long to return a value using the `return` statement of a function. Instead, they do their work in the background, allowing other Javascript code to run in the meantime. When they are done, they call a function with their results. diff --git a/docs/redraw.md b/docs/redraw.md index fa80a5a4..3be49fa6 100644 --- a/docs/redraw.md +++ b/docs/redraw.md @@ -1,11 +1,11 @@ # redraw() -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature `m.redraw()` @@ -19,6 +19,6 @@ Argument | Type | Required | Description When callbacks outside of Mithril run, you need to notify Mithril's rendering engine that a redraw is needed. External callbacks could be `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, web socket library callbacks, event handlers in jQuery plugins, third party XHR request callbacks, etc. -To trigger a redraw, call `m.redraw()` +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 diff --git a/docs/render.md b/docs/render.md index 60eaf784..f3cf38fd 100644 --- a/docs/render.md +++ b/docs/render.md @@ -1,6 +1,6 @@ # render(element, vnodes) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Why Virtual DOM](#why-virtual-dom) - [Differences from other API methods](#differences-from-other-api-methods) @@ -8,7 +8,7 @@ --- -### API +### Signature `m.render(element, vnodes)` @@ -52,6 +52,8 @@ Another difference is that `m.render` method expects a [vnode](vnodes.md) (or a ### Standalone usage +`var render = require("mithril/render")` + The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It is approximately 500 lines of code (3kb min+gzip) and implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril and can be used as a standalone library. Despite being incredibly small, the render module is fully functional and self-suficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). diff --git a/docs/request.md b/docs/request.md index 173233c1..6509c82d 100644 --- a/docs/request.md +++ b/docs/request.md @@ -1,6 +1,6 @@ # request(options) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) - [Loading icons and error messages](#loading-icons-and-error-messages) @@ -15,7 +15,7 @@ --- -### API +### Signature `promise = m.request([url,] options)` diff --git a/docs/route.md b/docs/route.md index 6bf16529..50b59bc4 100644 --- a/docs/route.md +++ b/docs/route.md @@ -1,14 +1,14 @@ # route(root, defaultRoute, routes) -- [API](#api) -- [Static members](#static-members) - - [route.set](#routeset) - - [route.get](#routeget) - - [route.prefix](#routeprefix) - - [route.link](#routelink) -- [RouteResolver](#routeresolver) - - [routeResolver.onmatch](#routeresolveronmatch) - - [routeResolver.render](#routeresolverrender) +- [Signature](#signature) + - [Static members](#static-members) + - [route.set](#routeset) + - [route.get](#routeget) + - [route.prefix](#routeprefix) + - [route.link](#routelink) + - [RouteResolver](#routeresolver) + - [routeResolver.onmatch](#routeresolveronmatch) + - [routeResolver.render](#routeresolverrender) - [How it works](#how-it-works) - [Typical usage](#typical-usage) - [Navigating to different routes](#navigating-to-different-routes) @@ -21,7 +21,7 @@ --- -### API +### Signature `m.route(root, defaultRoute, routes)` diff --git a/docs/stream.md b/docs/stream.md index 3fc826d0..12dd85ed 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -1,17 +1,17 @@ # stream() -- [API](#api) -- [Static members](#static-members) - - [stream.combine](#streamcombine) - - [stream.merge](#streammerge) - - [stream.HALT](#streamhalt) - - [stream["fantasy-land/of"]](#streamfantasy-landof) -- [Instance members](#static-members) - - [stream.map](#streammap) - - [stream.end](#streamend) - - [stream["fantasy-land/of"]](#streamfantasy-landof) - - [stream["fantasy-land/map"]](#streamfantasy-landmap) - - [stream["fantasy-land/ap"]](#streamfantasy-landap) +- [Signature](#signature) + - [Static members](#static-members) + - [stream.combine](#streamcombine) + - [stream.merge](#streammerge) + - [stream.HALT](#streamhalt) + - [stream["fantasy-land/of"]](#streamfantasy-landof) + - [Instance members](#static-members) + - [stream.map](#streammap) + - [stream.end](#streamend) + - [stream["fantasy-land/of"]](#streamfantasy-landof) + - [stream["fantasy-land/map"]](#streamfantasy-landmap) + - [stream["fantasy-land/ap"]](#streamfantasy-landap) - [Basic usage](#basic-usage) - [Streams as variables](#streams-as-variables) - [Bidirectional bindings](#bidirectional-bindings) @@ -25,7 +25,7 @@ --- -### API +### Signature Creates a stream diff --git a/docs/testing.md b/docs/testing.md index cee5a78a..63f231cf 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,6 +1,6 @@ # Testing -Mithril comes with a testing framework called [ospec](../ospec/README.md). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). +Mithril comes with a testing framework called [ospec](https://github.com/lhorie/mithril.js/tree/rewrite/ospec). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). The easist way to setup the test runner is to create an NPM script for it. Open your project's `package.json` file and edit the `test` line under the `scripts` section: @@ -38,6 +38,8 @@ To run the test, use the command `npm test`. Ospec considers any Javascript file npm test ``` +--- + ### Good testing practices Generally speaking, there are two ways to write tests: upfront and after the fact. diff --git a/docs/trust.md b/docs/trust.md index efedf37a..53da8ed7 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -1,6 +1,6 @@ # trust(html) -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) - [Security considerations](#security-considerations) - [Scripts that do not run](#scripts-that-do-not-run) @@ -8,7 +8,7 @@ --- -### API +### Signature Generates a trusted HTML [vnode](vnodes.md) diff --git a/docs/version.md b/docs/version.md index 5cc6f219..683cd41e 100644 --- a/docs/version.md +++ b/docs/version.md @@ -1,11 +1,11 @@ # version -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature `m.version` diff --git a/docs/withAttr.md b/docs/withAttr.md index aff44df9..23ab5a1c 100644 --- a/docs/withAttr.md +++ b/docs/withAttr.md @@ -1,13 +1,13 @@ # withAttr(attrName, callback) -- [API](#api) +- [Signature](#signature) - [How to use](#how-to-use) - [Predictable event target](#predictable-event-target) - [Attributes and properties](#attributes-and-properties) --- -### API +### Signature Creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument. From 78929fb1d123b613aebfb870e192dff354da42c4 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Thu, 1 Dec 2016 06:51:39 +0000 Subject: [PATCH 19/60] Bundled output for commit 9f6a1086e46c30bfec30f78e6d48c918c00fcbf4 [skip ci] --- mithril.js | 2 +- mithril.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mithril.js b/mithril.js index cbc2ae2c..5ba769ea 100644 --- a/mithril.js +++ b/mithril.js @@ -1123,7 +1123,7 @@ m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0-rc.6" +m.version = "1.0.0-rc.5" if (typeof module !== "undefined") module["exports"] = m else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 8d83f61d..909ccbdd 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -37,4 +37,4 @@ function f(){switch(q.charAt(0)){case "#":return h("hash").slice(q.length);case q+f}var n="function"===typeof a.history.pushState,p="function"===typeof setImmediate?setImmediate:setTimeout,q="#!",t;return{setPrefix:function(a){q=a},getPath:f,setPath:k,defineRoutes:function(h,m,d){function k(a){var k=f(),l={},n=b(k,l,l),r;for(r in h){var p=new RegExp("^"+r.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(p.test(n)){n.replace(p,function(){for(var b=r.match(/:[^\/]+/g)||[],d=[].slice.call(arguments,1,-2),f=0;f Date: Thu, 1 Dec 2016 02:30:43 -0500 Subject: [PATCH 20/60] add note about unmount prevention --- docs/v1.x-migration.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index 6f4a415c..931f7689 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -15,6 +15,7 @@ - [`m.route` and anchor tags](#mroute-and-anchor-tags) - [Reading/writing the current route](#readingwriting-the-current-route) - [Accessing route params](#accessing-route-params) +- [Preventing unmounting](#preventing-unmounting) - [`m.request`](#mrequest) - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) @@ -408,6 +409,37 @@ m.route(document.body, "/booga", { --- +## Preventing unmounting + +It is no longer possible to prevent unmounting via `onunload`'s `e.preventDefault()`. Instead you should explicitly call `m.route.set` when the expected conditions are met. + +### `v0.2.x` + +```javascript +var Component = { + controller: function() { + this.onunload = function(e) { + if (condition) e.preventDefault() + } + }, + view: function() { + return m("a[href=/]", {config: m.route}) + } +} +``` + +### `v1.x` + +```javascript +var Component = { + view: function() { + return m("a", {onclick: function() {if (!condition) m.route.set("/")}}) + } +} +``` + +--- + ## m.request Promises returned by [m.request](request.md) are no longer `m.prop` getter-setters. In addition, `initialValue` is no longer a supported option. From f54f15d4ce748659dcdb9dbd53654ae55b71d143 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 2 Dec 2016 13:22:37 -0500 Subject: [PATCH 21/60] isolate render --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index f50049c4..d849a317 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ requestService.setCompletionCallback(redrawService.redraw) m.mount = require("./mount") m.route = require("./route") m.withAttr = require("./util/withAttr") -m.render = redrawService.render +m.render = require("./render").render m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp From 54599d9675731c025d8c6ec8f401e58191cdf984 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Fri, 2 Dec 2016 18:23:46 +0000 Subject: [PATCH 22/60] Bundled output for commit f54f15d4ce748659dcdb9dbd53654ae55b71d143 [skip ci] --- README.md | 2 +- mithril.js | 3 ++- mithril.min.js | 28 ++++++++++++++-------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 71c22280..e1be8c2b 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.41 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.42 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/mithril.js b/mithril.js index 5ba769ea..46f2ae23 100644 --- a/mithril.js +++ b/mithril.js @@ -1117,7 +1117,8 @@ m.withAttr = function(attrName, callback1, context) { return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } -m.render = redrawService.render +var _27 = coreRenderer(window) +m.render = _27.render m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp diff --git a/mithril.min.js b/mithril.min.js index 909ccbdd..7935777e 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,20 +1,20 @@ -new function(){function n(a,h,l,b,f,k){return{tag:a,key:h,attrs:l,children:b,text:f,dom:k,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var h,l,b=[],f={};h=N.exec(a);){var k=h[1],v=h[2];""===k&&""!==v?l=v:"#"===k?f.id=v:"."===k?b.push(v):"["===h[3][0]&&((k=h[6])&&(k=k.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), +new function(){function n(a,h,l,b,f,k){return{tag:a,key:h,attrs:l,children:b,text:f,dom:k,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var h,l,b=[],f={};h=O.exec(a);){var k=h[1],v=h[2];""===k&&""!==v?l=v:"#"===k?f.id=v:"."===k?b.push(v):"["===h[3][0]&&((k=h[6])&&(k=k.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), "class"===h[4]?b.push(k):f[h[4]]=k||!0)}0a.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== +l)})};"undefined"===typeof Promise&&("undefined"!==typeof window?window.Promise=t:"undefined"!==typeof global&&(global.Promise=t));var E=function(a){function h(a,b){if(b instanceof Array)for(var f=0;fa.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function q(a,b){if("function"===typeof a)if(b instanceof Array)for(var d=0;dm.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(H){h(H)}}; +r&&m.setRequestHeader("Content-Type","application/json; charset=utf-8");d.deserialize===v&&m.setRequestHeader("Accept","application/json, text/*");d.withCredentials&&(m.withCredentials=d.withCredentials);"function"===typeof d.config&&(m=d.config(m,d)||m);m.onreadystatechange=function(){if(4===m.readyState)try{var a=d.extract!==p?d.extract(m,d):d.deserialize(d.extract(m,d));if(200<=m.status&&300>m.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(G){h(G)}}; r&&null!=d.data?m.send(d.data):m.send()}))},jsonp:function(d){return b(new h(function(b,h){var m=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+n++,r=a.document.createElement("script");a[m]=function(f){r.parentNode.removeChild(r);b(q(d.type,f));delete a[m]};r.onerror=function(){r.parentNode.removeChild(r);h(Error("JSONP request failed"));delete a[m]};null==d.data&&(d.data={});d.url=f(d.url,d.data);d.data[d.callbackKey||"callback"]=m;r.src=k(d.url,d.data);a.document.documentElement.appendChild(r)}))}, -setCompletionCallback:function(a){m=a}}}(window,"undefined"!==typeof Promise?Promise:t),O=function(a){function h(g,c,a,b,d,f,h){for(;a=m&&u>=y;){var x=c[m],n=a[y];if(x!==n||e)if(null==x)m++;else if(null==n)y++;else if(x.key===n.key)m++,y++,k(g,x,n,d,p(c,m,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b);else if(x=c[z],x!==n||e)if(null==x)z--;else if(null==n)y++;else if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),(e||y= m&&u>=y;){x=c[z];n=a[u];if(x!==n||e)if(null==x)z--;else{if(null!=n)if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b),null!=x.dom&&(b=x.dom),z--;else{if(!w){w=c;var x=z,D={},t;for(t=0;t Date: Fri, 2 Dec 2016 10:43:47 -0800 Subject: [PATCH 23/60] Break out m.sync removal, it's important --- docs/v1.x-migration.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index 931f7689..ede1dc92 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -17,6 +17,7 @@ - [Accessing route params](#accessing-route-params) - [Preventing unmounting](#preventing-unmounting) - [`m.request`](#mrequest) +- [`m.sync` removed](#msync-removed) - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) - [`vnode` equality checks](#vnode-equality-checks) @@ -25,7 +26,7 @@ ## `m.prop` removed -In `v1.x`, `m.prop` is now a more powerful stream micro-library, but it's no longer part of core. +In `v1.x`, `m.prop()` is now a more powerful stream micro-library, but it's no longer part of core. ### `v0.2.x` @@ -475,7 +476,13 @@ setTimeout(function() { }, 1000) ``` -The equivalent of `m.sync` is now `Promise.all` +Additionally, if the `extract` option is passed to `m.request` the return value of the provided function will be used directly to resolve its promise, and the `deserialize` callback is ignored. + +--- + +## `m.sync` removed + +`m.sync` has been removed in favor of `Promise.all` ### `v0.2.x` @@ -501,8 +508,6 @@ Promise.all([ }) ``` -Additionally, if the `extract` option is passed to `m.request` the return value of the provided function will be used directly to resolve its promise, and the `deserialize` callback is ignored. - --- ## `xlink` namespace required From a07a662a821c4437c0b65aadeda9fb649ddba69e Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Fri, 2 Dec 2016 12:30:52 -0800 Subject: [PATCH 24/60] Document removal of sync redraw (#1447) --- docs/v1.x-migration.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index ede1dc92..38bb000a 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -21,6 +21,7 @@ - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) - [`vnode` equality checks](#vnode-equality-checks) +- [Synchronous redraw removed](#synchronous-redraw-removed) --- @@ -543,3 +544,21 @@ Arrays now represent [fragments](fragment.md), which are structurally significan ## `vnode` equality checks If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views). + +--- + +## Synchronous redraw removed + +In v0.2.x it was possible to force mithril to redraw immediately by passing a truthy value to `m.redraw()`. This behavior complicated usage of `m.redraw()` and caused some hard-to-reason about issues and has been removed. + +### `v0.2.x` + +```javascript +m.redraw(true); // redraws immediately & synchronously +``` + +### `v1.x` + +```javascript +m.redraw(); // schedules a redraw on the next requestAnimationFrame tick +``` From 0080d1dff081e2a40f74fa16161768c58f3251a0 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 2 Dec 2016 17:42:34 -0500 Subject: [PATCH 25/60] fix request redrawing semantics --- request/request.js | 50 ++++++++++++++++++++--------------- request/tests/test-request.js | 38 ++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/request/request.js b/request/request.js index a005f240..91bc55cd 100644 --- a/request/request.js +++ b/request/request.js @@ -5,33 +5,36 @@ var buildQueryString = require("../querystring/build") module.exports = function($window, Promise) { var callbackCount = 0 - var count = 0 var oncompletion function setCompletionCallback(callback) {oncompletion = callback} - function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} + function finalizer() { + var count = 0 + function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} - function finalize(promise) { - var then = promise.then - promise.then = function() { - count++ - var next = then.apply(promise, arguments) - next.then(complete, function(e) { - complete() - throw e - }) - return finalize(next) + return function finalize(promise) { + var then = promise.then, catcher = promise.catch + promise.then = function() { + count++ + var next = then.apply(promise, arguments) + next.then(complete, function(e) { + complete() + throw e + }) + return finalize(next) + } + return promise } - return promise } function request(args, extra) { - return finalize(new Promise(function(resolve, reject) { - if (typeof args === "string") { - var url = args - args = extra || {} - if (args.url == null) args.url = url - } + var finalize = finalizer() + if (typeof args === "string") { + var url = args + args = extra || {} + if (args.url == null) args.url = url + } + var promise = new Promise(function(resolve, reject) { if (args.method == null) args.method = "GET" args.method = args.method.toUpperCase() @@ -79,11 +82,13 @@ module.exports = function($window, Promise) { if (useBody && (args.data != null)) xhr.send(args.data) else xhr.send() - })) + }) + return args.redraw === false ? promise : finalize(promise) } function jsonp(args) { - return finalize(new Promise(function(resolve, reject) { + var finalize = finalizer() + var promise = new Promise(function(resolve, reject) { var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ var script = $window.document.createElement("script") $window[callbackName] = function(data) { @@ -101,7 +106,8 @@ module.exports = function($window, Promise) { args.data[args.callbackKey || "callback"] = callbackName script.src = assemble(args.url, args.data) $window.document.documentElement.appendChild(script) - })) + }) + return args.redraw === false? promise : finalize(promise) } function interpolate(url, data) { diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 272acd73..7ddc4325 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -6,10 +6,13 @@ var Request = require("../../request/request") var Promise = require("../../promise/promise") o.spec("xhr", function() { - var mock, xhr + var mock, xhr, complete o.beforeEach(function() { mock = xhrMock() - xhr = new Request(mock, Promise).request + var requestService = Request(mock, Promise) + xhr = requestService.request + complete = o.spy() + requestService.setCompletionCallback(complete) }) o.spec("success", function() { @@ -295,6 +298,37 @@ o.spec("xhr", function() { o(typeof xhr.send).equals("function") } }) + o("requests don't block each other", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 200, responseText: "[]"} + } + }) + xhr("/item").then(function() { + return xhr("/item") + }) + xhr("/item").then(function() { + return xhr("/item") + }) + setTimeout(function() { + o(complete.callCount).equals(4) + done() + }, 20) + }) + o("requests trigger finally once with a chained then", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 200, responseText: "[]"} + } + }) + var promise = xhr("/item") + promise.then(function() {}).then(function() {}) + promise.then(function() {}).then(function() {}) + setTimeout(function() { + o(complete.callCount).equals(1) + done() + }, 20) + }) }) o.spec("failure", function() { o("rejects on server error", function(done) { From cdb9017a728f3112d45054f4dad34fb329901458 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 2 Dec 2016 17:51:07 -0500 Subject: [PATCH 26/60] update docs, add descriptions for api methods --- docs/api.md | 190 +++++++++++++++++++++++++++++++++++---- docs/buildQueryString.md | 12 +++ docs/examples.md | 5 +- docs/fragment.md | 7 ++ docs/generate.js | 52 +++++++++++ docs/guides.md | 16 ++++ docs/hyperscript.md | 19 ++++ docs/introduction.md | 8 +- docs/jsonp.md | 18 ++++ docs/layout.html | 29 ++++++ docs/lib/prism/prism.css | 6 ++ docs/lib/prism/prism.js | 9 ++ docs/methods.md | 20 +++++ docs/mount.md | 22 +++++ docs/parseQueryString.md | 12 +++ docs/promise.md | 24 ++++- docs/redraw.md | 11 +++ docs/render.md | 12 +++ docs/request.md | 52 +++++++++-- docs/route.md | 19 ++++ docs/stream.md | 11 +++ docs/style.css | 45 ++++++++++ docs/trust.md | 11 ++- docs/withAttr.md | 37 ++++++-- package.json | 6 +- 25 files changed, 613 insertions(+), 40 deletions(-) create mode 100644 docs/generate.js create mode 100644 docs/guides.md create mode 100644 docs/layout.html create mode 100644 docs/lib/prism/prism.css create mode 100644 docs/lib/prism/prism.js create mode 100644 docs/methods.md create mode 100644 docs/style.css diff --git a/docs/api.md b/docs/api.md index 9b301bdb..77c2b0d8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,22 +1,174 @@ # API -- [m](hyperscript.md) -- [m.render](render.md) -- [m.mount](mount.md) -- [m.route](route.md) - - [m.route.set](route.md#routeset) - - [m.route.get](route.md#routeget) - - [m.route.prefix](route.md#routeprefix) - - [m.route.link](route.md#routelink) -- [m.request](request.md) -- [m.jsonp](jsonp.md) -- [m.parseQueryString](parseQueryString.md) -- [m.buildQueryString](buildQueryString.md) -- [m.withAttr](withAttr.md) -- [m.trust](trust.md) -- [m.fragment](fragment.md) -- [m.redraw](redraw.md) -- [m.version](version.md) +### Cheatsheet + +Here are examples for the most commonly used methods. If a method is not listed below, it's meant for advanced usage. + +#### m(selector, attrs, children) - [docs](hyperscript.md) + +```javascript +m("div.class#id", {title: "title"}, ["children"]) +``` + +--- + +#### m.mount(element, component) - [docs](mount.md) + +```javascript +var state = { + count: 0, + inc: function() {state.count++} +} + +var Counter = { + view: function() { + return m("div", {onclick: state.inc}, state.count) + } +} + +m.mount(document.body, Counter) +``` + +--- + +#### m.route(root, defaultRoute, routes) - [docs](route.md) + +```javascript +var Home = { + view: function() { + return "Welcome" + } +} + +m.route(document.body, "/home", { + "/home": Home, // defines `http://localhost/#!/home` +}) +``` + +#### m.route.set(path) - [docs](route.md#routeset) + +```javascript +m.route.set("/home") +``` + +#### m.route.get() - [docs](route.md#routeget) + +```javascript +var currentRoute = m.route.get() +``` + +#### m.route.prefix(prefix) - [docs](route.md#routeprefix) + +Call this before `m.route()` + +```javascript +m.route.prefix("#!") +``` + +#### m.route.link() - [docs](route.md#routelink) + +```javascript +m("a[href='/Home']", {oncreate: m.route.link}, "Go to home page") +``` + +--- + +#### m.request(options) - [docs](request.md) + +```javascript +m.request({ + method: "PUT", + url: "/api/v1/users/:id", + data: {id: 1, name: "test"} +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +#### m.jsonp(options) - [docs](jsonp.md) + +```javascript +m.jsonp({ + url: "/api/v1/users/:id", + data: {id: 1}, + callbackKey: "callback", +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +#### m.parseQueryString(querystring) - [docs](parseQueryString.md) + +```javascript +var object = m.parseQueryString("a=1&b=2") +// {a: "1", b: "2"} +``` + +--- + +#### m.buildQueryString(object) - [docs](buildQueryString.md) + +```javascript +var querystring = m.buildQueryString({a: "1", b: "2"}) +// "a=1&b=2" +``` + +--- + +#### m.withAttr(attrName, callback) - [docs](withAttr.md) + +```javascript +var state: { + value = "" + setValue: function(v) {value = v} +} + +var Component = { + view: function() { + return m("input", { + oninput: m.withAttr("value", state.setValue), + value: state.value, + }) + } +} + +m.mount(document.body, Component) +``` + +--- + +#### m.trust(htmlString) - [docs](trust.md) + +```javascript +m.render(document.body, m.trust("

Hello

")) +``` + +--- + +#### m.redraw() - [docs](redraw.md) + +```javascript +var count = 0 +function inc() { + setInterval(function() { + count++ + m.redraw() + }, 1000) +} + +var Counter = { + oninit: inc, + view: function() { + return m("div", count) + } +} + +m.mount(document.body, Counter) +``` -- [Promise](promise.md) -- [Stream](stream.md) \ No newline at end of file diff --git a/docs/buildQueryString.md b/docs/buildQueryString.md index 0588da00..03f723fa 100644 --- a/docs/buildQueryString.md +++ b/docs/buildQueryString.md @@ -1,10 +1,22 @@ # buildQueryString(object) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) --- +### Description + +Turns an object into a string of form `a=1&b=2` + +```javascript +var querystring = m.buildQueryString({a: "1", b: "2"}) +// "a=1&b=2" +``` + +--- + ### Signature `querystring = m.buildQueryString(object)` diff --git a/docs/examples.md b/docs/examples.md index d52df077..140ad3f6 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,8 +1,11 @@ # Examples +Here are some examples of Mithril in action + - [Animation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/animation/mosaic.html) - [DBMonster](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) - [Markdown Editor](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/editor/index.html) - SVG: [Clock](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/tiger.html) - [ThreadItJS](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/threaditjs/index.html) -- [TodoMVC](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/todomvc/index.html) \ No newline at end of file +- [TodoMVC](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/todomvc/index.html) + diff --git a/docs/fragment.md b/docs/fragment.md index 4b180a91..cd1ff53a 100644 --- a/docs/fragment.md +++ b/docs/fragment.md @@ -1,10 +1,17 @@ # fragment(attrs, children) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) --- +### Description + +Allows attaching lifecycle methods to a fragment [vnode](vnodes.md) + +--- + ### Signature Generates a fragment [vnode](vnodes.md) diff --git a/docs/generate.js b/docs/generate.js new file mode 100644 index 00000000..797ffbfd --- /dev/null +++ b/docs/generate.js @@ -0,0 +1,52 @@ +var fs = require("fs") +var path = require("path") +var marked = require("marked") +var layout = fs.readFileSync("./docs/layout.html", "utf-8") +var version = JSON.parse(fs.readFileSync("./package.json", "utf-8")).version +try {fs.mkdirSync("docs/archive/" + version)} catch (e) {} +try {fs.mkdirSync("docs/archive/" + version + "/lib")} catch (e) {} +try {fs.mkdirSync("docs/archive/" + version + "/lib/prism")} catch (e) {} + +var guides = fs.readFileSync("docs/guides.md", "utf-8") +var methods = fs.readFileSync("docs/methods.md", "utf-8") + +generate("docs") + +function generate(pathname) { + if (fs.lstatSync(pathname).isDirectory()) { + fs.readdirSync(pathname).forEach(function(filename) { + generate(pathname + "/" + filename) + }) + } + else if (!pathname.match(/tutorials|archive/)) { + if (pathname.match(/\.md$/)) { + var outputFilename = pathname.replace(/\.md$/, ".html") + var markdown = fs.readFileSync(pathname, "utf-8") + var fixed = markdown + .replace(/(`[^`]+?)<(.*`)/gim, "$1<$2") // fix generic syntax + .replace(/`((?:\S| -> |, )+)(\|)(\S+)`/gim, function(match, a, b, c) { // fix pipes in code tags + return "" + (a + b + c).replace(/\|/g, "|") + "" + }) + .replace(/(^# .+?(?:\r?\n){2,}?)(?:(-(?:.|\r|\n)+?)((?:\r?\n){2,})|)/m, function(match, title, nav, space) { // inject menu + var file = path.basename(pathname) + var link = new RegExp("([ \t]*)(- )(\\[.+?\\]\\(" + file + "\\))") + var replace = function(match, space, li, link) { + return space + li + "**" + link + "**" + (nav ? "\n" + nav.replace(/(^|\n)/g, "$1\t" + space) : "") + } + var modified = guides.match(link) ? guides.replace(link, replace) : methods.replace(link, replace) + return title + modified + "\n\n" + }) + .replace(/\.md/gim, ".html") // fix links + var html = layout + .replace(/\[body\]/, marked(fixed)) + .replace(/
([^<]+?)<\/h5>/gim, function(match, id, text) { // fix anchors + return "
" + text + "
" + }) + fs.writeFileSync("docs/archive/" + version + "/" + outputFilename.replace(/^docs\//, ""), html, "utf-8") + } + else { + fs.writeFileSync("docs/archive/" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") + } + } +} + diff --git a/docs/guides.md b/docs/guides.md new file mode 100644 index 00000000..f7a7f0b9 --- /dev/null +++ b/docs/guides.md @@ -0,0 +1,16 @@ +- Tutorials + - [Installation](installation.md) + - [Introduction](introduction.md) + - [Tutorial](tutorial.md) + - [Testing](testing.md) + - [Examples](examples.md) +- Key concepts + - [Vnodes](vnodes.md) + - [Components](components.md) + - [Lifecycle methods](lifecycle-methods.md) + - [Keys](keys.md) +- Migration + - [Migrating from 0.2.x](migration.md) +- Meta + - [Chat room](https://gitter.im/lhorie/mithril.js) + - [Credits](credits.md) \ No newline at end of file diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 44190ec4..9d608dcd 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -1,5 +1,6 @@ # m(selector, attributes, children) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Flexibility](#flexibility) @@ -17,6 +18,24 @@ --- +### Description + +Represents an HTML element in a Mithril view + +```javascript +m("div", {class: "foo"}, "hello") +// represents
hello
+``` + +You can also [use HTML syntax](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E) via a Babel plugin. + +```markup +/** jsx m */ +
hello
+``` + +--- + ### Signature `vnode = m(selector, attributes, children)` diff --git a/docs/introduction.md b/docs/introduction.md index 88e5e2a8..52c12158 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -12,13 +12,13 @@ ### What is Mithril? -Mithril is a framework for developing Javascript-based Single Page Applications. It's designed to be fast, small and economical. +Mithril is a framework for building Single Page Applications. It's small but batteries-included. --- ### Getting started -The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. 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 but it'll only take 10 minutes. Let's create an HTML file to follow along: @@ -84,6 +84,8 @@ m("main", [ ]) ``` +Note: If you would rather use `` syntax, [you can do so by using Babel](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E). + --- ### Components @@ -180,7 +182,7 @@ Basically, XHR is just a way to talk to a server. Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial. -First we create a function that calls `m.request`. +First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `data` is the payload that we're sending to the endpoint and `useCredentials` means to enable cookies (a requirement for the REM API to work) ```javascript var count = 0 diff --git a/docs/jsonp.md b/docs/jsonp.md index cf760338..f0ee9b38 100644 --- a/docs/jsonp.md +++ b/docs/jsonp.md @@ -1,11 +1,29 @@ # jsonp(options) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) --- +### Description + +Makes JSON-P requests. Typically, it's useful to interact with servers that allow JSON-P but that don't have CORS enabled. + +```javascript +m.jsonp({ + url: "/api/v1/users/:id", + data: {id: 1}, + callbackKey: "callback", +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + ### Signature `promise = m.jsonp(options)` diff --git a/docs/layout.html b/docs/layout.html new file mode 100644 index 00000000..6e96bb1b --- /dev/null +++ b/docs/layout.html @@ -0,0 +1,29 @@ + + + + Mithril.js + + + + + +
+
+

Mithril

+ +
+
+
+
+ [body] +
+ License: MIT. © Leo Horie. +
+
+ + +e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; +Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; +Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; +; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 00000000..9b19b4cc --- /dev/null +++ b/docs/methods.md @@ -0,0 +1,20 @@ +- Core + - [m](hyperscript.md) + - [m.render](render.md) + - [m.mount](mount.md) + - [m.route](route.md) + - [m.request](request.md) + - [m.jsonp](jsonp.md) + - [m.parseQueryString](parseQueryString.md) + - [m.buildQueryString](buildQueryString.md) + - [m.withAttr](withAttr.md) + - [m.trust](trust.md) + - [m.fragment](fragment.md) + - [m.redraw](redraw.md) + - [m.version](version.md) + - [Promise](promise.md) +- Optional + - [Stream](stream.md) +- Tooling + - [Bundler](bundler.md) + - [Ospec](ospec.md) diff --git a/docs/mount.md b/docs/mount.md index 857c0018..a188668b 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -1,5 +1,6 @@ # mount(root, component) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Performance considerations](#performance-considerations) @@ -7,6 +8,27 @@ --- +### Description + +Activates a component, enabling it to autoredraw on user events + +```javascript +var state = { + count: 0, + inc: function() {state.count++} +} + +var Counter = { + view: function() { + return m("div", {onclick: state.inc}, state.count) + } +} + +m.mount(document.body, Counter) +``` + +--- + ### Signature `m.mount(element, component)` diff --git a/docs/parseQueryString.md b/docs/parseQueryString.md index c27f01ed..e3d23dcb 100644 --- a/docs/parseQueryString.md +++ b/docs/parseQueryString.md @@ -1,10 +1,22 @@ # parseQueryString(string) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) --- +### Description + +Turns a string of the form `?a=1&b=2` to an object + +```javascript +var object = m.parseQueryString("a=1&b=2") +// {a: "1", b: "2"} +``` + +--- + ### Signature `object = m.parseQueryString(string)` diff --git a/docs/promise.md b/docs/promise.md index 7a8a71d5..119d3a51 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -1,5 +1,6 @@ # Promise(executor) +- [Description](#description) - [Signature](#signature) - [Static members](#static-members) - [Promise.resolve](#promiseresolve) @@ -14,7 +15,16 @@ - [Promise absorption](#promise-absorption) - [Error handling](#error-handling) - [Shorthands](#shorthands) -- [Waiting for multiple promises](#waiting-for-multiple-promises) +- [Multiple promises](#multiple-promises) +- [Why not callbacks](#why-not-callbacks) + +--- + +### Description + +A [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) polyfill. + +A Promise is a mechanism for working with asynchronous computations. --- @@ -262,7 +272,7 @@ promise --- -### Waiting for multiple promises +### Multiple promises In some occasions, you may need to make HTTP requests in parallel, and run code after all requests complete. This can be accomplished by `Promise.all` @@ -287,3 +297,13 @@ Promise.all([ In the example above, there are two user searches happening in parallel. Once they both complete, we take the names of all the users and alert them. This example also illustrates another benefit of smaller functions: we reused the `getUserNames` function we had created above. + +--- + +### Why not callbacks + +Callbacks are another mechanism for working with asynchrounous computations, and are indeed more adequate to use if an asynchronous computation may occur more than one time (for example, an `onscroll` event handler). + +However, for asynchronous computations that only occur once in response to an action, promises can be refactored more effectively, reducing code smells known as pyramids of doom (deeply nested series of callbacks with unmanaged state being used across several closure levels). + +In addition, promises can considerably reduce boilerplate related to error handling. \ No newline at end of file diff --git a/docs/redraw.md b/docs/redraw.md index 3be49fa6..ebc17283 100644 --- a/docs/redraw.md +++ b/docs/redraw.md @@ -1,10 +1,21 @@ # redraw() +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) --- +### Description + +Updates the DOM after a change in the application data layer. + +You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril view, or after request completion when using `m.request`/`m.jsonp`. + +You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries. + +--- + ### Signature `m.redraw()` diff --git a/docs/render.md b/docs/render.md index f3cf38fd..3218c761 100644 --- a/docs/render.md +++ b/docs/render.md @@ -1,5 +1,6 @@ # render(element, vnodes) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Why Virtual DOM](#why-virtual-dom) @@ -8,6 +9,17 @@ --- +### Description + +Renders a template to the DOM + +```javascript +m.render(document.body, "hello") +// hello +``` + +--- + ### Signature `m.render(element, vnodes)` diff --git a/docs/request.md b/docs/request.md index d97432cb..cbd887a6 100644 --- a/docs/request.md +++ b/docs/request.md @@ -1,5 +1,6 @@ # request(options) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) @@ -12,7 +13,25 @@ - [Non-JSON responses](#non-json-responses) - [Retrieving response details](#retrieving-response-details) - [Why JSON instead of HTML](#why-json-instead-of-html) -- [Why XMLHttpRequest instead of fetch](#why-xmlhttprequest-instead-of-fetch) +- [Why XHR instead of fetch](#why-xhr-instead-of-fetch) +- [Avoid anti-patterns](#avoid-anti-patterns) + +--- + +### Description + +Makes XHR (aka AJAX) requests, and returns a [promise](promise.md) + +```javascript +m.request({ + method: "PUT", + url: "/api/v1/users/:id", + data: {id: 1, name: "test"} +}) +.then(function(result) { + console.log(result) +}) +``` --- @@ -422,7 +441,7 @@ Data services may be organized in many different ways depending on the nature of --- -### Why XMLHttpRequest instead of fetch +### Why XHR instead of fetch [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is a newer Web API for fetching resources from servers, similar to `XMLHttpRequest`. @@ -431,11 +450,13 @@ Mithril's `m.request` uses `XMLHttpRequest` instead of `fetch()` for a number of - `fetch` is not fully standardized yet, and may be subject to specification changes. - `XMLHttpRequest` calls can be aborted before they resolve (e.g. to avoid race conditions in for instant search UIs). - `XMLHttpRequest` provides hooks for progress listeners for long running requests (e.g. file uploads). -- `XMLHttpRequest` is supported by all browsers, whereas `fetch()` is not supported by Internet Explorer and Safari. +- `XMLHttpRequest` is supported by all browsers, whereas `fetch()` is not supported by Internet Explorer, Safari and Android (non-Chromium). -Currently, due to lack of browser support, `fetch()` typically requires a [polyfill](https://github.com/github/fetch), which is over 11kb uncompressed - nearly three times larger than Mithril's `m.request`. +Currently, due to lack of browser support, `fetch()` typically requires a [polyfill](https://github.com/github/fetch), which is over 11kb uncompressed - nearly three times larger than Mithril's XHR module. -Despite being much smaller, `m.request` supports many important and not-so-trivial-to-implement features like [URL interpolation](#dynamic-urls), querystring serialization and [JSON-P requests](jsonp.md). The `fetch` polyfill does not support any of those. +Despite being much smaller, Mithril's XHR module supports many important and not-so-trivial-to-implement features like [URL interpolation](#dynamic-urls), querystring serialization and [JSON-P requests](jsonp.md), in addition to its ability to integrate seamlessly to Mithril's autoredrawing subsystem. The `fetch` polyfill does not support any of those, and requires extra libraries and boilerplates to achieve the same level of functionality. + +In addition, Mithril's XHR module is optimized for JSON-based endpoints and makes that most common case appropriately terse - i.e. `m.request(url)` - whereas `fetch` requires an additional explicit step to parse the response data as JSON: `fetch(url).then(function(response) {return response.json()})` The `fetch()` API does have a few technical advantages over `XMLHttpRequest` in a few uncommon cases: @@ -443,3 +464,24 @@ The `fetch()` API does have a few technical advantages over `XMLHttpRequest` in - it integrates to the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which provides an extra layer of control over how and when network requests happen. This API also allows access to push notifications and background synchronization features. In typical scenarios, streaming won't provide noticeable performance benefits because it's generally not advisable to download megabytes of data to begin with. Also, the memory gains from repeatedly reusing small buffers may be offset or nullified if they result in excessive browser repaints. For those reasons, choosing `fetch()` streaming instead of `m.request` is only recommended for extremely resource intensive applications. + +--- + +### Avoid anti-patterns + +#### Promises are not the response data + +The `m.request` method returns a [Promise](promise.md), not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if Javascript waited for it, it would freeze the application until the data was available. + +```javascript +// AVOID +var users = m.request("/api/v1/users") +console.log("list of users:", users) +// `users` is NOT a list of users, it's a promise + +// PREFER +m.request("/api/v1/users").then(function(users) { + console.log("list of users:", users) +}) +``` + diff --git a/docs/route.md b/docs/route.md index 50b59bc4..b16d1336 100644 --- a/docs/route.md +++ b/docs/route.md @@ -1,5 +1,6 @@ # route(root, defaultRoute, routes) +- [Description](#description) - [Signature](#signature) - [Static members](#static-members) - [route.set](#routeset) @@ -21,6 +22,24 @@ --- +### Description + +Navigate between "pages" within an application + +```javascript +var Home = { + view: function() { + return "Welcome" + } +} + +m.route(document.body, "/home", { + "/home": Home, // defines `http://localhost/#!/home` +}) +``` + +--- + ### Signature `m.route(root, defaultRoute, routes)` diff --git a/docs/stream.md b/docs/stream.md index 12dd85ed..7b6110f8 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -1,5 +1,6 @@ # stream() +- [Description](#description) - [Signature](#signature) - [Static members](#static-members) - [stream.combine](#streamcombine) @@ -25,6 +26,16 @@ --- +### Description + +A Stream is a reactive data structure, similar to cells in spreadsheet applications. + +For example, in a spreadsheet, if `A1 = B1 + C1`, then changing the value of `B1` or `C1` automatically changes the value of `A1`. + +Similarly, you can make a stream depend on other streams so that changing the value of one automatically updates the other. This is useful when you have very expensive computations and want to only run them when necessary, as opposed to, say, on every redraw. + +--- + ### Signature Creates a stream diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 00000000..faef030c --- /dev/null +++ b/docs/style.css @@ -0,0 +1,45 @@ +body,table,h5 {font:normal 16px 'Open Sans';} +header,main {margin:auto;max-width:1000px;} +header section {position:absolute;width:250px;} +nav a {border-left:1px solid #ddd;padding:0 10px;} +nav a:first-child {border:0;padding-left:0;} +main {margin-bottom:100px;} +main section {margin-left:270px;} +h1 {margin:0 0 15px;} +h5 {font-style:italic;} +pre,code {background:#eee;font-family:monospace;} +pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;} +code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:3px;white-space:pre;} +pre code {border:0;margin:0;padding:0;} +table {border-collapse:collapse;width:100%;} +tbody tr:nth-child(odd) {background:#fafafa;} +thead tr,tbody tr:nth-child(even) {background:#f3f3f3;} +th {text-align:left;} +th,td {padding:3px 10px;vertical-align:top;} +a {color:#1e5799;display:inline-block;text-decoration:none;} +a:hover {text-decoration:underline;} +hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;} + +#signature + p code {padding:3px 10px;} +h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;} +h1 + ul + hr {display:none;} +h1 + ul li {border-bottom:1px solid #eee;list-style:none;margin:0;padding:0;} +h1 + ul li:last-child {border-bottom:0;} +h1 + ul ul {margin:0 0 10px;padding:0 0 0 15px;} +h1 + ul ul li {border:0;} + +@media (max-width: 767px) { + main section {margin:0;} + h1 + ul + hr {display:block;} + header section,h1 + ul {margin:0 0 20px;position:static;width:auto;} +} +@media (max-width: 1024px) { + #signature + p + table,#signature + p + table tbody,#signature + p + table tr,#signature + p + table th,#signature + p + table td {display:block;} + #signature + p + table thead {display:none;} + #signature + p + table td:before {display:inline-block;font-style:italic;padding:0 10px 0 0;width:100px;} + #signature + p + table tr:not(:last-child) td:nth-child(1):before {content:"Argument:";} + #signature + p + table td:nth-child(2):before {content:"Type:";} + #signature + p + table td:nth-child(3):before {content:"Required:";} + #signature + p + table td:nth-child(4):before {content:"Description:";} + #signature + p + table tr:last-child td:nth-child(3) {display:none;} +} diff --git a/docs/trust.md b/docs/trust.md index 53da8ed7..d960e743 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -1,5 +1,6 @@ # trust(html) +- [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) - [Security considerations](#security-considerations) @@ -8,9 +9,15 @@ --- -### Signature +### Description -Generates a trusted HTML [vnode](vnodes.md) +Turns an HTML string into unescaped HTML. **Do not use `m.trust` on unsanitized user input.** + +Always try to use an [alternative method](#avoid-trusting-html) first, before considering using `m.trust`. + +--- + +### Signature `vnode = m.trust(html)` diff --git a/docs/withAttr.md b/docs/withAttr.md index 23ab5a1c..9fd7a7df 100644 --- a/docs/withAttr.md +++ b/docs/withAttr.md @@ -1,18 +1,39 @@ # withAttr(attrName, callback) +- [Description](#description) - [Signature](#signature) -- [How to use](#how-to-use) +- [How it works](#how-it-works) - [Predictable event target](#predictable-event-target) - [Attributes and properties](#attributes-and-properties) --- +### Description + +Returns an event handler that runs `callback` with the value of the specified DOM attribute + +```javascript +var state: { + value = "" + setValue: function(v) {value = v} +} + +var Component = { + view: function() { + return m("input", { + oninput: m.withAttr("value", state.setValue), + value: state.value, + }) + } +} + +m.mount(document.body, Component) +``` + +--- + ### Signature -Creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument. - -This helper function is provided to help decouple the browser's event model from application code. - `m.withAttr(attrName, callback, thisArg?)` Argument | Type | Required | Description @@ -26,7 +47,11 @@ Argument | Type | Required | Description --- -### How to use +### How it works + +The `m.withAttr` method creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument. + +This helper function is provided to help decouple the browser's event model from application code. ```javascript // standalone usage diff --git a/package.json b/package.json index c34192fe..a046b2d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mithril", - "version": "1.0.0-rc.5", + "version": "1.0.0-rc.6", "description": "A framework for building brilliant applications", "author": "Leo Horie", "license": "MIT", @@ -12,13 +12,15 @@ "build-browser": "node bundler/cli browser.js -o mithril.js", "build-min": "node bundler/cli browser.js -o mithril.min.js -m", "lintdocs": "node docs/lint", + "gendocs": "node docs/generate", "lint": "eslint .", "test": "node ospec/bin/ospec", "cover": "istanbul cover --print both ospec/bin/ospec" }, "devDependencies": { "eslint": "^2.10.2", - "istanbul": "^0.4.3" + "istanbul": "^0.4.3", + "marked": "^0.3.6" }, "publishConfig": { "tag": "rewrite" From 3ac9f954d6a4df4626cd30304eceeafb03115c47 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Fri, 2 Dec 2016 22:52:04 +0000 Subject: [PATCH 27/60] Bundled output for commit cdb9017a728f3112d45054f4dad34fb329901458 [skip ci] --- README.md | 2 +- mithril.js | 52 +++++++++++++++++--------------- mithril.min.js | 80 +++++++++++++++++++++++++------------------------- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index e1be8c2b..0256ede0 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.42 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.45 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/mithril.js b/mithril.js index 46f2ae23..10f5ef56 100644 --- a/mithril.js +++ b/mithril.js @@ -202,31 +202,34 @@ var buildQueryString = function(object) { } var _8 = function($window, Promise) { var callbackCount = 0 - var count = 0 var oncompletion function setCompletionCallback(callback) {oncompletion = callback} - function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} - function finalize(promise0) { - var then0 = promise0.then - promise0.then = function() { - count++ - var next = then0.apply(promise0, arguments) - next.then(complete, function(e) { - complete() - throw e - }) - return finalize(next) + function finalizer() { + var count = 0 + function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} + return function finalize(promise0) { + var then0 = promise0.then, catcher = promise0.catch + promise0.then = function() { + count++ + var next = then0.apply(promise0, arguments) + next.then(complete, function(e) { + complete() + throw e + }) + return finalize(next) + } + return promise0 } - return promise0 } function request(args, extra) { - return finalize(new Promise(function(resolve, reject) { - if (typeof args === "string") { - var url = args - args = extra || {} - if (args.url == null) args.url = url - } + var finalize = finalizer() + if (typeof args === "string") { + var url = args + args = extra || {} + if (args.url == null) args.url = url + } + var promise0 = new Promise(function(resolve, reject) { if (args.method == null) args.method = "GET" args.method = args.method.toUpperCase() var useBody = typeof args.useBody === "boolean" ? args.useBody : args.method !== "GET" && args.method !== "TRACE" @@ -266,10 +269,12 @@ var _8 = function($window, Promise) { } if (useBody && (args.data != null)) xhr.send(args.data) else xhr.send() - })) + }) + return args.redraw === false ? promise0 : finalize(promise0) } function jsonp(args) { - return finalize(new Promise(function(resolve, reject) { + var finalize = finalizer() + var promise0 = new Promise(function(resolve, reject) { var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ var script = $window.document.createElement("script") $window[callbackName] = function(data) { @@ -287,7 +292,8 @@ var _8 = function($window, Promise) { args.data[args.callbackKey || "callback"] = callbackName script.src = assemble(args.url, args.data) $window.document.documentElement.appendChild(script) - })) + }) + return args.redraw === false? promise0 : finalize(promise0) } function interpolate(url, data) { if (data == null) return url @@ -1124,7 +1130,7 @@ m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0-rc.5" +m.version = "1.0.0-rc.6" if (typeof module !== "undefined") module["exports"] = m else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 7935777e..cec7cb88 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,40 +1,40 @@ -new function(){function n(a,h,l,b,f,k){return{tag:a,key:h,attrs:l,children:b,text:f,dom:k,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var h,l,b=[],f={};h=O.exec(a);){var k=h[1],v=h[2];""===k&&""!==v?l=v:"#"===k?f.id=v:"."===k?b.push(v):"["===h[3][0]&&((k=h[6])&&(k=k.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===h[4]?b.push(k):f[h[4]]=k||!0)}0a.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== -a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function q(a,b){if("function"===typeof a)if(b instanceof Array)for(var d=0;dm.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(G){h(G)}}; -r&&null!=d.data?m.send(d.data):m.send()}))},jsonp:function(d){return b(new h(function(b,h){var m=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+n++,r=a.document.createElement("script");a[m]=function(f){r.parentNode.removeChild(r);b(q(d.type,f));delete a[m]};r.onerror=function(){r.parentNode.removeChild(r);h(Error("JSONP request failed"));delete a[m]};null==d.data&&(d.data={});d.url=f(d.url,d.data);d.data[d.callbackKey||"callback"]=m;r.src=k(d.url,d.data);a.document.documentElement.appendChild(r)}))}, -setCompletionCallback:function(a){m=a}}}(window,"undefined"!==typeof Promise?Promise:t),N=function(a){function h(g,c,a,b,d,f,h){for(;a=m&&u>=y;){var x=c[m],n=a[y];if(x!==n||e)if(null==x)m++;else if(null==n)y++;else if(x.key===n.key)m++,y++,k(g,x,n,d,p(c,m,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b);else if(x=c[z],x!==n||e)if(null==x)z--;else if(null==n)y++;else if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),(e||y= -m&&u>=y;){x=c[z];n=a[u];if(x!==n||e)if(null==x)z--;else{if(null!=n)if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b),null!=x.dom&&(b=x.dom),z--;else{if(!w){w=c;var x=z,D={},t;for(t=0;tb.indexOf("?")?"?":"&";b+=d+c}return b} +function h(b){try{return""!==b?JSON.parse(b):null}catch(l){throw Error(b);}}function q(b){return b.responseText}function p(b,a){if("function"===typeof b)if(a instanceof Array)for(var c=0;cm.status||304===m.status)f(p(b.type,a));else{var c=Error(m.responseText), +d;for(d in a)c[d]=a[d];l(c)}}catch(G){l(G)}};g&&null!=b.data?m.send(b.data):m.send()});return!1===b.redraw?t:m(t)},jsonp:function(b){var l=g(),h=new f(function(f,h){var l=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,g=a.document.createElement("script");a[l]=function(c){g.parentNode.removeChild(g);f(p(b.type,c));delete a[l]};g.onerror=function(){g.parentNode.removeChild(g);h(Error("JSONP request failed"));delete a[l]};null==b.data&&(b.data={});b.url=c(b.url,b.data);b.data[b.callbackKey|| +"callback"]=l;g.src=d(b.url,b.data);a.document.documentElement.appendChild(g)});return!1===b.redraw?h:l(h)},setCompletionCallback:function(a){r=a}}}(window,"undefined"!==typeof Promise?Promise:w),N=function(a){function f(e,k,a,b,c,d,f){for(;a=B&&x>=y;){var v=k[B],r=a[y];if(v!==r||n)if(null==v)B++;else if(null==r)y++;else if(v.key===r.key)B++,y++,h(e,v,r,c,p(k,B,d),n,l),n&&v.tag===r.tag&&m(e,q(v),d);else if(v=k[t],v!==r||n)if(null==v)t--;else if(null==r)y++;else if(v.key===r.key)h(e,v,r,c,p(k, +t+1,d),n,l),(n||y=B&&x>=y;){v=k[t];r=a[x];if(v!==r||n)if(null==v)t--;else{if(null!=r)if(v.key===r.key)h(e,v,r,c,p(k,t+1,d),n,l),n&&v.tag===r.tag&&m(e,q(v),d),null!=v.dom&&(d=v.dom),t--;else{if(!E){E=k;var v=t,u={},C;for(C=0;C Date: Fri, 2 Dec 2016 20:40:38 -0500 Subject: [PATCH 28/60] ensure docs/generate doesn't break on first try --- docs/generate.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/generate.js b/docs/generate.js index 797ffbfd..f8763522 100644 --- a/docs/generate.js +++ b/docs/generate.js @@ -3,6 +3,7 @@ var path = require("path") var marked = require("marked") var layout = fs.readFileSync("./docs/layout.html", "utf-8") var version = JSON.parse(fs.readFileSync("./package.json", "utf-8")).version +try {fs.mkdirSync("docs/archive/")} catch (e) {} try {fs.mkdirSync("docs/archive/" + version)} catch (e) {} try {fs.mkdirSync("docs/archive/" + version + "/lib")} catch (e) {} try {fs.mkdirSync("docs/archive/" + version + "/lib/prism")} catch (e) {} From 2f7c4983f33580bebe48fdbb5fdeb513114bb2e8 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 2 Dec 2016 20:41:07 -0500 Subject: [PATCH 29/60] ensure null doesn't displace next siblings --- render/render.js | 11 +++++++++-- render/tests/test-updateNodes.js | 13 +++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/render/render.js b/render/render.js index c31a79ce..9a579a66 100644 --- a/render/render.js +++ b/render/render.js @@ -124,9 +124,16 @@ module.exports = function($window) { else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) else { - if (old.length === vnodes.length && vnodes[0] != null && vnodes[0].key == null) { + var isUnkeyed = false + for (var i = 0; i < vnodes.length; i++) { + if (vnodes[i] != null) { + isUnkeyed = vnodes[i].key == null + break + } + } + if (old.length === vnodes.length && isUnkeyed) { for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i] || old[i] == null && vnodes[i] == null) continue + if (old[i] === vnodes[i]) continue else if (old[i] == null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index e56a828c..8614b162 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -879,4 +879,17 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) + o("null stays in place", function() { + var vnodes = [{tag: "div"}, {tag: "a"}] + var temp = [null, {tag: "a"}] + var updated = [{tag: "div"}, {tag: "a"}] + + render(root, vnodes) + var before = vnodes[1].dom + render(root, temp) + render(root, updated) + var after = updated[1].dom + + o(before).equals(after) + }) }) From 4a4bd7a5483b71731abed5dd9901c02ceea088e5 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 2 Dec 2016 20:44:03 -0500 Subject: [PATCH 30/60] use animation best practices in dbmon --- examples/dbmonster/angular/app.js | 3 +- examples/dbmonster/mithril-0.2.x/app.js | 44 - examples/dbmonster/mithril-0.2.x/index.html | 17 - .../dbmonster/mithril-0.2.x/mithril-0.2.4.js | 2190 ----------------- examples/dbmonster/mithril/app.js | 4 +- examples/dbmonster/react/app.js | 5 +- examples/dbmonster/vue/app.js | 82 +- examples/dbmonster/vue/index.html | 2 +- 8 files changed, 48 insertions(+), 2299 deletions(-) delete mode 100644 examples/dbmonster/mithril-0.2.x/app.js delete mode 100644 examples/dbmonster/mithril-0.2.x/index.html delete mode 100644 examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js diff --git a/examples/dbmonster/angular/app.js b/examples/dbmonster/angular/app.js index 30ffd4f8..7e317be5 100644 --- a/examples/dbmonster/angular/app.js +++ b/examples/dbmonster/angular/app.js @@ -32,9 +32,10 @@ var AppComponent = ng.core.Component({selector: "my-app"}) this.update() }, update: function() { + requestAnimationFrame(function() {self.update()}) + var self = this self.databases = ENV.generateData().toArray() - setTimeout(function() {self.update()}, ENV.timeout) if (renderStage === 0) { renderStage = 1 diff --git a/examples/dbmonster/mithril-0.2.x/app.js b/examples/dbmonster/mithril-0.2.x/app.js deleted file mode 100644 index 56ea6006..00000000 --- a/examples/dbmonster/mithril-0.2.x/app.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict" - -var data = [] - -var root = document.getElementById("app") -update() - -function update() { - data = ENV.generateData().toArray() - - Monitoring.renderRate.ping() - - m.redraw(); - - setTimeout(update, ENV.timeout) -} - -m.mount(root, { - view : function() { - return m("div", [ - m("table", { className: "table table-striped latest-data" }, [ - m("tbody", - data.map(function(db) { - return m("tr", {key: db.dbname}, [ - m("td", { className: "dbname" }, db.dbname), - m("td", { className: "query-count" }, [ - m("span", { className: db.lastSample.countClassName }, db.lastSample.nbQueries) - ]), - db.lastSample.topFiveQueries.map(function(query) { - return m("td", { className: query.elapsedClassName }, [ - m("span", query.formatElapsed), - m("div", { className: "popover left" }, [ - m("div", { className: "popover-content" }, query.query), - m("div", { className: "arrow" }) - ]) - ]) - }) - ]) - }) - ) - ]) - ]) - } -}); diff --git a/examples/dbmonster/mithril-0.2.x/index.html b/examples/dbmonster/mithril-0.2.x/index.html deleted file mode 100644 index d6000ee0..00000000 --- a/examples/dbmonster/mithril-0.2.x/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - dbmon (Mithril 0.2.x) - - -
- - - - - - - diff --git a/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js b/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js deleted file mode 100644 index b98d933a..00000000 --- a/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js +++ /dev/null @@ -1,2190 +0,0 @@ -;(function (global, factory) { // eslint-disable-line - "use strict" - /* eslint-disable no-undef */ - var m = factory(global) - if (typeof module === "object" && module != null && module.exports) { - module.exports = m - } else if (typeof define === "function" && define.amd) { - define(function () { return m }) - } else { - global.m = m - } - /* eslint-enable no-undef */ -})(typeof window !== "undefined" ? window : this, function (global, undefined) { // eslint-disable-line - "use strict" - - m.version = function () { - return "v0.2.3" - } - - var hasOwn = {}.hasOwnProperty - var type = {}.toString - - function isFunction(object) { - return typeof object === "function" - } - - function isObject(object) { - return type.call(object) === "[object Object]" - } - - function isString(object) { - return type.call(object) === "[object String]" - } - - var isArray = Array.isArray || function (object) { - return type.call(object) === "[object Array]" - } - - function noop() {} - - var voidElements = { - AREA: 1, - BASE: 1, - BR: 1, - COL: 1, - COMMAND: 1, - EMBED: 1, - HR: 1, - IMG: 1, - INPUT: 1, - KEYGEN: 1, - LINK: 1, - META: 1, - PARAM: 1, - SOURCE: 1, - TRACK: 1, - WBR: 1 - } - - // caching commonly used variables - var $document, $location, $requestAnimationFrame, $cancelAnimationFrame - - // self invoking function needed because of the way mocks work - function initialize(mock) { - $document = mock.document - $location = mock.location - $cancelAnimationFrame = mock.cancelAnimationFrame || mock.clearTimeout - $requestAnimationFrame = mock.requestAnimationFrame || mock.setTimeout - } - - // testing API - m.deps = function (mock) { - initialize(global = mock || window) - return global - } - - m.deps(global) - - /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ - - function parseTagAttrs(cell, tag) { - var classes = [] - var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g - var match - - while ((match = parser.exec(tag))) { - if (match[1] === "" && match[2]) { - cell.tag = match[2] - } else if (match[1] === "#") { - cell.attrs.id = match[2] - } else if (match[1] === ".") { - classes.push(match[2]) - } else if (match[3][0] === "[") { - var pair = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3]) - cell.attrs[pair[1]] = pair[3] || "" - } - } - - return classes - } - - function getVirtualChildren(args, hasAttrs) { - var children = hasAttrs ? args.slice(1) : args - - if (children.length === 1 && isArray(children[0])) { - return children[0] - } else { - return children - } - } - - function assignAttrs(target, attrs, classes) { - var classAttr = "class" in attrs ? "class" : "className" - - for (var attrName in attrs) { - if (hasOwn.call(attrs, attrName)) { - if (attrName === classAttr && - attrs[attrName] != null && - attrs[attrName] !== "") { - classes.push(attrs[attrName]) - // create key in correct iteration order - target[attrName] = "" - } else { - target[attrName] = attrs[attrName] - } - } - } - - if (classes.length) target[classAttr] = classes.join(" ") - } - - /** - * - * @param {Tag} The DOM node tag - * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs - * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, - * or splat (optional) - */ - function m(tag, pairs) { - var args = [] - - for (var i = 1, length = arguments.length; i < length; i++) { - args[i - 1] = arguments[i] - } - - if (isObject(tag)) return parameterize(tag, args) - - if (!isString(tag)) { - throw new Error("selector in m(selector, attrs, children) should " + - "be a string") - } - - var hasAttrs = pairs != null && isObject(pairs) && - !("tag" in pairs || "view" in pairs || "subtree" in pairs) - - var attrs = hasAttrs ? pairs : {} - var cell = { - tag: "div", - attrs: {}, - children: getVirtualChildren(args, hasAttrs) - } - - assignAttrs(cell.attrs, attrs, parseTagAttrs(cell, tag)) - return cell - } - - function forEach(list, f) { - for (var i = 0; i < list.length && !f(list[i], i++);) { - // function called in condition - } - } - - function forKeys(list, f) { - forEach(list, function (attrs, i) { - return (attrs = attrs && attrs.attrs) && - attrs.key != null && - f(attrs, i) - }) - } - // This function was causing deopts in Chrome. - function dataToString(data) { - // data.toString() might throw or return null if data is the return - // value of Console.log in some versions of Firefox (behavior depends on - // version) - try { - if (data != null && data.toString() != null) return data - } catch (e) { - // silently ignore errors - } - return "" - } - - // This function was causing deopts in Chrome. - function injectTextNode(parentElement, first, index, data) { - try { - insertNode(parentElement, first, index) - first.nodeValue = data - } catch (e) { - // IE erroneously throws error when appending an empty text node - // after a null - } - } - - function flatten(list) { - // recursively flatten array - for (var i = 0; i < list.length; i++) { - if (isArray(list[i])) { - list = list.concat.apply([], list) - // check current index again and flatten until there are no more - // nested arrays at that index - i-- - } - } - return list - } - - function insertNode(parentElement, node, index) { - parentElement.insertBefore(node, - parentElement.childNodes[index] || null) - } - - var DELETION = 1 - var INSERTION = 2 - var MOVE = 3 - - function handleKeysDiffer(data, existing, cached, parentElement) { - forKeys(data, function (key, i) { - existing[key = key.key] = existing[key] ? { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || - $document.createElement("div") - } : {action: INSERTION, index: i} - }) - - var actions = [] - for (var prop in existing) { - if (hasOwn.call(existing, prop)) { - actions.push(existing[prop]) - } - } - - var changes = actions.sort(sortChanges) - var newCached = new Array(cached.length) - - newCached.nodes = cached.nodes.slice() - - forEach(changes, function (change) { - var index = change.index - if (change.action === DELETION) { - clear(cached[index].nodes, cached[index]) - newCached.splice(index, 1) - } - if (change.action === INSERTION) { - var dummy = $document.createElement("div") - dummy.key = data[index].attrs.key - insertNode(parentElement, dummy, index) - newCached.splice(index, 0, { - attrs: {key: data[index].attrs.key}, - nodes: [dummy] - }) - newCached.nodes[index] = dummy - } - - if (change.action === MOVE) { - var changeElement = change.element - var maybeChanged = parentElement.childNodes[index] - if (maybeChanged !== changeElement && changeElement !== null) { - parentElement.insertBefore(changeElement, - maybeChanged || null) - } - newCached[index] = cached[change.from] - newCached.nodes[index] = changeElement - } - }) - - return newCached - } - - function diffKeys(data, cached, existing, parentElement) { - var keysDiffer = data.length !== cached.length - - if (!keysDiffer) { - forKeys(data, function (attrs, i) { - var cachedCell = cached[i] - return keysDiffer = cachedCell && - cachedCell.attrs && - cachedCell.attrs.key !== attrs.key - }) - } - - if (keysDiffer) { - return handleKeysDiffer(data, existing, cached, parentElement) - } else { - return cached - } - } - - function diffArray(data, cached, nodes) { - // diff the array itself - - // update the list of DOM nodes by collecting the nodes from each item - forEach(data, function (_, i) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) - }) - // remove items from the end of the array if the new array is shorter - // than the old one. if errors ever happen here, the issue is most - // likely a bug in the construction of the `cached` data structure - // somewhere earlier in the program - forEach(cached.nodes, function (node, i) { - if (node.parentNode != null && nodes.indexOf(node) < 0) { - clear([node], [cached[i]]) - } - }) - - if (data.length < cached.length) cached.length = data.length - cached.nodes = nodes - } - - function buildArrayKeys(data) { - var guid = 0 - forKeys(data, function () { - forEach(data, function (attrs) { - if ((attrs = attrs && attrs.attrs) && attrs.key == null) { - attrs.key = "__mithril__" + guid++ - } - }) - return 1 - }) - } - - function isDifferentEnough(data, cached, dataAttrKeys) { - if (data.tag !== cached.tag) return true - - if (dataAttrKeys.sort().join() !== - Object.keys(cached.attrs).sort().join()) { - return true - } - - if (data.attrs.id !== cached.attrs.id) { - return true - } - - if (data.attrs.key !== cached.attrs.key) { - return true - } - - if (m.redraw.strategy() === "all") { - return !cached.configContext || cached.configContext.retain !== true - } - - if (m.redraw.strategy() === "diff") { - return cached.configContext && cached.configContext.retain === false - } - - return false - } - - function maybeRecreateObject(data, cached, dataAttrKeys) { - // if an element is different enough from the one in cache, recreate it - if (isDifferentEnough(data, cached, dataAttrKeys)) { - if (cached.nodes.length) clear(cached.nodes) - - if (cached.configContext && - isFunction(cached.configContext.onunload)) { - cached.configContext.onunload() - } - - if (cached.controllers) { - forEach(cached.controllers, function (controller) { - if (controller.onunload) { - controller.onunload({preventDefault: noop}) - } - }) - } - } - } - - function getObjectNamespace(data, namespace) { - if (data.attrs.xmlns) return data.attrs.xmlns - if (data.tag === "svg") return "http://www.w3.org/2000/svg" - if (data.tag === "math") return "http://www.w3.org/1998/Math/MathML" - return namespace - } - - var pendingRequests = 0 - m.startComputation = function () { pendingRequests++ } - m.endComputation = function () { - if (pendingRequests > 1) { - pendingRequests-- - } else { - pendingRequests = 0 - m.redraw() - } - } - - function unloadCachedControllers(cached, views, controllers) { - if (controllers.length) { - cached.views = views - cached.controllers = controllers - forEach(controllers, function (controller) { - if (controller.onunload && controller.onunload.$old) { - controller.onunload = controller.onunload.$old - } - - if (pendingRequests && controller.onunload) { - var onunload = controller.onunload - controller.onunload = noop - controller.onunload.$old = onunload - } - }) - } - } - - function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { - // schedule configs to be called. They are called after `build` finishes - // running - if (isFunction(data.attrs.config)) { - var context = cached.configContext = cached.configContext || {} - - // bind - configs.push(function () { - return data.attrs.config.call(data, node, !isNew, context, - cached) - }) - } - } - - function buildUpdatedNode( - cached, - data, - editable, - hasKeys, - namespace, - views, - configs, - controllers - ) { - var node = cached.nodes[0] - - if (hasKeys) { - setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) - } - - cached.children = build( - node, - data.tag, - undefined, - undefined, - data.children, - cached.children, - false, - 0, - data.attrs.contenteditable ? node : editable, - namespace, - configs - ) - - cached.nodes.intact = true - - if (controllers.length) { - cached.views = views - cached.controllers = controllers - } - - return node - } - - function handleNonexistentNodes(data, parentElement, index) { - var nodes - if (data.$trusted) { - nodes = injectHTML(parentElement, index, data) - } else { - nodes = [$document.createTextNode(data)] - if (!(parentElement.nodeName in voidElements)) { - insertNode(parentElement, nodes[0], index) - } - } - - var cached - - if (typeof data === "string" || - typeof data === "number" || - typeof data === "boolean") { - cached = new data.constructor(data) - } else { - cached = data - } - - cached.nodes = nodes - return cached - } - - function reattachNodes( - data, - cached, - parentElement, - editable, - index, - parentTag - ) { - var nodes = cached.nodes - if (!editable || editable !== $document.activeElement) { - if (data.$trusted) { - clear(nodes, cached) - nodes = injectHTML(parentElement, index, data) - } else if (parentTag === "textarea") { - //