├── .npmignore ├── .gitignore ├── tsconfig.json ├── censor.d.ts ├── package.json ├── link.d.ts ├── index.mjs ├── index.d.ts ├── redraw.d.ts ├── match.d.ts ├── self-sufficient.d.ts ├── store.d.ts ├── link.mjs ├── index.js ├── redraw.mjs ├── censor.mjs ├── docs ├── censor.md ├── link.md ├── api.md ├── store.md ├── each.md ├── match.md ├── self-sufficient.md ├── migrate.md └── redraw.md ├── each.d.ts ├── match.mjs ├── link.js ├── redraw.js ├── censor.js ├── store.mjs ├── match.js ├── store.js ├── each.mjs ├── README.md ├── self-sufficient.mjs ├── each.js ├── self-sufficient.js ├── migrate └── v1.js └── LICENSE.txt /.npmignore: -------------------------------------------------------------------------------- 1 | /yarn.lock 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /npm-debug.log 2 | /node_modules 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /censor.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sanitize an attributes object of its lifecycle methods. 3 | */ 4 | export default function censor(attrs: T, extras?: string[]): T; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-helpers", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "module": "index.mjs", 6 | "license": "(BlueOak-1.0.0 AND MIT)", 7 | "peerDependencies": { 8 | "@types/mithril": "2", 9 | "mithril": "2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /link.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Mithril from "mithril"; 3 | 4 | /** 5 | * Link a list of children to a key, replacing them if it ever changes. 6 | */ 7 | export default function link( 8 | key: PropertyKey, 9 | ...children: Array 10 | ): Mithril.Vnode; 11 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a module collecting all the other submodules for easy access. 3 | */ 4 | 5 | export {default as censor} from "./censor.mjs" 6 | export {default as SelfSufficient} from "./self-sufficient.mjs" 7 | export {default as makeStore} from "./store.mjs" 8 | export {default as makeRedraw} from "./redraw.mjs" 9 | export {default as each} from "./each.mjs" 10 | export {default as link} from "./link.mjs" 11 | export {when, cond, match} from "./match.mjs" 12 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * mithril-helpers definitions, for your convenience 4 | */ 5 | 6 | export {default as censor} from "./censor"; 7 | export {default as makeStore, Store} from "./store"; 8 | export {default as makeRedraw, Redraw, RedrawState} from "./redraw"; 9 | export {default as SelfSufficient, SelfSufficientParams, SelfSufficientState} from "./self-sufficient"; 10 | export {default as each} from "./each" 11 | export {default as link} from "./link" 12 | export {when, cond, match} from "./match" 13 | -------------------------------------------------------------------------------- /redraw.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A redraw wrapper 3 | */ 4 | export interface Redraw { 5 | (): void; 6 | readonly ready: () => void; 7 | } 8 | 9 | /** 10 | * A state wrapper. A `SelfSufficient` instance satisfies this, but it's weak 11 | * just to keep from being overly restrictive. (This is the only method we use.) 12 | */ 13 | export interface RedrawState { 14 | redraw(): void; 15 | } 16 | 17 | 18 | /** 19 | * Create a new redraw wrapper. 20 | */ 21 | export default function makeRedraw(state?: RedrawState): Redraw; 22 | -------------------------------------------------------------------------------- /match.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Mithril from "mithril"; 3 | 4 | export function when( 5 | cond: boolean, 6 | then: () => Mithril.Children, 7 | orElse: () => Mithril.Children 8 | ): Mithril.Vnode; 9 | 10 | export function cond( 11 | ...cases: Array<{if: unknown, then: () => Mithril.Children}> 12 | ): Mithril.Vnode; 13 | 14 | export function match( 15 | value: T, 16 | ...cases: Array<{if: T, then: () => Mithril.Children}> 17 | ): Mithril.Vnode; 18 | -------------------------------------------------------------------------------- /self-sufficient.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * mithril-helpers definitions, for your convenience 3 | */ 4 | 5 | /// 6 | import * as Mithril from "mithril"; 7 | 8 | export interface SelfSufficientParams { 9 | root: Mithril.Vnode; 10 | view(state: SelfSufficientState): Mithril.Children; 11 | } 12 | 13 | /** 14 | * The core component. 15 | */ 16 | export interface SelfSufficientState { 17 | safe(): boolean; 18 | redraw(): void; 19 | redrawSync(): void; 20 | } 21 | 22 | declare const SelfSufficient: Mithril.ComponentTypes 23 | 24 | export default SelfSufficient; 25 | -------------------------------------------------------------------------------- /store.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A getter/setter store that invokes a redraw whenever it's set. 3 | */ 4 | export interface Store { 5 | (): T; 6 | (newValue: U): U; 7 | } 8 | 9 | /** 10 | * Create a new store with an initial value of `undefined`. 11 | */ 12 | export default function makeStore(): Store; 13 | 14 | /** 15 | * Create a new store with an initial value. 16 | */ 17 | export default function makeStore(initial: T): Store; 18 | 19 | /** 20 | * Create a new store with an initial value and a change observer. 21 | */ 22 | export default function makeStore(initial: T, onchange: (next: T, prev: T) => any): Store; 23 | -------------------------------------------------------------------------------- /link.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to easily link to an identity key. Keys are always converted to 3 | * properties, so `null`/`undefined` are equivalent to `"null"` and 4 | * `"undefined"` respectively. 5 | * 6 | * This is just a preview of something I'm looking to add in my redesign. 7 | */ 8 | 9 | import Vnode from "mithril/render/vnode" 10 | 11 | export default function link(key, ...children) { 12 | return Vnode("[", null, null, [ 13 | Vnode("[", typeof key === "symbol" ? key : "" + key, null, 14 | Vnode.normalizeChildren( 15 | Array.isArray(children[0]) ? children[0] : children 16 | ) 17 | ) 18 | ]) 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a module collecting all the other submodules for easy access. 3 | * 4 | * Note: this should *not* be loaded as a browser script. 5 | */ 6 | 7 | "use strict" 8 | 9 | if (typeof exports !== "object" || !exports) { 10 | throw new Error( 11 | "This is a CommonJS module, and shouldn't be loaded directly!" 12 | ) 13 | } 14 | 15 | exports.censor = require("./censor.js") 16 | exports.SelfSufficient = require("./self-sufficient.js") 17 | exports.makeStore = require("./store.js") 18 | exports.makeRedraw = require("./redraw.js") 19 | exports.each = require("./each.js") 20 | exports.link = require("./link.js") 21 | var match = require("./match.js") 22 | exports.when = match.when 23 | exports.cond = match.cond 24 | exports.match = match.match 25 | -------------------------------------------------------------------------------- /redraw.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this when you want to use `m.redraw` to link from streams or other 3 | * synchronously-updating cells. 4 | * 5 | * Note: call `redraw.ready` in `oncreate` to unlock it. 6 | * 7 | * Also, you may call `redraw.lock` in `onbeforeupdate` and `redraw.ready` in 8 | * `onupdate`, to avoid unnecessary rendering calls. 9 | */ 10 | 11 | import mithril from "mithril" 12 | 13 | // Ideally, this would use a hook to know when Mithril starts/finishes 14 | // redrawing. 15 | const p = Promise.resolve() 16 | 17 | export default function makeRedraw(state) { 18 | let ready = false 19 | const redraw = state != null 20 | ? () => { if (ready) state.redraw() } 21 | : () => { if (ready) p.then(mithril.redraw) } 22 | 23 | redraw.ready = () => { 24 | ready = true 25 | } 26 | 27 | return redraw 28 | } 29 | -------------------------------------------------------------------------------- /censor.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to filter out any of Mithril's magic attributes while still keeping 3 | * your interfaces as unrestrictive as possible. 4 | */ 5 | 6 | // Note: this avoids as much allocation and overhead as possible. 7 | const hasOwn = {}.hasOwnProperty 8 | const magic = [ 9 | "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", 10 | "onbeforeremove", "onremove", 11 | ] 12 | 13 | export default function censor(attrs, extras) { 14 | const excludeSet = new Set(magic) 15 | if (extras != null) { 16 | for (const item of extras) excludeSet.add(item) 17 | } 18 | 19 | for (let i = 0; i < exclude.length; i++) { 20 | if (hasOwn.call(attrs, exclude[i])) { 21 | const result = {} 22 | 23 | for (const key in attrs) { 24 | if (hasOwn.call(attrs, key) && !excludeSet.has(key)) { 25 | result[key] = attrs[key] 26 | } 27 | } 28 | 29 | return result 30 | } 31 | } 32 | 33 | return attrs 34 | } 35 | -------------------------------------------------------------------------------- /docs/censor.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/censor 4 | 5 | Exposes `censor(attrs, extras?)`, which removes all of Mithril's magic attributes from an object (making a copy if necessary to avoid actually modifying the object) as well as whatever extras you want to remove, so you may safely pass it directly as an attribute object to other methods. Helpful to [avoid overly restrictive interfaces](https://mithril.js.org/components.html#avoid-restrictive-interfaces) without running into [odd buggy behavior with magic methods](https://github.com/MithrilJS/mithril.js/issues/1775). 6 | 7 | ```js 8 | return m("div", m.helpers.censor(vnode.attrs), [...children]) 9 | ``` 10 | 11 | ## Usage 12 | 13 | - `m.helpers.censor(attrs, extras?) -> attrs` 14 | 15 | - `attrs` is a single attributes object you want to censor. 16 | - `extras` is an optional array of whatever other attributes you want to censor. 17 | - Returns a new shallow clone of the attributes with keys omitted appropriately if anything needed censored, the original attributes object otherwise. 18 | -------------------------------------------------------------------------------- /each.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Mithril from "mithril"; 3 | 4 | /** 5 | * Render a list by key. 6 | * 7 | * @param key A key selector function returning the key to use. 8 | * @param child A function returning the child vnode to render. 9 | * @returns A keyed fragment. 10 | */ 11 | export default function each( 12 | list: ArrayLike | Iterable, 13 | by: (value: T, index: number) => PropertyKey, 14 | child: (value: T, index: number) => Mithril.Children 15 | ): Mithril.Vnode; 16 | 17 | /** 18 | * Render a list by key. 19 | * 20 | * @param key A property on each item representing the key to use. 21 | * @param child A function returning the child vnode to render. 22 | * @returns A keyed fragment. 23 | */ 24 | export default function each( 25 | list: ArrayLike | Iterable, 26 | // Select keys whose values are property keys 27 | by: {[P in keyof T]: T[P] extends PropertyKey ? P : never}[keyof T], 28 | child: (value: T, index: number) => Mithril.Children 29 | ): Mithril.Vnode; 30 | -------------------------------------------------------------------------------- /match.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to match based on conditions more easily. 3 | */ 4 | 5 | import Vnode from "mithril/render/vnode" 6 | 7 | function resolve(then) { 8 | return Vnode('[', null, null, [Vnode.normalize(then)]) 9 | } 10 | 11 | export function when(cond, then, orElse) { 12 | cond = !!cond 13 | return Vnode("[", null, null, [ 14 | Vnode("[", cond, null, [Vnode.normalize(cond ? then() : orElse())]) 15 | ]) 16 | } 17 | 18 | export function cond(...args) { 19 | for (let i = 0; i < args.length; i++) { 20 | args[i] = args[i].if ? resolve(args[i].then()) : null 21 | } 22 | return Vnode('[', null, null, args) 23 | } 24 | 25 | export function match(value, ...args) { 26 | if (value === value) { 27 | for (let i = 0; i < args.length; i++) { 28 | args[i] = args[i].if === value ? resolve(args[i].then()) : null 29 | } 30 | } else { 31 | for (let i = 0; i < args.length; i++) { 32 | const cond = args[i].if 33 | args[i] = cond !== cond ? resolve(args[i].then()) : null 34 | } 35 | } 36 | return Vnode('[', null, null, args) 37 | } 38 | -------------------------------------------------------------------------------- /link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to easily link to an identity key. Keys are always converted to 3 | * properties, so `null`/`undefined` are equivalent to `"null"` and 4 | * `"undefined"` respectively. 5 | * 6 | * This is just a preview of something I'm looking to add in my redesign. 7 | */ 8 | 9 | ;(function () { 10 | "use strict" 11 | 12 | var Vnode 13 | 14 | function link(key, first) { 15 | if (typeof key !== "symbol") key = "" + key 16 | var children = first 17 | if (!Array.isArray(children)) { 18 | children = [] 19 | for (var i = 1; i < arguments.length; i++) { 20 | children.push(arguments[i]) 21 | } 22 | } 23 | return Vnode("[", null, null, [ 24 | Vnode("[", key, null, Vnode.normalizeChildren(children)) 25 | ]) 26 | } 27 | 28 | if (typeof module === "object" && module && module.exports) { 29 | Vnode = require("mithril/render/vnode") 30 | module.exports = link 31 | } else if (typeof m === "function") { 32 | Vnode = m.vnode 33 | (m.helpers || (m.helpers = {})).link = link 34 | } else { 35 | throw new Error("Mithril must be loaded first!") 36 | } 37 | })() 38 | -------------------------------------------------------------------------------- /redraw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this when you want to use `m.redraw` to link from streams or other 3 | * synchronously-updating cells. 4 | * 5 | * Note: call `redraw.ready` in `oncreate` to unlock it. 6 | * 7 | * Also, you may call `redraw.lock` in `onbeforeupdate` and `redraw.ready` in 8 | * `onupdate`, to avoid unnecessary rendering calls. 9 | */ 10 | 11 | ;(function () { 12 | "use strict" 13 | 14 | // Ideally, this would use a hook to know when Mithril starts/finishes 15 | // redrawing. 16 | var p = Promise.resolve() 17 | var mithril 18 | 19 | if (typeof module === "object" && module && module.exports) { 20 | module.exports = makeRedraw 21 | mithril = require("mithril") 22 | } else if (typeof m !== "function") { 23 | throw new Error("Mithril must be loaded first!") 24 | } else { 25 | (m.helpers || (m.helpers = {})).makeRedraw = makeRedraw 26 | mithril = m 27 | } 28 | 29 | function makeRedraw(state) { 30 | var ready = false 31 | var redraw = state != null 32 | ? function () { if (ready) state.redraw() } 33 | : function () { if (ready) p.then(mithril.redraw) } 34 | 35 | redraw.ready = function () { 36 | ready = true 37 | } 38 | 39 | return redraw 40 | } 41 | })() 42 | -------------------------------------------------------------------------------- /censor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to filter out any of Mithril's magic attributes while still keeping 3 | * your interfaces as unrestrictive as possible. 4 | */ 5 | 6 | ;(function () { 7 | "use strict" 8 | 9 | if (typeof module === "object" && module && module.exports) { 10 | module.exports = censor 11 | } else if (typeof m !== "function") { 12 | throw new Error("Mithril must be loaded first!") 13 | } else { 14 | (m.helpers || (m.helpers = {})).censor = censor 15 | } 16 | 17 | // Note: this avoids as much allocation and overhead as possible. 18 | var hasOwn = {}.hasOwnProperty 19 | 20 | return function censor(attrs, extras) { 21 | var exclude = [ 22 | "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", 23 | "onbeforeremove", "onremove", 24 | ] 25 | if (extras != null) exclude.push.apply(exclude, extras) 26 | 27 | for (let i = 0; i < exclude.length; i++) { 28 | if (hasOwn.call(attrs, exclude[i])) { 29 | var result = {} 30 | 31 | for (var key in attrs) { 32 | if (hasOwn.call(attrs, key) && !exclude.includes(key)) { 33 | result[key] = attrs[key] 34 | } 35 | } 36 | 37 | return result 38 | } 39 | } 40 | 41 | return attrs 42 | })() 43 | -------------------------------------------------------------------------------- /store.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this if you want the ease of use you'd get with v0.2's `m.prop()`, but 3 | * you don't want to pull in an entire stream library just to have it. It also 4 | * integrates with `m.redraw`, so it works in a lot more situations 5 | */ 6 | 7 | // So engines don't think to "optimize" the memory layout by making a shared 8 | // closure for both functions. 9 | // 10 | // Note: this uses `onchange` itself as the lock, so it doesn't require an extra 11 | // variable. 12 | function makeObserved(store, onchange) { 13 | return function () { 14 | if (!arguments.length) return store 15 | const old = store 16 | const func = onchange 17 | const value = (store = arguments[0]) 18 | 19 | if (func) { 20 | onchange = null 21 | try { 22 | func(value, old) 23 | } finally { 24 | onchange = func 25 | } 26 | } 27 | 28 | return value 29 | } 30 | } 31 | 32 | export default function makeStore(store, onchange) { 33 | if (typeof onchange === "function") { 34 | // Attribute used here for whenever Terser finally implements this 35 | // https://github.com/terser/terser/issues/350 36 | return /*@__NOINLINE__*/ makeObserved(store, onchange) 37 | } else { 38 | return function () { 39 | return arguments.length ? (store = arguments[0]) : store 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/link.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/link 4 | 5 | Exposes an `link(key, ...children)` utility. This is something I've been considering for the next major API redesign of Mithril after Leo's v1.0 redesign, and I wanted to give people the ability to test it and hopefully get some early feedback on the API front. 6 | 7 | In v2, you'd ordinarily do something like this to programmatically reset elements: 8 | 9 | ```js 10 | function ResettableToggle() { 11 | let toggleKey = false 12 | const reset = () => { toggleKey = !toggleKey } 13 | 14 | return { 15 | view: () => [ 16 | m("button", {onclick: reset}, "Reset toggle"), 17 | [m(Toggle, {key: toggleKey})] 18 | ] 19 | } 20 | } 21 | ``` 22 | 23 | With this utility, it'd look closer to this, slightly more conise and potentially a lot more intuitive: 24 | 25 | ```js 26 | function ResettableToggle() { 27 | let toggleKey = false 28 | const reset = () => { toggleKey = !toggleKey } 29 | 30 | return { 31 | view: () => [ 32 | m("button", {onclick: reset}, "Reset toggle"), 33 | link(toggleKey, m(Toggle)), 34 | ] 35 | } 36 | } 37 | ``` 38 | 39 | ## Usage 40 | 41 | - `m.helpers.link(key, ...children) -> vnode` 42 | 43 | - `key` is the key to use - it must be a property key. 44 | - `...children` are a list of children to include. It works the same way `m.fragment` and `m` accept their children - if you pass an array as the sole argument, it's read literally and not copied. 45 | - Returns a single element keyed fragment vnode. 46 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | [*Up*](./README.md) 2 | 3 | # API 4 | 5 | Everything is exposed as submodules, where you can pull them all in piecemeal. They are available as both CommonJS modules (exported by name) and global scripts that attach themselves individually to `m.helpers`, where `m` is Mithril's main export. In addition, `/index.js` is a CommonJS-only module bundling all of them except for what's in `/migrate`. 6 | 7 | Alternatively, if you're using ES modules, every module apart from what's in `/migrate` is also available as an ES module with the extension `.mjs`, with `/index.mjs` exposing named exports and the rest as default exports. Do note that some of them require Mithril, and so you will need something like `rollup-plugin-commonjs` + `rollup-plugin-node-resolve` or Webpack's builtin resolver to resolve Mithril. 8 | 9 | Every module except those in `/migrate` also feature complete TypeScript definitions. That way, if you prefer TypeScript, you're not left out, either. 10 | 11 | If you wish to bundle these, you can just concatenate the ones you use or use Browserify/etc. to bundle them along with your app. 12 | 13 | - [`mithril-helpers/censor` - `censor(attrs)`](./censor.md) 14 | - [`mithril-helpers/store` - `makeStore(initial?, onchange?)`](./store.md) 15 | - [`mithril-helpers/self-sufficient` - `m(SelfSufficient, {root, view})`](./self-sufficient.md) 16 | - [`mithril-helpers/redraw` - `makeRedraw(state?)`](./redraw.md) 17 | - [`mithril-helpers/each` - `each(list, by, view)`](./each.md) 18 | - [`mithril-helpers/link` - `link(key, ...children)`](./link.md) 19 | - [`mithril-helpers/match` - `when(cond, then, orElse)`, `cond(...cases)` and `match(value, ...cases)`](./match.md) 20 | - [`mithril-helpers/migrate` - Mithril migration utilities](./migrate.md) 21 | -------------------------------------------------------------------------------- /match.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to match based on conditions more easily. 3 | */ 4 | 5 | ;(function () { 6 | "use strict" 7 | 8 | var Vnode, helpers 9 | 10 | if (typeof module === "object" && module && module.exports) { 11 | Vnode = require("mithril/render/vnode") 12 | helpers = module.exports 13 | } else if (typeof m === "function") { 14 | Vnode = m.vnode 15 | helpers = m.helpers || (m.helpers = {}) 16 | } else { 17 | throw new Error("Mithril must be loaded first!") 18 | } 19 | 20 | function resolve(then) { 21 | return Vnode('[', null, null, [Vnode.normalize(then)]) 22 | } 23 | 24 | helpers.when = function (cond, then, orElse) { 25 | cond = !!cond 26 | return Vnode("[", null, null, [ 27 | Vnode("[", cond, null, [Vnode.normalize(cond ? then() : orElse())]) 28 | ]) 29 | } 30 | 31 | helpers.cond = function () { 32 | var children = [] 33 | for (var i = 0; i < arguments.length; i++) { 34 | var arg = arguments[i] 35 | children.push(arg.if ? resolve(arg.then()) : null) 36 | } 37 | return Vnode('[', null, null, children) 38 | } 39 | 40 | helpers.match = function (value) { 41 | var children = [] 42 | if (value === value) { 43 | for (var i = 1; i < arguments.length; i++) { 44 | var arg = arguments[i] 45 | children.push(arg.if === value ? resolve(arg.then()) : null) 46 | } 47 | } else { 48 | for (var i = 1; i < arguments.length; i++) { 49 | var arg = arguments[i] 50 | var cond = arg.if 51 | children.push(cond !== cond ? resolve(arg.then()) : null) 52 | } 53 | } 54 | return Vnode('[', null, null, children) 55 | } 56 | })() 57 | -------------------------------------------------------------------------------- /docs/store.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/store 4 | 5 | Exposes `store = makeStore(initial?, onchange?)`, which is very much like a spiritial successor to Mithril v0.2's `m.prop()`, just without all the magic promise wrapping stuff, and with a more useful API. 6 | 7 | - When you call `store()`, you get the value. 8 | - When you call `store(newValue)`, it invokes `onchange(newValue, oldValue)` if it exists, for easy observation. 9 | 10 | I also threw in a couple niceities to make it even better. 11 | 12 | 1. I optimized the case of no `onchange`, so it is faster and takes less memory. 13 | 2. Changing the value from an `onchange` doesn't call the function recursively. 14 | 15 | ```js 16 | // Simple example 17 | function TextBox() { 18 | var title = m.helpers.makeStore("") 19 | 20 | return { 21 | view: ({attrs}) => m("input", { 22 | value: title(), 23 | onchange: e => title(e.target.value), 24 | onkeydown: function (e) { 25 | if (e.keyCode === 13) { 26 | attrs.onsubmit(title()) 27 | return false 28 | } else if (e.keyCode === 27) { 29 | title("") 30 | attrs.oncancel() 31 | return false 32 | } 33 | }, 34 | }), 35 | } 36 | } 37 | ``` 38 | 39 | ## Usage 40 | 41 | - `m.helpers.makeStore(initial?, onchange?) -> store` 42 | 43 | - Accepts an optional initial value, defaulting to `undefined`. 44 | - Accepts an optional `(old, new) -> any` change listener, defaulting to a no-op. 45 | - Returns a new store. 46 | 47 | - `store() -> value` 48 | 49 | - Returns the currently stored value. 50 | 51 | - `store(newValue) -> newValue` 52 | 53 | - Accepts a new value to set the store to. 54 | - Returns the newly stored value. 55 | - Calls `onchange` if not set recursively within it. 56 | -------------------------------------------------------------------------------- /store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this if you want the ease of use you'd get with v0.2's `m.prop()`, but 3 | * you don't want to pull in an entire stream library just to have it. 4 | * 5 | * It's also safe against recursive modifications when observing - they instead 6 | * don't notify like they would normally, avoiding a quick stack overflow. 7 | */ 8 | 9 | ;(function () { 10 | "use strict" 11 | 12 | if (typeof exports === "object" && exports) { 13 | module.exports = makeStore 14 | } else if (typeof m !== "function") { 15 | throw new Error("Mithril must be loaded first!") 16 | } else { 17 | (m.helpers || (m.helpers = {})).makeStore = makeStore 18 | } 19 | 20 | // So engines don't think to "optimize" the memory layout by making a shared 21 | // closure for both objects. 22 | // 23 | // Note: this uses `onchange` itself as the lock, so it doesn't require an 24 | // extra variable. 25 | function makeObserved(store, onchange) { 26 | return function () { 27 | if (!arguments.length) return store 28 | var old = store 29 | var func = onchange 30 | var value = (store = arguments[0]) 31 | 32 | if (func) { 33 | onchange = null 34 | try { 35 | func(value, old) 36 | } finally { 37 | onchange = func 38 | } 39 | } 40 | 41 | return value 42 | } 43 | } 44 | 45 | function makeStore(store, onchange) { 46 | if (typeof onchange === "function") { 47 | // Attribute used here for whenever Terser finally implements this 48 | // https://github.com/terser/terser/issues/350 49 | return /*@__NOINLINE__*/ makeObserved(store, onchange) 50 | } else { 51 | return function () { 52 | return arguments.length ? (store = arguments[0]) : store 53 | } 54 | } 55 | } 56 | })() 57 | -------------------------------------------------------------------------------- /docs/each.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/each 4 | 5 | Exposes an `each(list, (item, i) => key, (item, i) => vnode)` utility. This is something I've been considering for the next major API redesign of Mithril after Leo's v1.0 redesign, and I wanted to give people the ability to test it and hopefully get some early feedback on the API front. 6 | 7 | In v2, you'd ordinarily do something like this to return keyed fragments: 8 | 9 | ```js 10 | function makeUserInputs(users) { 11 | return users.map(u => m("div.user-info", {key: u.id}, 12 | m("label.user-name", "Name: ", m("input[type=text]", u.name)), 13 | m("label.user-bio", "Bio: ", m("textarea", u.bio)), 14 | m("label.user-loc", "Location: ", m("input[type=text]", u.location)) 15 | )) 16 | } 17 | ``` 18 | 19 | With this utility, it'd look closer to this, and is sometimes a little more concise and less noisy: 20 | 21 | ```js 22 | function makeUserInputs(users) { 23 | return each(users, "id", u => m("div.user-info", 24 | m("label.user-name", "Name: ", m("input[type=text]", u.name)), 25 | m("label.user-bio", "Bio: ", m("textarea", u.bio)), 26 | m("label.user-loc", "Location: ", m("input[type=text]", u.location)) 27 | )) 28 | } 29 | 30 | // Or if you wanted to be more explicit about it - this is about the same length 31 | function makeUserInputs(users) { 32 | return each(users, u => u.id, u => m("div.user-info", 33 | m("label.user-name", "Name: ", m("input[type=text]", u.name)), 34 | m("label.user-bio", "Bio: ", m("textarea", u.bio)), 35 | m("label.user-loc", "Location: ", m("input[type=text]", u.location)) 36 | )) 37 | } 38 | ``` 39 | 40 | It also separates the key from the child, and separating keys from attributes may make it a bit easier to read. 41 | 42 | ## Usage 43 | 44 | - `m.helpers.each(list, by, view) -> vnode` 45 | 46 | - `list` is the list of items to iterate. 47 | - `by` is either an `(item, index) => key` function or a property key to read the key from. In either case, it's coerced to a property key. 48 | - `view` is an `(item, index) => child` function where `child` is a vnode. 49 | - Returns a keyed fragment vnode. 50 | -------------------------------------------------------------------------------- /each.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to create keyed fragments more safely. Keys are always converted to 3 | * properties, so `null`/`undefined` *are* valid keys and are equivalent to 4 | * `"null"` and `"undefined"` respectively. Note that keys still must be unique 5 | * when coerced to property keys. 6 | * 7 | * This is just a preview of something I'm looking to add in my redesign. 8 | */ 9 | 10 | import Vnode from "mithril/render/vnode" 11 | 12 | // This is coded specifically for efficiency, so it's necessarily a bit messy. 13 | // The `Array.from` calls are also written to be easily inlined by JIT engines. 14 | // 15 | // Here's it simplified, with roughly equivalent semantics: 16 | // 17 | // ```js 18 | // export default function each(list, by, view) { 19 | // // So it doesn't get coerced in the loop 20 | // if (typeof by !== "function" && typeof by !== "symbol") by = "" + by 21 | // const found = Object.create(null) 22 | // return m.fragment(Array.from(list, (item, i) => { 23 | // let key = typeof by === "function" ? by(item, i) : item[by] 24 | // if (typeof key !== "symbol") key = "" + key 25 | // if (found[key]) throw new Error("Duplicate keys are not allowed.") 26 | // found[key] = true 27 | // return m.fragment({key}, view(item, i)) 28 | // })) 29 | // } 30 | // ``` 31 | 32 | function cast(view, found, item, i, key) { 33 | if (typeof key !== "symbol") key = "" + key 34 | if (found.has(key)) throw new Error("Duplicate keys are not allowed.") 35 | found.add(key) 36 | return Vnode("[", key, null, [Vnode.normalize(view(item, i))], null, null) 37 | } 38 | 39 | export default function each(list, by, view) { 40 | const found = new Set() 41 | let children 42 | if (typeof by === "function") { 43 | children = Array.from(list, (item, i) => 44 | cast(view, found, item, i, by(item, i)) 45 | ) 46 | } else { 47 | // So it doesn't get coerced in the loop 48 | if (typeof by !== "symbol") by = "" + by 49 | children = Array.from(list, (item, i) => 50 | cast(view, found, item, i, item[by]) 51 | ) 52 | } 53 | 54 | return Vnode("[", null, null, children, null, null) 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mithril-helpers 2 | 3 | Just a collection of Mithril helpers. Each helper features two variants: `.js` for a CommonJS + global bundle targeting a baseline of ES5.1 and `.mjs` for an ES module targeting a baseline of ES6, and they all feature TypeScript definitions alongside them. The `index.js` file specifically targets CommonJS only. No direct support for AMD/RequireJS loaders exist. 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install --save isiahmeadows/mithril-helpers --save-exact 9 | yarn add git+https://github.com/isiahmeadows/mithril-helpers --exact 10 | ``` 11 | 12 | Note: you *must* depend on exact Git hashes, since this package is completely unversioned, and could change in breaking ways at any point. (For similar reasons, the package version will remain at `0.0.0`.) When upgrading, you should assess the diff for each file you use, to see if they require any special migration or not. 13 | 14 | ## API 15 | 16 | See [here](https://github.com/isiahmeadows/mithril-helpers/tree/master/docs/api.md). 17 | 18 | ## Issues/Contributing 19 | 20 | If you come up with any other things that you feel should be included, or you find a bug in something I did, please file an issue, and I'll consider the use case. In particular, I could use some assistance with ensuring the stuff in `/migrate` actually works, so if you find bugs in it, please tell me right away, and I'll address it. 21 | 22 | If you want to implement something yourself, definitely go for it and send a PR. Just note the following (for legal reasons): when you submit a pull request, you agree to license your contribution under the relevant license(s) in this repository, and you also agree that you have sufficient rights to do so. 23 | 24 | Also, make sure that if you want to add a helper, your helpers should do one thing and one thing only. They should be almost zero-cost to add to a project, and ideally they shouldn't even depend on other helpers. 25 | 26 | ## License 27 | 28 | All the source code here is licensed under Blue Oak Model License 1.0.0 except for the `/migrate` modules, whose licenses are specified in those files (at the time of writing, it's a single file licensed MIT). Feel free to use it however you wish (within these restrictions, of course). The documentation is licensed under CC-BY 4.0 unless otherwise specified. 29 | 30 | See [here](https://github.com/isiahmeadows/mithril-helpers/tree/master/LICENSE.txt) for precise details on the licenses for each as well as their legal texts. 31 | -------------------------------------------------------------------------------- /self-sufficient.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to isolate your component from Mithil's redraw system, so those who 3 | * use `m.render` directly can still use your component. It's also useful in its 4 | * own right if you're using `m.render` directly, since you can use this to 5 | * batch your redraws in child components, and it's also useful for implementing 6 | * subtree redraws. 7 | */ 8 | 9 | import render from "mithril/render" 10 | import Vnode from "mithril/render/Vnode" 11 | 12 | let frameId, insts 13 | 14 | function invokeRedraw() { 15 | const scheduled = insts 16 | 17 | insts = frameId = null 18 | 19 | scheduled.forEach(inst => { 20 | // We need at least some fault tolerance - it'd be weird if someone 21 | // else's errors prevented one of our redraws. 22 | try { 23 | const inst = prev[i] 24 | inst.l = true 25 | render( 26 | inst.s.dom, 27 | [(0, inst.s.attrs.view)(inst)], 28 | inst.r 29 | ) 30 | } catch (e) { 31 | setTimeout(() => { throw e }, 0) 32 | } finally { 33 | inst.l = false 34 | } 35 | }) 36 | } 37 | 38 | function unschedule(inst) { 39 | if (insts == null) return 40 | if (insts.size === 1) { 41 | // Skip the mutation 42 | if (!insts.has(inst)) return 43 | cancelAnimationFrame(frameId) 44 | insts = frameId = null 45 | } else { 46 | insts.delete(inst) 47 | } 48 | } 49 | 50 | export default class SelfSufficient { 51 | constructor(vnode) { 52 | this.s = vnode 53 | this.r = () => { this.redraw() } 54 | this.l = null 55 | } 56 | 57 | view(vnode) { 58 | this.s = vnode 59 | return vnode.attrs.root 60 | } 61 | 62 | oncreate(vnode) { 63 | this.onupdate(vnode) 64 | } 65 | 66 | onupdate(vnode) { 67 | this.s = vnode 68 | if (this.l) throw new Error("State is currently locked!") 69 | unschedule(this) 70 | this.l = true 71 | try { 72 | render(vnode.dom, [(0, vnode.attrs.view)(this)], this.r) 73 | } finally { 74 | this.l = false 75 | } 76 | } 77 | 78 | onremove(vnode) { 79 | this.s = this.r = null 80 | this.l = true 81 | unschedule(this) 82 | render(vnode.dom) 83 | } 84 | 85 | // Public API 86 | safe() { 87 | return !this.l 88 | } 89 | 90 | redraw() { 91 | if (!this.l) { 92 | if (insts == null) { 93 | insts = new Set([this]) 94 | frameId = requestAnimationFrame(invokeRedraw) 95 | } else { 96 | insts.add(this) 97 | } 98 | } 99 | } 100 | 101 | redrawSync() { 102 | if (this.s == null) throw new TypeError("Can't redraw after unmount.") 103 | if (this.l == null) throw new TypeError("Can't redraw without a root.") 104 | this.onupdate(this.s) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /docs/match.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/match 4 | 5 | Exposes a `when(cond, then, orElse)` utility, a `cond(...cases)` utility, and a `match(value, ...cases)` utility. 6 | 7 | Ordinarily, you'd do things like this: 8 | 9 | ```js 10 | return showViewOne 11 | ? m(".view-one", ...) 12 | : m(".view-two", ...) 13 | ``` 14 | 15 | Thing is, that doesn't actually reset the lifecycle. The `doSomeMagicalThingTwo` doesn't get called if you flip that boolean, and that's bound to result in unwanted behavior if you treat it naïvely. 16 | 17 | ```js 18 | return showViewOne 19 | ? m(".view-one", {oncreate() { doSomeMagicalThingOne() }}, ...) 20 | : m(".view-two", {oncreate() { doSomeMagicalThingTwo() }}, ...) 21 | ``` 22 | 23 | This lets you instead do this, with everything automagically tracked and lifecycle hooks precisely as you'd expect: 24 | 25 | ```js 26 | return when(showViewOne, 27 | () => m(".view-one", {oncreate() { doSomeMagicalThingOne() }}, ...)}, 28 | () => m(".view-two", {oncreate() { doSomeMagicalThingTwo() }}, ...)}, 29 | ) 30 | ``` 31 | 32 | If you wanted to add more in a chain, you could use `cond`: 33 | 34 | ```js 35 | return cond( 36 | {if: view === "one", then: () => m(".view-one", {oncreate() { doSomeMagicalThingOne() }}, ...)}, 37 | {if: view === "two", then: () => m(".view-two", {oncreate() { doSomeMagicalThingTwo() }}, ...)}, 38 | // ... 39 | ) 40 | ``` 41 | 42 | This common case can itself be simplified with `match`. 43 | 44 | ```js 45 | return match(view, 46 | {if: "one", then: () => m(".view-one", {oncreate() { doSomeMagicalThingOne() }}, ...)}, 47 | {if: "two", then: () => m(".view-two", {oncreate() { doSomeMagicalThingTwo() }}, ...)}, 48 | ) 49 | ``` 50 | 51 | ## Usage 52 | 53 | - `m.helpers.when(cond, then, orElse)` 54 | 55 | - `cond` is the condition to test. 56 | - `then` returns the result you want to render if `cond` is truthy. 57 | - `orElse` returns the result you want to render if `cond` is falsy. 58 | - Returns a fragment vnode that ensures that when the condition changes, the tree is torn down and rebuilt between cases. 59 | 60 | - `m.helpers.cond(case1, case2, ...)` 61 | 62 | - `case1.if`, `case2.if`, and so on are the conditions to test. 63 | - `case1.then`, `case2.then`, and so on are the views to render, rendered if their corresponding `if` is truthy. 64 | - Returns a fragment vnode that when rendered, renders as if each rendered case was keyed by argument position. (For optimization reasons, this uses an unkeyed fragment internally, but this is an implementation detail.) 65 | 66 | - `m.helpers.match(value, case1, case2, ...)` 67 | 68 | - `value` is the value to compare against, via the ES SameValueZero algorithm (basically `===`, but with `NaN`s considered equal) 69 | - `case1.if`, `case2.if`, and so on are the conditions to compare against `value`. 70 | - `case1.then`, `case2.then`, and so on are the views to render, rendered if their corresponding `if` is truthy. 71 | - Returns a fragment vnode that when rendered, renders as if each rendered case was keyed by argument position. (For optimization reasons, this uses an unkeyed fragment internally, but this is an implementation detail.) 72 | -------------------------------------------------------------------------------- /each.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to create keyed fragments more safely. Keys are always converted to 3 | * properties, so `null`/`undefined` *are* valid keys and are equivalent to 4 | * `"null"` and `"undefined"` respectively. Note that keys still must be unique 5 | * when coerced to property keys. 6 | * 7 | * This is just a preview of something I'm looking to add in my redesign. 8 | */ 9 | 10 | ;(function () { 11 | "use strict" 12 | 13 | var Vnode 14 | var symbolIterator = typeof Symbol === "function" 15 | ? Symbol.iterator 16 | : null 17 | var from = Array.from || function (list, func) { 18 | var result = [] 19 | if ( 20 | symbolIterator != null && 21 | typeof list[symbolIterator] === "function" 22 | ) { 23 | var iter = list[symbolIterator]() 24 | var i = 0 25 | for (var next = iter.next(); !next.done; next = iter.next()) { 26 | result.push(func(next.value, i)) 27 | i++ 28 | } 29 | } else { 30 | for (var i = 0; i < list.length; i++) { 31 | result.push(func(list[i], i)) 32 | } 33 | } 34 | return result 35 | } 36 | 37 | // This is coded specifically for efficiency, so it's necessarily a bit 38 | // messy. The `Array.from` calls are also written to be easily inlined by 39 | // JIT engines. 40 | // 41 | // Here's it simplified, with roughly equivalent semantics: 42 | // 43 | // ```js 44 | // function each(list, by, view) { 45 | // // So it doesn't get coerced in the loop 46 | // if (typeof by !== "function" && typeof by !== "symbol") by = "" + by 47 | // var found = Object.create(null) 48 | // return m.fragment(from(list, function (item, i) { 49 | // var key = typeof by === "function" ? by(item, i) : item[by] 50 | // if (typeof key !== "symbol") key = "" + key 51 | // if (found[key]) throw new Error("Duplicate keys are not allowed.") 52 | // found[key] = true 53 | // return m.fragment({key: key}, view(item, i)) 54 | // })) 55 | // } 56 | // ``` 57 | 58 | function cast(view, found, item, i, key) { 59 | if (typeof key !== "symbol") key = "" + key 60 | if (found[key]) throw new Error("Duplicate keys are not allowed.") 61 | found[key] = true 62 | return Vnode("[", key, null, [Vnode.normalize(view(item, i))], null, null) 63 | } 64 | 65 | function each(list, by, view) { 66 | var found = Object.create(null) 67 | var children 68 | if (typeof by === "function") { 69 | children = from(list, function (item, i) { 70 | return cast(view, found, item, i, by(item, i)) 71 | }) 72 | } else { 73 | // So it doesn't get coerced in the loop 74 | if (typeof by !== "symbol") by = "" + by 75 | children = from(list, function (item, i) { 76 | return cast(view, found, item, i, item[by]) 77 | }) 78 | } 79 | 80 | return Vnode("[", null, null, children, null, null) 81 | } 82 | 83 | if (typeof module === "object" && module && module.exports) { 84 | Vnode = require("mithril/render/vnode") 85 | module.exports = each 86 | } else if (typeof m === "function") { 87 | Vnode = m.vnode 88 | (m.helpers || (m.helpers = {})).each = each 89 | } else { 90 | throw new Error("Mithril must be loaded first!") 91 | } 92 | })() 93 | -------------------------------------------------------------------------------- /docs/self-sufficient.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/self-sufficient 4 | 5 | Exposes a `SelfSufficient` component, for making self-sufficient (i.e. no dependency on `m.redraw`) object/closure and class components, respectively. It's also useful if you're using `m.render` directly, since you don't have to worry about implementing batching and all the other annoying crap. 6 | 7 | - `m(m.helpers.SelfSufficient, {root: vnode, view: (state) -> children})` - Create a new instance to do subtree management. 8 | - `root` is a static DOM vnode. 9 | - `view` is what you want your subtree to look like. It's a function called on each redraw. 10 | - Lifecycle methods work as you would expect, with the caveat that `onbeforeremove` can't block removal. 11 | - `state.safe()` - Whether it's safe to invoke `redrawSync`. 12 | - `state.redraw()` - Schedule a redraw for this subtree. 13 | - `state.redrawSync()` - Force a synchronous redraw for this vnode. 14 | 15 | Here's a few examples of how it's used: 16 | 17 | ```js 18 | // Objects 19 | const Comp = { 20 | oninit() { this.clicked = false }, 21 | view(vnode) { 22 | return m(m.helpers.SelfSufficient, {root: m("div"), view: state => [ 23 | m(".foo", "What is this?"), 24 | this.clicked 25 | ? m(".bar.fail", "Why did you click me?!?") 26 | : m(".bar", { 27 | onclick: () => { this.clicked = true }, 28 | }, "Just kidding, nothing to see here..."), 29 | ]}) 30 | } 31 | } 32 | 33 | // Closures 34 | function Comp() { 35 | var clicked = false 36 | 37 | return { 38 | view(vnode) { 39 | return m(m.helpers.SelfSufficient, {root: m("div"), view: state => [ 40 | m(".foo", "What is this?"), 41 | this.clicked 42 | ? m(".bar.fail", "Why did you click me?!?") 43 | : m(".bar", { 44 | onclick: state.link(() => { this.clicked = true }), 45 | }, "Just kidding, nothing to see here..."), 46 | ]}) 47 | } 48 | }) 49 | } 50 | 51 | // Classes 52 | class Comp { 53 | constructor() { 54 | this.clicked = false 55 | } 56 | 57 | view(vnode) { 58 | return m(m.helpers.SelfSufficient, {root: m("div"), view: state => [ 59 | m(".foo", "What is this?"), 60 | this.clicked 61 | ? m(".bar.fail", "Why did you click me?!?") 62 | : m(".bar", { 63 | onclick: state.link(() => { this.clicked = true }), 64 | }, "Just kidding, nothing to see here..."), 65 | ]}) 66 | } 67 | } 68 | ``` 69 | 70 | ## Usage 71 | 72 | - `m(m.helpers.SelfSufficient, {root, view})` 73 | 74 | - `root` is the vnode whose dom is to be bound to. 75 | - `view` is a `(state) -> vnode` function used to generate the tree. 76 | - Note: this does *not* register a global redraw handler, but instead handles the batching logic itself and performs its own redraws independently (with a custom redraw handler for events, too). 77 | 78 | - `state.safe() -> boolean` 79 | 80 | - Returns `true` if you are able to redraw synchronously (i.e. no other redraw is occurring and the component is live), `false` otherwise. 81 | 82 | - `state.redraw() -> undefined` 83 | 84 | - Schedules an async redraw, batching the call if necessary. 85 | 86 | - `state.redrawSync() -> undefined` 87 | 88 | - Performs an immediate redraw, cancelling any scheduled async redraw if necessary. 89 | - Throws if any redraw is occurring known to the helper. (This prevents accidental sync redraw loops.) 90 | -------------------------------------------------------------------------------- /self-sufficient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this to isolate your component from Mithil's redraw system, so those who 3 | * use `m.render` directly can still use your component. It's also useful in its 4 | * own right if you're using `m.render` directly, since you can use this to 5 | * batch your redraws in child components, and it's also useful for implementing 6 | * subtree redraws. 7 | */ 8 | 9 | ;(function (factory) { 10 | "use strict" 11 | 12 | if (typeof module === "object" && module && module.exports) { 13 | module.exports = factory( 14 | require("mithril/render"), 15 | require("mithril/render/vnode") 16 | ) 17 | } else if (typeof m === "function") { 18 | (m.helpers || (m.helpers = {})).SelfSufficient = factory( 19 | m.render, m.vnode 20 | ) 21 | } else { 22 | throw new Error("Mithril must be loaded first!") 23 | } 24 | })(function (render, Vnode) { 25 | "use strict" 26 | 27 | var frameId, insts 28 | 29 | function invokeRedraw() { 30 | var scheduled = insts 31 | 32 | insts = frameId = null 33 | 34 | for (var i = 0; i < scheduled.length; i++) { 35 | // We need at least some fault tolerance - it'd be weird if someone 36 | // else's errors prevented one of our redraws. 37 | var inst = scheduled[i] 38 | inst.l = true 39 | try { 40 | render( 41 | inst.s.dom, 42 | [(0, inst.s.attrs.view)(inst)], 43 | inst.r 44 | ) 45 | } catch (e) { 46 | setTimeout(function () { throw e }, 0) 47 | } finally { 48 | inst.l = false 49 | } 50 | } 51 | } 52 | 53 | function unschedule(inst) { 54 | if (insts == null) return 55 | if (insts.length === 1) { 56 | if (insts[0] !== inst) return 57 | cancelAnimationFrame(frameId) 58 | insts = frameId = null 59 | } else { 60 | var index = insts.indexOf(inst) 61 | if (index >= 0) insts.splice(index, 0) 62 | } 63 | } 64 | 65 | function SelfSufficient(vnode) { 66 | var self = this 67 | this.s = vnode 68 | this.r = function () { self.redraw() } 69 | this.l = null 70 | } 71 | 72 | SelfSufficient.prototype.view = function (vnode) { 73 | this.s = vnode 74 | return vnode.attrs.root 75 | } 76 | 77 | SelfSufficient.prototype.oncreate = 78 | SelfSufficient.prototype.onupdate = function (vnode) { 79 | if (this.l) throw new Error("State is currently locked!") 80 | unschedule(this) 81 | this.l = true 82 | try { 83 | render(vnode.dom, [(0, vnode.attrs.view)(this)], this.r) 84 | } finally { 85 | this.l = false 86 | } 87 | } 88 | 89 | SelfSufficient.prototype.onremove = function (vnode) { 90 | this.s = this.r = null 91 | this.l = true 92 | unschedule(this) 93 | render(vnode.dom) 94 | } 95 | 96 | // Public API 97 | SelfSufficient.prototype.safe = function () { 98 | return !this.l 99 | } 100 | 101 | SelfSufficient.prototype.redraw = function () { 102 | if (!this.l) { 103 | if (insts == null) { 104 | insts = [this] 105 | frameId = requestAnimationFrame(invokeRedraw) 106 | } else { 107 | var end = insts.length - 1 108 | // Already at the end, nothing left to do. 109 | if (insts[end] === this) return 110 | var index = insts.indexOf(this) 111 | 112 | if (index >= 0) { 113 | // In case this winds up spammy, I can't take the naïve approach. 114 | while (index < end) { 115 | insts[index] = insts[index + 1] 116 | index++ 117 | } 118 | insts[end] = this 119 | } else { 120 | insts.push(this) 121 | } 122 | } 123 | } 124 | } 125 | 126 | SelfSufficient.prototype.redrawSync = function () { 127 | if (this.s == null) throw new TypeError("Can't redraw after unmount.") 128 | if (this.l == null) throw new TypeError("Can't redraw without a root.") 129 | this.onupdate(this.s) 130 | } 131 | 132 | return SelfSufficient 133 | }) 134 | -------------------------------------------------------------------------------- /docs/migrate.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # Migration with mithril-helpers/migrate 4 | 5 | This includes an array of utilities, shims, etc., for migrating from older to newer Mithril versions. Currently, there is only one submodule, but there may be more in the future. 6 | 7 | - [`mithril-helpers/migrate/v1` - Mithril v0.2 → v1 migration](#mithril-helpersmigratev1) 8 | 9 | ## mithril-helpers/migrate/v1 10 | 11 | An array of utilities and shims for migrating from v0.2 to v1. It shims most of the important functionality in v0.2 using v1 APIs. 12 | 13 | The shim is exposed as `v1`, but throughout this listing, we use `m` to refer to the export for clarity. 14 | 15 | - `m(".selector", ...)` is mostly shimmed using v1's `m()`, including `config`. 16 | - `context.retain` in `config` is ignored, since Mithril v1 has no equivalent. 17 | - `context.onunload` is implemented, but its `e.preventDefault()` is not. 18 | - `config` is shimmed using v1's lifecycle methods, so you can't migrate a single `config` incrementally. 19 | - `config`'s final `vdom` parameter is a v1 node, not a v0.2 one. 20 | - v1's magic methods are available for DOM vnodes *only*, not for component vnodes. 21 | - The `xlink` namespace is *not* shimmed for you - you must add it now. 22 | - Note: raw components are *not* allowed. 23 | - Note: inner fragments are compiled to v1 fragments, not ignored. 24 | - `m(Component, ...)` and `m.component(Component, ...)` are shimmed using v1's `m`, including the arbitrary-argument API. 25 | - All the v1 lifecycle methods may *also* be used. 26 | - Node instances are also components as they previously were. 27 | - The state is shimmed as `this = vnode.state = {ctrl}` 28 | - `{subtree: "retain"}` is *not* shimmed. 29 | - Instead, you should implement `onbeforeupdate`, change your `view` to call that first (and return `{subtree: "retain"}` if it returns falsy), and then after migrating, remove the indirection. 30 | - `m.trust(text)` returns a String object (not primitive) as it did in v0.2, but it also has the relevant v1 vnode properties copied into it. 31 | - `m.prop(value?)` is fully shimmed with original semantics. 32 | - It is actually properly shimmed; it doesn't use v1's streams. 33 | - `m.withAttr(attr, callback, context?)` just uses v1's equivalent implementation 34 | - `m.startComputation()`, `m.endComputation()`, and `m.redraw.mode()` are not shimmed, as redraws are controlled per-component using lifecycle methods. 35 | - `m.deferred()` is shimmed fully except for two caveats due to its internal use of ES6 promises: 36 | - `.then` is invoked *asynchronously*, unlike in v0.2. 37 | - `Error` subclasses, when caught, are *not* rethrown out of band like in v0.2. 38 | - `m.sync(items)` just uses `Promise.all` and wraps the result into an `m.prop()`. 39 | - `m.request(options)` delegates to v1's `m.request(options)` and `m.jsonp(options)` as appropriate, but the result is properly propified with `options.initialValue` supported. 40 | - For `options.unwrap{Success,Error}`, just use `extract`/`deserialize` appropriately instead. 41 | - `m.mount(elem, Comp)` and `m.route(elem, defaultRoute, routes)` are mostly shimmed. 42 | - Support for raw vnode roots is *not* shimmed. 43 | - `m.redraw.strategy` is *not* shimmed. 44 | - The rest of the `m.route` API is fully shimmed: 45 | - `m.route.mode` calls v1's `m.route.prefix(prefix)` on set. 46 | - `m.route()` returns v1's `m.route.get()` 47 | - `config: m.route` delegates to v1's `m.route.link` 48 | - `m.route(route, params?, shouldReplaceHistoryEntry?)` invokes v1's `m.route.set(route, params?, options?)` 49 | - `m.route.param(key?)` returns v1's `m.route.param(key?)` 50 | - `m.deps(window)` is *not* shimmed. There are other ways of testing components, most of which are far better, anyways. It also was exposed primarily for *internal* testing. 51 | 52 | Here are a few critical notes you need to read before using this: 53 | 54 | - Unlike v0.2, you can no longer cache trees in v1; you must create them fresh each time now. 55 | - An easy way to sidestep this is by creating a function that returns a tree, and calling that in the view. 56 | - If you were doing this for memory reasons, it's almost guaranteed to be a premature optimization, and it would've been pretty useless anyways. (Also, the fact selectors are cached now only makes this more so.) 57 | - `m.migrate(Comp)` brands a component for migration, so this knows it should be migrated. 58 | - You *must* call `m.migrate(Comp)` on all v0.2 components to brand them before they can be migrated with this shim. Components created via `m()` and `m.component()` are automatically branded this way, but you have to add them to all custom components. 59 | - Note that you could (and should) temporarily do `m.migrate = function (Comp) { return Comp }` before you switch to this library and rewire your `mithril` imports. 60 | -------------------------------------------------------------------------------- /docs/redraw.md: -------------------------------------------------------------------------------- 1 | [*Up*](./api.md) 2 | 3 | # mithril-helpers/redraw 4 | 5 | A utility to prevent unnecessary redraws and avoid buggy behavior when linking streams/stores to `m.redraw`. 6 | 7 | - `redraw = makeRedraw()` - Make an async redraw wrapper around the global redraw system. 8 | 9 | - `redraw = makeRedraw(state)` - Make an async redraw wrapper around a `SelfSufficient` instance. 10 | 11 | - `redraw()` - Invoke the redraw wrapper. 12 | 13 | - `redraw.ready()` - Open the redraw wrapper, so it may start scheduling redraws. 14 | 15 | Now, you're probably wondering *what* buggy behavior I'm referring to. Consider this example, which would result in a nasty surprise: 16 | 17 | ```js 18 | const Comp = { 19 | oninit({state}) { 20 | // ... 21 | state.foo.map(m.redraw) 22 | }, 23 | 24 | oncreate({dom, state}) { 25 | state.plugin = $(dom).somePlugin() 26 | state.plugin.setFoo("initial") 27 | }, 28 | 29 | onupdate({dom, state}) { 30 | state.plugin.setFoo(state.foo() ? "foo" : "bar") 31 | }, 32 | 33 | view({attrs, state}) { 34 | // ... 35 | } 36 | } 37 | ``` 38 | 39 | If `state.foo` is a stream that already has a value, you've just called `m.redraw` *inside* `oninit`. In v1, this will likely *not* do what you want, instead most likely causing `onupdate` to be called *inside* `oninit`. This would look like a Mithril problem, except it's because when you called `m.redraw`, it just rendered the same tree recursively. Here's what it's really doing: 40 | 41 | - Render `m(Comp)`: 42 | - Call `oninit`: 43 | - Render `m(Comp)`: 44 | - Try to update existing children 45 | - No children found 46 | - Error! 47 | 48 | Moving the `state.foo.map(m.redraw)` to `oncreate` could fix it, but we can't simply move it to the beginning, either: 49 | 50 | ```js 51 | const Comp = { 52 | oninit({state}) { 53 | // ... 54 | }, 55 | 56 | oncreate({dom, state}) { 57 | state.foo.map(m.redraw) 58 | 59 | state.plugin = $(dom).somePlugin() 60 | state.plugin.setFoo("initial") 61 | }, 62 | 63 | onupdate({dom, state}) { 64 | state.plugin.setFoo(state.foo() ? "foo" : "bar") 65 | }, 66 | 67 | view({attrs, state}) { 68 | // ... 69 | } 70 | } 71 | ``` 72 | 73 | Now, it at least looks like it might work, except: 74 | 75 | - Render `m(Comp)`: 76 | - Call `oninit` 77 | - Render children 78 | - Call `oncreate`: 79 | - Render `m(Comp)`: 80 | - Update children 81 | - Call `onupdate` 82 | - `state.plugin` is `undefined`, so you can't call `state.plugin.setFoo` 83 | - Error! 84 | 85 | Moving the `state.foo.map(m.redraw)` to the bottom of `oncreate` could fix it mostly, but it's a very ugly hack, and only works for one stream/property (and it doesn't work with stores from here, either). 86 | 87 | ```js 88 | const Comp = { 89 | oninit({state}) { 90 | // ... 91 | }, 92 | 93 | oncreate({dom, state}) { 94 | state.plugin = $(dom).somePlugin() 95 | state.plugin.setFoo("initial") 96 | 97 | state.foo.map(m.redraw) 98 | }, 99 | 100 | onupdate({dom, state}) { 101 | state.plugin.setFoo(state.foo() ? "foo" : "bar") 102 | }, 103 | 104 | view({attrs, state}) { 105 | // ... 106 | } 107 | } 108 | ``` 109 | 110 | Alternatively, you could use `Promise.resolve` or `setTimeout`, but those are just as ugly and hackish. 111 | 112 | ```js 113 | const Comp = { 114 | oninit({state}) { 115 | // ... 116 | }, 117 | 118 | oncreate({dom, state}) { 119 | state.foo.map(() => { Promise.resolve().then(m.redraw) }) 120 | 121 | state.plugin = $(dom).somePlugin() 122 | state.plugin.setFoo("initial") 123 | }, 124 | 125 | onupdate({dom, state}) { 126 | state.plugin.setFoo(state.foo() ? "foo" : "bar") 127 | }, 128 | 129 | view({attrs, state}) { 130 | // ... 131 | } 132 | } 133 | ``` 134 | 135 | Here's what this utility lets you do: 136 | 137 | ```js 138 | const Comp = { 139 | oninit({state}) { 140 | // ... 141 | state.redraw = m.helpers.makeRedraw() 142 | 143 | // Right where it belongs, and nothing is scheduled until `oncreate` is 144 | // called 145 | state.foo.map(state.redraw) 146 | }, 147 | 148 | oncreate({dom, state}) { 149 | // And now everything is scheduled asynchronously 150 | state.redraw.ready() 151 | 152 | state.plugin = $(dom).somePlugin() 153 | state.plugin.setFoo("initial") 154 | }, 155 | 156 | onupdate({dom, state}) { 157 | state.plugin.setFoo(state.foo() ? "foo" : "bar") 158 | }, 159 | 160 | view({attrs, state}) { 161 | // ... 162 | } 163 | } 164 | ``` 165 | 166 | It also optionally takes a `state` parameter (a `SelfSufficient` instance), in which it will proxy calls to. 167 | 168 | ```js 169 | const Comp = { 170 | __proto__: new m.helpers.SelfSufficient(), 171 | 172 | oninit({state}) { 173 | // ... 174 | }, 175 | 176 | view({attrs}) { 177 | return m(m.helpers.SelfSufficient, { 178 | oninit: ({state}) => { 179 | state.redrawWrap = m.helpers.makeRedraw(state) 180 | 181 | // Right where it belongs, and nothing is scheduled until 182 | // `oncreate` is called 183 | state.foo.map(state.redrawWrap) 184 | }, 185 | 186 | oncreate: ({dom, state}) => { 187 | // And now everything is scheduled asynchronously 188 | state.redrawWrap.ready() 189 | 190 | state.plugin = $(dom).somePlugin() 191 | state.plugin.setFoo("initial") 192 | }, 193 | 194 | onupdate: ({state}) => { 195 | state.plugin.setFoo(this.foo() ? "foo" : "bar") 196 | }, 197 | 198 | view: ({state}) => { 199 | // ... 200 | }, 201 | }) 202 | } 203 | } 204 | ``` 205 | 206 | ## Usage 207 | 208 | - `m.helpers.makeRedraw() -> redraw` 209 | 210 | - Returns a new redraw wrapper around the global redraw system (i.e. `m.redraw`). 211 | 212 | - `m.helpers.makeRedraw(state) -> redraw` 213 | 214 | - Returns a new redraw wrapper around `state`. 215 | - `state` should be a `SelfSufficient` component's `vnode.state`. 216 | 217 | - `redraw() -> void` 218 | 219 | - If ready, schedules an async redraw with the underlying instance (i.e. `SelfSufficient` state or `m.redraw`). 220 | - Returns `undefined`. 221 | 222 | - `redraw.ready() -> void` 223 | 224 | - Opens the redraw wrapper so it can begin scheduling redraws. 225 | - Returns `undefined` 226 | -------------------------------------------------------------------------------- /migrate/v1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A general migration utility to ease v0.2-v1 migration. 3 | * 4 | * This file is licensed under MIT, as it takes a *lot* of code from Mithril 5 | * v0.2 itself. 6 | */ 7 | 8 | ;(function () { 9 | if (typeof Promise !== "function") { 10 | throw new Error("A promise polyfill is required!") 11 | } 12 | 13 | var toString = {}.toString 14 | var hasOwn = {}.hasOwnProperty 15 | var mithril 16 | 17 | if (typeof module === "object" && module && module.exports) { 18 | module.exports = v1 19 | mithril = require("mithril") 20 | } else if (typeof m !== "function") { 21 | throw new Error("Mithril must be loaded first!") 22 | } else { 23 | if (!m.helpers) m.helpers = {} 24 | if (!m.helpers.migrate) m.helpers.migrate = {} 25 | m.helpers.migrate.v1 = v1 26 | mithril = m 27 | } 28 | 29 | var index = 0 30 | var migrated = typeof WeakMap === "function" 31 | ? new WeakMap() 32 | : { 33 | has: function (Comp) { return Comp.$migrating != null }, 34 | get: function (Comp) { return Comp.$migrating }, 35 | set: function (Comp, value) { Comp.$migrating = value } 36 | } 37 | 38 | v1.migrate = function (Comp) { 39 | migrated.set(Comp, index++) 40 | return Comp 41 | } 42 | 43 | // 44 | // # # ##### ##### #### ##### 45 | // ## ## # # # # # # # # 46 | // # ## # # # # # # # # # 47 | // # # ### ##### ##### # # ##### 48 | // # # ### # # # # # # 49 | // # # ### # # # #### # 50 | // 51 | 52 | function propify(promise, initialValue) { 53 | var prop = makeProp(initialValue) 54 | promise.then(prop) 55 | prop.then = function (resolve, reject) { 56 | return propify(promise.then(resolve, reject), initialValue) 57 | } 58 | prop.catch = function (reject) { 59 | return prop.then(null) 60 | } 61 | return prop 62 | } 63 | 64 | function makeClosure(store) { 65 | function prop() { 66 | if (arguments.length) store = arguments[0] 67 | return store 68 | } 69 | 70 | prop.toJSON = function () { 71 | if (store && typeof store.toJSON === "function") { 72 | return store.toJSON() 73 | } 74 | 75 | return store 76 | } 77 | 78 | return prop 79 | } 80 | 81 | v1.prop = makeProp 82 | function makeProp(store) { 83 | if ( 84 | store != null && ( 85 | typeof store === "object" || typeof store === "function" 86 | ) && typeof store.then === "function" 87 | ) { 88 | return propify(store) 89 | } 90 | 91 | return makeClosure(store) 92 | } 93 | 94 | // 95 | // # # #### #### # # ##### #### # # ###### # # ##### 96 | // ## ## # # # # ## ## # # # # ## # # ## # # 97 | // # ## # # # # # ## # # # # # # # # ##### # # # # 98 | // # # ### # # # # # ##### # # # # # # # # # # 99 | // # # ### # # # # # # # # # # ## # # ## # 100 | // # # ### #### #### # # # #### # # ###### # # # 101 | // 102 | 103 | function construct(controller, _, args) { 104 | if (!controller) return {} 105 | var result = Object.create(controller.prototype) 106 | return controller.apply(result, args) || result 107 | } 108 | 109 | var RenderLegacy = { 110 | oninit: function (vnode) { 111 | var component = vnode.attrs.component 112 | this.ctrl = construct(component.controller, vnode.attrs.args) 113 | if (component.oninit) component.oninit(vnode) 114 | }, 115 | 116 | oncreate: function (vnode) { 117 | var func = vnode.attrs.component.oncreate 118 | if (func != null) vnode.attrs.call(this, vnode) 119 | }, 120 | 121 | onbeforeupdate: function (vnode, old) { 122 | var func = vnode.attrs.component.onbeforeupdate 123 | return func != null ? func.call(this, vnode, old) : true 124 | }, 125 | 126 | onupdate: function (vnode) { 127 | var func = vnode.attrs.component.onupdate 128 | if (func != null) func.call(this, vnode) 129 | }, 130 | 131 | onbeforeremove: function (vnode) { 132 | var func = vnode.attrs.component.onbeforeremove 133 | return func != null ? func.call(this, vnode) : undefined 134 | }, 135 | 136 | onremove: function (vnode) { 137 | var func = vnode.attrs.component.onremove 138 | if (func != null) func.call(this, vnode) 139 | }, 140 | 141 | view: function (vnode) { 142 | return vnode.attrs.component.view.apply( 143 | vnode.attrs.component, 144 | [this.ctrl].concat(vnode.attrs.args) 145 | ) 146 | }, 147 | } 148 | 149 | function nonComponentInit() { 150 | throw new Error("v1 nodes aren't components anymore!") 151 | } 152 | 153 | function makeLegacy(component, args, key) { 154 | // Avoid the overhead of going through the factory 155 | return mithril.vnode(RenderLegacy, key, { 156 | component: component, 157 | args: args 158 | }, undefined, undefined, undefined) 159 | } 160 | 161 | v1.component = makeComponent 162 | function makeComponent(component) { 163 | if (!migrating.has(component)) { 164 | var output = mithril.apply(undefined, arguments) 165 | output.controller = output.view = nonComponentInit 166 | return output 167 | } 168 | 169 | // So we can keep components properly diffed 170 | var key = "mithril-helpers/self-sufficient:" + migrating.get(component) 171 | var args = [] 172 | 173 | for (var i = 1; i < arguments.length; i++) { 174 | args[i] = arguments[i] 175 | } 176 | 177 | // Append to the key, so things like `m(Foo, {key: 1})` and 178 | // `m(Bar, {key: 1})` are seen as different. 179 | if (args[0] && args[0].key != null) key += ":" + args[0].key 180 | // Avoid the overhead of going through the factory 181 | var output = makeLegacy(component, args, key) 182 | 183 | output.controller = function () { 184 | return construct(component.controller, args) 185 | } 186 | 187 | if (component.controller) { 188 | output.controller.prototype = component.controller.prototype 189 | } 190 | 191 | output.view = function (ctrl) { 192 | var currentArgs = [ctrl].concat(args) 193 | for (var i = 1; i < arguments.length; i++) { 194 | currentArgs.push(arguments[i]) 195 | } 196 | 197 | return component.view.apply(component, currentArgs) 198 | } 199 | 200 | // Our shim doesn't use this, but users might. 201 | output.view.$original = component.view 202 | 203 | return output 204 | } 205 | 206 | // 207 | // # # 208 | // ## ## 209 | // # ## # 210 | // # # 211 | // # # 212 | // # # 213 | // 214 | 215 | var onunloadEvent = { 216 | preventDefault: function () { 217 | throw new Error("Unmounting may no longer be prevented in v1!") 218 | } 219 | } 220 | 221 | var configAttrs = { 222 | oninit: function () { 223 | this._ = {} 224 | }, 225 | 226 | oncreate: function (vnode) { 227 | vnode.attrs.config.call(vnode, vnode.dom, false, this._) 228 | }, 229 | 230 | onupdate: function (vnode) { 231 | vnode.attrs.config.call(vnode, vnode.dom, true, this._) 232 | }, 233 | 234 | onremove: function () { 235 | if (this._.onunload) this._.onunload(onunloadEvent) 236 | }, 237 | } 238 | 239 | function checkComponent(component) { 240 | if (component.tag && !migrating.has(component)) { 241 | throw new Error("Raw vnodes are no longer valid components in v1!") 242 | } 243 | } 244 | 245 | function v1(tag, pairs) { 246 | if (tag && typeof tag.view === "function") { 247 | return makeComponent.apply(undefined, arguments) 248 | } 249 | 250 | var vnode = mithril.apply(this, arguments) 251 | 252 | if (typeof vnode.attrs.config === "function") { 253 | for (var key in configAttrs) { 254 | if (hasOwn.call(configAttrs, key)) { 255 | vnode.attrs[key] = configAttrs[key] 256 | } 257 | } 258 | } 259 | 260 | if (Array.isArray(vnode.children)) { 261 | for (var i = 0; i < vnode.children.length; i++) { 262 | if (vnode.children[i]) checkComponent(vnode.children[i]) 263 | } 264 | } 265 | 266 | return vnode 267 | } 268 | 269 | // 270 | // # # ##### ##### # # #### ##### 271 | // ## ## # # # # # # # 272 | // # ## # # # # # # #### # 273 | // # # ### # ##### # # # # 274 | // # # ### # # # # # # # # 275 | // # # ### # # # #### #### # 276 | // 277 | 278 | v1.trust = function (text) { 279 | var inst = mithril.trust(text) 280 | var string = new String(text) 281 | 282 | for (var key in inst) { 283 | if (hasOwn.call(inst, key)) string[key] = inst[key] 284 | } 285 | 286 | return string 287 | } 288 | 289 | // # 290 | // # # # # # ##### # # # # ##### ##### ##### 291 | // ## ## # # # # # # # # # # # # 292 | // # ## # # # # # ###### # # # # # # 293 | // # # ### # ## # # # # # ####### # # ##### 294 | // # # ### ## ## # # # # # # # # # # 295 | // # # ### # # # # # # # # # # # # 296 | // 297 | 298 | v1.withAttr = mithril.withAttr 299 | 300 | // 301 | // # # ##### ###### ##### ##### ## # # 302 | // ## ## # # # # # # # # # # # 303 | // # ## # # # ##### # # # # # # # # 304 | // # # ### ##### # # # ##### ###### # ## # 305 | // # # ### # # # # # # # # # ## ## 306 | // # # ### # # ###### ##### # # # # # # 307 | // 308 | 309 | v1.redraw = function (sync) { 310 | if (sync) throw new Error("Sync redraws are not possible in v1!") 311 | mithril.redraw() 312 | } 313 | 314 | // 315 | // # # # # #### # # # # ##### 316 | // ## ## ## ## # # # # ## # # 317 | // # ## # # ## # # # # # # # # # 318 | // # # ### # # # # # # # # # # 319 | // # # ### # # # # # # # ## # 320 | // # # ### # # #### #### # # # 321 | // 322 | 323 | v1.mount = function (elem, component) { 324 | if (component != null) checkComponent(component) 325 | return mithril.mount.apply(this, arguments) 326 | } 327 | 328 | // 329 | // # # ##### #### # # ##### ###### 330 | // ## ## # # # # # # # # 331 | // # ## # # # # # # # # ##### 332 | // # # ### ##### # # # # # # 333 | // # # ### # # # # # # # # 334 | // # # ### # # #### #### # ###### 335 | // 336 | 337 | function Resolver(component) { 338 | checkComponent(component) 339 | this.component = component 340 | } 341 | 342 | Resolver.prototype.render = function () { 343 | return makeLegacy(this.component, []) 344 | } 345 | 346 | v1.route = function (elem, arg1, arg2, vdom) { 347 | // m.route() 348 | if (arguments.length === 0) return mithril.route.get() 349 | // m.route(el, defaultRoute, routes) 350 | if (arguments.length === 3 && isString(arg1)) { 351 | var newRoutes = {} 352 | 353 | for (var key of arg2) { 354 | if (hasOwn.call(arg2, key)) { 355 | newRoutes[key] = new Resolver(arg2[key]) 356 | } 357 | } 358 | 359 | return mithril.route(elem, arg1, arg2) 360 | } 361 | 362 | if (root.addEventListener || root.attachEvent) { 363 | // config: v1.route 364 | mithril.route.link(vdom) 365 | return 366 | } 367 | 368 | // m.route(route, params, shouldReplaceHistoryEntry) 369 | if (isString(root)) { 370 | var replaceHistory = 371 | (arguments.length === 3 ? arg2 : arg1) === true || 372 | previousRoute === currentRoute 373 | 374 | v1.route.set(route, params, { 375 | replace: (arguments.length === 3 ? arg2 : arg1) === true, 376 | }) 377 | } 378 | } 379 | 380 | v1.route.param = function (key) { 381 | return mithril.route.param(key) 382 | } 383 | 384 | // Have to use a getter/setter to correctly mirror Mithril v1's API. 385 | var mode = "search" 386 | var modes = { 387 | pathname: "", 388 | hash: "#", 389 | search: "?", 390 | } 391 | 392 | mithril.route.prefix("?") 393 | 394 | Object.defineProperty(v1.route, "mode", { 395 | configurable: true, 396 | enumerable: true, 397 | get: function () { return mode }, 398 | set: function (value) { 399 | mode = value 400 | if (hasOwn.call(modes, value)) { 401 | mithril.route.prefix(modes[value]) 402 | } 403 | }, 404 | }) 405 | 406 | v1.route.buildQueryString = v1.buildQueryString 407 | v1.route.parseQueryString = v1.parseQueryString 408 | 409 | // 410 | // # # ##### ###### ###### ###### ##### ##### ###### ##### 411 | // ## ## # # # # # # # # # # # # 412 | // # ## # # # ##### ##### ##### # # # # ##### # # 413 | // # # ### # # # # # ##### ##### # # # 414 | // # # ### # # # # # # # # # # # # 415 | // # # ### ##### ###### # ###### # # # # ###### ##### 416 | // 417 | 418 | v1.deferred = makeDeferred 419 | function makeDeferred() { 420 | var deferred = { 421 | resolve: undefined, 422 | reject: undefined, 423 | promise: undefined, 424 | } 425 | 426 | deferred.promise = propify(new Promise(function (resolve, reject) { 427 | deferred.resolve = resolve 428 | deferred.reject = reject 429 | })) 430 | 431 | return deferred 432 | } 433 | 434 | // 435 | // # # #### # # # # #### 436 | // ## ## # # # ## # # # 437 | // # ## # #### # # # # # 438 | // # # ### # # # # # # 439 | // # # ### # # # # ## # # 440 | // # # ### #### # # # #### 441 | // 442 | 443 | v1.sync = function (items) { 444 | return propify(Promise.all(items)) 445 | } 446 | 447 | // 448 | // # # ##### ###### #### # # ###### #### ##### 449 | // ## ## # # # # # # # # # # 450 | // # ## # # # ##### # # # # ##### #### # 451 | // # # ### ##### # # # # # # # # # 452 | // # # ### # # # # # # # # # # # 453 | // # # ### # # ###### ### # #### ###### #### # 454 | // 455 | 456 | v1.request = function (options) { 457 | return propify( 458 | options.dataType === "jsonp" 459 | ? mithril.jsonp(options), 460 | : mithril.request(options), 461 | options.initialValue 462 | ) 463 | } 464 | 465 | // 466 | // # # ##### ###### # # ##### ###### ##### 467 | // ## ## # # # ## # # # # # # 468 | // # ## # # # ##### # # # # # ##### # # 469 | // # # ### ##### # # # # # # # ##### 470 | // # # ### # # # # ## # # # # # 471 | // # # ### # # ###### # # ##### ###### # # 472 | // 473 | 474 | v1.render = function (elem, vnode, forceRecreation) { 475 | if (migrating.has(vnode)) vnode = makeLegacy(vnode, []) 476 | if (forceRecreation) mithril.render(elem, null) 477 | return mithril.render(elem, vnode) 478 | } 479 | 480 | // 481 | // # # ##### ###### ##### #### 482 | // ## ## # # # # # # 483 | // # ## # # # ##### # # #### 484 | // # # ### # # # ##### # 485 | // # # ### # # # # # # 486 | // # # ### ##### ###### # #### 487 | // 488 | 489 | v1.deps = function () { 490 | throw new Error("m.deps() is no longer available in v1!") 491 | } 492 | })() 493 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The following copyright and license applies to '/migrate/v1.js': 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2017 Leo Horie and Isiah Meadows 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------------- 26 | 27 | The following copyright and license applies to all other source code files in this repository: 28 | 29 | Copyright (c) 2019 Isiah Meadows 30 | 31 | # Blue Oak Model License 32 | 33 | Version 1.0.0 34 | 35 | ## Purpose 36 | 37 | This license gives everyone as much permission to work with 38 | this software as possible, while protecting contributors 39 | from liability. 40 | 41 | ## Acceptance 42 | 43 | In order to receive this license, you must agree to its 44 | rules. The rules of this license are both obligations 45 | under that agreement and conditions to your license. 46 | You must not do anything with this software that triggers 47 | a rule that you cannot or will not follow. 48 | 49 | ## Copyright 50 | 51 | Each contributor licenses you to do everything with this 52 | software that would otherwise infringe that contributor's 53 | copyright in it. 54 | 55 | ## Notices 56 | 57 | You must ensure that everyone who gets a copy of 58 | any part of this software from you, with or without 59 | changes, also gets the text of this license or a link to 60 | . 61 | 62 | ## Excuse 63 | 64 | If anyone notifies you in writing that you have not 65 | complied with [Notices](#notices), you can keep your 66 | license by taking all practical steps to comply within 30 67 | days after the notice. If you do not do so, your license 68 | ends immediately. 69 | 70 | ## Patent 71 | 72 | Each contributor licenses you to do everything with this 73 | software that would otherwise infringe any patent claims 74 | they can license or become able to license. 75 | 76 | ## Reliability 77 | 78 | No contributor can revoke this license. 79 | 80 | ## No Liability 81 | 82 | ***As far as the law allows, this software comes as is, 83 | without any warranty or condition, and no contributor 84 | will be liable to anyone for any damages related to this 85 | software or this license, under any kind of legal claim.*** 86 | 87 | ---------------------------------------------------------------------------------- 88 | 89 | The following copyright and license applies to all other non-source-code files in the repository: 90 | 91 | Copyright (c) 2019 Isiah Meadows. Some rights reserved. 92 | 93 | Creative Commons Attribution 4.0 International Public License 94 | 95 | By exercising the Licensed Rights (defined below), You accept and agree 96 | to be bound by the terms and conditions of this Creative Commons 97 | Attribution 4.0 International Public License ("Public License"). To the 98 | extent this Public License may be interpreted as a contract, You are 99 | granted the Licensed Rights in consideration of Your acceptance of 100 | these terms and conditions, and the Licensor grants You such rights in 101 | consideration of benefits the Licensor receives from making the 102 | Licensed Material available under these terms and conditions. 103 | 104 | 105 | Section 1 -- Definitions. 106 | 107 | a. Adapted Material means material subject to Copyright and Similar 108 | Rights that is derived from or based upon the Licensed Material 109 | and in which the Licensed Material is translated, altered, 110 | arranged, transformed, or otherwise modified in a manner requiring 111 | permission under the Copyright and Similar Rights held by the 112 | Licensor. For purposes of this Public License, where the Licensed 113 | Material is a musical work, performance, or sound recording, 114 | Adapted Material is always produced where the Licensed Material is 115 | synched in timed relation with a moving image. 116 | 117 | b. Adapter's License means the license You apply to Your Copyright 118 | and Similar Rights in Your contributions to Adapted Material in 119 | accordance with the terms and conditions of this Public License. 120 | 121 | c. Copyright and Similar Rights means copyright and/or similar rights 122 | closely related to copyright including, without limitation, 123 | performance, broadcast, sound recording, and Sui Generis Database 124 | Rights, without regard to how the rights are labeled or 125 | categorized. For purposes of this Public License, the rights 126 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 127 | Rights. 128 | 129 | d. Effective Technological Measures means those measures that, in the 130 | absence of proper authority, may not be circumvented under laws 131 | fulfilling obligations under Article 11 of the WIPO Copyright 132 | Treaty adopted on December 20, 1996, and/or similar international 133 | agreements. 134 | 135 | e. Exceptions and Limitations means fair use, fair dealing, and/or 136 | any other exception or limitation to Copyright and Similar Rights 137 | that applies to Your use of the Licensed Material. 138 | 139 | f. Licensed Material means the artistic or literary work, database, 140 | or other material to which the Licensor applied this Public 141 | License. 142 | 143 | g. Licensed Rights means the rights granted to You subject to the 144 | terms and conditions of this Public License, which are limited to 145 | all Copyright and Similar Rights that apply to Your use of the 146 | Licensed Material and that the Licensor has authority to license. 147 | 148 | h. Licensor means the individual(s) or entity(ies) granting rights 149 | under this Public License. 150 | 151 | i. Share means to provide material to the public by any means or 152 | process that requires permission under the Licensed Rights, such 153 | as reproduction, public display, public performance, distribution, 154 | dissemination, communication, or importation, and to make material 155 | available to the public including in ways that members of the 156 | public may access the material from a place and at a time 157 | individually chosen by them. 158 | 159 | j. Sui Generis Database Rights means rights other than copyright 160 | resulting from Directive 96/9/EC of the European Parliament and of 161 | the Council of 11 March 1996 on the legal protection of databases, 162 | as amended and/or succeeded, as well as other essentially 163 | equivalent rights anywhere in the world. 164 | 165 | k. You means the individual or entity exercising the Licensed Rights 166 | under this Public License. Your has a corresponding meaning. 167 | 168 | 169 | Section 2 -- Scope. 170 | 171 | a. License grant. 172 | 173 | 1. Subject to the terms and conditions of this Public License, 174 | the Licensor hereby grants You a worldwide, royalty-free, 175 | non-sublicensable, non-exclusive, irrevocable license to 176 | exercise the Licensed Rights in the Licensed Material to: 177 | 178 | a. reproduce and Share the Licensed Material, in whole or 179 | in part; and 180 | 181 | b. produce, reproduce, and Share Adapted Material. 182 | 183 | 2. Exceptions and Limitations. For the avoidance of doubt, where 184 | Exceptions and Limitations apply to Your use, this Public 185 | License does not apply, and You do not need to comply with 186 | its terms and conditions. 187 | 188 | 3. Term. The term of this Public License is specified in Section 189 | 6(a). 190 | 191 | 4. Media and formats; technical modifications allowed. The 192 | Licensor authorizes You to exercise the Licensed Rights in 193 | all media and formats whether now known or hereafter created, 194 | and to make technical modifications necessary to do so. The 195 | Licensor waives and/or agrees not to assert any right or 196 | authority to forbid You from making technical modifications 197 | necessary to exercise the Licensed Rights, including 198 | technical modifications necessary to circumvent Effective 199 | Technological Measures. For purposes of this Public License, 200 | simply making modifications authorized by this Section 2(a) 201 | (4) never produces Adapted Material. 202 | 203 | 5. Downstream recipients. 204 | 205 | a. Offer from the Licensor -- Licensed Material. Every 206 | recipient of the Licensed Material automatically 207 | receives an offer from the Licensor to exercise the 208 | Licensed Rights under the terms and conditions of this 209 | Public License. 210 | 211 | b. No downstream restrictions. You may not offer or impose 212 | any additional or different terms or conditions on, or 213 | apply any Effective Technological Measures to, the 214 | Licensed Material if doing so restricts exercise of the 215 | Licensed Rights by any recipient of the Licensed 216 | Material. 217 | 218 | 6. No endorsement. Nothing in this Public License constitutes or 219 | may be construed as permission to assert or imply that You 220 | are, or that Your use of the Licensed Material is, connected 221 | with, or sponsored, endorsed, or granted official status by, 222 | the Licensor or others designated to receive attribution as 223 | provided in Section 3(a)(1)(A)(i). 224 | 225 | b. Other rights. 226 | 227 | 1. Moral rights, such as the right of integrity, are not 228 | licensed under this Public License, nor are publicity, 229 | privacy, and/or other similar personality rights; however, to 230 | the extent possible, the Licensor waives and/or agrees not to 231 | assert any such rights held by the Licensor to the limited 232 | extent necessary to allow You to exercise the Licensed 233 | Rights, but not otherwise. 234 | 235 | 2. Patent and trademark rights are not licensed under this 236 | Public License. 237 | 238 | 3. To the extent possible, the Licensor waives any right to 239 | collect royalties from You for the exercise of the Licensed 240 | Rights, whether directly or through a collecting society 241 | under any voluntary or waivable statutory or compulsory 242 | licensing scheme. In all other cases the Licensor expressly 243 | reserves any right to collect such royalties. 244 | 245 | 246 | Section 3 -- License Conditions. 247 | 248 | Your exercise of the Licensed Rights is expressly made subject to the 249 | following conditions. 250 | 251 | a. Attribution. 252 | 253 | 1. If You Share the Licensed Material (including in modified 254 | form), You must: 255 | 256 | a. retain the following if it is supplied by the Licensor 257 | with the Licensed Material: 258 | 259 | i. identification of the creator(s) of the Licensed 260 | Material and any others designated to receive 261 | attribution, in any reasonable manner requested by 262 | the Licensor (including by pseudonym if 263 | designated); 264 | 265 | ii. a copyright notice; 266 | 267 | iii. a notice that refers to this Public License; 268 | 269 | iv. a notice that refers to the disclaimer of 270 | warranties; 271 | 272 | v. a URI or hyperlink to the Licensed Material to the 273 | extent reasonably practicable; 274 | 275 | b. indicate if You modified the Licensed Material and 276 | retain an indication of any previous modifications; and 277 | 278 | c. indicate the Licensed Material is licensed under this 279 | Public License, and include the text of, or the URI or 280 | hyperlink to, this Public License. 281 | 282 | 2. You may satisfy the conditions in Section 3(a)(1) in any 283 | reasonable manner based on the medium, means, and context in 284 | which You Share the Licensed Material. For example, it may be 285 | reasonable to satisfy the conditions by providing a URI or 286 | hyperlink to a resource that includes the required 287 | information. 288 | 289 | 3. If requested by the Licensor, You must remove any of the 290 | information required by Section 3(a)(1)(A) to the extent 291 | reasonably practicable. 292 | 293 | 4. If You Share Adapted Material You produce, the Adapter's 294 | License You apply must not prevent recipients of the Adapted 295 | Material from complying with this Public License. 296 | 297 | 298 | Section 4 -- Sui Generis Database Rights. 299 | 300 | Where the Licensed Rights include Sui Generis Database Rights that 301 | apply to Your use of the Licensed Material: 302 | 303 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 304 | to extract, reuse, reproduce, and Share all or a substantial 305 | portion of the contents of the database; 306 | 307 | b. if You include all or a substantial portion of the database 308 | contents in a database in which You have Sui Generis Database 309 | Rights, then the database in which You have Sui Generis Database 310 | Rights (but not its individual contents) is Adapted Material; and 311 | 312 | c. You must comply with the conditions in Section 3(a) if You Share 313 | all or a substantial portion of the contents of the database. 314 | 315 | For the avoidance of doubt, this Section 4 supplements and does not 316 | replace Your obligations under this Public License where the Licensed 317 | Rights include other Copyright and Similar Rights. 318 | 319 | 320 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 321 | 322 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 323 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 324 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 325 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 326 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 327 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 328 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 329 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 330 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 331 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 332 | 333 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 334 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 335 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 336 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 337 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 338 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 339 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 340 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 341 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 342 | 343 | c. The disclaimer of warranties and limitation of liability provided 344 | above shall be interpreted in a manner that, to the extent 345 | possible, most closely approximates an absolute disclaimer and 346 | waiver of all liability. 347 | 348 | 349 | Section 6 -- Term and Termination. 350 | 351 | a. This Public License applies for the term of the Copyright and 352 | Similar Rights licensed here. However, if You fail to comply with 353 | this Public License, then Your rights under this Public License 354 | terminate automatically. 355 | 356 | b. Where Your right to use the Licensed Material has terminated under 357 | Section 6(a), it reinstates: 358 | 359 | 1. automatically as of the date the violation is cured, provided 360 | it is cured within 30 days of Your discovery of the 361 | violation; or 362 | 363 | 2. upon express reinstatement by the Licensor. 364 | 365 | For the avoidance of doubt, this Section 6(b) does not affect any 366 | right the Licensor may have to seek remedies for Your violations 367 | of this Public License. 368 | 369 | c. For the avoidance of doubt, the Licensor may also offer the 370 | Licensed Material under separate terms or conditions or stop 371 | distributing the Licensed Material at any time; however, doing so 372 | will not terminate this Public License. 373 | 374 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 375 | License. 376 | 377 | 378 | Section 7 -- Other Terms and Conditions. 379 | 380 | a. The Licensor shall not be bound by any additional or different 381 | terms or conditions communicated by You unless expressly agreed. 382 | 383 | b. Any arrangements, understandings, or agreements regarding the 384 | Licensed Material not stated herein are separate from and 385 | independent of the terms and conditions of this Public License. 386 | 387 | 388 | Section 8 -- Interpretation. 389 | 390 | a. For the avoidance of doubt, this Public License does not, and 391 | shall not be interpreted to, reduce, limit, restrict, or impose 392 | conditions on any use of the Licensed Material that could lawfully 393 | be made without permission under this Public License. 394 | 395 | b. To the extent possible, if any provision of this Public License is 396 | deemed unenforceable, it shall be automatically reformed to the 397 | minimum extent necessary to make it enforceable. If the provision 398 | cannot be reformed, it shall be severed from this Public License 399 | without affecting the enforceability of the remaining terms and 400 | conditions. 401 | 402 | c. No term or condition of this Public License will be waived and no 403 | failure to comply consented to unless expressly agreed to by the 404 | Licensor. 405 | 406 | d. Nothing in this Public License constitutes or may be interpreted 407 | as a limitation upon, or waiver of, any privileges and immunities 408 | that apply to the Licensor or You, including from the legal 409 | processes of any jurisdiction or authority. 410 | 411 | 412 | ======================================================================= 413 | 414 | Creative Commons is not a party to its public 415 | licenses. Notwithstanding, Creative Commons may elect to apply one of 416 | its public licenses to material it publishes and in those instances 417 | will be considered the “Licensor.” The text of the Creative Commons 418 | public licenses is dedicated to the public domain under the CC0 Public 419 | Domain Dedication. Except for the limited purpose of indicating that 420 | material is shared under a Creative Commons public license or as 421 | otherwise permitted by the Creative Commons policies published at 422 | creativecommons.org/policies, Creative Commons does not authorize the 423 | use of the trademark "Creative Commons" or any other trademark or logo 424 | of Creative Commons without its prior written consent including, 425 | without limitation, in connection with any unauthorized modifications 426 | to any of its public licenses or any other arrangements, 427 | understandings, or agreements concerning use of licensed material. For 428 | the avoidance of doubt, this paragraph does not form part of the 429 | public licenses. 430 | 431 | Creative Commons may be contacted at creativecommons.org. 432 | --------------------------------------------------------------------------------