From d32d59413e3ac3ea3128279aef5c000c6379954a Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Tue, 24 Sep 2024 14:19:23 -0700 Subject: [PATCH] Drop the docs and re-point to https://github.com/MithrilJS/docs --- .github/PULL_REQUEST_TEMPLATE.md | 9 +- .github/workflows/issue-create.yml | 17 +- .github/workflows/push-master.yml | 2 +- .github/workflows/test.yml | 8 +- docs/16.png | Bin 1580 -> 0 bytes docs/32.png | Bin 4242 -> 0 bytes docs/48.png | Bin 8298 -> 0 bytes docs/CNAME | 1 - docs/animation.md | 109 --- docs/api.md | 140 ---- docs/autoredraw.md | 130 ---- docs/buildPathname.md | 45 -- docs/buildQueryString.md | 56 -- docs/censor.md | 151 ---- docs/components.md | 673 ---------------- docs/es6.md | 189 ----- docs/examples.md | 17 - docs/favicon.ico | Bin 15086 -> 0 bytes docs/favicon.png | Bin 1352 -> 0 bytes docs/fragment.md | 76 -- docs/framework-comparison.md | 220 ------ docs/hyperscript.md | 529 ------------- docs/index.md | 398 ---------- docs/installation.md | 274 ------- docs/integrating-libs.md | 128 ---- docs/jsx.md | 442 ----------- docs/keys.md | 553 -------------- docs/layout.html | 81 -- docs/learning-mithril.md | 9 - docs/lifecycle-methods.md | 221 ------ docs/logo.svg | 2 - docs/migration-v02x.md | 935 ----------------------- docs/migration-v1x.md | 362 --------- docs/mount.md | 116 --- docs/nav-guides.md | 30 - docs/nav-methods.md | 18 - docs/parsePathname.md | 49 -- docs/parseQueryString.md | 75 -- docs/paths.md | 148 ---- docs/redraw.md | 59 -- docs/request.md | 534 ------------- docs/route.md | 887 --------------------- docs/signatures.md | 87 --- docs/simple-application.md | 736 ------------------ docs/stream.md | 593 -------------- docs/style.css | 105 --- docs/support.md | 7 - docs/testing.md | 178 ----- docs/trust.md | 188 ----- docs/vnodes.md | 177 ----- package.json | 14 +- scripts/_improve-rejection-crashing.js | 20 - scripts/_lint-docs/decode-response.js | 244 ------ scripts/_lint-docs/do-lint.js | 162 ---- scripts/_lint-docs/lint-code.js | 145 ---- scripts/_lint-docs/lint-http-link.js | 105 --- scripts/_lint-docs/lint-relative-link.js | 22 - scripts/_lint-docs/process-file.js | 230 ------ scripts/_lint-docs/task-queue.js | 43 -- scripts/_lint-docs/try-fetch.js | 224 ------ scripts/_upstream.js | 35 - scripts/_utils.js | 26 - scripts/generate-docs.js | 307 -------- scripts/lint-docs.js | 12 - scripts/minify-stream.js | 22 +- scripts/update-docs.js | 42 - 66 files changed, 30 insertions(+), 11387 deletions(-) delete mode 100644 docs/16.png delete mode 100644 docs/32.png delete mode 100644 docs/48.png delete mode 100644 docs/CNAME delete mode 100644 docs/animation.md delete mode 100644 docs/api.md delete mode 100644 docs/autoredraw.md delete mode 100644 docs/buildPathname.md delete mode 100644 docs/buildQueryString.md delete mode 100644 docs/censor.md delete mode 100644 docs/components.md delete mode 100644 docs/es6.md delete mode 100644 docs/examples.md delete mode 100644 docs/favicon.ico delete mode 100644 docs/favicon.png delete mode 100644 docs/fragment.md delete mode 100644 docs/framework-comparison.md delete mode 100644 docs/hyperscript.md delete mode 100644 docs/index.md delete mode 100644 docs/installation.md delete mode 100644 docs/integrating-libs.md delete mode 100644 docs/jsx.md delete mode 100644 docs/keys.md delete mode 100644 docs/layout.html delete mode 100644 docs/learning-mithril.md delete mode 100644 docs/lifecycle-methods.md delete mode 100644 docs/logo.svg delete mode 100644 docs/migration-v02x.md delete mode 100644 docs/migration-v1x.md delete mode 100644 docs/mount.md delete mode 100644 docs/nav-guides.md delete mode 100644 docs/nav-methods.md delete mode 100644 docs/parsePathname.md delete mode 100644 docs/parseQueryString.md delete mode 100644 docs/paths.md delete mode 100644 docs/redraw.md delete mode 100644 docs/request.md delete mode 100644 docs/route.md delete mode 100644 docs/signatures.md delete mode 100644 docs/simple-application.md delete mode 100644 docs/stream.md delete mode 100644 docs/style.css delete mode 100644 docs/support.md delete mode 100644 docs/testing.md delete mode 100644 docs/trust.md delete mode 100644 docs/vnodes.md delete mode 100644 scripts/_improve-rejection-crashing.js delete mode 100644 scripts/_lint-docs/decode-response.js delete mode 100644 scripts/_lint-docs/do-lint.js delete mode 100644 scripts/_lint-docs/lint-code.js delete mode 100644 scripts/_lint-docs/lint-http-link.js delete mode 100644 scripts/_lint-docs/lint-relative-link.js delete mode 100644 scripts/_lint-docs/process-file.js delete mode 100644 scripts/_lint-docs/task-queue.js delete mode 100644 scripts/_lint-docs/try-fetch.js delete mode 100644 scripts/_upstream.js delete mode 100644 scripts/_utils.js delete mode 100644 scripts/generate-docs.js delete mode 100644 scripts/lint-docs.js delete mode 100644 scripts/update-docs.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b02ca099..6fb98646 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,15 +17,12 @@ - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) -- [ ] Documentation change -## Checklist: +## Checklist - [ ] My code follows the code style of this project. -- [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. -- [ ] I have read the **CONTRIBUTING** document. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. -- [ ] I have updated `docs/changelog.md` +- [ ] My change requires a documentation update, and I've opened a pull request to update them already: https://github.com/MithrilJS/docs/pulls/NNNN +- [ ] I have read https://mithril.js.org/contributing.html. diff --git a/.github/workflows/issue-create.yml b/.github/workflows/issue-create.yml index 8735af93..852f2a17 100644 --- a/.github/workflows/issue-create.yml +++ b/.github/workflows/issue-create.yml @@ -2,20 +2,11 @@ name: Ping triage on issue create on: issues: types: [opened] + pull_request_target: + types: [opened] permissions: issues: write repository-projects: write jobs: - add_triage: - runs-on: ubuntu-latest - steps: - - run: gh issue edit "$ISSUE_URL" --add-project 'Triage/bugs' - env: - ISSUE_URL: ${{ github.event.issue.url }} - GITHUB_TOKEN: ${{ github.token }} - - notify: - uses: ./.github/workflows/_post-comment.yml - with: - url: ${{ github.event.issue.url }} - message: '@MithrilJS/triage Please take a look.' + notify_triage: + uses: MithrilJS/infra/.github/workflows/notify-triage.yml@main diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 9b95880c..c69dd678 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -1,6 +1,6 @@ name: Warn on pushing to `master` on: - pull_request: + pull_request_target: types: [opened] branches: [master] permissions: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69bc69bb..4c491625 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: test on: - pull_request: + pull_request_target: branches: [ next ] workflow_dispatch: workflow_call: @@ -14,12 +14,6 @@ permissions: # easier than parsing everything out. jobs: - lint-docs: - uses: ./.github/workflows/_npm-task.yml - with: - task: lint:docs - continue-on-error: true - lint-js: uses: ./.github/workflows/_npm-task.yml with: diff --git a/docs/16.png b/docs/16.png deleted file mode 100644 index f1794514f0eef90999713088f73184ebbabd63a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1580 zcmV+{2GjY8P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 QnC}Mw3sZs#^BkG>AArNSfB*mh diff --git a/docs/48.png b/docs/48.png deleted file mode 100644 index 8da689d8c76b0a01b596fc2902285d67731ed17b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8298 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 diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 5c153480..00000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -mithril.js.org diff --git a/docs/animation.md b/docs/animation.md deleted file mode 100644 index c29884df..00000000 --- a/docs/animation.md +++ /dev/null @@ -1,109 +0,0 @@ - - -# Animations - -- [Technology choices](#technology-choices) -- [Animation on element creation](#animation-on-element-creation) -- [Animation on element removal](#animation-on-element-removal) -- [Performance](#performance) - ---- - -### Technology choices - -Animations are often used to make applications come alive. Nowadays, browsers have good support for CSS animations, and there are [various](https://greensock.com/gsap) [libraries](https://github.com/julianshapiro/velocity) that provide fast JavaScript-based animations. There's also an upcoming [Web API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API) and a [polyfill](https://github.com/web-animations/web-animations-js) if you like living on the bleeding edge. - -Mithril.js does not provide any animation APIs per se, since these other options are more than sufficient to achieve rich, complex animations. Mithril.js does, however, offer hooks to make life easier in some specific cases where it's traditionally difficult to make animations work. - ---- - -### Animation on element creation - -Animating an element via CSS when the element is created couldn't be simpler. Just add an animation to a CSS class normally: - -```css -.fancy {animation:fade-in 0.5s;} -@keyframes fade-in { - from {opacity:0;} - to {opacity:1;} -} -``` - -```javascript -var FancyComponent = { - view: function() { - return m(".fancy", "Hello world") - } -} - -m.mount(document.body, FancyComponent) -``` - ---- - -### Animation on element removal - -The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril.js offers the [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element. - -Let's create an `exit` animation that fades `opacity` from 1 to 0. - -```css -.exit {animation:fade-out 0.5s;} -@keyframes fade-out { - from {opacity:1;} - to {opacity:0;} -} -``` - -Now let's create a contrived component that shows and hides the `FancyComponent` we created in the previous section: - -```javascript -var on = true - -var Toggler = { - view: function() { - return [ - m("button", {onclick: function() {on = !on}}, "Toggle"), - on ? m(FancyComponent) : null, - ] - } -} -``` - -Next, let's modify `FancyComponent` so that it fades out when removed: - -```javascript -var FancyComponent = { - onbeforeremove: function(vnode) { - vnode.dom.classList.add("exit") - return new Promise(function(resolve) { - vnode.dom.addEventListener("animationend", resolve) - }) - }, - view: function() { - return m(".fancy", "Hello world") - } -} -``` - -`vnode.dom` points to the root DOM element of the component (`
`). We use the classList API here to add an `exit` class to `
`. - -Then we return a Promise that resolves when the `animationend` event fires. When we return a promise from `onbeforeremove`, Mithril.js waits until the promise is resolved and only then it removes the element. In this case, it waits for the exit animation to finish. - -We can verify that both the enter and exit animations work by mounting the `Toggler` component: - -```javascript -m.mount(document.body, Toggler) -``` - -Note that the `onbeforeremove` hook only fires on the element that loses its `parentNode` when an element gets detached from the DOM. This behavior is by design and exists to prevent a potential jarring user experience where every conceivable exit animation on the page would run on a route change. If your exit animation is not running, make sure to attach the `onbeforeremove` handler as high up the tree as it makes sense to ensure that your animation code is called. - ---- - -### Performance - -When creating animations, it's recommended that you only use the `opacity` and `transform` CSS rules, since these can be hardware-accelerated by modern browsers and yield better performance than animating `top`, `left`, `width`, and `height`. - -It's also recommended that you avoid the `box-shadow` rule and selectors like `:nth-child`, since these are also resource intensive options. If you want to animate a `box-shadow`, consider [putting the `box-shadow` rule on a pseudo element, and animate that element's opacity instead](https://tobiasahlin.com/blog/how-to-animate-box-shadow/). Other things that can be expensive include large or dynamically scaled images and overlapping elements with different `position` values (e.g. an absolute positioned element over a fixed element). diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 6141fb58..00000000 --- a/docs/api.md +++ /dev/null @@ -1,140 +0,0 @@ - - -# API - -### 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 `https://example.com/#!/home` -}) -``` - -#### m.route.set(path) - [docs](route.md#mrouteset) - -```javascript -m.route.set("/home") -``` - -#### m.route.get() - [docs](route.md#mrouteget) - -```javascript -var currentRoute = m.route.get() -``` - -#### m.route.prefix = prefix - [docs](route.md#mrouteprefix) - -Invoke this before `m.route()` to change the routing prefix. - -```javascript -m.route.prefix = "#!" -``` - -#### m(m.route.Link, ...) - [docs](route.md#mroutelink) - -```javascript -m(m.route.Link, {href: "/Home"}, "Go to home page") -``` - ---- - -#### m.request(options) - [docs](request.md) - -```javascript -m.request({ - method: "PUT", - url: "/api/v1/users/:id", - params: {id: 1, name: "test"} -}) -.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.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) -``` diff --git a/docs/autoredraw.md b/docs/autoredraw.md deleted file mode 100644 index b5b50890..00000000 --- a/docs/autoredraw.md +++ /dev/null @@ -1,130 +0,0 @@ - - -# The auto-redraw system - -Mithril.js implements a virtual DOM diffing system for fast rendering, and in addition, it offers various mechanisms to gain granular control over the rendering of an application. - -When used idiomatically, Mithril.js employs an auto-redraw system that synchronizes the DOM whenever changes are made in the data layer. The auto-redraw system becomes enabled when you call `m.mount` or `m.route` (but it stays disabled if your app is bootstrapped solely via `m.render` calls). - -The auto-redraw system simply consists of triggering a re-render function behind the scenes after certain functions complete. - -### After event handlers - -Mithril.js automatically redraws after DOM event handlers that are defined in a Mithril.js view: - -```javascript -var MyComponent = { - view: function() { - return m("div", {onclick: doSomething}) - } -} - -function doSomething() { - // a redraw happens synchronously after this function runs -} - -m.mount(document.body, MyComponent) -``` - -You can disable an auto-redraw for specific events by setting `e.redraw` to `false`. - -```javascript -var MyComponent = { - view: function() { - return m("div", {onclick: doSomething}) - } -} - -function doSomething(e) { - e.redraw = false - // no longer triggers a redraw when the div is clicked -} - -m.mount(document.body, MyComponent) -``` - - -### After m.request - -Mithril.js automatically redraws after [`m.request`](request.md) completes: - -```javascript -m.request("/api/v1/users").then(function() { - // a redraw happens after this function runs -}) -``` - -You can disable an auto-redraw for a specific request by setting the `background` option to true: - -```javascript -m.request("/api/v1/users", {background: true}).then(function() { - // does not trigger a redraw -}) -``` - - -### After route changes - -Mithril.js automatically redraws after [`m.route.set()`](route.md#mrouteset) calls and after route changes via links using [`m.route.Link`](route.md#mroutelink). - -```javascript -var RoutedComponent = { - view: function() { - return [ - // a redraw happens asynchronously after the route changes - m(m.route.Link, {href: "/"}), - m("div", { - onclick: function() { - m.route.set("/") - } - }), - ] - } -} - -m.route(document.body, "/", { - "/": RoutedComponent, -}) -``` - ---- - -### When Mithril.js does not redraw - -Mithril.js does not redraw after `setTimeout`, `setInterval`, `requestAnimationFrame`, raw `Promise` resolutions and 3rd party library event handlers (e.g. Socket.io callbacks). In those cases, you must manually call [`m.redraw()`](redraw.md). - -Mithril.js also does not redraw after lifecycle methods. Parts of the UI may be redrawn after an `oninit` handler, but other parts of the UI may already have been redrawn when a given `oninit` handler fires. Handlers like `oncreate` and `onupdate` fire after the UI has been redrawn. - -If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw. - -```javascript -function StableComponent() { - var height = 0 - - return { - oncreate: function(vnode) { - height = vnode.dom.offsetHeight - m.redraw() - }, - view: function() { - return m("div", "This component is " + height + "px tall") - } - } -} -``` - -Mithril.js does not auto-redraw vnode trees that are rendered via `m.render`. This means redraws do not occur after event changes and `m.request` calls for templates that were rendered via `m.render`. Thus, if your architecture requires manual control over when rendering occurs (as can sometimes be the case when using libraries like Redux), you should use `m.render` instead of `m.mount`. - -Remember that `m.render` expects a vnode tree, and `m.mount` expects a component: - -```javascript -// wrap the component in a m() call for m.render -m.render(document.body, m(MyComponent)) - -// don't wrap the component for m.mount -m.mount(document.body, MyComponent) -``` - -Mithril.js may also avoid auto-redrawing if the frequency of requested redraws is higher than one animation frame (typically around 16ms). This means, for example, that when using fast-firing events like `onresize` or `onscroll`, Mithril.js will automatically throttle the number of redraws to avoid lag. diff --git a/docs/buildPathname.md b/docs/buildPathname.md deleted file mode 100644 index df1220e8..00000000 --- a/docs/buildPathname.md +++ /dev/null @@ -1,45 +0,0 @@ - -# buildPathname(object) - -- [Description](#description) -- [Signature](#signature) -- [How it works](#how-it-works) - ---- - -### Description - -Turns a [path template](paths.md) and a parameters object into a string of form `/path/user?a=1&b=2` - -```javascript -var pathname = m.buildPathname("/path/:id", {id: "user", a: "1", b: "2"}) -// "/path/user?a=1&b=2" -``` - ---- - -### Signature - -`pathname = m.buildPathname(object)` - -Argument | Type | Required | Description ------------- | ------------------------------------------ | -------- | --- -`path` | `String` | Yes | A URL path -`query ` | `Object` | Yes | A key-value map to be converted into a string -**returns** | `String` | | A string representing the URL with the query string - -[How to read signatures](signatures.md) - ---- - -### How it works - -The `m.buildPathname` creates a [path name](paths.md) from a path template and a parameters object. It's useful for building URLs, and it's what [`m.route`](route.md) and [`m.request`](request.md) use internally to interpolate paths. It uses [`m.buildQueryString`](buildQueryString.md) to generate the query parameters to append to the path name. - -```javascript -var pathname = m.buildPathname("/path/:id", {id: "user", a: 1, b: 2}) - -// pathname is "/path/user?a=1&b=2" -``` diff --git a/docs/buildQueryString.md b/docs/buildQueryString.md deleted file mode 100644 index 1bbb8f5f..00000000 --- a/docs/buildQueryString.md +++ /dev/null @@ -1,56 +0,0 @@ - - -# 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)` - -Argument | Type | Required | Description ------------- | ------------------------------------------ | -------- | --- -`query` | `Object` | Yes | A key-value map to be converted into a string -**returns** | `String` | | A string representing the input object - -[How to read signatures](signatures.md) - ---- - -### How it works - -The `m.buildQueryString` creates a querystring from an object. It's useful for manipulating URLs - -```javascript -var querystring = m.buildQueryString({a: 1, b: 2}) - -// querystring is "a=1&b=2" -``` - -#### Deep data structures - -Deep data structures are serialized in a way that is understood by popular web application servers such as PHP, Rails and ExpressJS - -```javascript -var querystring = m.buildQueryString({a: ["hello", "world"]}) - -// querystring is "a[0]=hello&a[1]=world" -``` - diff --git a/docs/censor.md b/docs/censor.md deleted file mode 100644 index 96dde569..00000000 --- a/docs/censor.md +++ /dev/null @@ -1,151 +0,0 @@ - - -# censor(object, extra) - -- [Description](#description) -- [Signature](#signature) -- [How it works](#signature) - ---- - -### Description - -Returns a shallow-cloned object with lifecycle attributes and any given custom attributes omitted. - -```javascript -var attrs = {one: "two", enabled: false, oninit: function() {}} -var censored = m.censor(attrs, ["enabled"]) -// {one: "two"} -``` - ---- - -### Signature - -`censored = m.censor(object, extra)` - -Argument | Type | Required | Description ------------- | ------------------------------------------ | -------- | --- -`object` | `Object` | Yes | A key-value map to be converted into a string -`extra` | `Array` | No | Additional properties to omit. -**returns** | `Object` | | The original object if no properties to omit existed on it, a shallow-cloned object with the removed properties otherwise. - -[How to read signatures](signatures.md) - ---- - -### How it works - -Ordinarily, you don't need this method, and you'll just want to specify the attributes you want. But sometimes, it's more convenient to send all attributes you don't know to another element. This is often perfectly reasonable, but it can lead you into a major trap with lifecycle methods getting called twice. - -```javascript -function SomePage() { - return { - view: function() { - return m(SomeFancyView, { - oncreate: function() { - sendViewHit(m.route.get(), "some fancy view") - }, - }) - }, - } -} - -function SomeFancyView() { - return { - view: function(vnode) { - return m("div", vnode.attrs, [ // !!! - // ... - ]) - }, - } -} -``` - -This looks benign, but this creates a problem: you're sending two hits each time this view is navigated. This is where `m.censor` come in: it lets you strip that `oncreate` from the attributes so it only gets called once and so the caller can remain sane and rest assured they aren't dealing with super weird bugs because of it. - -```javascript -// Fixed -function SomeFancyView() { - return { - view: function(vnode) { - return m("div", m.censor(vnode.attrs), [ - // ... - ]) - }, - } -} -``` - -You can also run into similar issues with keys: - -```javascript -function SomePage() { - return { - view: function() { - return m(Layout, { - pageTitle: "Some Page", - key: someKey, - }, [ - // ... - ]) - }, - } -} - -function Layout() { - return { - view: function(vnode) { - return [ - m("header", [ - m("h1", "My beautiful web app"), - m("nav"), - ]), - m(".body", vnode.attrs, [ // !!! - m("h2", vnode.attrs.pageTitle), - vnode.children, - ]) - ] - }, - } -} -``` - -This would end up [throwing an error](keys.md#key-restrictions) because here's what Mithril.js sees when creating the `Layout` vnode: - -```javascript -return [ - m("header", [ - m("h1", "My beautiful web app"), - m("nav"), - ]), - m(".body", {pageTitle: "Some Page", key: someKey}, [ - m("h2", "Some Page"), - [/* ... */], - ]) -] -``` - -You wouldn't likely catch that at first glance, especially in much more real-world scenarios where there might be indirection and/or other issues. To correct this, you similarly have to censor out the `key:` attribute. You can also censor out the custom `pageTitle` attribute, too, since it doesn't provide any real value being in the DOM. - -```javascript -// Fixed -function Layout() { - return { - view: function(vnode) { - return [ - m("header", [ - m("h1", "My beautiful web app"), - m("nav"), - ]), - m(".body", m.censor(vnode.attrs, ["pageTitle"]), [ - m("h2", vnode.attrs.pageTitle), - vnode.children, - ]) - ] - }, - } -} -``` diff --git a/docs/components.md b/docs/components.md deleted file mode 100644 index 7ef3733f..00000000 --- a/docs/components.md +++ /dev/null @@ -1,673 +0,0 @@ - - -# Components - -- [Structure](#structure) -- [Lifecycle methods](#lifecycle-methods) -- [Passing data to components](#passing-data-to-components) -- [State](#state) - - [Closure component state](#closure-component-state) - - [POJO component state](#pojo-component-state) -- [Classes](#classes) - - [Class component state](#class-component-state) -- [Special attributes](#special-attributes) -- [Avoid anti-patterns](#avoid-anti-patterns) - -### Structure - -Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse. - -Any JavaScript object that has a `view` method is a Mithril.js component. Components can be consumed via the [`m()`](hyperscript.md) utility: - -```javascript -// define your component -var Example = { - view: function(vnode) { - return m("div", "Hello") - } -} - -// consume your component -m(Example) - -// equivalent HTML -//
Hello
-``` - ---- - -### Lifecycle methods - -Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes. Note that `vnode` is passed as an argument to each lifecycle method, as well as to `view` (with the _previous_ vnode passed additionally to `onbeforeupdate`): - -```javascript -var ComponentWithHooks = { - oninit: function(vnode) { - console.log("initialized") - }, - oncreate: function(vnode) { - console.log("DOM created") - }, - onbeforeupdate: function(newVnode, oldVnode) { - return true - }, - onupdate: function(vnode) { - console.log("DOM updated") - }, - onbeforeremove: function(vnode) { - console.log("exit animation can start") - return new Promise(function(resolve) { - // call after animation completes - resolve() - }) - }, - onremove: function(vnode) { - console.log("removing DOM element") - }, - view: function(vnode) { - return "hello" - } -} -``` - -Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types. - -```javascript -function initialize(vnode) { - console.log("initialized as vnode") -} - -m(ComponentWithHooks, {oninit: initialize}) -``` - -Lifecycle methods in vnodes do not override component methods, nor vice versa. Component lifecycle methods are always run after the vnode's corresponding method. - -Take care not to use lifecycle method names for your own callback function names in vnodes. - -To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md). - ---- - -### Passing data to components - -Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function: - -```javascript -m(Example, {name: "Floyd"}) -``` - -This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`: - -```javascript -var Example = { - view: function (vnode) { - return m("div", "Hello, " + vnode.attrs.name) - } -} -``` - -NOTE: Lifecycle methods can also be defined in the `attrs` object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril.js itself. Use them in `attrs` only when you specifically wish to use them as lifecycle methods. - ---- - -### State - -Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns. - -Note that unlike many other frameworks, mutating component state does *not* trigger [redraws](autoredraw.md) or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by [m.request](request.md) complete or when the browser navigates to different routes. Mithril.js' component state mechanisms simply exist as a convenience for applications. - -If a state change occurs that is not as a result of any of the above conditions (e.g. after a `setTimeout`), then you can use `m.redraw()` to trigger a redraw manually. - -#### Closure component state - -In the above examples, each component is defined as a POJO (Plain Old JavaScript Object), which is used by Mithril.js internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope. - -With a closure component, state can simply be maintained by variables that are declared within the outer function: - -```javascript -function ComponentWithState(initialVnode) { - // Component state variable, unique to each instance - var count = 0 - - // POJO component instance: any object with a - // view function which returns a vnode - return { - oninit: function(vnode){ - console.log("init a closure component") - }, - view: function(vnode) { - return m("div", - m("p", "Count: " + count), - m("button", { - onclick: function() { - count += 1 - } - }, "Increment count") - ) - } - } -} -``` - -Any functions declared within the closure also have access to its state variables. - -```javascript -function ComponentWithState(initialVnode) { - var count = 0 - - function increment() { - count += 1 - } - - function decrement() { - count -= 1 - } - - return { - view: function(vnode) { - return m("div", - m("p", "Count: " + count), - m("button", { - onclick: increment - }, "Increment"), - m("button", { - onclick: decrement - }, "Decrement") - ) - } - } -} -``` - -Closure components are consumed in the same way as POJOs, e.g. `m(ComponentWithState, { passedData: ... })`. - -A big advantage of closure components is that we don't need to worry about binding `this` when attaching event handler callbacks. In fact `this` is never used at all and we never have to think about `this` context ambiguities. - - ---- - -#### POJO component state - -It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods. - -#### At initialization - -For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple "blueprint" state initialization. - -In the example below, `data` becomes a property of the `ComponentWithInitialState` component's `vnode.state` object. - -```javascript -var ComponentWithInitialState = { - data: "Initial content", - view: function(vnode) { - return m("div", vnode.state.data) - } -} - -m(ComponentWithInitialState) - -// Equivalent HTML -//
Initial content
-``` - -#### Via vnode.state - -As you can see, state can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component. - -```javascript -var ComponentWithDynamicState = { - oninit: function(vnode) { - vnode.state.data = vnode.attrs.text - }, - view: function(vnode) { - return m("div", vnode.state.data) - } -} - -m(ComponentWithDynamicState, {text: "Hello"}) - -// Equivalent HTML -//
Hello
-``` - -#### Via the this keyword - -State can also be accessed via the `this` keyword, which is available to all lifecycle methods as well as the `view` method of a component. - -```javascript -var ComponentUsingThis = { - oninit: function(vnode) { - this.data = vnode.attrs.text - }, - view: function(vnode) { - return m("div", this.data) - } -} - -m(ComponentUsingThis, {text: "Hello"}) - -// Equivalent HTML -//
Hello
-``` - -Be aware that when using ES5 functions, the value of `this` in nested anonymous functions is not the component instance. There are two recommended ways to get around this JavaScript limitation, use arrow functions, or if those are not supported, use `vnode.state`. - ---- - -### Classes - -If it suits your needs (like in object-oriented projects), components can also be written using classes: - -```javascript -class ClassComponent { - constructor(vnode) { - this.kind = "class component" - } - view() { - return m("div", `Hello from a ${this.kind}`) - } - oncreate() { - console.log(`A ${this.kind} was created`) - } -} -``` - -Class components must define a `view()` method, detected via `.prototype.view`, to get the tree to render. - -They can be consumed in the same way regular components can. - -```javascript -// EXAMPLE: via m.render -m.render(document.body, m(ClassComponent)) - -// EXAMPLE: via m.mount -m.mount(document.body, ClassComponent) - -// EXAMPLE: via m.route -m.route(document.body, "/", { - "/": ClassComponent -}) - -// EXAMPLE: component composition -class AnotherClassComponent { - view() { - return m("main", [ - m(ClassComponent) - ]) - } -} -``` - -#### Class component state - -With classes, state can be managed by class instance properties and methods, and accessed via `this`: - -```javascript -class ComponentWithState { - constructor(vnode) { - this.count = 0 - } - increment() { - this.count += 1 - } - decrement() { - this.count -= 1 - } - view() { - return m("div", - m("p", "Count: " + count), - m("button", { - onclick: () => {this.increment()} - }, "Increment"), - m("button", { - onclick: () => {this.decrement()} - }, "Decrement") - ) - } -} -``` - -Note that we must use arrow functions for the event handler callbacks so the `this` context can be referenced correctly. - ---- - -### Mixing component kinds - -Components can be freely mixed. A class component can have closure or POJO components as children, etc... - ---- - -### Special attributes - -Mithril.js places special semantics on several property keys, so you should normally avoid using them in normal component attributes. - -- [Lifecycle methods](lifecycle-methods.md): `oninit`, `oncreate`, `onbeforeupdate`, `onupdate`, `onbeforeremove`, and `onremove` -- `key`, which is used to track identity in keyed fragments -- `tag`, which is used to tell vnodes apart from normal attributes objects and other things that are non-vnode objects. - ---- - -### Avoid anti-patterns - -Although Mithril.js is flexible, some code patterns are discouraged: - -#### Avoid fat components - -Generally speaking, a "fat" component is a component that has custom instance methods. In other words, you should avoid attaching functions to `vnode.state` or `this`. It's exceedingly rare to have logic that logically fits in a component instance method and that can't be reused by other components. It's relatively common that said logic might be needed by a different component down the road. - -It's easier to refactor code if that logic is placed in the data layer than if it's tied to a component state. - -Consider this fat component: - -```javascript -// views/Login.js -// AVOID -var Login = { - username: "", - password: "", - setUsername: function(value) { - this.username = value - }, - setPassword: function(value) { - this.password = value - }, - canSubmit: function() { - return this.username !== "" && this.password !== "" - }, - login: function() {/*...*/}, - view: function() { - return m(".login", [ - m("input[type=text]", { - oninput: function (e) { this.setUsername(e.target.value) }, - value: this.username, - }), - m("input[type=password]", { - oninput: function (e) { this.setPassword(e.target.value) }, - value: this.password, - }), - m("button", {disabled: !this.canSubmit(), onclick: this.login}, "Login"), - ]) - } -} -``` - -Normally, in the context of a larger application, a login component like the one above exists alongside components for user registration and password recovery. Imagine that we want to be able to prepopulate the email field when navigating from the login screen to the registration or password recovery screens (or vice versa), so that the user doesn't need to re-type their email if they happened to fill the wrong page (or maybe you want to bump the user to the registration form if a username is not found). - -Right away, we see that sharing the `username` and `password` fields from this component to another is difficult. This is because the fat component encapsulates its state, which by definition makes this state difficult to access from outside. - -It makes more sense to refactor this component and pull the state code out of the component and into the application's data layer. This can be as simple as creating a new module: - -```javascript -// models/Auth.js -// PREFER -var Auth = { - username: "", - password: "", - setUsername: function(value) { - Auth.username = value - }, - setPassword: function(value) { - Auth.password = value - }, - canSubmit: function() { - return Auth.username !== "" && Auth.password !== "" - }, - login: function() {/*...*/}, -} - -module.exports = Auth -``` - -Then, we can clean up the component: - -```javascript -// views/Login.js -// PREFER -var Auth = require("../models/Auth") - -var Login = { - view: function() { - return m(".login", [ - m("input[type=text]", { - oninput: function (e) { Auth.setUsername(e.target.value) }, - value: Auth.username - }), - m("input[type=password]", { - oninput: function (e) { Auth.setPassword(e.target.value) }, - value: Auth.password - }), - m("button", { - disabled: !Auth.canSubmit(), - onclick: Auth.login - }, "Login") - ]) - } -} -``` - -This way, the `Auth` module is now the source of truth for auth-related state, and a `Register` component can easily access this data, and even reuse methods like `canSubmit`, if needed. In addition, if validation code is required (for example, for the email field), you only need to modify `setEmail`, and that change will do email validation for any component that modifies an email field. - -As a bonus, notice that we no longer need to use `.bind` to keep a reference to the state for the component's event handlers. - -#### Don't forward `vnode.attrs` itself to other vnodes - -Sometimes, you might want to keep an interface flexible and your implementation simpler by forwarding attributes to a particular child component or element, in this case [Bootstrap's modal](https://getbootstrap.com/docs/4.1/components/modal/). It might be tempting to forward a vnode's attributes like this: - -```javascript -// AVOID -var Modal = { - // ... - view: function(vnode) { - return m(".modal[tabindex=-1][role=dialog]", vnode.attrs, [ - // forwarding `vnode.attrs` here ^ - // ... - ]) - } -} -``` - -If you do it like above, you could run into issues when using it: - -```javascript -var MyModal = { - view: function() { - return m(Modal, { - // This toggles it twice, so it doesn't show - onupdate: function(vnode) { - if (toggle) $(vnode.dom).modal("toggle") - } - }, [ - // ... - ]) - } -} -``` - -Instead, you should forward *single* attributes into vnodes: - -```javascript -// PREFER -var Modal = { - // ... - view: function(vnode) { - return m(".modal[tabindex=-1][role=dialog]", vnode.attrs.attrs, [ - // forwarding `attrs:` here ^ - // ... - ]) - } -} - -// Example -var MyModal = { - view: function() { - return m(Modal, { - attrs: { - // This toggles it once - onupdate: function(vnode) { - if (toggle) $(vnode.dom).modal("toggle") - } - }, - // ... - }) - } -} -``` - -#### Don't manipulate `children` - -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. - -Avoid destructuring the `children` property for this purpose. - -```javascript -// AVOID -var Header = { - view: function(vnode) { - return m(".section", [ - m(".header", vnode.children[0]), - m(".tagline", vnode.children[1]), - ]) - } -} - -m(Header, [ - m("h1", "My title"), - m("h2", "Lorem ipsum"), -]) - -// awkward consumption use case -m(Header, [ - [ - m("h1", "My title"), - m("small", "A small note"), - ], - m("h2", "Lorem ipsum"), -]) -``` - -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 -var BetterHeader = { - view: function(vnode) { - return m(".section", [ - m(".header", vnode.attrs.title), - m(".tagline", vnode.attrs.tagline), - ]) - } -} - -m(BetterHeader, { - title: m("h1", "My title"), - tagline: m("h2", "Lorem ipsum"), -}) - -// clearer consumption use case -m(BetterHeader, { - title: [ - m("h1", "My title"), - m("small", "A small note"), - ], - tagline: m("h2", "Lorem ipsum"), -}) -``` - -#### 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. - -For that reason you should avoid recreating components. Instead, consume components idiomatically. - -```javascript -// AVOID -var ComponentFactory = function(greeting) { - // creates a new component on every call - return { - view: function() { - return m("div", greeting) - } - } -} -m.render(document.body, m(ComponentFactory("hello"))) -// calling a second time recreates div from scratch rather than doing nothing -m.render(document.body, m(ComponentFactory("hello"))) - -// PREFER -var Component = { - view: function(vnode) { - return m("div", vnode.attrs.greeting) - } -} -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: - -```javascript -// 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: - -```javascript -// 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) - ] - } -}) -``` diff --git a/docs/es6.md b/docs/es6.md deleted file mode 100644 index b673f7d1..00000000 --- a/docs/es6.md +++ /dev/null @@ -1,189 +0,0 @@ - -# ES6+ on legacy browsers - -- [Setup](#setup) -- [Using Babel with Webpack](#using-babel-with-webpack) - ---- - -Mithril.js is written in ES5, but it's fully compatible with ES6 and later as well. All modern browsers do support it natively, up to and even including native module syntax. (They don't support Node's magic module resolution, so you can't use `import * as _ from "lodash-es"` or similar. They just support relative and URL paths.) And so you can feel free to use [arrow functions for your closure components and classes for your class components](components.md). - -But, if like many of us, you still need to support older browsers like Internet Explorer, you'll need to transpile that down to ES5, and this is what this page is all about, using [Babel](https://babeljs.io) to make modern ES6+ code work on older browsers. - ---- - -### Setup - -First, if you haven't already, make sure you have [Node](https://nodejs.org/en/) installed. It comes with [npm](https://www.npmjs.com/) pre-bundled, something we'll need soon. - -Once you've got that downloaded, open a terminal and run these commands: - -```bash -# Replace this with the actual path to your project. Quote it if it has spaces, -# and single-quote it if you're on Linux/Mac and it contains a `$$` anywhere. -cd "/path/to/your/project" - -# If you have a `package.json` there already, skip this command. -npm init -``` - -Now, you can go one of a couple different routes: - -- [Use Babel standalone, with no bundler at all](#using-babel-standalone) -- [Use Babel and bundle with Webpack](#using-babel-with-webpack) - -#### Using Babel standalone - -First, we need to install a couple dependencies we need. - -- `@babel/cli` installs the core Babel logic as well as the `babel` command. -- `@babel/preset-env` helps Babel know what to transpile and how to transpile them. - -```bash -npm install @babel/cli @babel/preset-env --save-dev -``` - -Now, create a `.babelrc` file and set up with `@babel/preset-env`. - -```json -{ - "presets": ["@babel/preset-env"], - "sourceMaps": true -} -``` - -And finally, if you have *very* specific requirements on what you need to support, you may want to [configure Browserslist](https://github.com/browserslist/browserslist) so Babel (and other libraries) know what features to target. - -*By default, if you don't configure anything, Browserslist uses a fairly sensible query: `> 0.5%, last 2 versions, Firefox ESR, not dead`. Unless you have very specific circumstances that require you to change this, like if you need to support IE 8 with a lot of polyfills, don't bother with this step.* - -Whenever you want to compile your project, run this command, and everything will be compiled. - -```bash -babel src --out-dir dist -``` - -You may find it convenient to use an npm script so you're not having to remember this and typing it out every time. Add a `"build"` field to the `"scripts"` object in your `package.json`: - -```json -{ - "scripts": { - "build": "babel src --out-dir dist" - } -} -``` - -And now, the command is a little easier to type and remember. - -```bash -npm run build -``` - -#### Using Babel with Webpack - -If you want to use Webpack to bundle, it's a few more steps to set up. First, we need to install all the dependencies we need for both Babel and Webpack. - -- `webpack` is the core Webpack code and `webpack-cli` gives you the `webpack` command. -- `@babel/core` is the core Babel code, a peer dependency for `babel-loader`. -- `babel-loader` lets you teach Webpack how to use Babel to transpile your files. -- `@babel/preset-env` helps Babel know what to transpile and how to transpile them. - -```bash -npm install webpack webpack-cli @babel/core babel-loader @babel/preset-env --save-dev -``` - -Now, create a `.babelrc` file and set up with `@babel/preset-env`. - -```json -{ - "presets": ["@babel/preset-env"], - "sourceMaps": true -} -``` - -Next, if you have *very* specific requirements on what you need to support, you may want to [configure Browserslist](https://github.com/browserslist/browserslist) so Babel (and other libraries) know what features to target. - -*By default, if you don't configure anything, Browserslist uses a fairly sensible query: `> 0.5%, last 2 versions, Firefox ESR, not dead`. Unless you have very specific circumstances that require you to change this, like if you need to support IE 8 with a lot of polyfills, don't bother with this step.* - -And finally, set up Webpack by creating a file called `webpack.config.js`. - -```javascript -const path = require('path') - -module.exports = { - entry: path.resolve(__dirname, 'src/index.js'), - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'app.js', - }, - module: { - rules: [{ - test: /\.js$/, - exclude: /\/node_modules\//, - use: { - loader: 'babel-loader' - } - }] - } -} -``` - -This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `dist/app.js`. - -Now, to run the bundler, you just run this command: - -```bash -webpack -d --watch -``` - -You may find it convenient to use an npm script so you're not having to remember this and typing it out every time. Add a `"build"` field to the `"scripts"` object in your `package.json`: - -```json -{ - "scripts": { - "start": "webpack -d --watch" - } -} -``` - -And now, the command is a little easier to type and remember. - -```bash -npm start -``` - -For production builds, you'll want to minify your scripts. Luckily, this is also pretty easy: it's just running Webpack with a different option. - -```bash -webpack -p -``` - -You may want to also add this to your npm scripts, so you can build it quickly and easily. - -```json -{ - "scripts": { - "start": "webpack -d --watch", - "build": "webpack -p" - } -} -``` - -And then running this is a little easier to remember. - -```bash -npm run build -``` - -And of course, you can do this in automatic production build scripts, too. Here's how it might look if you're using [Heroku](https://www.heroku.com/), for example: - -```json -{ - "scripts": { - "start": "webpack -d --watch", - "build": "webpack -p", - "heroku-postbuild": "webpack -p" - } -} -``` diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index e591936b..00000000 --- a/docs/examples.md +++ /dev/null @@ -1,17 +0,0 @@ - - -# Examples - -Here are some examples of Mithril.js in action - -- [Animation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/animation/mosaic.html) -- [Community Added Examples](https://how-to-mithril.js.org) -- [DBMonster](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html) -- [Markdown Editor](https://raw.githack.com/MithrilJS/mithril.js/master/examples/editor/index.html) -- SVG: [Clock](https://raw.githack.com/MithrilJS/mithril.js/master/examples/svg/clock.html), [Ring](https://raw.githack.com/MithrilJS/mithril.js/master/examples/svg/ring.html), [Tiger](https://raw.githack.com/MithrilJS/mithril.js/master/examples/svg/tiger.html) -- [ThreadItJS](https://raw.githack.com/MithrilJS/mithril.js/master/examples/threaditjs/index.html) -- [TodoMVC](https://raw.githack.com/MithrilJS/mithril.js/master/examples/todomvc/index.html) -- [Conway Game Of Life](https://flems.io/#0=N4IgtglgJlA2CmIBcAWFA6ATCgNCAzgMYBOA9rLMgNoCsOmA7ALp4BmEC+1oAdgIZhESEOgAWAFzCU8hUj3Hx5yECAC+OXgKEiAVlxlyFS4bJ75xAAgDK44vAEWLAXgth05uwIA6PU+YuyYAAOpPjwzhYASuiBIWE+fpbEfDwA5uEu0clp8Aly-gDuEOKipACulpnoRSXl4nlmlgBufLBl8PgR0S1tHT4N-vgQAEawEGkAwqSkxFCdLlRUALQAjDgWAAwsy2sWK9sb6-s4VLvHp+tbJ7urB+u3J6v3+0z9vvmWYKRQ8LARwD5HBB8JEyjweONUkhrLZ7GAABSsVphACUOEBFmGpD4s2hwHUGJ+sD4AE9oTZPAiVhsaWiMdkoKRIAAveBQcmwgTwlY0Ok8RxDVkcynwgDMGz5jiKUBKwrh8IAHLT0fyLGNWPBCCTCAg5VyJT5VG9EhY7OYcZULPCwHAUc4AHwWAGqm2wdDA0HgyGI5HwFEY13oImk7k0g0uuDoBlMiCsqDc3kByOC+Bi8OOQPSkqK2lJt3qzXahDw9Om+DiMrEfmuw1vAaWGqicYAIXKPDmEXhY0g4jtTkd8NkMzmfftGIAhIPprN8O7fG0fvguxAe3aAD5rgLTuZznVlRfw1Yo-08esWcSkKwjMZpACy31+nb4hEI6yGo0ho6dGOfhCo75vVImAiJFYHiVU7ArKsLF-WtT3eRoAlaQgrw-NJ5itbtii-eF3ymYd8BwocZ1HDE8O3fAMUcdAwD4IJ4VwkYv3-EYqC2CwAGotwItiWAsd9TmArjiLmQSmBPRxqPYWAFGIeFGxbNs5mXVcJMkqM2TKQhUwvVDAPvIl1nxE8z1ogBreAJl+P4XGtW0HStFMcOgAAPL9nUcBBLBEjCqFciwAFJ+NjeB1lvPgSnQVhYGneF-IAemC1kUVeVUvOCtDUgwwhkL0yElycxjMvwkipwItT0u0ih-ioiwLLJCxXJVSSLB6dpoS8EBOuayTgQAQTGJp4GhUCwh6xwfPGjLAMo1UjQjN0sRxKAqB84CXCq2A3kcSDK2rOA4NMvgLNbZaADEyDAK9WU7V03xC9y83cELGOS+lyz200UnSEt7pugAqJK-RoujrWOyzrLs2AUVev0T3ms8ctgQgymJBQrOq2zXUe1UAHlhh0TVxHQeql0DJbZhREH6MHaycZayrrPwAaICGiJGzqeEqFG+A+LajoofQCmVs2phnsywiqYQNISm23rWCtcnsVmVbrLF-rBr9b9VRaiAFdpihmc1iwAB4XEwO0PJaySleW1WKHVo3WYyCwedqxwEZ1+WrSoTB1lFdX533AXNqdobj2162bcjYX7dgR2WbZlxbHad2LE9qO9atUPE-CR1RUttOMxj5WRbV90w5dt2vY92rVAsX4wkj62s4NsDc+cFwC+bqPbZV0WK475PiFTmv07rjFVDU3boJrHhPZNMogigCL4Dy9Dbvs-tm-xwnCGJ0nBeFqnaJpzbSK93eiZJ+ASTJku7YHgD8pP0G06KwCL6jxwj9LuOxefuhFimV1quAfirQBQFB6azUo4NSJkIIfVngdeexoPimjBAAcVxgAGU3tDByVtW6Bg9GCCEaR4QRytsXfMetCw6lTIGAsWoGGUM4nsWB-FywABUVzwE5mw7e1CbYaSgMkAolC04z35CPHg2CcHwiXivdGTMobHjrusQMwYSSULUvXRu4RhHSLAVtOah0EL+GjCyCGht8H0zVOWL67YmQYzAhEK++8b531-stTh7gZjiAYl+DY6AaAWCWBYcKkUrG6Nqu4MY2lfqRIiqIKKMUZgMUDFYkK8Y7SJWpBKCwgNBYpjYYDQMpTjzqPgjtFIjIwCuNnKfBi58HI+P7uXDWzsIgpzhnLYxc9PZnnGMUU6swIiugco4YAjgAzgzGVAC6TJrqMLgOsAAcmUMAwx4ByQqS9KpGISD2AUFYeAbI1HmJNOZeA6z4AFEwVoCZcAHIZEdFbM04gLSXNVCM8QCyfkL3QXwKALRfDwBwXQiYRYXaTO3vCLWQinqkK9BQnmakmF0JYcWTFGpsWpjtFxFYaklGr0aYCtBiFUikDOgE0QzybIDkRe85FIIyHel6dPLBuCKXwRNNkm6Lg4VMtHFbLJdSYxxgRegL5xB0jE35mpP5ALsZXPQWEcQAARX4pIGVtOZSYoMOqdHwBlTieV6BFWUsGOWBZKy9XwoNWK5ML1TWyotVa35EJ-ml15WeUlKjbEbSZHEVMSMUZoxsbAdYAa17XhfojTwpzzlQAiLEUIqZY3ryyusAVUbCLWssK4mqqomgQHudCeEMyIq2HwHiEx6xNrp3TvYnaSDqzwk6jEay3Ue6SR1HwfAdaAjWWgd0gA-BYTqrRnadQsB1EAPwQXdTTuYEkuo+0tVYIYFZ0IAAGAASYAmZoDZjyYaypFhEqYFUEEFye6potVEPACAqQJD7qPSemUog2GJX2ayX9Fgb13ofUXV2CAXL7pWHsCwn7IxZh-ee-9BLb33rTgSMecgdQQEIGZStrbrZ9zLhQGIFEE7GxcOOTaY6hpgazfG9CPyo7zRalPSearEILM7NW8Qtb62TPrlPQhcyTqlyWVdF6d0LCbO2bskpBzqltqgvyYRcg-mVp43xp0Jjm1Ce3sc1eZyLnYymmWitVpNPEGHTMgTLbhNjwGZ2sesyRDCxXc57Ta6N0zIQx+498HT2IdQ3u3Tj7JIeIPrfe+i1S6vzPtuAjUdGaY0NbHESqVv6SUc64oydVb7Qmo-VRt1lNEvLYx58rUdOEsfHqgvl6CACSPAggVE7PY4xwizMFA0zBXjVn+NrLVHwHZ0bXDjE0XwFyb4FBBHWPzdYrB+SCeYmnBEnViQjd7VQDbvwwtrZAOMFr4gqDiBJEEeATgADkPAtk7OIJd14IAjJgcOxUL4PxoTXa2bsnDl2wuOCCDW3ZPAkCdTYksAAnEwf67nMuQB4P91wk3EfmHgLNsD82wNYdEN9YaDcHKLYRZwj2xOLDiVqhl2rQLOMVAvPyWy7X219q60gKtNb+v+dGzt0bi3VB6cdGtjznVhi07kL24AWGEl4atAawnfoMOZe52nPRHH-DcNIEESIpACglscCztnfW62c75yifsVEESXZlRrrXBQ-sWCoAiZsouEdOik4t6ENy7kPK0Go9Y3PPte4sI8wQl2p57fhE73jcgjJu5BzBUFKRtKQo1NC1hJmhsjdByAPqCfwUweT-QhAnUw+rYj87mPg33fUtpcQbM6f-edUwaQCwNfZZqBSvDQtFhWxR7ADb3XrVy3dYN3x43U8zdewt0LUgvebd-aoKXprR3ctSf9xYS7kQJXWPhAFFESA7fw6QIcJHkGCnTbR0gXY-MkDiucdY3JC2Qd5rUS28PS+Kgr8G2vy72riQ6LAIRPvpouMEfhNqfmGOfkEJfjSHNq0O1FosapQo-kgBqr-iGNjK-ovs1h-tpqvsNr8J9imEAWNiDuKGAdAcfqjlAdSLAb0Dfi6gBmiK7HHhqnapJvZFPOTnVmnGeOruQMMDiAPizhZr1lpjZmVnZhPi1PtlbvwTiFtqXurprtrhXrACXmPI7jPheH3ioa7nAJVpJFwdTv4MHvALjKwAXkIUPj1uztZjpstvZi1I5l2qYF8uMLsgoRofCHwbAAIcQJ-n8Oob3BHqXM9h5nPMxpKIRp2iAKIJgL2nuswjCtCHBrQnijCpQqoHupwlwbXNwTwG4F8GCIEoyCjIIPINPlACSBYEZPrl+AiKYeYQXqoTfg+GoSiFPE9iALEBwLssoAISNl0WEAgPvBAPkMoDyKKJfmoBoCAPwIIMoDEEOl0a4YoOIMoIDFbNuvIEsEiJALAA1PePIGEMQMkOIOsFYCkPgEsGcsQHrAANwYhLAFDwDDBmTFBLBYguRLCCiQjQhYizC7KfGkAuSPGqhLBfDMjAnfG-FpD-HDhAlfFgmOBfE-Gxh-GYgInEDQnIkWB1A3h448ByDwBgmeyyG+GCFWwElLDPqvrvoWAMAbB3q4mA4wAYnUjMkYgAk-DEDQgrB3r8TkDQAWAADEmorAGwrAKwuJOgZQ5gesJISwqx8g0I+AgO2knx5YLxigpJbwVuyhOuVsUAwIQQf+I0EGuJ0U8A3x0U2u0IBQyQQQMpcp4gCpSphgaxqp6p8ASwOISkup8E0+s+2uzcxpapZp4G1plpEGuxMU3WFgDpdEzp8prAipyp4gXpz4PpfpYIUAAZPgIuUeKm-Q4gqJ+AOOjI8ZTJLkew1ZewKAApSwDAApIpjAAAbAqAwKKGCV4KWc+GZKkGQLmUgASTiEsIOSCuWvIPCBeJib3usCKTQBDhDsMMMKKBYDQAFAuSgBsAqIQMubWRsLvj2X2bhoOUpO6TFLyYucuaud2SWdyUCckMaXKUgAqJyaeOIGGaaaSEgOMNSaMKQLhieSjFZjMEgCEOMLJCBeQOBSKawAhYhSedseILsQIBwGSH1Hca0MhYYGiUKJgNWbhTsS8XSRmViLAHmSWaycaXCSsKKAKaKJgB+b2QoC5KhT8LIGcWMSDkSTwCSSWWxaheWSCnaXWfyTWXWSKaKFAAwB2XwAGYWXTkgKUENMQNrL2QIWeUOe2COe4WOROcaWsTOc3liPOaKTuXuQeZuQuUuSuWuYeceQ+f2eebmZeXBZZfuRDopc7kgM+K6WzM6L2XEMUDxUgHYGjM7CeReNQR+Z7NzhYI6JsRiDReyURXMnKiAbWSxaeQOTpeyKKSsMNvuYQDBVedCPBQhcRcJSFHyfAGACedSbSW+hmRYOKDlUJb6WMKkHHtpPILstFdacJRWXaZsHsAKRsKaKkAIfCJgDQHQHNQtfNTgCEhbCeY+dic+RAK+XsDQHFXqcLKGSaRGVaaCRiKdbGaNYmU6RiL5hYEqOlaqFpXlUpBVeKZKdKeYt2tVEacdb+ZGWdaqDOj1UsMUPVcOn1dBRiLKSmWmR6SqSOv1cQLiRtXyQKfgEKamiKQqPAGgCCriaBRjbyRYJBUjfmTwOgDOoFVyS5flRVTjXjVRfkUaiCs3M9a5bpaKe9VKaSUMb8ETDxVwMIDQJfm2WoCwCADeGZELVQJoAscID2KIHcdICAJWJQMIBIOIEEHWvFPFGCEEAOaRmAPFIrcrQAAKYDoAhIoAm3FBK0cA0TjDoB6BdGnbnbKBEB3FBDrHqBy3aDJA2h8BdFq3KCa3a1IC6362G2BDxQB0rxm2rVtnoArDxRhniCx0CAryO0U0u14Bu3aCe0QDe0zF+3KCm0cA-GchgDB3EDq2xG8bh2R3NbR1Mi20lDK2V2UgW1W1W3xQeBwjO36AgD50e0kBF0+1MCqBAA) -- [Tic Tac Toe](https://flems.io/#0=N4IgzgxgTg9gNnEAuA2gBgDRoLoZAMwEs4BTMZFUAOwEMBbE5EAOgAsAXOxPCGK9kvyYAeOISoBrAASsoJfAF4AOiA7sADmCQB6bfj7swzAOYwYx0jXWEjvOtohgwAfnz1iATwUBZGlTA0cABuNABkACY26nA0XmAA7lYqUnJwyuDsHqRgrCQk7CoAfCB4YCSkEOyEfORIIGhIAKwATAC0DQBsaCAAvhjU9Ix1zABW5DwGguxMvP7sUgDK7HL0UgpSdMxgyyT0SlT7s9tSEIEQAOqE4eysaxvhcGuFUsD7UvdwzPFXNwAU31RwjB4sxxFQSFBLtdWABKN4pfIAVygVA++x6+0ONXmxnyACU-EC6ABJfh3X50cQYDY0AAeMKeUl8N2Y+DgZigv2ZrGYUEJMDovwZACopL8AIxSADUNNpUlaG3EDJllKocIOVCO82isQhYDur1RUmWiJISBekBgciQmBpUAk5oA5AANR1SPrwtxwMrm4CW61SW10Gj2p0AeUdHqoGM12KkACMYCHwgtCAAvMh3PGs4gCTm-NWM1EAUikAGY1us0FIMNm+VRcb9MF0YerMbG5lJcewACKEGjGACC0BgTjWvxoEAg1LK7GpV3pjMN71I8ygd0nEGYs1O7F+s5QC+w6vecnYyNRUHR7a1XfyfYHPeBqIUE6nM-y8-Ci4Uz2XUlXFINynbc+F3fd8hQWdmFIBsbnlMUF2lKRxRhY94TPC8UmvDVbwBKhxGMBZ8gAIQ8O46AeJd4UIfAKQeLZ0xIBlgHed54RXfIpDAdRJ0zdY6z8RtxWpejPjAJipFFSjxMkmVUJPNjANnMBh1gMcBK2aJCHYABRIIIQ8MTGIzGdeIgMhFM4+YVKfeIXykbNlj8HjRxICDDDU0cwCsgCuNs-shxHDTuPyVTgqMORwkRCzfm7B8gvUsBqRQdCjWs0LDASuyHNs59eRIaLYviwKcpStK2L8+Z8MI-V1hUrynFAzUaD3PL7JhZrwKgsKEsasBsC61rfh6rLSufY9fMwlEpBqhswHhHopHKMoXg4hFthDPcZMUmMYyxTsZINeF8IhYjDFQXATrBCEkCoREEAweFzxRc0TRIJ6jR1Dw9WpeFE2TO6HrgT73gkjNzUwa7oXNJYVkFAAONA0HVfaO2OYN7WIioBBTcySHJGTqQkEgPGpTGJAZX81qNW8eL4rMczgPNfmzdRYHUXSAEdfkdEmPEdYnSZhDAZOYAGoHCRTAMRdRwlawqFnxxnZ1ZmDBDAAAFDneZCOBTUdEWKepbNchocJ93xkWPnFpNJelrjxHCEg5U0ohAVJZ3aTV9mYE5nm+dJwX+dbYyJal+ExfDu5HNBfwIT3J2XYwWX5dxpW+NrL4dNYGBETa-HRYY8PWwwpEZpknCDoxkMJAJQEBQziyKKo6n-0Akg6HUTIm-4m3w6Zlm2Y57ndcCA2MEdQ2HfmQdiV7u5O+7jxe7AFBu3rolST3W0l57-GjFg4x4IVVDsHhABCMXTvXUJQikOeF7vu0JF7sSYV+R+rbLl7UUr6MbxxlTgrAAKlhdYR026RwYr-O4V8YEXh-lhI6Ugq7o3mDYS4VACINgXusMSQsPBUz-PCQCetTSM19uoMe+sSCC1NrsC22YiDMwhD7EeAd+aCykCHIunwS6lyNMpMKjNjCwFlmRX4EBGQQBQOKY8Yc7bhGYMGahw8-ZgBGoHAWk9yF0MmjPHhpM6qx35po5hsA6CaxoIQKAmiVIoD0ZNFRVhfgADlER0HjBCQxmCbrrggQxG+50jCqI8oyaCJADJQCMvzRkZi44QH1s7TRIdWyJOSWQX471FLTVRH47BEI0G3ggLkCAEhiT4B7HyeILdHhQKNLRX4F82b63KTQieUclGdXEEkxEKTebTxppVa+-i7iOjcTAKQYZwRulQelBEsD-5sRjGxPJaIAG4TjBTPBHwInKzbuxIRXFvoQjqcwU5diUBi1-oNCmNE6IFJwURfGFJa7YxIJURWryibcXxswfm1IpCXOtvTCyALhYsXWmLS5a8bkXkGv6AmUopTQqCWM9Ylz1rrOWe6eEpTPkVKqTU9+SCK4PGKXGcEtJ2BgJmusMUvywXMWokadZ2Y7DqDcr8YBAg6VUD+gsimm9G6F3Wrykg-LQZsR2T8h4It4Qf2Zbk8uf8KWbOrmuMg7Atp1NZe8UZhSAlSHuggaBskMx3DQOa22yY7imrgDa740JfhIxRpSw6NASYkSUZcG4qZLX4KZUxYhwybbgwJvVJiNro6aXrI2TAEapLcRDS46hL5ni-GAPzc0qJ5LUj0U6SMgiDXoqNSEu4c0iKkSMjtD1xxdn-iCIQEg8RzRZtassLQwBfmAr+XxHoPRiEcUFI6LSfFuH-kqtsLIZop2VSkPofgAazQAAMAAkwBjLOr+DCbQYsI17uaD0dQtJV3SoXTIEghBjAcHNBurdoyXV7oPSG7Qx7T3nvWu8NkLskCrslJKTdT7d37oYhGk9Z6oyXrAmIcp5pI3PHgZ8G+Uhn4X2ZcwPRaH77UtpReAh-aLIwmgwuzD2Hn6jqw+POhZk+LUdoYItGgDOyaxiD9KACxeByGOgs5trapDtuAJ2uxSBgCXPJg8QdoaqNItaKcSW3CUCXJcfaSeSBuEqaRWlNGt4QFmDgPGEMvH3j8bbWKYT7Au1iZktJp40LebMHYAZozUAlPfo2LzeM+dnNUHFuwKggtgCwcIPB5ajI5CbSgNteVfQpATIEwAcUGIbC90z4wjE+ewCFHhNEwvY3qTqYTfgSakAuYdCyDW-DY7qTj3GPovBzQuaklzzSwuUwVqAuAPhDt8jpljxxksMBM1IMz7bQ1ov4UoqQzhPNjuMCl6kfpMikDEzu1g-7gPoufZB1dg7JNTeTGm34ls+ITcq553uS2c2Yb7cyg7Q6pAloXeaFAc3WDikFo6MoOMpA0ATNNiNqWPMjN5j9rLk7C2vtMlIMCrAhJmnC9TYM3rfU5xXYRjxXi2EkCcyGbsDHTStlI5e944cV2hNcRmubfsqh8G4WqBVF2RngZDefDV6CpBDZIGGaJI2wIrAEONxkAIgQgkgLABA+mmxYCZ6Zlt5mO1WdEz2qTj3fwOfm4MVoMBomTpC-BxDG0dXRffnF1d3Ppl862yh-xS0bDGlyLNfxF9V2owG-MEBoWQGTn05GsNY2hT6rYqcOAFwfisFJQsnFjn3NGkFPp+Armlu2blzbVDlHfjc95xCFPUm0+Cm53nuAQ79jHj2u2fYmw6B534L8IEEBPFTFtuEMmUgvcQB953mAzESggE5cQCETAjPeO4OAcoWXqj+CYM0JA4oACcvR+ggFoAwJg24nB99mAIIQdR9iin-ImWkrRwaEXNImSWEJWhH4ANyegMK0NwlI4AeCdL4fwgQQiOjvxz8HlQw3rbmhIyno-7vC5A3p3rlgdAgH378An5MTmhlhoAwEc5OYubGaH5WjOxQDmjiinrcTwBXBdhyCCCgFSCRA8Tsbmi-q0hkE0GP7sjmbxB8jqBkEjCIjbC0QeDyaTD8DmjMrX75DxB5BUA-5oxbD1byZ2r-i8ThCRANi4HIG0GwHsDwEQzlhKFoImCDBhoUHfTUGkDKFGj0FsjAjmjMFWBiGV5) diff --git a/docs/favicon.ico b/docs/favicon.ico deleted file mode 100644 index 961f6c3a5fb98e377c004ed13aaf6fe1b263eba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 - -# fragment(attrs, children) - -- [Description](#description) -- [Signature](#signature) -- [How it works](#how-it-works) - ---- - -### Description - -Allows attaching lifecycle methods to a fragment [vnode](vnodes.md) - -```javascript -var groupVisible = true -var log = function() { - console.log("group is now visible") -} - -m("ul", [ - m("li", "child 1"), - m("li", "child 2"), - groupVisible ? m.fragment({oninit: log}, [ - // a fragment containing two elements - m("li", "child 3"), - m("li", "child 4"), - ]) : null -]) -``` - ---- - -### Signature - -Generates a fragment [vnode](vnodes.md) - -`vnode = m.fragment(attrs, children)` - -Argument | Type | Required | Description ------------ | --------------------------------------------------- | -------- | --- -`attrs` | `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 fragment [vnode](vnodes.md#structure) - -[How to read signatures](signatures.md) - ---- - -### How it works - -`m.fragment()` creates a [fragment vnode](vnodes.md) with attributes. It is meant for advanced use cases involving [keys](keys.md) or [lifecyle methods](lifecycle-methods.md). - -A fragment vnode represents a list of DOM elements. If you want a regular element vnode that represents only one DOM element and don't require keyed logic, you should use [`m()`](hyperscript.md) instead. - -Normally you can use simple arrays or splats instead to denote a list of nodes: - -```javascript -var groupVisible = true - -m("ul", - m("li", "child 1"), - m("li", "child 2"), - groupVisible ? [ - // a fragment containing two elements - m("li", "child 3"), - m("li", "child 4"), - ] : null -) -``` - -However, JavaScript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead. - -There are a few benefits that come from using `m.fragment` instead of handwriting a vnode object structure: m.fragment creates [monomorphic objects](vnodes.md#monomorphic-class), which have better performance characteristics than creating objects dynamically. In addition, using `m.fragment` makes your intentions clear to other developers, and it makes it less likely that you'll mistakenly set attributes on the vnode object itself rather than on its `attrs` map. diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md deleted file mode 100644 index 1612baa4..00000000 --- a/docs/framework-comparison.md +++ /dev/null @@ -1,220 +0,0 @@ - - -# Framework comparison - -- [Why not X?](#why-not-insert-favorite-framework-here?) -- [Why use Mithril.js?](#why-use-mithril?) -- [React](#react) -- [Angular](#angular) -- [Vue](#vue) - -If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril.js would help you solve your problems more effectively. - ---- - -## Why not [insert favorite framework here]? - -The reality is that most modern frameworks are fast, well-suited to build complex applications, and maintainable if you know how to use them effectively. There are examples of highly complex applications in the wild using just about every popular framework: Udemy uses Angular, AirBnB uses React, Gitlab uses Vue, Guild Wars 2 uses Mithril.js (yes, inside the game!). Clearly, these are all production-quality frameworks. - -As a rule of thumb, if your team is already heavily invested in another framework/library/stack, it makes more sense to stick with it, unless your team agrees that there's a very strong reason to justify a costly rewrite. - -However, if you're starting something new, do consider giving Mithril.js a try, if nothing else, to see how much value Mithril.js adopters have been getting out of under 10kb (gzipped) of code. Mithril.js is used by many well-known companies (e.g. Vimeo, Nike, Fitbit), and it powers large open-sourced platforms too (e.g. Lichess, Flarum). - ---- - -## Why use Mithril.js? - -In one sentence: because **Mithril.js is pragmatic**. This [10 minute guide](index.md) is a good example: that's how long it takes to learn components, XHR and routing - and that's just about the right amount of knowledge needed to build useful applications. - -Mithril.js is all about getting meaningful work done efficiently. Doing file uploads? [The docs show you how](request.md#file-uploads). Authentication? [Documented too](route.md#authentication). Exit animations? [You got it](animation.md). No extra libraries, no magic. - ---- - -## Comparisons - -### React - -React is a view library maintained by Facebook. - -React and Mithril.js share a lot of similarities. If you already learned React, you already know almost all you need to build apps with Mithril. - -- They both use virtual DOM, lifecycle methods and key-based reconciliation -- They both organize views via components -- They both use JavaScript as a flow control mechanism within views - -The most obvious difference between React and Mithril.js is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that React-based architectures can vary wildly from project to project, and that those projects are that much more likely to cross the 1MB size line. - -Mithril.js has built-in modules for common necessities such as routing and XHR, and the [guide](simple-application.md) demonstrates idiomatic usage. This approach is preferable for teams that value consistency and ease of onboarding. - -#### Performance - -Both React and Mithril.js care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using `innerHTML`). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's `shouldComponentUpdate` hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions. - -Mithril.js follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run. - -Here's a comparison of library load times, i.e. the time it takes to parse and run the JavaScript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: - -React | Mithril.js -------- | ------- -55.8 ms | 4.5 ms - -Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. - -Since this is a micro-benchmark, you are encouraged to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate. - -For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril.js is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril.js rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril.js' initialization speed is noticeably better than React. - -Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for [React](https://jsfiddle.net/bfoeay4f/) and [Mithril.js](https://jsfiddle.net/fft0ht7n/). Their best results are shown below: - -React | Mithril.js -------- | ------- -99.7 ms | 42.8 ms - -What these numbers show is that not only does Mithril.js initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use. - -##### Update performance - -Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running. - -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/react/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Sample results are shown below: - -React | Mithril.js -------- | ------- -12.1 ms | 6.4 ms - -##### Development performance - -Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, [here's the 10,000 node benchmark from above using the development version of React](https://jsfiddle.net/r1jfckrd/). - -##### Drop-in replacements - -There are [several](https://preactjs.com/) [projects](https://github.com/Lucifier129/react-lite) [that](https://infernojs.org/) [claim](https://github.com/alibaba/rax) API parity with React (some via compatibility layer libraries), but they are not fully compatible (e.g. PropType support is usually stubbed out, synthetic events are sometimes not supported, and some APIs have different semantics). Note that these libraries typically also include features of their own that are not part of the official React API, which may become problematic down the road if one decides to switch back to React Fiber. - -Claims about small download size (compared to React) are accurate, but most of these libraries are slightly larger than Mithril.js' renderer module. Preact is the only exception. - -Be wary of aggressive performance claims, as benchmarks used by some of these projects are known to be out-of-date and flawed (in the sense that they can be - and are - exploited). Boris Kaul (author of some of the benchmarks) has [written in detail about how benchmarks are gamed](https://medium.com/@localvoid/how-to-win-in-web-framework-benchmarks-8bc31af76ce7). Another thing to keep in mind is that some benchmarks aggressively use advanced optimization features and thus demonstrate *potential* performance, i.e. performance that is possible given some caveats, but realistically unlikely unless you actively spend the time to go over your entire codebase identifying optimization candidates and evaluating the regression risks brought by the optimization caveats. - -In the spirit of demonstrating *typical* performance characteristics, the benchmarks presented in this comparison page are implemented in an apples-to-apples, naive, idiomatic way (i.e. the way you would normally write 99% of your code) and do not employ tricks or advanced optimizations to make one or other framework look artificially better. You are encouraged to contribute a PR if you feel any DbMonster implementation here could be written more idiomatically. - -#### Complexity - -Both React and Mithril.js have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril.js can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading). - -While complex toolchains are also possible with Mithril.js and other frameworks alike, it's *strongly* recommended that you follow the [KISS](https://en.wikipedia.org/wiki/KISS_principle) and [YAGNI](https://en.wikipedia.org/wiki/You_aren't_gonna_need_it) principles when using Mithril. - -#### Learning curve - -Both React and Mithril.js have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril.js components is nearly identical. There are obviously more APIs to learn in Mithril.js, since Mithril.js also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios. - -Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel. - -#### Documentation - -React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts. Unfortunately, since React is limited to being only a view library, its documentation does not explore how to use React idiomatically in the context of a real-life application. As a result, there are many popular state management libraries and thus architectures using React can differ drastically from company to company (or even between projects). - -Mithril.js documentation also includes [introductory](index.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. - -Mithril.js documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries. - ---- - -### Angular - -Angular is a web application framework maintained by Google. - -Angular and Mithril.js are fairly different, but they share a few similarities: - -- Both support componentization -- Both have an array of tools for various aspects of web applications (e.g. routing, XHR) - -The most obvious difference between Angular and Mithril.js is in their complexity. This can be seen most easily in how views are implemented. Mithril.js views are plain JavaScript, and flow control is done with JavaScript built-in mechanisms such as ternary operators or `Array.prototype.map`. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate JavaScript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in JavaScript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates JavaScript functions dynamically for performance, and [a slower mode](https://docs.angularjs.org/api/ng/directive/ngCsp) for dealing with Content Security Policy restrictions). - -#### Performance - -Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large `$scope` structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril.js is often faster than Angular, due to the ease of auditing that Mithril.js' small codebase size affords. - -It's difficult to make a comparison of load times between Angular and Mithril.js for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril.js are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application. - -With that being said, the smallest known Angular 2 bundle is a [29kb hello world](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/) compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril.js hello world - including the entire Mithril.js core with batteries and everything - would be about 10kb gzipped. - -Also, remember that frameworks like Angular and Mithril.js are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb. - -##### Update performance - -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/angular/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: - -Angular | Mithril.js -------- | ------- -11.5 ms | 6.4 ms - -#### Complexity - -Angular is superior to Mithril.js in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare [Angular's API surface](https://angular.io/docs/ts/latest/api/) with [Mithril.js'](api.md). You can make your own judgment on which API is more self-descriptive and more relevant to your needs. - -Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what. - -#### Learning curve - -If we compare apples to apples, Angular 2 and Mithril.js have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools. - -With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what JavaScript does natively in Mithril.js - Angular's `ng-if`/`ngIf` is a *directive*, which uses a custom *parser* and *compiler* to evaluate an expression string and emulate lexical scoping... and so on. Mithril.js tends to be a lot more transparent, and therefore easier to reason about. - -#### Documentation - -Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for. - -Mithril.js documentation includes [introductory](index.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. - -Mithril.js documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries. - - ---- - -### Vue - -Vue is a view library similar to Angular. - -Vue and Mithril.js have a lot of differences but they also share some similarities: - -- They both use virtual DOM and lifecycle methods -- Both organize views via components - -Vue 2 uses a fork of Snabbdom as its virtual DOM system. In addition, Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive system that overwrites native methods in a component's data tree (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril.js or React views, rather than Angular's compiled rendering functions. - -Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril.js (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril.js is around 4kb gzipped). Both have similar performance characteristics, but benchmarks usually suggest Mithril.js is slightly faster. - -#### Performance - -Here's a comparison of library load times, i.e. the time it takes to parse and run the JavaScript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: - -Vue | Mithril.js -------- | ------- -21.8 ms | 4.5 ms - -Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. - -##### Update performance - -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and JavaScript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [Vue implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/vue/index.html) and a [Mithril.js implementation](https://raw.githack.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: - -Vue | Mithril.js ------- | ------- -9.8 ms | 6.4 ms - -#### Complexity - -Vue is heavily inspired by Angular and has many things that Angular does (e.g. directives, filters, bi-directional bindings, `v-cloak`), but also has things inspired by React (e.g. components). As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins). Vue provides both bi-directional data binding and an optional Redux-like state management library, but unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach can cause architectural fragmentation in long-lived projects. - -Mithril.js has far less concepts and typically organizes applications in terms of components and a data layer. All component creation styles in Mithril.js output the same vnode structure using native JavaScript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup. - -#### Documentation - -Both Vue and Mithril.js have good documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts. - -However, due to Vue's many-ways-to-do-one-thing approach, some things may not be adequately documented. For example, there's no documentation on hyperscript syntax or usage. - -Mithril.js documentation typically errs on the side of being overly thorough if a topic involves things outside of the scope of Mithril. For example, when a topic involves a 3rd party library, Mithril.js documentation walks through the installation process for the 3rd party library. Mithril.js documentation also often demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries. - -Mithril.js' tutorials also cover a lot more ground than Vue's: the [Vue tutorial](https://vuejs.org/v2/guide/#Getting-Started) finishes with a static list of foodstuff. [Mithril.js' 10 minute guide](index.md) covers the majority of its API and goes over key aspects of real-life applications, such as fetching data from a server and routing (and there's a [longer, more thorough tutorial](simple-application.md) if that's not enough). diff --git a/docs/hyperscript.md b/docs/hyperscript.md deleted file mode 100644 index 6f4fdfd3..00000000 --- a/docs/hyperscript.md +++ /dev/null @@ -1,529 +0,0 @@ - - -# m(selector, attributes, children) - -- [Description](#description) -- [Signature](#signature) -- [How it works](#how-it-works) -- [Flexibility](#flexibility) -- [CSS selectors](#css-selectors) -- [Attributes passed as the second argument](#attributes-passed-as-the-second-argument) -- [DOM attributes](#dom-attributes) -- [Style attribute](#style-attribute) -- [Events](#events) -- [Properties](#properties) -- [Components](#components) -- [Lifecycle methods](#lifecycle-methods) -- [Keys](#keys) -- [SVG and MathML](#svg-and-mathml) -- [Making templates dynamic](#making-templates-dynamic) -- [Converting HTML](#converting-html) -- [Avoid anti-patterns](#avoid-anti-patterns) - ---- - -### Description - -Represents an HTML element in a Mithril.js view - -```javascript -m("div.foo", {style: {color: "red"}}, "hello") -// renders to this HTML: -//
hello
-``` - -You can also use an HTML-like syntax called [JSX](jsx.md), using Babel to convert it to equivalent hyperscript calls. This is equivalent to the above. - -```jsx -
hello
-``` - ---- - -### Signature - -`vnode = m(selector, attrs, children)` - -Argument | Type | Required | Description ------------- | ------------------------------------------ | -------- | --- -`selector` | `String|Object|Function` | Yes | A CSS selector or a [component](components.md) -`attrs` | `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) - -[How to read signatures](signatures.md) - ---- - -### How it works - -Mithril.js provides a hyperscript function `m()`, which allows expressing any HTML structure using JavaScript syntax. It accepts a `selector` string (required), an `attrs` object (optional) and a `children` array (optional). - -```javascript -m("div", {id: "box"}, "hello") - -// renders to this HTML: -//
hello
-``` - -The `m()` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a JavaScript object that represents the DOM element to be created. - -```javascript -// a vnode -var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]} -``` - -To transform a vnode into an actual DOM element, use the [`m.render()`](render.md) function: - -```javascript -m.render(document.body, m("br")) // puts a
in -``` - -Calling `m.render()` multiple times does **not** recreate the DOM tree from scratch each time. Instead, each call will only make a change to a DOM tree if it is absolutely necessary to reflect the virtual DOM tree passed into the call. This behavior is desirable because recreating the DOM from scratch is very expensive, and causes issues such as loss of input focus, among other things. By contrast, updating the DOM only where necessary is comparatively much faster and makes it easier to maintain complex UIs that handle multiple user stories. - ---- - -### Flexibility - -The `m()` function is both *polymorphic* and *variadic*. In other words, it's very flexible in what it expects as input parameters: - -```javascript -// simple tag -m("div") //
- -// attributes and children are optional -m("a", {id: "b"}) //
-m("span", "hello") // hello - -// tag with child nodes -m("ul", [ //
    - m("li", "hello"), //
  • hello
  • - m("li", "world"), //
  • world
  • -]) //
- -// array is optional -m("ul", //
    - m("li", "hello"), //
  • hello
  • - m("li", "world") //
  • world
  • -) //
-``` - ---- - -### CSS selectors - -The first argument of `m()` can be any CSS selector that can describe an HTML element. It accepts any valid CSS combinations of `#` (id), `.` (class) and `[]` (attribute) syntax. - -```javascript -m("div#hello") -//
- -m("section.container") -//
- -m("input[type=text][placeholder=Name]") -// - -m("a#exit.external[href='https://example.com']", "Leave") -// Leave -``` - -If you omit the tag name, Mithril.js assumes a `div` tag. - -```javascript -m(".box.box-bordered") //
-``` - -Typically, it's recommended that you use CSS selectors for static attributes (i.e. attributes whose value do not change), and pass an attributes object for dynamic attribute values. - -```javascript -var currentURL = "/" - -m("a.link[href=/]", { - class: currentURL === "/" ? "selected" : "" -}, "Home") - -// renders to this HTML: -// Home -``` - -### Attributes passed as the second argument - -You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details). - -```JS -m("button", { - class: "my-button", - onclick: function() {/* ... */}, - oncreate: function() {/* ... */} -}) -``` - -If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent. - -If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null` or `undefined`, it is ignored. - -If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`. - ---- - -### DOM attributes - -Mithril.js uses both the JavaScript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes. - -For example, in the JavaScript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril.js, all of the following are supported: - -```javascript -m("input", {readonly: true}) // lowercase -m("input", {readOnly: true}) // uppercase -m("input[readonly]") -m("input[readOnly]") -``` - -This even includes custom elements. For example, you can use [A-Frame](https://aframe.io/docs/0.8.0/introduction/) within Mithril.js, no problem! - -```javascript -m("a-scene", [ - m("a-box", { - position: "-1 0.5 -3", - rotation: "0 45 0", - color: "#4CC3D9", - }), - - m("a-sphere", { - position: "0 1.25 -5", - radius: "1.25", - color: "#EF2D5E", - }), - - m("a-cylinder", { - position: "1 0.75 -3", - radius: "0.5", - height: "1.5", - color: "#FFC65D", - }), - - m("a-plane", { - position: "0 0 -4", - rotation: "-90 0 0", - width: "4", - height: "4", - color: "#7BC8A4", - }), - - m("a-sky", { - color: "#ECECEC", - }), -]) -``` - -And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using Brick's `brick-deck` (DEAD LINK, FIXME: http //brick.mozilla.io/docs/brick-deck) as an example, they have a `selected-index` attribute with a corresponding `selectedIndex` getter/setter property. - -```javascript -m("brick-deck[selected-index=0]", [/* ... */]) // lowercase -m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase -// I know these look odd, but `brick-deck`'s `selectedIndex` property is a -// string, not a number. -m("brick-deck", {"selected-index": "0"}, [/* ... */]) -m("brick-deck", {"selectedIndex": "0"}, [/* ... */]) -``` - -For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element `my-special-element` that has an `elem.whitelist` array getter/setter property, you could do this, and it'd work as you'd expect: - -```javascript -m("my-special-element", { - whitelist: [ - "https://example.com", - "https://neverssl.com", - "https://google.com", - ], -}) -``` - -If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example: - -```javascript -// These two are equivalent -m("a-entity#player") -m("a-entity", {id: "player"}) -``` - -Do note that all the properties with magic semantics, like lifecycle attributes, `onevent` handlers, `key`s, `class`, and `style`, those are still treated the same way they are for normal HTML elements. - ---- - -### Style attribute - -Mithril.js supports both strings and objects as valid `style` values. In other words, all of the following are supported: - -```javascript -m("div", {style: "background:red;"}) -m("div", {style: {background: "red"}}) -m("div[style=background:red]") -``` - -Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed. - -You can use both hyphenated CSS property names (like `background-color`) and camel cased DOM `style` property names (like `backgroundColor`). You can also define [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables), if your browser supports them. - -Mithril.js does not attempt to add units to number values. It simply stringifies them. - ---- - -### Events - -Mithril.js supports event handler binding for all DOM events, including events whose specs do not define an `on${event}` property, such as `touchstart` - -```javascript -function doSomething(e) { - console.log(e) -} - -m("div", {onclick: doSomething}) -``` - -Mithril.js accepts functions and [EventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventListener) objects. So this will also work: - -```javascript -var clickListener = { - handleEvent: function(e) { - console.log(e) - } -} - -m("div", {onclick: clickListener}) -``` - -By default, when an event attached with hyperscript fires, this will trigger Mithril.js' auto-redraw after your event callback returns (assuming you are using `m.mount` or `m.route` instead of `m.render` directly). You can disable auto-redraw specifically for a single event by setting `e.redraw = false` on it: - -```javascript -m("div", { - onclick: function(e) { - // Prevent auto-redraw - e.redraw = false - } -}) -``` - ---- - -### Properties - -Mithril.js supports DOM functionality that is accessible via properties such as `` to ``, this being due to JSX being based on XML and not HTML. - -When using hyperscript, you often need to translate HTML to hyperscript syntax to use it. To help speed up this process along, you can use a [community-created HTML-to-Mithril-template converter](https://arthurclemens.github.io/mithril-template-converter/index.html) to do much of it for you. diff --git a/docs/keys.md b/docs/keys.md deleted file mode 100644 index 6ac6b7e8..00000000 --- a/docs/keys.md +++ /dev/null @@ -1,553 +0,0 @@ - - -# Keys - -- [What are keys?](#what-are-keys?) - - [Key restrictions](#key-restrictions) -- [Linking model data in lists of views](#linking-model-data-to-views) -- [Keeping collections of animated objects glitch-free](#keeping-collections-of-animated-objects-glitch-free) -- [Reinitializing views with single-child keyed fragments](#reinitializing-views-with-single-child-keyed-fragments) -- [Common gotchas](#common-gotchas) - - [Wrapping keyed elements](#wrapping-keyed-elements) - - [Putting keys inside the component](#putting-keys-inside-the-component) - - [Keying elements unnecessarily](#keying-elements-unnecessarily) - - [Mixing key types](#mixing-key-types) - - [Hiding keyed elements with holes](#hiding-keyed-elements-with-holes) - - [Duplicate keys](#duplicate-keys) - ---- - -### What are keys? - -Keys represent tracked identities. You can add them to [element, component, and fragment vnodes](vnodes.md) via the magic `key` attribute, and they look something like this when used: - -```javascript -m(".user", {key: user.id}, [/* ... */]) -``` - -They are useful in a few scenarios: - -- When you're rendering model data or other stateful data, you need keys to keep the local state tied to the right subtree. -- When you're independently animating multiple adjacent nodes using CSS and you could remove any one of them individually, you need keys to ensure the animations stick with the elements and don't end up unexpectedly jumping to other nodes. -- When you need to reinitialize a subtree on command, you need to add a key and then change it and redraw whenever you want to reinitialize it. - -#### Key restrictions - -**Important:** For all fragments, their children must contain either exclusively vnodes with key attributes (keyed fragment) or exclusively vnodes without key attributes (unkeyed fragment). Key attributes can only exist on vnodes that support attributes in the first place, namely [element, component, and fragment vnodes](vnodes.md). Other vnodes, like `null`, `undefined`, and strings, can't have attributes of any kind, so they can't have key attributes and thus cannot be used in keyed fragments. - -What this translates to is stuff like `[m(".foo", {key: 1}), null]` and `["foo", m(".bar", {key: 2})]` won't work, but `[m(".foo", {key: 1}), m(".bar", {key: 2})]` and `[m(".foo"), null]` will. If you forget this, you'll get a very helpful error explaining this. - -### Linking model data in lists of views - -When you're rendering lists, especially editable list, you're often dealing with things like editable TODOs and such. These have state and identities, and you have to give Mithril.js the information it needs to track them. - -Suppose we have a simple social media post listing, where you can comment on posts and where you can hide posts for reasons like reporting them. - -```javascript -// `User` and `ComposeWindow` omitted for brevity -function CommentCompose() { - return { - view: function(vnode) { - var post = vnode.attrs.post - return m(ComposeWindow, { - placeholder: "Write your comment...", - submit: function(text) { - return Model.addComment(post, text) - }, - }) - } - } -} - -function Comment() { - return { - view: function(vnode) { - var comment = vnode.attrs.comment - return m(".comment", - m(User, {user: comment.user}), - m(".comment-body", comment.text), - m("a.comment-hide", - {onclick: function() { - Model.hideComment(comment).then(m.redraw) - }}, - "I don't like this" - ) - ) - } - } -} - -function PostCompose() { - return { - view: function(vnode) { - var comment = vnode.attrs.comment - return m(ComposeWindow, { - placeholder: "Write your post...", - submit: Model.createPost, - }) - } - } -} - -function Post(vnode) { - var showComments = false - var commentsFetched = false - - return { - view: function(vnode) { - var post = vnode.attrs.post - var comments = showComments ? Model.getComments(post) : null - return m(".post", - m(User, {user: post.user}), - m(".post-body", post.text), - m(".post-meta", - m("a.post-comment-count", - {onclick: function() { - if (!showComments && !commentsFetched) { - commentsFetched = true - Model.fetchComments(post).then(m.redraw) - } - showComments = !showComments - }}, - post.commentCount, " comment", - post.commentCount === 1 ? "" : "s", - ), - m("a.post-hide", - {onclick: function() { - Model.hidePost(post).then(m.redraw) - }}, - "I don't like this" - ) - ), - showComments ? m(".post-comments", - comments == null - ? m(".comment-list-loading", "Loading...") - : [ - m(".comment-list", comments.map(function(comment) { - return m(Comment, {comment: comment}) - })), - m(CommentCompose, {post: post}), - ] - ) : null - ) - } - } -} - -function Feed() { - Model.fetchPosts().then(m.redraw) - return { - view: function() { - var posts = Model.getPosts() - return m(".feed", - m("h1", "Feed"), - posts == null ? m(".post-list-loading", "Loading...") - : m(".post-view", - m(PostCompose), - m(".post-list", posts.map(function(post) { - return m(Post, {post: post}) - })) - ) - ) - } - } -} -``` - -It encapsulates a lot of functionality as you can tell, but I'd like to zoom into two things: - -```javascript -// In the `Feed` component -m(".post-list", posts.map(function(post) { - return m(Post, {post: post}) -})) - -// In the `Post` component -m(".comment-list", comments.map(function(comment) { - return m(Comment, {comment: comment}) -})) -``` - -Each of these refers to a subtree with associated state Mithril.js has no idea about. (Mithril.js only knows about vnodes, nothing else.) When you leave those unkeyed, things can and will get weird and unexpected. In this case, try clicking on the "N comments" to show the comments, typing into the comment compose box at the bottom of it, then clicking "I don't like this" on a post above it. [Here's a live demo for you to try it out on, complete with a mock model. (Note: if you're on Edge or IE, you may run into issues due to the link's hash length.)](https://flems.io/#0=N4Igxg9gdgzhA2BTEAucD4EMAONEBMQAaEGMAJw1QG0AGIgZgCYB2AXRIDMBLJGG0FEwBbZGgB0ACwAuw+MXRRpiJahAgAvkUEixIYRHyJ44gFb8SkJSulqAOiACueAAQxp5bmGkO7UPwBumOQuALKGxi4AvC4AFJyOUN7c0LEAlC7AfnbSQSFgmGCSiNEuAPIARqaI3uIUiJjKsVCO8PBp2T7+0glJ0ilQLgVFBLEA1ogAnkQu3FDc0hlZ3TnkiNKO5IPDxdQTk2wuAIQxLW0uAPxDhbv7hyhxO4h7U4cxcwvpHd0a2d29yWgLmEmAmAFU8OR0plOjk8i5nIhyAA5XSlTigpHiObKLbrcSIlG6dKwjzrTbbG6jByElAOFwAagRkNRohmAP6qSWpJyAHpeS4AJJGTBtaZCgDk+Bcpmc0mZJXgEDWwmw3AAHpF8Nw1t54OKKo55QsXPgIIgYFAJfKYI5sNhlV0cs7pPyXAAJAAqnoACgBlcQuP0QWawZSYfAzQUuSQRlzSEMAd08ymBkxcEETgwAjo4vGMXMj1vBuJx05CAl5EDzXQKEy5E8qC8EIIlpSbMFB20l4I4jC5sJRsEiXABhMoAJT9MYaRnIMDpKx8tZj0mkuBQ-IA5gtJI4KnUIMJedwYNxMJJRBHMzBeUqVWrNfAALSQefPwcQdWTGvw7jCLcwXIeBSgcGR1wXfljEQLdO2kZ9k24LcZGfTAABZMAAVkQNDxCgYtS0mQ9jzwgiy15DkBlvNZcwtHwQD+F1VnJLZgXEGjHDo2J-0A4CZmWJjlwqQoxi3Sg2weDxOKIGtmJgB1YEQT1JmHB4HAqJUKgcGSlxyDQ0nEaRiigeJEkBEyNIgCpuV05c1g2ViBOXQTuHwB5HDzfBxACND0h0wSXUJVlEHclldH85zIsJH0vAckKXDBCcABk6jWRpEEqapvESpLYks6yIsE35bP00lSp+RiejMzlBhBcFIRgWJIESRYYSXeFCRgUpqDYUlOGVOI-1KWgAG5ZhcAAeIZWyUMbuAZBkMk68RsGcSRYjqxAISRL5SXsikXB9ShhFPRBxFFeBYk6749Mq+EwE2NYlG28gZgddwYEqyigU4dYihe6EnOYhzKRGfBYhpSFtJcb6TJswT9tYzaAYMoyVFMvoBiuyF4cinIHvIJ7pBe0pCRrcqXQp6Ris6WGYb+yQfQgD7Ab2ljQeKcGHHe6RPuIGHqqx3GXUR2rMRexqMQmch2M7M1hDwxxhAqHbgBOqAHgARhmEF1QeJhaH0m68ekQzjIx8zYhLdxhZNnmupia3TZBbALZq7GkVtgKyRBtrvZctyEU87zfLSQqTcCyFQqRcP-bXRB1WkB4paxe9EAV5RE782STcgYRRCUUcZqTmHMRl8g5aPRXldV9WHnoYFMD1lwGENsOc8pjvqeNoqe+7zoae6e6jwL3nSkympTfqdLmladovsFn6GaL-ObEannbdF64wYhkAebpEBGQHZnTdc9lF7hv3Iq35GGtiHniNHouWtR83Yat09WqByK89HmBqAfq5N4LgnbiBdm7LGhIvbX3ZlfOOrl3LBx8tnWyglaQKlel3HImcS4pxlmnDOCdpAoLjjTE2VNKZ9ypoPHIdMVQQACIgAAQpMYUH93AzAWOnW2w8WrDT6gNWIQ0YijXGlNUBSAoBbiMnNBa0DSxxCdtQbgbBsTShOO8ZQCtXIZCUc1JQC1gFKJUWVUkEiVDSMkKUfRTo7KwPwomQ6x1ToQNSGsOA8BGFLDcOsT0-5ECtmIe4hAjCZiYVoLQDI1DKpb2-r9aQ-0o70wSZIF64d4lFCZh9ZODMsm83ScvEea8ckpJXn-WOS4tzrBesnC+rNbJbwJkTF6ZUKkuiqdIPJC4BaYy5HAuxvt7bHFOHPS4x8PriBgCWMAiBoQPDOPAVplV8ZpWUHk2pvSTI4OgVvDJqTITpDNujd+0Ccj2xWmtWI38A6INciHEhEcNhJKaTYNJXccGSSIbHH+RTC7F3rt8vSfcgVLP8EuSQrlEDrJ6ZbDe-TgYHXoYwlhbD7ZvRPlQ-ylT1hlOKTC92cLrk+wOr-NeACT5qMOCcFwCyxmkqUP-QB+BVFTKrHMmlc9QWkgjPgXFSgNmwpPjMbZ8LiWsSOkeU6502ixGoDnPZKNAU9EKavBl98MU1jYK-Y5dTTkJN+bzcl7hKUXJgOtIlLoEFBzucg9uqDlzoJec9SESrsFEM+YnJVFDlzeq0MsoykK+W4LqYSmsW8kXMNYeDelhqmUcHGYsLl1M-D6XSAoPASBzL8DQJrTCKBMIAE5NDaBAEIUQagzAWEUMoVQaBIYlHcJ4bwvgwV0xRlfWJpJKyIETAK92AQoARGgR1SEpQB0RHOmuecBIoYNNgcIXeM6kTaRzguhwS7yAflipsas-M10gB4tDYAMByBgGjjLaK261hGyVfujdz4y27pmA4AAAtDQkG7gp2sElQgeKa-h0xXu9RAAB1OYZpEz1LhMENw+4TprjmFudEoo8CdHhDgzh8x+iindIgJCMhOidvatwHtfasbjqMMOmDPMx2DqMJOjwMAVonzDfOxdedgMrvtfunBwQGhcbjhayK2oYCYA0gQB4toKjwf6FImYQRezxQw1ghMSRVnxXfhRxAeqXJYfPPAXD+H5QxAZImcDmZxAdKA0aAgfppCTCQEIujZ15YGWKEZyZ0zZkN2fEwYFjytPiHlpM+zSApB4eQsZlwDhMBGggC2uOLpAvBfcA5s67nIulFCI0SQYCm7cT0zhiLMh5POaC1XMglA2iGcixkJk3N1QJceXpV1y5oBzFWsGzZsREABB05FHBpReuGWCB07yopOIqZyMN6Qo38SpbCxlmQoEQCxYTE1xLygAgjfIGNhb6XitRey0ZPL6oCsLH0zVkrLgZtzdNpVjAV3Wr1b3o1hi9rKatbOVgGZkgEBzgeIFxojGVo-cQH9+Ac5Wt+vtd+k2d6OPM0QM+KT8Hob7sNGuaAAnHnAGgGAaZYwyN9KEy6VHCxZNIZiFJasH3lxA6nUx8nxDtlHJMic0V3tmeU+Q-AVDdOXQKzWPgCukH-O9xU4NmIvh3ukJh4lkAeSNuRTSP539FUoCD0AwaoDSOoNisGN-btvb8Xkec1RkINGYgM5BzzVjvsF267wGBrsmZ+I52wGDiHAPosgBAymEokxWz5ANeIMPOPBLM+J1soh-Wt7hCMCYHlQb1UcPjLHnO8vIq+r-ZrgDF8xwGv10RpLJGTeafN6K4eqqos2+nTG+3B0EcGoj5FBdaTMiOtD4SfSt72MGufBUQwP5+YxsMrHvvMXH42GfBCowrfBJ457PmaP+u44J+MFIQNReY3apMkLggovxc+qz-7EA0YzRWnlCWCY8YIV8xzmr0xGutcF7yU72ZuMS-07L6vrTFvpoa9aMJ1gd68W850HdYgP8XcIN3d7VPdChwd-skQ1I-cA8XAg9NgE0w9xAF9lwo8wgIgTBp41khVyZ1dbo89W038T4nMh0r54QzVMwg0HZS4+dadoMQ8a8YAAAxBmAgXnfnQjWBI3X-U3VIf-KvajE+YA+jUApjO3WyavP+UoJgxMFgsZDfEwKzA1deDFFweZTlCApvRdO3YgVdWIDvYAdBB+HvOHQSO9HmQfYfaGB+bZSfEAZjdwZ8UQWbPAwXXeTALw+CGNV8YufwyKJfAnFfcQy+UnSKBRWII4NQjQgAMlSOOBjV4P4PwH60EiyL4JSQEOp3IEmwF0ii0PED2RYNT0WDZw2nYkP0wETGPyKim15j+3UN0NKGSM6JYJUw0FP0eQfhjWfiUGfUPgb3MPKJGJ12LmiCiBiE1jGRlwMN9z5la3sPh0COCNn0hQiJdCiMJ1XzyOXEqLnyhVoI3nqIPxF2aNaJPy+3ogv2gGtBAW4FvwDQfzp2Py2LJz6O6KuEcJPjCO4IOP1W4IWI5TaC7iBP7xr2fCdkRIgAjEQ2hgcCShRO1CkRwIcAeIeDlXKObwRKdmhiyLy1dnfl305xNhvigINX4hjQeBjW9W9hvRU0dzmNVCR34n3gTV7y7l6lhzWIWRrAoOTSoL8Dpj4NGFxkqL2S6UOTRn30aLuJaOEN9lENI1iLX3p2kI+iyyIMs3WEVP8zpPXV+gIAiP3UkE1nRJABlPwDxKVSGUWOhJAjhPXScKRKVFRKkXtMxL9K3FxJAGPweGBO8ON3BIXXfyPGAz+Lb1MJBNJP5nOXAXflDXKLpLyV5JPgeB5lZOz1V0fzFOf0oMHgVgMBaliDNAelHnECH3wHFEdLSHTWgizTUE1mYBQCYAADZi0dBy0JAwAYAq0rAa1bA0BGz0xlgXAYZoB4IMQTp9QHgABBTwUUGYUTWAFHJEUsEaf9MFDdNqOckTBAyYZOJAdUA8wYFwUUJCKAZ8LhYQbpGZawcgG8weYIlwY82cxuXbOYZwrHYQeucQbCYQT8v4e9NUbwHdE8-8ncR8zwSLUC8CyCo8wkLdWCtYWYACeCpbEuBgdOG8ucofcgOcZ8CubUZwB4IiiCw8vwYImYYiYDeCsin3TWbAdUNwBAVyFwDSESEi-i5UCiqi7gGilwTWYivwOc7AHlRDLWaS28kEACx8hMbAUClgNChiqAFipHdPROPjTANikSlAmlaARAIS9xbgAAL3ikHXwiEoYSRE4CVBNznyMCgCErM3wCMi1giQAFIhKCLaKlLTzTxzyHhLIwAxghL+olAUdbL4opL6LJTdLEc8AUc4MTQ-ycFUISwtwNYXBkKZB0LGKMqShMdVN4L4r4Izw7LFLUrSLTLN0xKJKmAwrhLyKzKHLLKZL+KRIxIZpA4ABiWgTAFgISyAe8B4EazgeaoSuS-AbErcVC9OSSzqs8rAC80MEsfCQfJUGKsq3Spw3w4yv8razAHa1yhOIS2UdwQiMI6wflNwOSmZQfdYRMRAFQISlSxC58dSzS7S282qxKhqlwWgcQAADiUq-NOvWGMoAD4XAAAqI+NGv8v6wCpATgEuFK463Y0ImxZipwi45i0Ii4ky7q8geZCyxa+SqReuOKhcsG5Kzq0G-bB4BYe8sAKazYOAGm4+HEJEAmpwom-5P7RhTBE6kEi4lASWmOcqgfOWhWkIP86a5UWaqG7W0WkErI+CrGtSiADSiGsC2Gv4F9BCRACoMYBYZ8fYTgCuUQLqTsf8dKQMla+C2gAKzIaaZ6kuDErEtEkAMaYqOcpgTCH24AP2ic1Aj2xDXAkOlwMOlwcJKOmOmwOOoOnExO0O-qrS9O8czO33eOnO3O5Ow8l9B2p2i0O8+YEEZQUupDP87232oul6wOoMhwPO28iOwuhc4uzula8ulOtOtugejukAJu8PJOlOgu8e-2rOoMkMnuuczWQKhe2Oku7OrcbuiutK8QX0z23KohJ8rsYu58AtK+gtLioSva5HEKiGgmo+hSlATAXGkcE+xOM+zykuUafq++2fQ7LmwreAISy6na6K2K-q9ugOqenele-qq2m2u212hugYfWLqe+mDSGzCF2+u92ne0MHgLDPq28tBxoDBlwJgLBuYBoEIXB-Bt2xuohuYEhrhAm0Ip2Gqlm+q+KSGmG1Kr8rhz+H8zCmCuKXC5u-qx+jqoRqCmNeCpalaxqoS9isyzi7ijxPigSo6-q9R1q1EiSyGrSzqvK+8wqh4Eq6QThgfac+C8xgqoqnGmx-qxxh8yTWbcgVxg+imyFA20bQCwG020x+Ro8sW7ovS1wP8gxrWLiniksaUXR6B28gxyiox7pSG3szq5RhSkJ4Gucw2gG42pmnSwmyJiqgyrxhofC4Bludm3hpK0CwR3W7w-WqJyqo0aqv8jmpp02lp-q3Jxm028CjaoRtszNGqbNEALspgFATWTQDgEAPasYbNagQcvQeDSQTweQEgTYeQNAcCDcfkRIbAUSYiXkLZnZl9JgcQSGtCS53cHZsBOYStBQezYcNQSrbgbAWwLQDZtQPBF9XCTWO5hQfZtQI5yCXkRMGy8i7EKAXkdwOWUUCyiiMuYF8QUF2gd5lSPQb535gc0tXQNQDyVyF9BgcQSlhgcF4CSFtcY5mFuFryOYJF2bLsVF-CXkMl-AClqlql3Fz5tAAlv5tgDQIAA) - -Instead of doing what you would expect, it instead gets really confused and does the wrong thing: it closes the comment list you had open and the post after the one you had the comments open on now just persistently shows "Loading..." even though it thinks it's already loaded the comments. This is because the comments are lazily loaded and they just assume the same comment is passed each time (which sounds relatively sane here), but in this case, it's not. This is because of how Mithril.js patches unkeyed fragments: it patches them one by one iteratively in a very simple fashion. So in this case, the diff might look like this: - -- Before: `A, B, C, D, E` -- Patched: `A, B, C -> D, D -> E, E -> (removed)` - -And since the component remains the same (it's always `Comment`), only the attributes change and it's not replaced. - -To fix this bug, you simply add a key, so Mithril.js knows to potentially move state around if necessary to fix the issue. [Here's a live, working example of everything fixed.](https://flems.io/#0=N4Igxg9gdgzhA2BTEAucD4EMAONEBMQAaEGMAJw1QG0AGIgZgCYB2AXRIDMBLJGG0FEwBbZGgB0ACwAuw+MXRRpiJahAgAvkUEixIYRHyJ44gFb8SkJSulqAOiACueAAQxp5bmGkO7UPwBumOQuALKGxi4AvC4AFJyOUN7c0LEAlC7AfnbSQSFgmGCSiNEuAPIARqaI3uIUiJjKsVCO8PBp2T7+0glJ0ilQLgVFBLEA1ogAnkQu3FDc0hlZ3TnkiNKO5IPDxdQTk2wuAIQxLW0uAPxDhbv7hyhxO4h7U4cxcwvpHd0a2d29yWgLmEmAmAFU8OR0plOjk8i5nIhyAA5XSlTigpHiObKLbrcSIlG6dKwjzrTbbG6jByElAOFwAagRkNRohmAP6qSWpJyAHpeS4AJJGTBtaZCgDk+Bcpmc0mZJXgEDWwmw3AAHpF8Nw1t54OKKo55QsXPgIIgYFAJfKYI5sNhlV0cs7pPyXAAJAAqnoACgBlcQuP0QWawZSYfAzQUuSQRlzSEMAd08ymBkxcEETgwAjo4vGMXMj1vBuJx05CAl5EDzXQKEy5E8qC8EIIlpSbMFB20l4I4jC5sJRsEiXABhMoAJT9MYaRnIMDpKx8tZj0mkuBQ-IA5gtJI4KnUIMJedwYNxMJJRBHMzBeUqVWrNfAALSQefPwcQdWTGvw7jCLcwXIeBSgcGR1wXfljEQLdO2kZ9k24LcZGfTAABZMAAVkQNDxCgYtS0mQ9jzwgiy15DkBlvNZcwtHwQD+F1VnJLZgXEGjHDo2J-0A4CZmWJjlwqQoxi3Sg2weDxOKIGtmJgB1YEQT1JmHB4HAqJUKgcGSlxyDQ0nEaRiigeJEkBEyNIgCpuV05c1g2ViBOXQTuHwB5HDzfBxACND0h0wSXUJVlEHclldH85zIsJH0vAckKXDBCcABk6jWRpEEqapvESpLYks6yIsE35bP00lSp+RiejMzlBhBcFIRgWJIESRYYSXeFCRgUpqDYUlOGVOI-1KWgAG5ZhcAAeIZWyUMbuAZBkMk68RsGcSRYjqxAISRL5SXsikXB9ShhFPRBxFFeBYk6749Mq+EwE2NYlG28gZgddwYEqyigU4dYihe6EnOYhzKRGfBYhpSFtJcb6TJswT9tYzaAYMoyVFMvoBiuyF4cinIHvIJ7pBe0pCRrcqXQp6Ris6WGYb+yQfQgD7Ab2ljQeKcGHHe6RPuIGHqqx3GXUR2rMRexqMQmch2M7M1hDwxxhAqHbgBOqAHgARhmEF1QeJhaH0m68ekQzjIx8zYhLdxhZNnmupia3TZBbALZq7GkVtgKyRBtrvZctyEU87zfLSQqTcCyFQqRcP-bXRB1WkB4paxe9EAV5RE782STcgYRRCUUcZqTmHMRl8g5aPRXldV9WHnoYFMD1lwGENsOc8pjvqeNoqe+7zoae6e6jwL3nSkympTfqdLmladovsFn6GaL-ObEannbdF64wYhkAebpEBGQHZnTdc9lF7hv3Iq35GGtiHniNHouWtR83Yat09WqByK89HmBqAfq5N4LgnbiBdm7LGhIvbX3ZlfOOrl3LBx8tnWyglaQKlel3HImcS4pxlmnDOCdpAoLjjTE2VNKZ9ypoPHIdMVQQACIgAAQpMYUH93AzAWOnW2w8WrDT6gNWIQ0YijXGlNUBSAoBbiMnNBa0DSxxCdtQbgbBsTShOO8ZQCtXIZCUc1JQC1gFKJUWVUkEiVDSMkKUfRTo7KwPwomQ6x1ToQNSGsOA8BGFLDcOsT0-5ECtmIe4hAjCZiYVoLQDI1DKpb2-r9aQ-0o70wSZIF64d4lFCZh9ZODMsm83ScvEea8ckpJXn-WOS4tzrBesnC+rNbJbwJkTF6ZUKkuiqdIPJC4BaYy5HAuxvt7bHFOHPS4x8PriBgCWMAiBoQPDOPAVplV8ZpWUHk2pvSTI4OgVvDJqTITpDNujd+0Ccj2xWmtWI38A6INciHEhEcNhJKaTYNJXccGSSIbHH+RTC7F3rt8vSfcgVLP8EuSQrlEDrJ6ZbDe-TgYHXoYwlhbD7ZvRPlQ-ylT1hlOKTC92cLrk+wOr-NeACT5qMOCcFwCyxmkqUP-QB+BVFTKrHMmlc9QWkgjPgXFSgNmwpPjMbZ8LiWsSOkeU6502ixGoDnPZKNAU9EKavBl98MU1jYK-Y5dTTkJN+bzcl7hKUXJgOtIlLoEFBzucg9uqDlzoJec9SESrsFEM+YnJVFDlzeq0MsoykK+W4LqYSmsW8kXMNYeDelhqmUcHGYsLl1M-D6XSAoPASBzL8DQJrTCKBMIAE5NDaBAEIUQagzAWEUMoVQaBIYlHcJ4bwvgwV0xRlfWJpJKyIETAK92AQoARGgR1SEpQB0RHOmuecBIoYNNgcIXeM6kTaRzguhwS7yAflipsas-M10gB4tDYAMByBgGjjLaK261hGyVfujdz4y27pmA4AAAtDQkG7gp2sElQgeKa-h0xXu9RAAB1OYZpEz1LhMENw+4TprjmFudEoo8CdHhDgzh8x+iindIgJCMhOidvatwHtfasbjqMMOmDPMx2DqMJOjwMAVonzDfOxdedgMrvtfunBwQGhcbjhayK2oYCYA0gQB4toKjwf6FImYQRezxQw1ghMSRVnxXfhRxAeqXJYfPPAXD+H5QxAZImcDmZxAdKA0aAgfppCTCQEIujZ15YGWKEZyZ0zZkN2fEwYFjytPiHlpM+zSApB4eQsZlwDhMBGggC2uOLpAvBfcA5s67nIulFCI0SQYCm7cT0zhiLMh5POaC1XMglA2iGcixkJk3N1QJceXpV1y5oBzFWsGzZsREABB05FHBpReuGWCB07yopOIqZyMN6Qo38SpbCxlmQoEQCxYTE1xLygAgjfIGNhb6XitRey0ZPL6oCsLH0zVkrLgZtzdNpVjAV3Wr1b3o1hi9rKatbOVgGZkgEBzgeIFxojGVo-cQH9+Ac5Wt+vtd+k2d6OPM0QM+KT8Hob7sNGuaAAnHnAGgGAaZYwyN9KEy6VHCxZNIZiFJasH3lxA6nUx8nxDtlHJMic0V3tmeU+Q-AVDdOXQKzWPgCukH-O9xU4NmIvh3ukJh4lkAeSNuRTSP539FUoCD0AwaoDSOoNisGN-btvb8Xkec1RkINGYgM5BzzVjvsF267wGBrsmZ+I52wGDiHAPosgBAymEokxWz5ANeIMPOPBLM+J1soh-Wt7hCMCYHlQb1UcPjLHnO8vIq+r-ZrgDF8xwGv10RpLJGTeafN6K4eqqos2+nTG+3B0EcGoj5FBdaTMiOtD4SfSt72MGufBUQwP5+YxsMrHvvMXH42GfBCowrfBJ457PmaP+u44J+MFIQNReY3apMkLggovxc+qz-7EA0YzRWnlCWCY8YIV8xzmr0xGutcF7yU72ZuMS-07L6vrTFvpoa9aMJ1gd68W850HdYgP8XcIN3d7VPdChwd-skQ1I-cA8XAg9NgE0w9xAF9lwo8wgIgTBp41khVyZ1dbo89W038T4nMh0r54QzVMwg0HZS4+dadoMQ8a8YAAAxBmAgXnfnQjWBI3X-U3VIf-KvajE+YA+jUApjO3WyavP+UoJgxMFgsZDfEwKzA1deDFFweZTlCApvRdO3YgVdWIDvYAdBB+HvOHQSO9HmQfYfaGB+bZSfEAZjdwZ8UQWbPAwXXeTALw+CGNV8YufwyKJfAnFfcQy+UnSKBRWII4NQjQgAMlSOOBjV4P4PwH60EiyL4JSQEOp3IEmwF0ii0PED2RYNT0WDZw2nYkP0wETGPyKim15j+3UN0NKGSM6JYJUw0FP0eQfhjWfiUGfUPgb3MPKJGJ12LmiCiBiE1jGRlwMN9z5la3sPh0COCNn0hQiJdCiMJ1XzyOXEqLnyhVoI3nqIPxF2aNaJPy+3ogv2gGtBAW4FvwDQfzp2Py2LJz6O6KuEcJPjCO4IOP1W4IWI5TaC7iBP7xr2fCdkRIgAjEQ2hgcCShRO1CkRwIcAeIeDlXKObwRKdmhiyLy1dnfl305xNhvigINX4n2AeDHzPkANHmZINW9W9hvRU0dzmNVCR34n3gTV7y7l6lhzWIWRrAoOTSoL8Dpj4NGFxkqL2S6UOTRn30aLuJaOEN9lENI1iLX3p2kI+iyyIMs3WDVP8zpPXV+gIAiP3UkE1nRJAEVPwDxKVSGUWOhJAjhPXScKRKVFRKkRdMxODK3FxJAGPweGBO8ON3BIXXfyPGAz+Lb1MJBNJP5nOXAXflDXKLpLyUZKmAeCZXRXcBLJPi5Oz1V0f2lOf0oMHgVgMBaliDNAelHnECH3wHFDdLSHTWgizTUE1gADZNYUAmAAAOYtHQctCQMAGAKtKwGtWwNALs9MZYFwGGaAeCDEE6fUB4AAQU8FFBmFE1gBRyRFLBGn-TBQ3Tak3JEwQMmGTiQHVGvMGBcFFCQigGfC4WEG6RmWsHIHfMHmCJcDvI3Mbl2zmGcKx2EHrnEGwmEBAr+HvTVG8B3XvKgp3B-M8EiwQqQpQtvMJC3QwrWFmAAiwqWxLgYHTnfM3KH3IDnGfArm1GcAeFouQpvL8GCJmGImAywsYp901mwHVDcAQFchcA0hEnoqkuVGYtYu4HYpcE1jor8E3OwB5UQy1jUo-JBGgp-ITGwAQpYEIu4qgH4qR3T0Tj40wEEvkpQJpWgEQFkvcW4AAC94pB18JZKGEkROAlQTc58jAoBZKzN8AjItYIkABSWS6iji3Sh808J8h4SyMAMYWS-qJQFHDy+KVSriuUiyxHPAFHODE0SCnBVCEsLcDWFwPCmQIini4qkoTHVTLCrK+CM8TynSgqhihyzdRS5SpgRKuSpixy7yly9SqSkSMSGaQOAAYloEwBYFksgHvAeHms4C2tks0vwGxK3AIvThUpGsfKwGfNDBLHwkHyVHSsaosqcN8LssgtOswHOoCoTlktlHcEIjCOsH5TcE0pmUH3WETEQBUFkv0pwufCMpMrMo-I6pyu6pcFoHEAnN0tAoevWDsoAD4XAAAqI+AmyCyGmCpATgEufKu63Y0ImxPipwi4vi0Ii4+ysa8geZZynarSqReuTK7cxGvKkahG-bB4BYL8sAVazYOANm4+HEJEKmpwmm-5P7RhTBe6kEi4lAZWmOJqgfDWrWkISCta5UDaic02+WkErIrCkmwyiAYy5GxC9Gv4F9BCRACoMYBYZ8fYTgCuUQLqTsf8dKMM-arC2gaKzIaaP6kuDErEtEkAMaYqTcpgTCMO4ACO5c1AoOxDXAuOlwBOlwcJFOtOmwDOmOnE7O+Oqa0ywupc4u33TOsu8u3Om8l9L2n2i0T8+YEEZQeupDSC0O8Omu-66O8MhwCuj8pO6u7c2u4e-axuvOgugeqeoekAHu8PHOvOquxeyOku8MyMsezczWGKre9Ouu0urcUepuwq8QIM4OiqohX8rsWu58AtF+gtUS2Sy65HeK5Gqmm+7SlATAcmkcO+xOB+kKkuUaKaz+2fQ7EWwreAWSl686tKjKqaweqOles+veqal2t2j2-2rugYfWLqT+mDFGzCP2zuwOs+0MHgLDSaj8ghxoIhlwJgEhuYBoEIchyhgO7umhuYOhrhKm0Ip2dqvmrq+KFGtGgq0CkRz+cCki9CuKCi3uqa7+4amR1CmNLC3a-anq2SoSxykSsSjxSS6S26qawxga1E5SlG0ykayqr8mqh4eq6QYRgfNcrCxx6q2qsmtxqa7x78yTWbcgfxq+pmyFK20bGCmG+2+xzR28hW7oyy1wSCqxrWUS8SksaUcx1Bj8qxlimx7pFG4cka3R7SuJuGzc626G22nm8y6m5J5q6ykJhoKi2BluQW8R3KhC6R827wy2lJlqo0NqyCoWnp+2vpqa8p7m+2pC46mR-szNGqbNEATWZgFATWTQDgEAS6sYbNagGcvQeDSQTweQEgTYeQNAcCDcfkRIbAUSYiXkE5s5l9JgcQFGtCZ53cM5sBOYStBQezYcNQSrbgbAWwLQI5tQPBF9XCTWD5hQS5tQG5yCXkRMdypi7EKAXkdwOWUUZyiiMuWF8QeF2gQFlSPQUF8F6c0tXQNQDyVyF9BgcQZlhgRF4CZFtcW5tFjFryOYHF2bLsfF-CXkBl-AJlllll8l4FtAKliFtgDQIAA) - -```javascript -// In the `Feed` component -m(".post-list", posts.map(function(post) { - return m(Post, {key: post.id, post: post}) -})) - -// In the `Post` component -m(".comment-list", comments.map(function(comment) { - return m(Comment, {key: comment.id, comment: comment}) -})) -``` - -Note that for the comments, while it would technically work without keys in this case, it would similarly break if you were to add anything like nested comments or the ability to edit them, and you'd have to add keys to them. - -### Keeping collections of animated objects glitch-free - -On certain occasions, you might be wanting to animate lists, boxes, and similar. Let's start out with this simple code: - -```javascript -var colors = ["red", "yellow", "blue", "gray"] -var counter = 0 - -function getColor() { - var color = colors[counter] - counter = (counter + 1) % colors.length - return color -} - -function Boxes() { - var boxes = [] - - function add() { - boxes.push({color: getColor()}) - } - - function remove(box) { - var index = boxes.indexOf(box) - boxes.splice(index, 1) - } - - return { - view: function() { - return [ - m("button", {onclick: add}, "Add box, click box to remove"), - m(".container", boxes.map(function(box, i) { - return m(".box", - { - "data-color": box.color, - onclick: function() { remove(box) }, - }, - m(".stretch") - ) - })), - ] - }, - } -} -``` - -It looks pretty innocent, but [try a live example](https://flems.io/#0=N4IgzgxgTg9gNnEAuA2gBgDRoLoZAMwEs4BTMZFUAOwEMBbE5EAOgAsAXOxPCGK9kvyYgAvhmr1GSFgCtyPPgKHSAbjSgACXnBhQwGgLwaUAHRBQSAEzMYNZgJ4kEMAO427IAEZwAriXdmAOZQNPZm2CZUapq8PvwkmkZokZH4cRDshHwagSTsAMLwugAUAJQawCns0VpFibU6eiix8VARVCbsLQL1xd0JGgDUGgCM5QCkDbpgzKRUgeysVRbsPlBUU1CRIilUaVQZWRsAQjAAHmRlFVU1nudkhsbtVfuH2TSWlleVHeyddxcZgAHHxgVjFYDaXRIHJ5QqNMoiUpVHYdX6vTLZCx0GAqEjFAHlH6dTo1QhUSwkM6PAFkZjkylnADy+AJ52Rv3+9xmYCBcEIEHxDKptjGKN2nRWaw2xL+1UIJBcMIxR2+VRJ7Cl62M6rlnToxTMnh87HYfHcwD4EH5EAA1jCPpYxB4AIKfDQA2zWgW2j3nDRmjTY3H+EClDC6jUGszMXj8GjkhLuWkzOg0IHFFV8NlnWyEImRvWavLSjTRlgAmyFjWdWU1oskkCWGjsGgAWihW2QfrOsbqEc5DaLVpt9o0Wao3yDJBxeJz5TE1aLi8H9f1hpYYGLXSWYaXnQ5a-YSPDheecpXnVRqMidGYOLi7GKlhgEB8DH4zDulnstlOgNKEAFDoIFiASJhPBoTwnCA8AnBIN4qHIaQAGYAHYkBGURxBAWgGCYWMwHkEA4yUdgIJgH8KnHRQ23wehiHsGEXSgQgaDgWwwBoJC2zABJCHwABuDQbyoPt40TTQfg0DRLEIXk4FCZVSDOQTIhk-AVLonQlSDVwNBcEIgTUqhRIAAVtEh7HwEIGH0CB7GtEhrg2DQ0EmYAy3UQJyRhAAmGdhNYEhCECDh-MCgzCEsRYIroYTNW4sB8F0OgYVgVsBGKNsABYAFZKUCUphNRGS8vc6i0ygHyqBhNAgpCsL2BhAA2SKXGi2KNDa+KAxCJCUqgNK9My-F8sK4qRPU0Y0AqzyqpquKGtC8KNAC3qOpi1glr6pLBuGjKW3xXKCpIIqSu2XYv39aSNGClbmo0HLAumzauue+Lps0qk6rcjQaBNGATJkhbfLWl7TKurcVggVgXJk7jCDTTFaq0RzSDW-QSBoPi23JNsYBNNz9CoVL2I0ckiCoQgBGBv0oEpKAYRGIFqTAeBoo9RS7RM0SUGbVsOzqAwLEsbANGYaG8lh6jILtYJCYpGEAGJ8Fmi6qH5lt207AxHGcFxxcl7cZc8uXbQVuJLBV-AIHqqbNYFnXhe8PwjalndZZoeXYCtlXZqEh2tcF3XglCd2Tbhs3vYt32lY0ZWAA5k5K2C+NIRDkJAPyRiQFCUNEXAQH5KhbWQyhcMkJg6Bp1hWO4EA1kQaQOHYIEwCQAB6Tu4iBC2+zoTua8WeuzL85g0GYHKh9r+v73JZg5Fg9h7CBKRwGgQggXIkRsBEIA). In that example, click to make a couple boxes, pick a box, and follow its size. We want the size and spin to be tied to the box (denoted by color) and not the position in the grid. You'll notice that instead, the size ends up jumping suddenly up, but it stays constant with location. This means we need to give them keys. - -In this case, giving them unique keys is pretty easy: just create a counter that you increment each time you read it. - -```diff - var colors = ["red", "yellow", "blue", "gray"] - var counter = 0 - - function getColor() { - var color = colors[counter] - counter = (counter + 1) % colors.length - return color - } - - function Boxes() { - var boxes = [] - var nextKey = 0 - - function add() { -- boxes.push({color: getColor()}) -+ var key = nextKey -+ nextKey++ -+ boxes.push({key: key, color: getColor()}) - } - - function remove(box) { - var index = boxes.indexOf(box) - boxes.splice(index, 1) - } - - return { - view: function() { - return [ - m("button", {onclick: add}, "Add box, click box to remove"), - m(".container", boxes.map(function(box, i) { - return m(".box", - { -+ key: box.key, - "data-color": box.color, - onclick: function() { remove(box) }, - }, - m(".stretch") - ) - })), - ] - }, - } - } -``` - -[Here's a fixed demo for you to play with, to see how it works differently.](https://flems.io/#0=N4IgzgxgTg9gNnEAuA2gBgDRoLoZAMwEs4BTMZFUAOwEMBbE5EAOgAsAXOxPCGK9kvyYgAvhmr1GSFgCtyPPgKHSAbjSgACXnBhQwGgLwaUAHRBQSAEzMYNZgJ4kEMAO427IAEZwAriXdmAOZQNPZm2CZUapq8PvwkmkZokZH4cRDshHwagSTsAMLwugAUAJQawCns0VpFibU6eiix8VARVCbsLQL1xd0JGgDUGgCM5QCkDbpgzKRUgeysVRbsPlBUU1CRIilUaVQZWRsAQjAAHmRlFVU1nudkhsbtnTVUJGfsANIk9o-JHR12PtDtkaJZLFdKoCXuoNABrH6PN4fb5haHsZFfH6DQZVTp3C4zAAOPjArGKwAR9iQ8J+tm0uhpuQKdTKIlKVR2AM6wMy2QsdBgKhIxQJ5ShnRhmkIVEs70eBLIzBlcrOAHl8KLzhz0YqZmAiXBCBARSr3rYxpzdp0VmsNhL2C9CCQXDTeUdIXjHexbetjF7JZximZPD52Ow+O5gHwIEaIHCaWDLGIPABBcEaAn0uNwzPnDQRjQCoX+EClDAB710YMsXj8GgyhLuPXMOg0InFd18LVnWyEcWVwO+jbVszMAk2QfezoOwNz71UmkE5hUivo+dzkCWGjsGgAWgZW2QebOzEPa430+9MZzbvSfKokKLJEFwp75TEU8Dn-XV86o5YMAfTyCAljLL92B1S92XLStnm9H9Oi5LlIjoVsYDidhiksGAIB8Bh+HHGBLHsWxTkJUoQAUOgiWIBImE8GhPCcKjwCcEgQSochpAAFhGJARlEcQQFoBgmDPMB5BAOslHYBjiN+YANHwRQ93wehiGpDRUygQgaDgWwwBoLi9zABJCHwABuDQUKoM9FAbN5NChDQNEsQgDTgUI3VIM5LMiVz8F8tSdFdItXA0FwQiJfyqFsgABKl8BCBh9AgexYxIa4Ng0NBJiUtsoECGUaQAJhfazWBIQhAg4MqKsiwhLEWeq6Gsn1jLAFSoDoGlYF3ARij3HiAFY5UCUprK5VyRryioNEK4qqBpNBKuq2r2BpAA2BqXCalqNB2tqCxCLjut68KBpFUbxsmmyAtGNA5oK9QltataarqjRyuOvbmtYd6Ts686+pgK6hpukgJqm7ZdiIs5stcqrPs2jQeIqh6-oO9G2oeoL3hW3KNBoMMYFi1zFpK76MbiuGgJWUDEeJqhCDbB8aXSzLvv0EgaDMvcZT3DD2Fy-QqF0Ns4A0GUiBZgRybzKA5SgGkRiJBGwHgJrMy8+NYtslBt13A86gMCxLGwDRmHpkDWHmxj42CDDZRpABifAnphqhDZ3fdDwMRxnBcS3reAro7aUh24SduJLDd-AIFW+7vaNv3Te8PwQ5t8P7ZoR3YFjt2nqs5OfeN-3glCLOw8ZyO8+jguXY0V2AA426m1izNITjuJAUqW6QHiW9EXAQCNKg4W4ygRMkJg6EIRZdO4EA1kQaQOHYIkwCQAB6He4iJaP7LoHf58X4h4tK5g0GYHjT4X1gl9bGVmDkVj2HsIkpHAaBCCJOSRDYBEEAA) - -### Reinitializing views with single-child keyed fragments - -When you're dealing with stateful entities in models and such, it's often useful to render model views with keys. Suppose you have this layout: - -```javascript -function Layout() { - // ... -} - -function Person() { - // ... -} - -m.route(rootElem, "/", { - "/": Home, - "/person/:id": { - render: function() { - return m(Layout, - m(Person, {id: m.route.param("id")}) - ) - } - }, - // ... -}) -``` - -Chances are, your `Person` component probably looks something like this: - -```javascript -function Person(vnode) { - var personId = vnode.attrs.id - var state = "pending" - var person, error - - m.request("/api/person/:id", {params: {id: personId}}).then( - function(p) { person = p; state = "ready" }, - function(e) { error = e; state = "error" } - ) - - return { - view: function() { - if (state === "pending") return m(LoadingIcon) - if (state === "error") { - return error.code === 404 - ? m(".person-missing", "Person not found.") - : m(".person-error", - "An error occurred. Please try again later" - ) - } - return m(".person", - m(m.route.Link, - { - class: "person-edit", - href: "/person/:id/edit", - params: {id: personId}, - }, - "Edit" - ), - m(".person-name", "Name: ", person.name), - // ... - ) - } - } -} -``` - -Say, you added a way to link to other people from this component, like maybe adding a "manager" field. - -```javascript -function Person(vnode) { - // ... - - return { - view: function() { - // ... - return m(".person", - m(m.route.Link, - { - class: "person-edit", - href: "/person/:id/edit", - params: {id: personId}, - }, - "Edit" - ), - m(".person-name", person.name), - // ... - m(".manager", - "Manager: ", - m(m.route.Link, - { - href: "/person/:id", - params: {id: person.manager.id} - }, - person.manager.name - ) - ), - // ... - ) - } - } -} -``` - -Assuming the person's ID was `1` and the manager's ID was `2`, you'd switch from `/person/1` to `/person/2`, remaining on the same route. But since you used [the route resolver `render` method](route.md#routeresolverrender), the tree was retained and you just changed from `m(Layout, m(Person, {id: "1"}))` to `m(Layout, m(Person, {id: "2"}))`. In this, the `Person` didn't change, and so it doesn't reinitialize the component. But for our case, this is bad, because it means the new user isn't being fetched. This is where keys come in handy. We could change the route resolver to this to fix it: - -```javascript -m.route(rootElem, "/", { - "/": Home, - "/person/:id": { - render: function() { - return m(Layout, - // Wrap it in an array in case we add other elements later on. - // Remember: fragments must contain either only keyed children - // or no keyed children. - [m(Person, - {id: m.route.param("id"), key: m.route.param("id")} - )] - ) - } - }, - // ... -}) -``` - -### Common gotchas - -There's several common gotchas that people run into with keys. Here's some of them, to help you understand why they don't work. - -#### Wrapping keyed elements - -These two snippets don't work the same way: - -```javascript -users.map(function(user) { - return m(".wrapper", [ - m(User, {user: user, key: user.id}) - ]) -}) - -users.map(function(user) { - return m(".wrapper", {key: user.id}, [ - m(User, {user: user}) - ]) -}) -``` - -The first binds the key to the `User` component, but the outer fragment created by `users.map(...)` is entirely unkeyed. Wrapping a keyed element this way doesn't work, and the result could be anything ranging from extra requests each time the list is changed to inner form inputs losing their state. The resulting behavior would similar to the [post list's broken example](#linking-model-data-to-views), but without the issue of state corruption. - -The second binds it to the `.wrapper` element, ensuring the outer fragment *is* keyed. This does what you likely wanted to do all along, and removing a user won't pose any issues with the state of other user instances. - -#### Putting keys inside the component - -Suppose, in the [person example](#reinitializing-views-with-single-child-keyed-fragments), you did this instead: - -```javascript -// AVOID -function Person(vnode) { - var personId = vnode.attrs.id - // ... - - return { - view: function() { - return m.fragment({key: personId}, - // what you previously had in the view - ) - } - } -} -``` - -This won't work, because the key doesn't apply to the component as a whole. It just applies to the view, and so you aren't re-fetching the data like you were hoping for. - -Prefer the solution used there, putting the key in the vnode *using* the component rather than inside the component itself. - -```javascript -// PREFER -return [m(Person, - {id: m.route.param("id"), key: m.route.param("id")} -)] -``` - -#### Keying elements unnecessarily - -It's a common misconception that keys are themselves identities. Mithril.js enforces for all fragments that their children must either all have keys or all lack keys, and will throw an error if you forget this. Suppose you have this layout: - -```javascript -m(".page", - m(".header", {key: "header"}), - m(".body"), - m(".footer"), -) -``` - -This obviously will throw, as `.header` has a key and `.body` and `.footer` both lack keys. But here's the thing: you don't need keys for this. If you find yourself using keys for things like this, the solution isn't to add keys, but to remove them. Only add them if you really, *really* need them. Yes, the underlying DOM nodes have identities, but Mithril.js doesn't need to track those identities to correctly patch them. It practically never does. Only with lists where each entry has some sort of associated state Mithril.js doesn't itself track, whether it be in a model, in a component, or in the DOM itself, do you need keys. - -One last thing: avoid static keys. They're always unnecessary. If you're not computing your `key` attribute, you're probably doing something wrong. - -Note that if you really need a single keyed element in isolation, [use a single-child keyed fragment](#reinitializing-views-with-single-child-keyed-fragments). It's just an array with a single child that's a keyed element, like `[m("div", {key: foo})]`. - -#### Mixing key types - -Keys are read as object property names. This means `1` and `"1"` are treated identically. If you want to keep your hair, don't mix key types if you can help it. If you do, you could wind up with duplicate keys and unexpected behavior. - -```javascript -// AVOID -var things = [ - {id: "1", name: "Book"}, - {id: 1, name: "Cup"}, -] -``` - -If you absolutely must and you have no control over this, use a prefix denoting its type so they remain distinct. - -```javascript -things.map(function(thing) { - return m(".thing", - {key: (typeof thing.id) + ":" + thing.id}, - // ... - ) -}) -``` - -##### Hiding keyed elements with holes - -Holes like `null`, `undefined`, and booleans are considered unkeyed vnodes, so code like this won't work: - -```javascript -// AVOID -things.map(function(thing) { - return shouldShowThing(thing) - ? m(Thing, {key: thing.id, thing: thing}) - : null -}) -``` - -Instead, filter the list before returning it, and Mithril.js will do the right thing. Most of the time, [`Array.prototype.filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) is precisely what you need and you should definitely try it out. - -```javascript -// PREFER -things - .filter(function(thing) { return shouldShowThing(thing) }) - .map(function(thing) { - return m(Thing, {key: thing.id, thing: thing}) - }) -``` - -#### Duplicate keys - -Keys for fragment items *must* be unique, or otherwise, it's unclear and ambiguous what key is supposed to go where. You may also have issues with elements not moving around like they're supposed to. - -```javascript -// AVOID -var things = [ - {id: "1", name: "Book"}, - {id: "1", name: "Cup"}, -] -``` - -Mithril.js uses an empty object to map keys to indices to know how to properly patch keyed fragments. When you have a duplicate key, it's no longer clear where that element moved to, and so Mithril.js will break in that circumstance and do unexpected things on update, especially if the list changed. Distinct keys are required for Mithril.js to properly connect old to new nodes, so you must choose something locally unique to use as a key. diff --git a/docs/layout.html b/docs/layout.html deleted file mode 100644 index 3e0fe4cd..00000000 --- a/docs/layout.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - Mithril.js - - - - - - - - -
-
- -

Mithril [archive-docs]

- -
-
-
-
- [body] -
- License: MIT. © Leo Horie. -
-
- - - - - - - - diff --git a/docs/learning-mithril.md b/docs/learning-mithril.md deleted file mode 100644 index ae628151..00000000 --- a/docs/learning-mithril.md +++ /dev/null @@ -1,9 +0,0 @@ - - -# Learning Resources - -Links to Mithril.js learning content: - -- [Mithril 0-60](https://vimeo.com/showcase/5584199) diff --git a/docs/lifecycle-methods.md b/docs/lifecycle-methods.md deleted file mode 100644 index e95c0e90..00000000 --- a/docs/lifecycle-methods.md +++ /dev/null @@ -1,221 +0,0 @@ - - -# Lifecycle methods - -- [Usage](#usage) -- [The DOM element lifecycle](#the-dom-element-lifecycle) -- [oninit](#oninit) -- [oncreate](#oncreate) -- [onupdate](#onupdate) -- [onbeforeremove](#onbeforeremove) -- [onremove](#onremove) -- [onbeforeupdate](#onbeforeupdate) -- [Avoid anti-patterns](#avoid-anti-patterns) - ---- - -### Usage - -[Components](components.md) and [virtual DOM nodes](vnodes.md) can have lifecycle methods, also known as *hooks*, which are called at various points during the lifetime of a DOM element. - -```javascript -// Sample hook in component -var ComponentWithHook = { - oninit: function(vnode) { - console.log("initialize component") - }, - view: function() { - return "hello" - } -} - -// Sample hook in vnode -function initializeVnode() { - console.log("initialize vnode") -} - -m(ComponentWithHook, {oninit: initializeVnode}) -``` - -All lifecyle methods receive the vnode as their first arguments, and have their `this` keyword bound to `vnode.state`. - -Lifecycle methods are only called as a side effect of a [`m.render()`](render.md) call. They are not called if the DOM is modified outside of Mithril. - ---- - -### The DOM element lifecycle - -A DOM element is typically created and appended to the document. It may then have attributes or child nodes updated when a UI event is triggered and data is changed; and the element may alternatively be removed from the document. - -After an element is removed, it may be temporarily retained in a memory pool. The pooled element may be reused in a subsequent update (in a process called *DOM recycling*). Recycling an element avoids incurring the performance cost of recreating a copy of an element that existed recently. - ---- - -### oninit - -The `oninit(vnode)` hook is called before a vnode is touched by the virtual DOM engine. `oninit` is guaranteed to run before its DOM element is attached to the document, and it is guaranteed to run on parent vnodes before their children, but it does not offer any guarantees regarding the existence of ancestor or descendant DOM elements. You should never access the `vnode.dom` from the `oninit` method. - -This hook does not get called when an element is updated, but it does get called if an element is recycled. - -Like in other hooks, the `this` keyword in the `oninit` callback points to `vnode.state`. - -The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`. - -```javascript -function ComponentWithState() { - var initialData - return { - oninit: function(vnode) { - initialData = vnode.attrs.data - }, - view: function(vnode) { - return [ - // displays data from initialization time: - m("div", "Initial: " + initialData), - // displays current data: - m("div", "Current: " + vnode.attrs.data) - ] - } - } -} - -m(ComponentWithState, {data: "Hello"}) -``` - -You should not modify model data synchronously from this method. Since `oninit` makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle. - ---- - -### oncreate - -The `oncreate(vnode)` hook is called after a DOM element is created and attached to the document. `oncreate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method. - -This hook does not get called when an element is updated. - -Like in other hooks, the `this` keyword in the `oncreate` callback points to `vnode.state`. DOM elements whose vnodes have an `oncreate` hook do not get recycled. - -The `oncreate` hook is useful for reading layout values that may trigger a repaint, starting animations and for initializing third party libraries that require a reference to the DOM element. - -```javascript -var HeightReporter = { - oncreate: function(vnode) { - console.log("Initialized with height of: ", vnode.dom.offsetHeight) - }, - view: function() {} -} - -m(HeightReporter, {data: "Hello"}) -``` - -You should not modify model data synchronously from this method. Since `oncreate` is run at the end of the render cycle, model changes created from this method will not be reflected in the UI until the next render cycle. - ---- - -### onupdate - -The `onupdate(vnode)` hook is called after a DOM element is updated, while attached to the document. `onupdate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method. - -This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled. - -DOM elements whose vnodes have an `onupdate` hook do not get recycled. - -The `onupdate` hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed. - -```javascript -function RedrawReporter() { - var count = 0 - return { - onupdate: function() { - console.log("Redraws so far: ", ++count) - }, - view: function() {} - } -} - -m(RedrawReporter, {data: "Hello"}) -``` - ---- - -### onbeforeremove - -The `onbeforeremove(vnode)` hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril.js only detaches the DOM element after the promise completes. - -This hook is only called on the DOM element that loses its `parentNode`, but it does not get called in its child elements. - -Like in other hooks, the `this` keyword in the `onbeforeremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onbeforeremove` hook do not get recycled. - -```javascript -var Fader = { - onbeforeremove: function(vnode) { - vnode.dom.classList.add("fade-out") - return new Promise(function(resolve) { - setTimeout(resolve, 1000) - }) - }, - view: function() { - return m("div", "Bye") - }, -} -``` - ---- - -### onremove - -The `onremove(vnode)` hook is called before a DOM element is removed from the document. If a `onbeforeremove` hook is also defined, the `onremove` hook runs after the promise returned from `onbeforeremove` is completed. - -This hook is called on any element that is removed from the document, regardless of whether it was directly detached from its parent or whether it is a child of another element that was detached. - -Like in other hooks, the `this` keyword in the `onremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onremove` hook do not get recycled. - -The `onremove` hook is useful for running clean up tasks. - -```javascript -function Timer() { - var timeout = setTimeout(function() { - console.log("timed out") - }, 1000) - - return { - onremove: function() { - clearTimeout(timeout) - }, - view: function() {} - } -} -``` - ---- - -### onbeforeupdate - -The `onbeforeupdate(vnode, old)` hook is called before a vnode is diffed in a update. If this function is defined and returns false, Mithril.js prevents a diff from happening to the vnode, and consequently to the vnode's children. - -This hook by itself does not prevent a virtual DOM subtree from being generated unless the subtree is encapsulated within a component. - -Like in other hooks, the `this` keyword in the `onbeforeupdate` callback points to `vnode.state`. - -This hook is useful to reduce lag in updates in cases where there is a overly large DOM tree. - ---- - -### Avoid anti-patterns - -Although Mithril.js is flexible, some code patterns are discouraged: - -#### Avoid premature optimizations - -You should only use `onbeforeupdate` to skip diffing as a last resort. Avoid using it unless you have a noticeable performance issue. - -Typically performance problems that can be fixed via `onbeforeupdate` boil down to one large array of items. In this context, typically "large" means any array that contains a large number of nodes, be it in a wide spread (the infamous 5000 row table), or in a deep, dense tree. - -If you do have a performance issue, first consider whether the UI presents a good user experience and change it if it doesn't. For example, it's highly unlikely that a user would ever sift through 5000 rows of raw table data, and highly likely that it would be easier for a user to use a search feature that returns only the top few most relevant items. - -If a design-based solution is not feasible, and you must optimize a UI with a large number of DOM element, apply `onbeforeupdate` on the parent node of the largest array and re-evaluate performance. In the vast majority of cases, a single check should be sufficient. In the rare case that it is not, rinse and repeat, but you should be increasingly wary of each new `onbeforeupdate` declaration. Multiple `onbeforeupdate`s are a code smell that indicates prioritization problems in the design workflow. - -Avoid applying the optimization to other areas of your application "just-in-case". Remember that, generally speaking, more code incurs a higher maintenance cost than less code, and `onbeforeupdate` related bugs can be especially difficult to troubleshoot if you rely on object identity for its conditional checks. - -Again, **the `onbeforeupdate` hook should only be used as a last resort.** diff --git a/docs/logo.svg b/docs/logo.svg deleted file mode 100644 index a86fb2b7..00000000 --- a/docs/logo.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/docs/migration-v02x.md b/docs/migration-v02x.md deleted file mode 100644 index 8681c0f9..00000000 --- a/docs/migration-v02x.md +++ /dev/null @@ -1,935 +0,0 @@ -# Migrating from v0.2.x - -v1.x and v2.x are largely API-compatible with v0.2.x, but there are some breaking changes. Migrating to v2.x is nearly identical, so the notes below apply mostly to both. - -If you are migrating, consider using the [mithril-codemods](https://www.npmjs.com/package/mithril-codemods) tool to help automate the most straightforward migrations. - -- [`m.prop` removed](#mprop-removed) -- [`m.component` removed](#mcomponent-removed) -- [`m.withAttr` removed](#mwithattr-removed) -- [`m.version` removed](#mversion-removed) -- [`config` function](#config-function) -- [Changes in redraw behaviour](#changes-in-redraw-behaviour) - - [No more redraw locks](#no-more-redraw-locks) - - [Cancelling redraw from event handlers](#cancelling-redraw-from-event-handlers) - - [Synchronous redraw changed](#synchronous-redraw) - - [`m.startComputation`/`m.endComputation` removed](#mstartcomputationmendcomputation-removed) -- [Component `controller` function](#component-controller-function) -- [Component arguments](#component-arguments) -- [Component vnode children](#component-children) -- [DOM vnode children](#dom-vnode-children) -- [Keys](#keys) -- [`view()` parameters](#view-parameters) -- [Passing components to `m()`](#passing-components-to-m) -- [Passing vnodes to `m.mount()` and `m.route()`](#passing-vnodes-to-mmount-and-mroute) -- [`m.route.mode`](#mroutemode) -- [`m.route()` and anchor tags](#mroute-and-anchor-tags) -- [Path templates](#path-templates) -- [Reading/writing the current route](#readingwriting-the-current-route) -- [Accessing route params](#accessing-route-params) -- [Building/Parsing query strings](#buildingparsing-query-strings) -- [Preventing unmounting](#preventing-unmounting) -- [Run code on component removal](#run-code-on-component-removal) -- [`m.request`](#mrequest) -- [Default `responseType` for `m.request`](#default-responsetype-for-mrequest) -- [`m.deferred` removed](#mdeferred-removed) -- [`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) - ---- - -## `m.prop` removed - -In v2.x, `m.prop()` was converted into now a more powerful stream micro-library, but it's no longer part of core. You can read about how to use the optional Streams module in [the documentation](stream.md). - -### v0.2.x - -```javascript -var m = require("mithril") - -var num = m.prop(1) -``` - -### v2.x - -```javascript -var m = require("mithril") -var prop = require("mithril/stream") - -var num = prop(1) -var doubled = num.map(function(n) { return n * 2 }) -``` - ---- - -## `m.component` removed - -In v0.2.x components could be created using either `m(Component)` or `m.component(Component)`. v2.x only support `m(Component)`. - -### v0.2.x - -```javascript -// These are equivalent -m.component(Component) -m(Component) -``` - -### v2.x - -```javascript -m(Component) -``` - ---- - -## `m.withAttr` removed - -In v0.2.x event listeners could use `oninput: m.withAttr("value", func)` and similar. In v2.x, just read them directly from the event's target. It synergized well with `m.prop`, but since that was removed in favor of an out of core solution and v1.x didn't see similar broad, idiomatic usage of streams, `m.withAttr` lost most of its usefulness. - -### v0.2.x - -```javascript -var value = m.prop("") - -// In your view -m("input[type=text]", { - value: value(), - oninput: m.withAttr("value", value), -}) -``` - -### v2.x - -```javascript -var value = "" - -// In your view -m("input[type=text]", { - value: value, - oninput: function (ev) { value = ev.target.value }, -}) -``` - ---- - -## `m.version` removed - -It served little use in general, and you can always add it back yourself. You should prefer feature detection for knowing what features are available, and the v2.x API is designed to better enable this. - ---- - -## `config` function - -In v0.2.x Mithril.js provided a single lifecycle method, `config`. v2.x provide much more fine-grained control over the lifecycle of a vnode. - -### v0.2.x - -```javascript -m("div", { - config: function(element, isInitialized) { - // runs on each redraw - // isInitialized is a boolean representing if the node has been added to the DOM - } -}) -``` - -### v2.x - -More documentation on these new methods is available in [lifecycle-methods.md](lifecycle-methods.md). - -```javascript -m("div", { - // Called before the DOM node is created - oninit: function(vnode) { /*...*/ }, - // Called after the DOM node is created - oncreate: function(vnode) { /*...*/ }, - // Called before the node is updated, return false to cancel - onbeforeupdate: function(vnode, old) { /*...*/ }, - // Called after the node is updated - onupdate: function(vnode) { /*...*/ }, - // Called before the node is removed, return a Promise that resolves when - // ready for the node to be removed from the DOM - onbeforeremove: function(vnode) { /*...*/ }, - // Called before the node is removed, but after onbeforeremove calls done() - onremove: function(vnode) { /*...*/ } -}) -``` - -If available the DOM-Element of the vnode can be accessed at `vnode.dom`. - ---- - -## Changes in redraw behaviour - -Mithril.js' rendering engine still operates on the basis of semi-automated global redraws, but some APIs and behaviours differ: - -### No more redraw locks - -In v0.2.x, Mithril.js allowed 'redraw locks' which temporarily prevented blocked draw logic: by default, `m.request` would lock the draw loop on execution and unlock when all pending requests had resolved - the same behaviour could be invoked manually using `m.startComputation()` and `m.endComputation()`. The latter APIs and the associated behaviour has been removed in v2.x without replacement. Redraw locking can lead to buggy UIs: the concerns of one part of the application should not be allowed to prevent other parts of the view from updating to reflect change. - -### Cancelling redraw from event handlers - -`m.mount()` and `m.route()` still automatically redraw after a DOM event handler runs. Cancelling these redraws from within your event handlers is now done by setting the `redraw` property on the passed-in event object to `false`. - -#### v0.2.x - -```javascript -m("div", { - onclick: function(e) { - m.redraw.strategy("none") - } -}) -``` - -#### v2.x - -```javascript -m("div", { - onclick: function(e) { - e.redraw = false - } -}) -``` - -### Synchronous redraw changed - -In v0.2.x it was possible to force Mithril.js to redraw immediately by passing a truthy value to `m.redraw()`. In v2.x, this functionality was split into two different methods for clarity. - -#### v0.2.x - -```javascript -m.redraw(true) // redraws immediately & synchronously -``` - -#### v2.x - -```javascript -m.redraw() // schedules a redraw on the next requestAnimationFrame tick -m.redraw.sync() // invokes a redraw immediately and waits for it to complete -``` - -### `m.startComputation`/`m.endComputation` removed - -They are considered anti-patterns and have a number of problematic edge cases, so they were removed without replacement in v2.x. - ---- - -## Component `controller` function - -In v2.x, there is no more `controller` property in components - use `oninit` instead. - -### v0.2.x - -```javascript -m.mount(document.body, { - controller: function() { - var ctrl = this - - ctrl.fooga = 1 - }, - - view: function(ctrl) { - return m("p", ctrl.fooga) - } -}) -``` - -### v2.x - -```javascript -m.mount(document.body, { - oninit: function(vnode) { - vnode.state.fooga = 1 - }, - - view: function(vnode) { - return m("p", vnode.state.fooga) - } -}) - -// OR - -m.mount(document.body, { - // this is bound to vnode.state by default - oninit: function(vnode) { - this.fooga = 1 - }, - - view: function(vnode) { - return m("p", this.fooga) - } -}) -``` - ---- - -## Component arguments - -Arguments to a component in v2.x must be an object, simple values like `String`/`Number`/`Boolean` will be treated as text children. Arguments are accessed within the component by reading them from the `vnode.attrs` object. - -### v0.2.x - -```javascript -var Component = { - controller: function(options) { - // options.fooga === 1 - }, - - view: function(ctrl, options) { - // options.fooga === 1 - } -} - -m("div", m.component(Component, { fooga: 1 })) -``` - -### v2.x - -```javascript -var Component = { - oninit: function(vnode) { - // vnode.attrs.fooga === 1 - }, - - view: function(vnode) { - // vnode.attrs.fooga === 1 - } -} - -m("div", m(Component, { fooga: 1 })) -``` - ---- - -## Component vnode children - -In v0.2.x, component vnode children were not normalized, just passed as extra arguments, and they were not flattened, either. (Internally, it was just returning a partially applied component that was diffed based on the component being partially applied.) In v2.x, component vnode children are passed via `vnode.children` as a resolved array of children, but like v0.2.x, the individual children themselves are not normalized, nor is the children array flattened. - -### v0.2.x - -```javascript -var Component = { - controller: function(value, renderProp) { - // value === "value" - // typeof renderProp === "function" - }, - - view: function(ctrl, value, renderProp) { - // value === "value" - // typeof renderProp === "function" - } -} - -m("div", m.component(Component, "value", function(key) { return "child" })) -``` - -### v2.x - -```javascript -var Component = { - oninit: function(vnode) { - // vnode.children[0] === "value" - // typeof vnode.children[1] === "function" - }, - - view: function(vnode) { - // vnode.children[0] === "value" - // typeof vnode.children[1] === "function" - }, -} - -m("div", m(Component, "value", function(key) { return "child" })) -``` - ---- - -## DOM vnode children - -In v0.2.x, the children of DOM nodes were represented literally with no normalization aside from using the children directly if only a single array child is present. It returned a structure more like this, with the strings represented literally. - -```javascript -m("div", "value", ["nested"]) - -// Becomes: -{ - tag: "div", - attrs: {}, - children: [ - "value", - ["nested"], - ] -} -``` - -In v2.x, children of DOM vnodes are normalized to objects of a single consistent structure. - -```javascript -m("div", "value", ["nested"]) - -// Becomes roughly: -{ - tag: "div", - attrs: null, - children: [ - {tag: "#", children: "value"}, - {tag: "[", children: [ - {tag: "#", children: "nested"}, - ]}, - ] -} -``` - -If only a single text child is present on a DOM vnode, it instead sets `text` to that value. - -```javascript -m("div", "value") - -// Becomes roughly: -{ - tag: "div", - attrs: null, - text: "", - children: undefined, -} -``` - -See [the vnode docs](vnodes.md) for more details on the v2.x vnode structure and how things are normalized. - -*Most of the v2.x vnode properties here are omitted for brevity.* - ---- - -## Keys - -In v0.2.x, you could mix keyed and unkeyed vnodes freely. - -In v2.x, children lists of both fragments and elements must be either all keyed or all unkeyed. Holes are considered unkeyed for the purposes of this check, too - it no longer ignores them. - -If you need to work around it, use the idiom of a fragment containing a single vnode, like `[m("div", {key: whatever})]`. - ---- - -## `view()` parameters - -In v0.2.x view functions are passed a reference to the `controller` instance and (optionally) any options passed to the component. In v2.x they are passed **only** the `vnode`, exactly like the `controller` function. - -### v0.2.x - -```javascript -m.mount(document.body, { - controller: function() {}, - - view: function(ctrl, options) { - // ... - } -}) -``` - -### v2.x - -```javascript -m.mount(document.body, { - oninit: function(vnode) { - // ... - }, - - view: function(vnode) { - // Use vnode.state instead of ctrl - // Use vnode.attrs instead of options - } -}) -``` - ---- - -## Passing components to `m()` - -In v0.2.x you could pass components as the second argument of `m()` w/o any wrapping required. To help with consistency in v2.x they must always be wrapped with a `m()` invocation. - -### v0.2.x - -```javascript -m("div", Component) -``` - -### v2.x - -```javascript -m("div", m(Component)) -``` - ---- - -## Passing vnodes to `m.mount()` and `m.route()` - -In v0.2.x, `m.mount(element, component)` tolerated [vnodes](vnodes.md) as second arguments instead of [components](components.md) (even though it wasn't documented). Likewise, `m.route(element, defaultRoute, routes)` accepted vnodes as values in the `routes` object. - -In v2.x, components are required instead in both cases. - -### v0.2.x - -```javascript -m.mount(element, m('i', 'hello')) -m.mount(element, m(Component, attrs)) - -m.route(element, '/', { - '/': m('b', 'bye') -}) -``` - -### v2.x - -```javascript -m.mount(element, {view: function () {return m('i', 'hello')}}) -m.mount(element, {view: function () {return m(Component, attrs)}}) - -m.route(element, '/', { - '/': {view: function () {return m('b', 'bye')}} -}) -``` - ---- - -## `m.route.mode` - -In v0.2.x the routing mode could be set by assigning a string of `"pathname"`, `"hash"`, or `"search"` to `m.route.mode`. In `v.1.x` it is replaced by `m.route.prefix = prefix` where `prefix` can any prefix. If it starts with `#`, it works in "hash" mode, `?` for "search" mode, and any other character (or the empty string) for "pathname" mode. It also supports combinations of the above like `m.route.prefix = "/path/#!"` or `?#`. - -The default was changed to also use a `#!` (hashbang) prefix instead of just `#`. So if you were using the default behavior and want to retain your existing URLs, specify `m.route.prefix = "#"` before initializing the routes. - -### v0.2.x - -```javascript -m.route.mode = "hash" -m.route.mode = "pathname" -m.route.mode = "search" -``` - -### v2.x - -```javascript -// Direct equivalents -m.route.prefix = "#" -m.route.prefix = "" -m.route.prefix = "?" -``` - ---- - -## `m.route()` and anchor tags - -Handling routable links now uses a special built-in component instead of an attribute. If you were using this on ` -``` - -`vnode = m(m.route.Link, attributes, children)` - -Argument | Type | Required | Description ---------------------- | ------------------------------------ | -------- | --- -`attributes.href` | `Object` | Yes | The target route to navigate to. -`attributes.disabled` | `Boolean` | No | Disables the element accessibly. -`attributes.selector` | `String|Object|Function` | No | A selector for [`m`](hyperscript.md), defaults to `"a"`. -`attributes.options` | `Object` | No | Sets the `options` passed to [`m.route.set`](#mrouteset). -`attributes.params` | `Object` | No | Sets the `params` passed to [`m.route.set`](#mrouteset). -`attributes` | `Object` | No | Any other attributes to be forwarded to `m`. -`children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md) for this link. -**returns** | `Vnode` | | A [vnode](vnodes.md). - -##### m.route.param - -Retrieves a route parameter from the last fully resolved route. A route parameter is a key-value pair. Route parameters may come from a few different places: - -- route interpolations (e.g. if a route is `/users/:id`, and it resolves to `/users/1`, the route parameter has a key `id` and value `"1"`) -- router querystrings (e.g. if the path is `/users?page=1`, the route parameter has a key `page` and value `"1"`) -- `history.state` (e.g. if history.state is `{foo: "bar"}`, the route parameter has key `foo` and value `"bar"`) - -`value = m.route.param(key)` - -Argument | Type | Required | Description ------------------ | --------------- | -------- | --- -`key` | `String` | No | A route parameter name (e.g. `id` in route `/users/:id`, or `page` in path `/users/1?page=3`, or a key in `history.state`) -**returns** | `String|Object` | | Returns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys - -Note that in the `onmatch` function of a RouteResolver, the new route hasn't yet been fully resolved, and `m.route.param()` will return the parameters of the previous route, if any. `onmatch` receives the parameters of the new route as an argument. - -##### m.route.SKIP - -A special value that can be returned from a [route resolver's `onmatch`](#routeresolveronmatch) to skip to the next route. - -#### RouteResolver - -A RouteResolver is a non-component object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. - -If an object can be detected as a component (by the presence of a `view` method or by being a `function`/`class`), it will be treated as such even if it has `onmatch` or `render` methods. Since a RouteResolver is not a component, it does not have lifecycle methods. - -As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules. - -`routeResolver = {onmatch, render}` - -When using components, you could think of them as special sugar for this route resolver, assuming your component is `Home`: - -```javascript -var routeResolver = { - onmatch: function() { return Home }, - render: function(vnode) { return [vnode] }, -} -``` - -##### routeResolver.onmatch - -The `onmatch` hook is called when the router needs to find a component to render. It is called once per router path changes, but not on subsequent redraws while on the same path. It can be used to run logic before a component initializes (for example authentication logic, data preloading, redirection analytics tracking, etc) - -This method also allows you to asynchronously define what component will be rendered, making it suitable for code splitting and asynchronous module loading. To render a component asynchronously return a promise that resolves to a component. - -For more information on `onmatch`, see the [advanced component resolution](#advanced-component-resolution) section - -`routeResolver.onmatch(args, requestedPath, route)` - -Argument | Type | Description ---------------- | ---------------------------------------- | --- -`args` | `Object` | The [routing parameters](#routing-parameters) -`requestedPath` | `String` | The router path requested by the last routing action, including interpolated routing parameter values, but without the prefix. When `onmatch` is called, the resolution for this path is not complete and `m.route.get()` still returns the previous path. -`route` | `String` | The router path requested by the last routing action, excluding interpolated routing parameter values -**returns** | `Component|Promise|undefined` | Returns a component or a promise that resolves to a component - -If `onmatch` returns a component or a promise that resolves to a component, this component is used as the `vnode.tag` for the first argument in the RouteResolver's `render` method. Otherwise, `vnode.tag` is set to `"div"`. Similarly, if the `onmatch` method is omitted, `vnode.tag` is also `"div"`. - -If `onmatch` returns a promise that gets rejected, the router redirects back to `defaultRoute`. You may override this behavior by calling `.catch` on the promise chain before returning it. - -##### routeResolver.render - -The `render` method is called on every redraw for a matching route. It is similar to the `view` method in components and it exists to simplify [component composition](#wrapping-a-layout-component). It also lets you escape from Mithril.js' normal behavior of replacing the entire subtree. - -`vnode = routeResolver.render(vnode)` - -Argument | Type | Description -------------------- | -------------------- | ----------- -`vnode` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If onmatch does not return a component or a promise that resolves to a component, the vnode's `tag` field defaults to `"div"` -`vnode.attrs` | `Object` | A map of URL parameter values -**returns** | `Array|Vnode` | The [vnodes](vnodes.md) to be rendered - -The `vnode` parameter is just `m(Component, m.route.param())` where `Component` is the resolved component for the route (after `routeResolver.onmatch`) and `m.route.param()` is as documented [here](#mrouteparam). If you omit this method, the default return value is `[vnode]`, wrapped in a fragment so you can use [key parameters](#key-parameter). Combined with a `:key` parameter, it becomes a [single-element keyed fragment](keys.md#reinitializing-views-with-single-child-keyed-fragments), since it ends up rendering to something like `[m(Component, {key: m.route.param("key"), ...})]`. - ---- - -#### How it works - -Routing is a system that allows creating Single Page Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh. - -It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism. - -Routing without page refreshes is made partially possible by the [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_pushState%28%29_method) API. Using this API, it's possible to programmatically change the URL displayed by the browser after a page has loaded, but it's the application developer's responsibility to ensure that navigating to any given URL from a cold state (e.g. a new tab) will render the appropriate markup. - -#### Routing strategies - -The routing strategy dictates how a library might actually implement routing. There are three general strategies that can be used to implement a SPA routing system, and each has different caveats: - -- `m.route.prefix = '#!'` (default) – Using the [fragment identifier](https://en.wikipedia.org/wiki/Fragment_identifier) (aka the hash) portion of the URL. A URL using this strategy typically looks like `https://localhost/#!/page1` -- `m.route.prefix = '?'` – Using the querystring. A URL using this strategy typically looks like `https://localhost/?/page1` -- `m.route.prefix = ''` – Using the pathname. A URL using this strategy typically looks like `https://localhost/page1` - -Using the hash strategy is guaranteed to work in browsers that don't support `history.pushState`, because it can fall back to using `onhashchange`. Use this strategy if you want to keep the hashes purely local. - -The querystring strategy allows server-side detection, but it doesn't appear as a normal path. Use this strategy if you want to support and potentially detect anchored links server-side and you are not able to make the changes necessary to support the pathname strategy (like if you're using Apache and can't modify your .htaccess). - -The pathname strategy produces the cleanest looking URLs, but requires setting up the server to serve the single page application code from every URL that the application can route to. Use this strategy if you want cleaner-looking URLs. - -Single page applications that use the hash strategy often use the convention of having an exclamation mark after the hash to indicate that they're using the hash as a routing mechanism and not for the purposes of linking to anchors. The `#!` string is known as a *hashbang*. - -The default strategy uses the hashbang. - ---- - -### Typical usage - -Normally, you need to create a few [components](components.md) to map routes to: - -```javascript -var Home = { - view: function() { - return [ - m(Menu), - m("h1", "Home") - ] - } -} - -var Page1 = { - view: function() { - return [ - m(Menu), - m("h1", "Page 1") - ] - } -} -``` - -In the example above, there are two components: `Home` and `Page1`. Each contains a menu and some text. The menu is itself being defined as a component to avoid repetition: - -```javascript -var Menu = { - view: function() { - return m("nav", [ - m(m.route.Link, {href: "/"}, "Home"), - m(m.route.Link, {href: "/page1"}, "Page 1"), - ]) - } -} -``` - -Now we can define routes and map our components to them: - -```javascript -m.route(document.body, "/", { - "/": Home, - "/page1": Page1, -}) -``` - -Here we specify two routes: `/` and `/page1`, which render their respective components when the user navigates to each URL. - ---- - -### Navigating to different routes - -In the example above, the `Menu` component has two `m.route.Link`s. That creates an element, by default an ``, and sets it up to where if the user clicks on it, it navigates to another route on its own. It doesn't navigate remotely, just locally. - -You can also navigate programmatically, via `m.route.set(route)`. For example, `m.route.set("/page1")`. - -When navigating between routes, the router prefix is handled for you. In other words, leave out the hashbang `#!` (or whatever prefix you set `m.route.prefix` to) when linking Mithril.js routes, including in both `m.route.set` and in `m.route.Link`. - -Do note that when navigating between components, the entire subtree is replaced. Use [a route resolver with a `render` method](#routeresolverrender) if you want to just patch the subtree. - ---- - -### Routing parameters - -Sometimes we want to have a variable id or similar data appear in a route, but we don't want to explicitly specify a separate route for every possible id. In order to achieve that, Mithril.js supports [parameterized routes](paths.md#path-parameters): - -```javascript -var Edit = { - view: function(vnode) { - return [ - m(Menu), - m("h1", "Editing " + vnode.attrs.id) - ] - } -} -m.route(document.body, "/edit/1", { - "/edit/:id": Edit, -}) -``` - -In the example above, we defined a route `/edit/:id`. This creates a dynamic route that matches any URL that starts with `/edit/` and is followed by some data (e.g. `/edit/1`, `edit/234`, etc). The `id` value is then mapped as an attribute of the component's [vnode](vnodes.md) (`vnode.attrs.id`) - -It's possible to have multiple arguments in a route, for example `/edit/:projectID/:userID` would yield the properties `projectID` and `userID` on the component's vnode attributes object. - -#### Key parameter - -When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from `/page/1` to `/page/2` given a route `/page/:id`, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the `onupdate` hook, rather than `oninit`/`oncreate`. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event. - -To achieve that, it's possible to combine route parameterization with [keys](#reinitializing-views-with-single-child-keyed-fragments) for a very convenient pattern: - -```javascript -m.route(document.body, "/edit/1", { - "/edit/:key": Edit, -}) -``` - -This means that the [vnode](vnodes.md) that is created for the root component of the route has a route parameter object `key`. Route parameters become `attrs` in the vnode. Thus, when jumping from one page to another, the `key` changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities). - -You can take that idea further to create components that recreate themselves when reloaded: - -`m.route.set(m.route.get(), {key: Date.now()})` - -Or even use the [`history state`](#history-state) feature to achieve reloadable components without polluting the URL: - -`m.route.set(m.route.get(), null, {state: {key: Date.now()}})` - -Note that the key parameter works only for component routes. If you're using a route resolver, you'll need to use a [single-child keyed fragment](keys.md#reinitializing-views-with-single-child-keyed-fragments), passing `key: m.route.param("key")`, to accomplish the same. - -#### Variadic routes - -It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes: - -```javascript -m.route(document.body, "/edit/pictures/image.jpg", { - "/edit/:file...": Edit, -}) -``` - -#### Handling 404s - -For isomorphic / universal JavaScript app, an url param and a variadic route combined is very useful to display custom 404 error page. - -In a case of 404 Not Found error, the server send back the custom page to client. When Mithril.js is loaded, it will redirect client to the default route because it can't know that route. - -```javascript -m.route(document.body, "/", { - "/": homeComponent, - // [...] - "/:404...": errorPageComponent -}); -``` - -#### History state - -It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form. - -For example, you could create a form like this: - -```javascript -var state = { - term: "", - search: function() { - // save the state for this route - // this is equivalent to `history.replaceState({term: state.term}, null, location.href)` - m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}}) - - // navigate away - location.href = "https://google.com/?q=" + state.term - } -} - -var Form = { - oninit: function(vnode) { - state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button - }, - view: function() { - return m("form", [ - m("input[placeholder='Search']", { - oninput: function (e) { state.term = e.target.value }, - value: state.term - }), - m("button", {onclick: state.search}, "Search") - ]) - } -} - -m.route(document.body, "/", { - "/": Form, -}) -``` - -This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce. - ---- - -### Changing router prefix - -The router prefix is a fragment of the URL that dictates the underlying [strategy](#routing-strategies) used by the router. - -```javascript -// set to pathname strategy -m.route.prefix = "" - -// set to querystring strategy -m.route.prefix = "?" - -// set to hash without bang -m.route.prefix = "#" - -// set to pathname strategy on a non-root URL -// e.g. if the app lives under `https://localhost/my-app` and something else -// lives under `https://localhost` -m.route.prefix = "/my-app" -``` - ---- - -### Advanced component resolution - -Instead of mapping a component to a route, you can specify a RouteResolver object. A RouteResolver object contains a `onmatch()` and/or a `render()` method. Both methods are optional but at least one of them must be present. - -```javascript -m.route(document.body, "/", { - "/": { - onmatch: function(args, requestedPath, route) { - return Home - }, - render: function(vnode) { - return vnode // equivalent to m(Home) - }, - } -}) -``` - -RouteResolvers are useful for implementing a variety of advanced routing use cases. - ---- - -#### Wrapping a layout component - -It's often desirable to wrap all or most of the routed components in a reusable shell (often called a "layout"). In order to do that, you first need to create a component that contains the common markup that will wrap around the various different components: - -```javascript -var Layout = { - view: function(vnode) { - return m(".layout", vnode.children) - } -} -``` - -In the example above, the layout merely consists of a `
` that contains the children passed to the component, but in a real life scenario it could be as complex as needed. - -One way to wrap the layout is to define an anonymous component in the routes map: - -```javascript -// example 1 -m.route(document.body, "/", { - "/": { - view: function() { - return m(Layout, m(Home)) - }, - }, - "/form": { - view: function() { - return m(Layout, m(Form)) - }, - } -}) -``` - -However, note that because the top level component is an anonymous component, jumping from the `/` route to the `/form` route (or vice-versa) will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable. - -If you would prefer to have the Layout component be diffed and maintained intact rather than recreated from scratch, you should instead use a RouteResolver as the root object: - -```javascript -// example 2 -m.route(document.body, "/", { - "/": { - render: function() { - return m(Layout, m(Home)) - }, - }, - "/form": { - render: function() { - return m(Layout, m(Form)) - }, - } -}) -``` - -Note that in this case, if the Layout component has `oninit` and `oncreate` lifecycle methods, they would only fire on the first route change (assuming all routes use the same layout). - -To clarify the difference between the two examples, example 1 is equivalent to this code: - -```javascript -// functionally equivalent to example 1 -var Anon1 = { - view: function() { - return m(Layout, m(Home)) - }, -} -var Anon2 = { - view: function() { - return m(Layout, m(Form)) - }, -} - -m.route(document.body, "/", { - "/": { - render: function() { - return m(Anon1) - } - }, - "/form": { - render: function() { - return m(Anon2) - } - }, -}) -``` - -Since `Anon1` and `Anon2` are different components, their subtrees (including `Layout`) are recreated from scratch. This is also what happens when components are used directly without a RouteResolver. - -In example 2, since `Layout` is the top-level component in both routes, the DOM for the `Layout` component is diffed (i.e. left intact if it has no changes), and only the change from `Home` to `Form` triggers a recreation of that subsection of the DOM. - ---- - -#### Redirection - -The RouteResolver's `onmatch` hook can be used to run logic before the top level component in a route is initialized. You can use either Mithril's `m.route.set()` or native HTML's `history` API. When redirecting with the `history` API, the `onmatch` hook must return a never-resolving Promise to prevent resolution of the matched route. `m.route.set()` cancels resolution of the matched route internally, so this isn't necessary with it. - -##### Example: authentication - -The example below shows how to implement a login wall that prevents users from seeing the `/secret` page unless they login. - -```javascript -var isLoggedIn = false - -var Login = { - view: function() { - return m("form", [ - m("button[type=button]", { - onclick: function() { - isLoggedIn = true - m.route.set("/secret") - } - }, "Login") - ]) - } -} - -m.route(document.body, "/secret", { - "/secret": { - onmatch: function() { - if (!isLoggedIn) m.route.set("/login") - else return Home - } - }, - "/login": Login -}) -``` - -When the application loads, `onmatch` is called and since `isLoggedIn` is false, the application redirects to `/login`. Once the user pressed the login button, `isLoggedIn` would be set to true, and the application would redirect to `/secret`. The `onmatch` hook would run once again, and since `isLoggedIn` is true this time, the application would render the `Home` component. - -For the sake of simplicity, in the example above, the user's logged in status is kept in a global variable, and that flag is merely toggled when the user clicks the login button. In a real life application, a user would obviously have to supply proper login credentials, and clicking the login button would trigger a request to a server to authenticate the user: - -```javascript -var Auth = { - username: "", - password: "", - - setUsername: function(value) { - Auth.username = value - }, - setPassword: function(value) { - Auth.password = value - }, - login: function() { - m.request({ - url: "/api/v1/auth", - params: {username: Auth.username, password: Auth.password} - }).then(function(data) { - localStorage.setItem("auth-token", data.token) - m.route.set("/secret") - }) - } -} - -var Login = { - view: function() { - return m("form", [ - m("input[type=text]", { - oninput: function (e) { Auth.setUsername(e.target.value) }, - value: Auth.username - }), - m("input[type=password]", { - oninput: function (e) { Auth.setPassword(e.target.value) }, - value: Auth.password - }), - m("button[type=button]", {onclick: Auth.login}, "Login") - ]) - } -} - -m.route(document.body, "/secret", { - "/secret": { - onmatch: function() { - if (!localStorage.getItem("auth-token")) m.route.set("/login") - else return Home - } - }, - "/login": Login -}) -``` - ---- - -#### Preloading data - -Typically, a component can load data upon initialization. Loading data this way renders the component twice. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that `loadUsers()` returns a Promise, but any Promise returned by `oninit` is currently ignored. The second render pass comes from the [`background` option for `m.request`](request.md). - -```javascript -var state = { - users: [], - loadUsers: function() { - return m.request("/api/v1/users").then(function(users) { - state.users = users - }) - } -} - -m.route(document.body, "/user/list", { - "/user/list": { - oninit: state.loadUsers, - view: function() { - return state.users.length > 0 ? state.users.map(function(user) { - return m("div", user.id) - }) : "loading" - } - }, -}) -``` - -In the example above, on the first render, the UI displays `"loading"` since `state.users` is an empty array before the request completes. Then, once data is available, the UI redraws and a list of user ids is shown. - -RouteResolvers can be used as a mechanism to preload data before rendering a component in order to avoid UI flickering and thus bypassing the need for a loading indicator: - -```javascript -var state = { - users: [], - loadUsers: function() { - return m.request("/api/v1/users").then(function(users) { - state.users = users - }) - } -} - -m.route(document.body, "/user/list", { - "/user/list": { - onmatch: state.loadUsers, - render: function() { - return state.users.map(function(user) { - return m("div", user.id) - }) - } - }, -}) -``` - -Above, `render` only runs after the request completes, making the ternary operator redundant. - ---- - -#### Code splitting - -In a large application, it may be desirable to download the code for each route on demand, rather than upfront. Dividing the codebase this way is known as code splitting or lazy loading. In Mithril.js, this can be accomplished by returning a promise from the `onmatch` hook: - -At its most basic form, one could do the following: - -```javascript -// Home.js -module.export = { - view: function() { - return [ - m(Menu), - m("h1", "Home") - ] - } -} -``` - -```javascript -// index.js -function load(file) { - return m.request({ - method: "GET", - url: file, - extract: function(xhr) { - return new Function("var module = {};" + xhr.responseText + ";return module.exports;") - } - }) -} - -m.route(document.body, "/", { - "/": { - onmatch: function() { - return load("Home.js") - }, - }, -}) -``` - -However, realistically, in order for that to work on a production scale, it would be necessary to bundle all of the dependencies for the `Home.js` module into the file that is ultimately served by the server. - -Fortunately, there are a number of tools that facilitate the task of bundling modules for lazy loading. Here's an example using [native dynamic `import(...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import), supported by many bundlers: - -```javascript -m.route(document.body, "/", { - "/": { - onmatch: function() { - return import('./Home.js') - }, - }, -}) -``` - ---- - -### Typed routes - -In certain advanced routing cases, you may want to constrain a value further than just the path itself, only matching something like a numeric ID. You can do that pretty easily by returning `m.route.SKIP` from a route. - -```javascript -m.route(document.body, "/", { - "/view/:id": { - onmatch: function(args) { - if (!/^\d+$/.test(args.id)) return m.route.SKIP - return ItemView - }, - }, - "/view/:name": UserView, -}) -``` - ---- - -### Hidden routes - -In rare circumstances, you may want to hide certain routes for some users, but not all. For instance, a user might be prohibited from viewing a particular user, and instead of showing a permission error, you'd rather pretend it doesn't exist and redirect to a 404 view instead. In this case, you can use `m.route.SKIP` to just pretend the route doesn't exist. - -```javascript -m.route(document.body, "/", { - "/user/:id": { - onmatch: function(args) { - return Model.checkViewable(args.id).then(function(viewable) { - return viewable ? UserView : m.route.SKIP - }) - }, - }, - "/:404...": PageNotFound, -}) -``` - ---- - -### Route cancellation / blocking - -RouteResolver `onmatch` can prevent route resolution by returning a promise that never resolves. This can be used to detect attempted redundant route resolutions and cancel them: - -```javascript -m.route(document.body, "/", { - "/": { - onmatch: function(args, requestedPath) { - if (m.route.get() === requestedPath) - return new Promise(function() {}) - }, - }, -}) -``` - ---- - -### Third-party integration - -In certain situations, you may find yourself needing to interoperate with another framework like React. Here's how you do it: - -- Define all your routes using `m.route` as normal, but make sure you only use it *once*. Multiple route points are not supported. -- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. `m.route` uses `m.mount` internally to hook everything up, so it's not magic. - -Here's an example with React: - -```jsx -class Child extends React.Component { - constructor(props) { - super(props) - this.root = React.createRef() - } - - componentDidMount() { - m.route(this.root, "/", { - // ... - }) - } - - componentDidUnmount() { - m.mount(this.root, null) - } - - render() { - return
- } -} -``` - -And here's the rough equivalent with Vue: - -```html -
-``` - -```javascript -Vue.component("my-child", { - template: `
`, - mounted: function() { - m.route(this.$refs.root, "/", { - // ... - }) - }, - destroyed: function() { - m.mount(this.$refs.root, null) - }, -}) -``` diff --git a/docs/signatures.md b/docs/signatures.md deleted file mode 100644 index 7838bf26..00000000 --- a/docs/signatures.md +++ /dev/null @@ -1,87 +0,0 @@ - - -# How to read signatures - -Signature sections typically look like this: - -`vnode = m(selector, attributes, children)` - -Argument | Type | Required | Description ------------- | ------------------------------------ | -------- | --- -`selector` | `String|Object` | Yes | A CSS selector or a component -`attributes` | `Object` | No | HTML attributes or element properties -`children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md). Can be written as [splat arguments](signatures.md#splats) -**returns** | `Vnode` | | A [vnode](vnodes.md) - -The signature line above the table indicates the general syntax of the method, showing the name of the method, the order of its arguments and a suggested variable name for its return value. - -The **Argument** column in the table indicates which part of the signature is explained by the respective table row. The `returns` row displays information about the return value of the method. - -The **Type** column indicates the expected type for the argument. - -A pipe (`|`) indicates that an argument is valid if it has any of the listed types. For example, `String|Object` indicates that `selector` can be a string OR an object. - -Angled brackets (`< >`) after an `Array` indicate the expected type for array items. For exampe, `Array` indicates that the argument must be an array and that all items in that array must be strings. Angled brackets after an `Object` indicate a map. For example, `Object` indicates that the argument must be an object, whose keys are strings and values are [components](components.md) - -Sometimes non-native types may appear to indicate that a specific object signature is required. For example, `Vnode` is an object that has a [virtual DOM node](vnodes.md) structure. - -The **Required** column indicates whether an argument is required or optional. If an argument is optional, you may set it to `null` or `undefined`, or omit it altogether, such that the next argument appears in its place. - ---- - -### Optional arguments - -Function arguments surrounded by square brackets `[ ]` are optional. In the example below, `url` is an optional argument: - -`m.request([url,] options)` - ---- - -### Splats - -A splat argument means that if the argument is an array, you can omit the square brackets and have a variable number of arguments in the method instead. - -In the example at the top, this means that `m("div", {id: "foo"}, ["a", "b", "c"])` can also be written as `m("div", {id: "foo"}, "a", "b", "c")`. - -Splats are useful in some compile-to-JS languages such as CoffeeScript, and also allow helpful shorthands for some common use cases. - ---- - -### Function signatures - -Functions are denoted with an arrow (`->`). The left side of the arrow indicates the types of the input arguments and the right side indicates the type for the return value. - -For example, `parseFloat` has the signature `String -> Number`, i.e. it takes a string as input and returns a number as output. - -Functions with multiple arguments are denoted with parenthesis: `(String, Array) -> Number` - ---- - -### Component signatures - -Components are denoted via calls to `m`, but with the initial selector argument set to a constant named in the relevant prose: - -`vnode = m(m.route.Link, attributes, children)` - -Argument | Type | Required | Description ---------------------- | ------------------------------------ | -------- | --- -`attributes.href` | `Object` | Yes | The target route to navigate to. -`attributes.selector` | `String|Object|Function` | No | This sets the tag name to use. Must be a valid selector for [`m`](hyperscript.md) if given, defaults to `"a"`. -`attributes.options` | `Object` | No | This sets the options passed to [`m.route.set`](#mrouteset). -`attributes` | `Object` | No | Other attributes to apply to the returned vnode may be passed. -`children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md) for this link. -**returns** | `Vnode` | | A [vnode](vnodes.md). - -Children here, if specified, are assumed to be able to be written as [splat arguments](#splats), unless otherwise specified in prose. - -An element with no sensible children and/or attributes may choose to elide the relevant parameter entirely: - -`vnode = m(Component, attributes)` - -Argument | Type | Required | Description ------------------ | -------- | -------- | --- -`attributes.href` | `Object` | Yes | The -`attributes` | `Object` | No | Other attributes to apply to the returned vnode -**returns** | `Vnode` | | A [vnode](vnodes.md) diff --git a/docs/simple-application.md b/docs/simple-application.md deleted file mode 100644 index 38f8aead..00000000 --- a/docs/simple-application.md +++ /dev/null @@ -1,736 +0,0 @@ - - -# Simple application - -Let's develop a simple application that shows off how to do most of the major things you would need to deal with while using Mithril. - -*An interactive example of the end result can be seen [here](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcICqkBOBZA9ktAHQBWYIANCAGYCWMFqA2qAHYCGAthGpjgSJQyFarHxtEkvgDcO2AARYIigLwLgAHTYLdCqHTDxUC5gF1K2vfvwckAGUPGFNAK5sEdCQAoAlBqtrPWwIeFdsHS5iEIBHVwgjby0dINSeeAALQhNNEABxAFEAFVzLFNTrcKgckAz4eAAHMFQAehauOkzsBgBaEKiaKABPYiIZFo4GuhbXHDBSwIq9AHdOjIBhEKJJOg4oZoV4bHiypYBfX0Wg4kyINm83D3gve5CwVyh4f2Sl62VsYgGIwKdRvD7wUYceAcK7WC5XM5lK6wcIhSQmYCIq5QWxIEyPTw+OhIb6w4KhcKRaIQOIJeBJMlpUJZPEKXKFEpURlBKo1OqNZptDpdXr9YiDEZjCZTGZzFq5BQAagUxNOv10q0ym2Qd2eewORxO3Ph5Wut3uBOePjBn1JpqW-2IKOwaPgIIUNvgxsupqxbCuYA4Mgg+PchPudqWITCEQUUVi8USP3VceZ2TZIAAChhOWr1byM-ymq12mtulA+hABsNRhBxpNprMVGB5SAlUocE7UbriKrudYAEaEIYmR3O11536ajZbXW7fYmQ0QSdwn1w7RnbRUcDQOBWthMEAAVlQABYj5QAIwAdjPF4ATAAOO8gRHsbi8dD-RxGETb8SSLqsjyB2Kg-m66jJroEh0GwnSjp2OJ2OBk4yHQEDLKGTwvH4AT2uSMaRN4uTEE22A9ECXpUKBAKUcQXCTA8Yb7t4ZGRim0aUnG3jxvgriIMQjhsAA1pQGgZCENA1C0yCdK27Zkb2SBibAUAcGABwAORkRRTg9J0VaaYiCiKfQ2BGAAch+7YKgqyqKWplkfmuFQXC5uibmwnmvpQ748Hw-wAGL4NgUTkP+EhSPAwGKEFIVcO6UEKDBcHOJaOEyGwhAQN8jpIUg3iZdlxBQkcYBKb4frWGhGFYeGuFJdYnGxlwxG0PFpR4SmQQSO8A7CnVLE5V13UVBAxANCEwaSAAIhANAcOCfj9imjqBsGy34eqnndcZzArborW5GpA4kCd0CdbkgV0OZbqcDwuS+CuFRHSAsENPxvZsB98DMPAQwNBAqiIAAHvAZjMA0amwBAWRQEQ2CqNdt0KPdEBmJ1jUpilP2DThw3AGO3aSOKN1OTw7rjdC2AAOahMQchQPEVWjbojPxAhKhdi6PZmeTEAHQoFzPWkbXncI4uXSA9jqXdH6PSLQSve9n0q79UMcDDcMI6oMvAmjGPUVj6o4-xeM+ATRM8yTjnwFZFPqFT8h0xC7MQCzrNu5zALjj2tv2wLW1BMLguvQO-HwBIxDh-UEh-QDQN9cKhtibkADKQYCyAT3cmY7lCxuW5viAaN8DLQx8RC4ViJFQHoHIijl5XiVXDVmEuMxGVZUQ7GpM1RG5AxsGAhwFf8Z1+1B9Yr2cDI9F3K4E+C9PPHRJX41CaJI2s7oEnzSYmktJRmnL3oxm5P88zZ4LeeKyvuSQOGnVFUQToZAwSBovn1h5wihdsD5Pyn4QCTAaH+GugFpDoF4vxcak15p0BBu6TSABiE+bAYGIG8EgfAKIeAkyHEgIYqcQBHycJjK4uQyFGFyBibkaIEbmwjNvX4-duJN34mJVq34nC+G-mfBEk4qGyXgC0VAxJaEsKancRhHdsI+BfgTQWbDWocPgFw7wcVQpiUUSVeo5k+HGkERuH0gCS4fj4E6DSEVIHRXQIQ4hCgvo-TEtHCOEgRo0EiiYLKoU9gKEvAANgaEggAaioJAHBOAAG5AgMVprBEw94AAMITYleW0NoEeY83TJniTTRJATUlIMWpHdJh0OAgx6KsJAmQTCXmSY0tJ-8sn4NcCNfJhTkkKG6QAZmKekzyWSdKURGpRHoRghgwB8RICA5S4zOy6T0opzSUgNDsEgWCNMTDJMGZktgpEcC6SMPpRACVkwDk1sJGm2A+JsFZCghaTyODzKHNgWRl4QkKDAPgAwSAFAoKQEC+Z4gcTYBMCg3pUL5mbLABrEcCgBw4lgMJeZnS2A7OWZ8kG8z1lAq2SYR8XzLxHlWboUG8AehEHENgKELwZlsDmS0g5Iy9IGS4KgLIwZFDJgpVSuAIU6USBMO4BGBhGV7P9Ac8WI1YXwpMEi3BqK4mLIxcs7ppKcXMucfxEabyPlfJ+X8gFQKkCvJCgjPodg6CzBML0sliL8BVLAHQAAXgSx17yVA9CHFqlIcq1IIsVSitFqrMXdIaQ6vFmy2DbJWUgklDqal1KKckgApJKrJMdI46AuVcm5dyHkQGLear14KAmGt+cSE1wLAj6u9bSzZtqFD2r9boUFIUIVQt6TCww8qVRsHFRAH1yLlUpHReG+NuKNkesjQmzV8y+XUsFfuBlTKMlSrcbHDFXKVB6vzbc0VEKICPhPSeyVr4LAgHFcJJgrBzH+WgWWBg24qh8CLIKGY31rlOnwFwUsIooAAAF7zEGScQU8AGJIMHosPauIB-qAz4GAWA3QGjRTOGYM4QA)* - -First let's create an entry point for the application. Create a file `index.html`: - -```html - - - - - - My Application - - - - - -``` - -The `` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the JavaScript file that controls the application. - -We could create the entire application in a single JavaScript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `bin/app.js`. - -There are many ways to setup a bundler tool, but most are distributed via npm. In fact, most modern JavaScript libraries and tools are distributed that way, including Mithril. To download npm, [install Node.js](https://nodejs.org/en/); npm is installed automatically with it. Once you have Node.js and npm installed, open the command line and run this command: - -```bash -npm init -y -``` - -If npm is installed correctly, a file `package.json` will be created. This file will contain a skeleton project meta-description file. Feel free to edit the project and author information in this file. - ---- - -To install Mithril.js, follow the instructions in the [installation](installation.md) page. Once you have a project skeleton with Mithril.js installed, we are ready to create the application. - -Let's start by creating a module to store our state. Let's create a file called `src/models/User.js` - -```javascript -// src/models/User.js -var User = { - list: [] -} - -module.exports = User -``` - -Now let's add code to load some data from a server. To communicate with a server, we can use Mithril.js' XHR utility, `m.request`. First, we include Mithril.js in the module: - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [] -} - -module.exports = User -``` - -Next we create a function that will trigger an XHR call. Let's call it `loadList` - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [], - loadList: function() { - // TODO: make XHR call - } -} - -module.exports = User -``` - -Then we can add an `m.request` call to make an XHR request. For this tutorial, we'll make XHR calls to the REM (DEAD LINK, FIXME: https //rem-rest-api.herokuapp.com/) API, a mock REST API designed for rapid prototyping. This API returns a list of users from the `GET https://mithril-rem.fly.dev/api/users` endpoint. Let's use `m.request` to make an XHR request and populate our data with the response of that endpoint. - -*Note: third-party cookies may have to be enabled for the REM endpoint to work.* - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [], - loadList: function() { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users", - withCredentials: true, - }) - .then(function(result) { - User.list = result.data - }) - }, -} - -module.exports = User -``` - -The `method` option is an [HTTP method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). To retrieve data from the server without causing side-effects on the server, we need to use the `GET` method. The `url` is the address for the API endpoint. The `withCredentials: true` line indicates that we're using cookies (which is a requirement for the REM API). - -The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril.js assumes a HTTP response body are in JSON format and automatically parses it into a JavaScript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `result.data` array to `User.list`. - -Notice we also have a `return` statement in `loadList`. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request. - -This simple model exposes two members: `User.list` (an array of user objects), and `User.loadList` (a method that populates `User.list` with server data). - ---- - -Now, let's create a view module so that we can display data from our User model module. - -Create a file called `src/views/UserList.js`. First, let's include Mithril.js and our model, since we'll need to use both: - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") -``` - -Next, let's create a Mithril.js component. A component is simply an object that has a `view` method: - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - view: function() { - // TODO add code here - } -} -``` - -By default, Mithril.js views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and since its syntax is just JavaScript, it's possible to leverage a lot of JavaScript tooling ecosystem. For example: - -- You can use [Babel](es6.md) to transpile ES6+ to ES5 for IE and to transpile [JSX](jsx.md) (an inline HTML-like syntax extension) to appropriate hyperscript calls. -- You can use [ESLint](https://eslint.org/) for easy linting with no special plugins. -- You can use [Terser](https://github.com/terser-js/terser) or [UglifyJS](https://github.com/mishoo/UglifyJS2) (ES5 only) to minify your code easily. -- You can use [Istanbul](https://github.com/istanbuljs/nyc) for code coverage. -- You can use [TypeScript](https://www.typescriptlang.org/) for easy code analysis. (There are [community-supported type definitions available](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mithril), so you don't need to roll your own.) - -Let's start off with hyperscript and create a list of items. Hyperscript is the idiomatic way to use Mithril.js, but [JSX](jsx.md) works pretty similarly. - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - view: function() { - return m(".user-list") - } -} -``` - -The `".user-list"` string is a CSS selector, and as you would expect, `.user-list` represents a class. When a tag is not specified, `div` is the default. So this view is equivalent to `
`. - -Now, let's reference the list of users from the model we created earlier (`User.list`) to dynamically loop through data: - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - view: function() { - return m(".user-list", User.list.map(function(user) { - return m(".user-list-item", user.firstName + " " + user.lastName) - })) - } -} -``` - -Since `User.list` is a JavaScript array, and since hyperscript views are just JavaScript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user. - -The problem, of course, is that we never called the `User.loadList` function. Therefore, `User.list` is still an empty array, and thus this view would render a blank page. Since we want `User.loadList` to be called when we render this component, we can take advantage of component [lifecycle methods](lifecycle-methods.md): - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - oninit: User.loadList, - view: function() { - return m(".user-list", User.list.map(function(user) { - return m(".user-list-item", user.firstName + " " + user.lastName) - })) - } -} -``` - -Notice that we added an `oninit` method to the component, which references `User.loadList`. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, `User.list` gets populated. - -Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common pitfall for developers new to JavaScript: calling the function immediately means that the XHR request will fire as soon as the source code is evaluated, even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected. - ---- - -Let's render the view from the entry point file `src/index.js` we created earlier: - -```javascript -// src/index.js -var m = require("mithril") - -var UserList = require("./views/UserList") - -m.mount(document.body, UserList) -``` - -The `m.mount` call renders the specified component (`UserList`) into a DOM element (`document.body`), erasing any DOM that was there previously. Opening the HTML file in a browser should now display a list of person names. - ---- - -Right now, the list looks rather plain because we have not defined any styles. So let's add a few of them. Let's first create a file called `styles.css` and include it in the `index.html` file: - -```html - - - - - - My Application - - - - - - -``` - -Now we can style the `UserList` component: - -```css -.user-list { - list-style: none; - margin: 0 0 10px; - padding: 0; -} - -.user-list-item { - background: #fafafa; - border: 1px solid #ddd; - color: #333; - display: block; - margin: 0 0 1px; - padding: 8px 15px; - text-decoration: none; -} - -.user-list-item:hover { - text-decoration: underline; -} -``` - -Reloading the browser window now should display some styled elements. - ---- - -Let's add routing to our application. - -Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril.js is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via JavaScript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML). - -We can add routing by changing the `m.mount` call to a `m.route` call: - -```javascript -// src/index.js -var m = require("mithril") - -var UserList = require("./views/UserList") - -m.route(document.body, "/list", { - "/list": UserList -}) -``` - -The `m.route` call specifies that the application will be rendered into `document.body`. The `"/list"` argument is the default route. That means the user will be redirected to that route if they land in a route that does not exist. The `{"/list": UserList}` object declares a map of existing routes, and what components each route resolves to. - -Refreshing the page in the browser should now append `#!/list` to the URL to indicate that routing is working. Since that route render UserList, we should still see the list of people on screen as before. - -The `#!` snippet is known as a hashbang, and it's a commonly used string for implementing client-side routing. It's possible to configure this string it via [`m.route.prefix`](route.md#mrouteprefix). Some configurations require supporting server-side changes, so we'll just continue using the hashbang for the rest of this tutorial. - ---- - -Let's add another route to our application for editing users. First let's create a module called `views/UserForm.js` - -```javascript -// src/views/UserForm.js - -module.exports = { - view: function() { - // TODO implement view - } -} -``` - -Then we can `require` this new module from `src/index.js` - -```javascript -// src/index.js -var m = require("mithril") - -var UserList = require("./views/UserList") -var UserForm = require("./views/UserForm") - -m.route(document.body, "/list", { - "/list": UserList -}) -``` - -And finally, we can create a route that references it: - -```javascript -// src/index.js -var m = require("mithril") - -var UserList = require("./views/UserList") -var UserForm = require("./views/UserForm") - -m.route(document.body, "/list", { - "/list": UserList, - "/edit/:id": UserForm, -}) -``` - -Notice that the new route has a `:id` in it. This is a route parameter; you can think of it as a wild card; the route `/edit/1` would resolve to `UserForm` with an `id` of `"1"`. `/edit/2` would also resolve to `UserForm`, but with an `id` of `"2"`. And so on. - -Let's implement the `UserForm` component so that it can respond to those route parameters: - -```javascript -// src/views/UserForm.js -var m = require("mithril") - -module.exports = { - view: function() { - return m("form", [ - m("label.label", "First name"), - m("input.input[type=text][placeholder=First name]"), - m("label.label", "Last name"), - m("input.input[placeholder=Last name]"), - m("button.button[type=submit]", "Save"), - ]) - } -} -``` - -And let's add some more styles to `styles.css`: - -```css -/* styles.css */ -body, .input, .button { - font: normal 16px Verdana; - margin: 0; -} - -.user-list { - list-style: none; - margin: 0 0 10px; - padding: 0; -} - -.user-list-item { - background: #fafafa; - border: 1px solid #ddd; - color: #333; - display: block; - margin: 0 0 1px; - padding: 8px 15px; - text-decoration: none; -} - -.user-list-item:hover { - text-decoration: underline; -} - -.label { - display: block; - margin: 0 0 5px; -} - -.input { - border: 1px solid #ddd; - border-radius: 3px; - box-sizing: border-box; - display: block; - margin: 0 0 10px; - padding: 10px 15px; - width: 100%; -} - -.button { - background: #eee; - border: 1px solid #ddd; - border-radius: 3px; - color: #333; - display: inline-block; - margin: 0 0 10px; - padding: 10px 15px; - text-decoration: none; -} - -.button:hover { - background: #e8e8e8; -} -``` - -Right now, this component does nothing to respond to user events. Let's add some code to our `User` model in `src/models/User.js`. This is how the code is right now: - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [], - loadList: function() { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users", - withCredentials: true, - }) - .then(function(result) { - User.list = result.data - }) - }, -} - -module.exports = User -``` - -Let's add code to allow us to load a single user - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [], - loadList: function() { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users", - withCredentials: true, - }) - .then(function(result) { - User.list = result.data - }) - }, - - current: {}, - load: function(id) { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users/" + id, - withCredentials: true, - }) - .then(function(result) { - User.current = result - }) - } -} - -module.exports = User -``` - -Notice we added a `User.current` property, and a `User.load(id)` method which populates that property. We can now populate the `UserForm` view using this new method: - -```javascript -// src/views/UserForm.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - oninit: function(vnode) {User.load(vnode.attrs.id)}, - view: function() { - return m("form", [ - m("label.label", "First name"), - m("input.input[type=text][placeholder=First name]", {value: User.current.firstName}), - m("label.label", "Last name"), - m("input.input[placeholder=Last name]", {value: User.current.lastName}), - m("button.button[type=submit]", "Save"), - ]) - } -} -``` - -Similar to the `UserList` component, `oninit` calls `User.load()`. Remember we had a route parameter called `:id` on the `"/edit/:id": UserForm` route? The route parameter becomes an attribute of the `UserForm` component's vnode, so routing to `/edit/1` would make `vnode.attrs.id` have a value of `"1"`. - -Now, let's modify the `UserList` view so that we can navigate from there to a `UserForm`: - -```javascript -// src/views/UserList.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - oninit: User.loadList, - view: function() { - return m(".user-list", User.list.map(function(user) { - return m(m.route.Link, { - class: "user-list-item", - href: "/edit/" + user.id, - }, user.firstName + " " + user.lastName) - })) - } -} -``` - -Here we swapped out the `.user-list-item` vnode with an `m.route.Link` with that class and the same children. We added an `href` that references the route we want. What this means is that clicking the link would change the part of URL that comes after the hashbang `#!` (thus changing the route without unloading the current HTML page). Behind the scenes, it uses an `
` to implement the link, and it all just works. - -If you refresh the page in the browser, you should now be able to click on a person and be taken to a form. You should also be able to press the back button in the browser to go back from the form to the list of people. - ---- - -The form itself still doesn't save when you press "Save". Let's make this form work: - -```javascript -// src/views/UserForm.js -var m = require("mithril") -var User = require("../models/User") - -module.exports = { - oninit: function(vnode) {User.load(vnode.attrs.id)}, - view: function() { - return m("form", { - onsubmit: function(e) { - e.preventDefault() - User.save() - } - }, [ - m("label.label", "First name"), - m("input.input[type=text][placeholder=First name]", { - oninput: function (e) {User.current.firstName = e.target.value}, - value: User.current.firstName - }), - m("label.label", "Last name"), - m("input.input[placeholder=Last name]", { - oninput: function (e) {User.current.lastName = e.target.value}, - value: User.current.lastName - }), - m("button.button[type=submit]", "Save"), - ]) - } -} -``` - -We added `oninput` events to both inputs, that set the `User.current.firstName` and `User.current.lastName` properties when a user types. - -In addition, we declared that a `User.save` method should be called when the "Save" button is pressed. Let's implement that method: - -```javascript -// src/models/User.js -var m = require("mithril") - -var User = { - list: [], - loadList: function() { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users", - withCredentials: true, - }) - .then(function(result) { - User.list = result.data - }) - }, - - current: {}, - load: function(id) { - return m.request({ - method: "GET", - url: "https://mithril-rem.fly.dev/api/users/" + id, - withCredentials: true, - }) - .then(function(result) { - User.current = result - }) - }, - - save: function() { - return m.request({ - method: "PUT", - url: "https://mithril-rem.fly.dev/api/users/" + User.current.id, - body: User.current, - withCredentials: true, - }) - } -} - -module.exports = User -``` - -In the `save` method at the bottom, we used the `PUT` HTTP method to indicate that we are upserting data to the server. - -Now try editing the name of a user in the application. Once you save a change, you should be able to see the change reflected in the list of users. - ---- - -Currently, we're only able to navigate back to the user list via the browser back button. Ideally, we would like to have a menu - or more generically, a layout where we can put global UI elements - -Let's create a file `src/views/Layout.js`: - -```javascript -// src/views/Layout.js -var m = require("mithril") - -module.exports = { - view: function(vnode) { - return m("main.layout", [ - m("nav.menu", [ - m(m.route.Link, {href: "/list"}, "Users") - ]), - m("section", vnode.children) - ]) - } -} -``` - -This component is fairly straightforward, it has a `