├── .browserslistrc
├── .gitignore
├── lerna.json
├── packages
├── mithril-hookup
│ ├── src
│ │ ├── index.js
│ │ ├── utils.js
│ │ └── hookup.js
│ ├── README.md
│ ├── package.json
│ └── dist
│ │ ├── mithril-hookup.js
│ │ ├── mithril-hookup.js.map
│ │ └── mithril-hookup.mjs
└── test-mithril-hookup
│ ├── cypress.json
│ ├── dist
│ ├── css
│ │ ├── toggle.css
│ │ ├── app.css
│ │ └── custom-hooks-usereducer.css
│ ├── js
│ │ ├── index.js.gz
│ │ ├── index.js.map.gz
│ │ └── index.js
│ └── index.html
│ ├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ ├── useRef.spec.js
│ │ ├── useEffect.spec.js
│ │ ├── useMemo.spec.js
│ │ ├── extra-arguments.spec.js
│ │ ├── useCallback.spec.js
│ │ ├── effect-timing.spec.js
│ │ ├── useReducer.spec.js
│ │ ├── useLayoutEffect.spec.js
│ │ ├── useState.spec.js
│ │ ├── custom-hooks.spec.js
│ │ ├── effect-render-counts.spec.js
│ │ ├── withHooks.spec.js
│ │ └── update-rules.spec.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── index.js
│ │ └── commands.js
│ ├── test
│ ├── debug.js
│ └── withHooks.js
│ ├── src
│ ├── toggle
│ │ └── index.js
│ ├── cypress-tests
│ │ ├── TestHookupUseRef.js
│ │ ├── TestHookupUseEffect.js
│ │ ├── TestHookupUseCallback.js
│ │ ├── TestEffectTiming.js
│ │ ├── TestHookupUseMemo.js
│ │ ├── TestEffectRenderCounts.js
│ │ ├── TestHookupUseState.js
│ │ ├── TestHookupUseLayoutEffect.js
│ │ ├── TestWithHooksExtraArguments.js
│ │ ├── TestUseReducer.js
│ │ ├── TestHookupUpdateRules.js
│ │ ├── TestWithHooks.js
│ │ └── TestHookupCustomHooks.js
│ ├── custom-hooks-usereducer
│ │ ├── customHooks.js
│ │ ├── index.js
│ │ └── Counter.js
│ └── index.js
│ └── package.json
├── babel.config.umd.js
├── babel.config.es.js
├── babel.config.base.js
├── scripts
├── webpack.config.dev.js
├── rollup.es.js
├── webpack.config.prod.js
├── rollup.umd.js
├── rollup.base.js
└── webpack.config.js
├── .eslintrc
├── package.json
└── README.md
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 1 version
2 | > 1%
3 | not dead
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | npm-debug.log*
4 | *.log
5 | *.lock
6 | package-lock.json
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "packages/*"
4 | ],
5 | "version": "0.2.7"
6 | }
7 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/src/index.js:
--------------------------------------------------------------------------------
1 | export * from "./hookup";
2 | export * from "./utils";
3 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:8080/#!",
3 | "video": false
4 | }
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/css/toggle.css:
--------------------------------------------------------------------------------
1 | .toggle .info {
2 | color: #666;
3 | margin: .5em 0 0 0;
4 | }
--------------------------------------------------------------------------------
/packages/mithril-hookup/README.md:
--------------------------------------------------------------------------------
1 | # mithril-hookup
2 |
3 | Use hooks in Mithril.
4 |
5 | [Documentation](../..//README.md)
6 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/js/index.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArthurClemens/mithril-hookup/HEAD/packages/test-mithril-hookup/dist/js/index.js.gz
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/js/index.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArthurClemens/mithril-hookup/HEAD/packages/test-mithril-hookup/dist/js/index.js.map.gz
--------------------------------------------------------------------------------
/babel.config.umd.js:
--------------------------------------------------------------------------------
1 | const plugins = require("./babel.config.base").plugins;
2 |
3 | const presets = [
4 | "@babel/preset-env"
5 | ];
6 |
7 | module.exports = {
8 | presets,
9 | plugins
10 | };
11 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/babel.config.es.js:
--------------------------------------------------------------------------------
1 | const plugins = require("./babel.config.base").plugins;
2 |
3 | const presets = [
4 | ["@babel/preset-env",
5 | {
6 | "targets": {
7 | "esmodules": true
8 | }
9 | }
10 | ]
11 | ];
12 |
13 | module.exports = {
14 | presets,
15 | plugins
16 | };
17 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/test/debug.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function(actual, expected) {
3 | console.log("expected"); // eslint-disable-line no-console
4 | console.log(expected); // eslint-disable-line no-console
5 | console.log("actual"); // eslint-disable-line no-console
6 | console.log(actual); // eslint-disable-line no-console
7 | };
8 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/css/app.css:
--------------------------------------------------------------------------------
1 | .layout {
2 | display: flex;
3 | width: 100%;
4 | }
5 | .menu {
6 | width: 25%;
7 | min-width: 200px;
8 | max-width: 250px;
9 | border-right: 1px solid #eee;
10 | padding: 15px;
11 | min-height: 100vh;
12 | }
13 | .component {
14 | display: flex;
15 | flex-direction: column;
16 | flex-grow: 1;
17 | padding: 30px;
18 | }
--------------------------------------------------------------------------------
/babel.config.base.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | const presets = []; // set in specific configs for es and umd
3 | const plugins = [
4 | "@babel/plugin-transform-arrow-functions",
5 | "@babel/plugin-transform-object-assign",
6 | "@babel/plugin-proposal-object-rest-spread"
7 | ];
8 |
9 | api.cache(false);
10 |
11 | return {
12 | presets,
13 | plugins,
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useRef.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useRef", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseRef");
7 | });
8 |
9 | it("should get the dom element and retrieve attributes", () => {
10 | cy.get("[data-test-id=render]").click();
11 | cy.get("[data-test-id=textContent]").should("contain", "QWERTY");
12 | });
13 |
14 | });
15 |
--------------------------------------------------------------------------------
/scripts/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | /* global process */
2 | const path = require("path");
3 | const createConfig = require("./webpack.config.js");
4 |
5 | const baseDir = process.cwd();
6 | const config = createConfig(false);
7 |
8 | config.mode = "development";
9 |
10 | config.devServer = {
11 | contentBase: path.resolve(baseDir, "./dist")
12 | };
13 |
14 | config.watchOptions = {
15 | ignored: /node_modules/
16 | };
17 |
18 | module.exports = config;
19 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/toggle/index.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { withHooks } from "mithril-hookup";
3 |
4 | const Toggle = ({ useState }) => {
5 | const [clicked, setClicked] = useState(false);
6 | return m(".toggle", [
7 | m("button",
8 | {
9 | className: `button ${clicked ? "is-info" : ""}`,
10 | onclick: () => setClicked(!clicked)
11 | },
12 | "Toggle"
13 | ),
14 | m(".info", clicked ? "On" : "Off")
15 | ]);
16 | };
17 |
18 | export default withHooks(Toggle);
19 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useEffect.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useEffect", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseEffect");
7 | });
8 |
9 | it("should render with the initial value", () => {
10 | cy.get("#root.dark-mode").should("not.exist");
11 | });
12 |
13 | it("should update the class list after setDarkModeEnabled", () => {
14 | cy.get("[data-test-id=dark] [data-test-id=button]").click();
15 | cy.get("#root.dark-mode").should("exist");
16 | });
17 |
18 | });
19 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/src/utils.js:
--------------------------------------------------------------------------------
1 | import { hookup } from "./hookup";
2 |
3 | const hookupComponent = component =>
4 | hookup((vnode, hooks) => (
5 | component({ ...vnode.attrs, ...hooks, children: vnode.children })
6 | ));
7 |
8 | export const withHooks = (component, customHooksFn, rest = {}) =>
9 | hookupComponent(
10 | hooks => {
11 | const customHooks = customHooksFn !== undefined && customHooksFn !== null
12 | ? customHooksFn(hooks)
13 | : {};
14 | return component({ ...hooks, ...customHooks, ...rest });
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "plugins": [],
9 | "parserOptions": {
10 | "sourceType": "module",
11 | "ecmaVersion": 2018
12 | },
13 | "rules": {
14 | "indent": [
15 | "error",
16 | 2
17 | ],
18 | "linebreak-style": [
19 | "error",
20 | "unix"
21 | ],
22 | "quotes": [
23 | "error",
24 | "double"
25 | ],
26 | "semi": [
27 | "error",
28 | "always"
29 | ],
30 | "no-console": 1
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/scripts/rollup.es.js:
--------------------------------------------------------------------------------
1 | /*
2 | Build to a module that has ES2015 module syntax but otherwise only syntax features that node supports
3 | https://github.com/rollup/rollup/wiki/jsnext:main
4 | */
5 | import { pkg, createConfig } from "./rollup.base.js";
6 | import babel from "rollup-plugin-babel";
7 |
8 | const baseConfig = createConfig();
9 | const targetConfig = Object.assign({}, baseConfig, {
10 | output: Object.assign(
11 | {},
12 | baseConfig.output,
13 | {
14 | file: `${pkg.main}.mjs`,
15 | format: "es"
16 | }
17 | )
18 | });
19 | targetConfig.plugins.push(
20 | babel({
21 | configFile: "../../babel.config.es.js"
22 | })
23 | );
24 | export default targetConfig;
25 |
26 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useMemo.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useMemo", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseMemo");
7 | });
8 |
9 | it("should store a memoized value and only update it after updating a variable", () => {
10 | cy.get("[data-test-id=memoizedValue]").invoke("text").then((memoizedValue) => {
11 | cy.get("[data-test-id=render]").click();
12 | cy.get("[data-test-id=memoizedValue]").should("contain", memoizedValue);
13 | cy.get("[data-test-id=expensive]").click();
14 | cy.get("[data-test-id=memoizedValue]").should("not.contain", memoizedValue);
15 | });
16 | });
17 |
18 | });
19 |
--------------------------------------------------------------------------------
/scripts/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | /* global process */
2 | const createConfig = require("./webpack.config.js");
3 | const CompressionPlugin = require("compression-webpack-plugin");
4 | const TerserPlugin = require("terser-webpack-plugin");
5 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
6 |
7 | const env = process.env;
8 | const config = createConfig(true);
9 | config.mode = "production";
10 |
11 | config.optimization = {
12 | minimizer: [new TerserPlugin({
13 | sourceMap: true
14 | })]
15 | };
16 |
17 | config.plugins.push(new CompressionPlugin());
18 |
19 | if (env.ANALYSE) {
20 | config.plugins.push(new BundleAnalyzerPlugin());
21 | }
22 |
23 | module.exports = config;
24 |
--------------------------------------------------------------------------------
/scripts/rollup.umd.js:
--------------------------------------------------------------------------------
1 | /*
2 | Build to an Universal Module Definition
3 | */
4 | import { pkg, createConfig } from "./rollup.base";
5 | import { terser } from "rollup-plugin-terser";
6 | import babel from "rollup-plugin-babel";
7 |
8 | const baseConfig = createConfig();
9 | const targetConfig = Object.assign({}, baseConfig, {
10 | output: Object.assign(
11 | {},
12 | baseConfig.output,
13 | {
14 | format: "umd",
15 | file: `${pkg.main}.js`,
16 | sourcemap: true,
17 | extend: true,
18 | }
19 | )
20 | });
21 | targetConfig.plugins.push(
22 | babel({
23 | configFile: "../../babel.config.umd.js"
24 | })
25 | );
26 | targetConfig.plugins.push(terser());
27 |
28 | export default targetConfig;
29 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/extra-arguments.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("withHooks - extra arguments", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestWithHooksExtraArguments");
7 | });
8 |
9 | it("should show extra arguments", () => {
10 | cy.get("[data-test-id=counter] [data-test-id=extra]").should("contain", "extra");
11 | });
12 |
13 | it("should increase the 'with custom hooks' count with setCount", () => {
14 | cy.get("[data-test-id=counter] [data-test-id=count]").should("contain", "99");
15 | cy.get("[data-test-id=counter] [data-test-id=add-count]").click();
16 | cy.get("[data-test-id=counter] [data-test-id=count]").should("contain", "100");
17 | });
18 |
19 | });
20 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useCallback.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useCallback", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseCallback");
7 | });
8 |
9 | it("should store a memoized callback function and only update it after updating a variable", () => {
10 | cy.get("[data-test-id=callbackReference]").should("contain", "false");
11 | cy.get("[data-test-id=render]").click();
12 | cy.get("[data-test-id=callbackReference]").should("contain", "false");
13 | cy.get("[data-test-id=updatePreviousCallback]").click();
14 | cy.get("[data-test-id=callbackReference]").should("contain", "true");
15 | cy.get("[data-test-id=render]").click();
16 | cy.get("[data-test-id=callbackReference]").should("contain", "true");
17 | });
18 |
19 | });
20 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseRef.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const DomElementRef = hookup((vnode, { useRef }) => {
5 | const domElement = useRef();
6 |
7 | return m("[data-test-id=DomElementRef]", [
8 | m("h2", "DomElementRef"),
9 | m("div",
10 | {
11 | oncreate: vnode => domElement.current = vnode.dom,
12 | },
13 | "QWERTY"
14 | ),
15 | m("p", [
16 | m("span", "element text: "),
17 | m("span[data-test-id=textContent]", domElement.current && domElement.current.textContent)
18 | ]),
19 | m("button[data-test-id=render]",
20 | { onclick: () => {} },
21 | "Trigger render"
22 | ),
23 | ]);
24 | });
25 |
26 | export default ({
27 | view: () => [
28 | m(DomElementRef),
29 | ]
30 | });
31 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/effect-timing.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("effect timing", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestEffectTiming");
7 | });
8 |
9 | it("should show layout effects called after effects", () => {
10 | cy.get("[data-test-id=EffectTimings] [data-test-id=button]").click();
11 | cy.get("[data-test-id=EffectTimings] [data-test-id=useEffect]").invoke("text").then((useEffect) => {
12 | cy.get("[data-test-id=EffectTimings] [data-test-id=useLayoutEffect]").invoke("text").then((useLayoutEffect) => {
13 | const useEffectTime = parseInt(useEffect, 10);
14 | const useLayoutEffectTime = parseInt(useLayoutEffect, 10);
15 | cy.expect(useEffectTime).to.be.greaterThan(useLayoutEffectTime);
16 | });
17 | });
18 | });
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useReducer.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useReducer", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestUseReducer");
7 | });
8 |
9 | it("should set the initial state using an init function", () => {
10 | cy.get("[data-test-id=ReducerInitFunction] [data-test-id=count]").should("contain", "99");
11 | cy.get("[data-test-id=ReducerInitFunction] [data-test-id=state]").should("contain", "{\"count\":99}");
12 | });
13 |
14 | it("should change the count using reducer functions", () => {
15 | cy.get("[data-test-id=ReducerCounter] [data-test-id=count]").should("contain", "10");
16 | cy.get("[data-test-id=ReducerCounter] [data-test-id=increment]").click();
17 | cy.get("[data-test-id=ReducerCounter] [data-test-id=count]").should("contain", "11");
18 | cy.get("[data-test-id=ReducerCounter] [data-test-id=state]").should("contain", "{\"count\":11}");
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test mithril-hookup
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useLayoutEffect.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useLayoutEffect", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseLayoutEffect");
7 | });
8 |
9 | it("should get the size of a dom element", () => {
10 | cy.get("[data-test-id=render]").click(); // SMELL: Required for consistent Cypress results
11 |
12 | cy.get("[data-test-id=elementSize]").should("contain", "100");
13 | cy.get("[data-test-id=measuredHeight]").should("contain", "100");
14 | cy.get("[data-test-id=button]").click();
15 | cy.get("[data-test-id=elementSize]").should("contain", "110");
16 | cy.get("[data-test-id=measuredHeight]").should("contain", "110");
17 | cy.get("[data-test-id=clear-button]").click();
18 | cy.get("[data-test-id=measuredHeight]").should("contain", "0");
19 | cy.get("[data-test-id=button]").click();
20 | cy.get("[data-test-id=measuredHeight]").should("contain", "120");
21 | });
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/scripts/rollup.base.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import commonjs from "rollup-plugin-commonjs";
3 | import pathmodify from "rollup-plugin-pathmodify";
4 |
5 | export const pkg = JSON.parse(fs.readFileSync("./package.json"));
6 | const name = "mithrilHookup";
7 | const external = Object.keys(pkg.peerDependencies || {});
8 |
9 | const globals = {};
10 | external.forEach(ext => {
11 | switch (ext) {
12 | case "mithril":
13 | globals["mithril"] = "m";
14 | break;
15 | default:
16 | globals[ext] = ext;
17 | }
18 | });
19 |
20 | export const createConfig = () => {
21 | const config = {
22 | input: "src/index.js",
23 | external,
24 | output: {
25 | name,
26 | globals,
27 | },
28 | plugins: [
29 |
30 | pathmodify({
31 | aliases: [
32 | {
33 | id: "mithril",
34 | resolveTo: "node_modules/mithril/mithril.js"
35 | }
36 | ]
37 | }),
38 |
39 | commonjs(),
40 | ]
41 | };
42 |
43 | return config;
44 | };
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/custom-hooks-usereducer/customHooks.js:
--------------------------------------------------------------------------------
1 | const customHooks = ({ useState }) => {
2 | // Use a name to access it from hook functions
3 | const hooks = {
4 | useCounter: () => {
5 | // A custom hook that uses another custom hook.
6 | const createNewCounter = () => ({
7 | id: new Date().getTime(),
8 | initialCount: Math.round(Math.random() * 10)
9 | });
10 | const firstCounter = createNewCounter();
11 | const [counters, addCounter, removeCounter] = hooks.useArray([firstCounter]);
12 | return [
13 | counters,
14 | () => addCounter(createNewCounter()),
15 | remove => removeCounter(remove)
16 | ];
17 | },
18 | useArray: (initialValue = []) => {
19 | const [arr, setArr] = useState(initialValue);
20 | return [
21 | arr,
22 | add => setArr(arr.concat(add)),
23 | remove => setArr(arr.filter(item => item !== remove))
24 | ];
25 | },
26 | };
27 | return hooks;
28 | };
29 |
30 | export default customHooks;
31 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseEffect.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const SideEffect = hookup((vnode, { useState, useEffect }) => {
5 | const [darkModeEnabled, setDarkModeEnabled] = useState(false);
6 | useEffect(
7 | () => {
8 | const className = "dark-mode";
9 | const element = document.querySelector("#root");
10 | if (darkModeEnabled) {
11 | element.classList.add(className);
12 | } else {
13 | element.classList.remove(className);
14 | }
15 | },
16 | [darkModeEnabled] // Only re-run when value has changed
17 | );
18 | return m("[data-test-id=dark]", [
19 | m("h2", "SideEffect"),
20 | m("p[data-test-id=darkModeEnabled]",
21 | `SideEffect mode enabled: ${darkModeEnabled}`
22 | ),
23 | m("button[data-test-id=button]",
24 | { onclick: () => setDarkModeEnabled(true) },
25 | "Set dark mode"
26 | ),
27 | ]);
28 | });
29 |
30 | export default ({
31 | view: () => [
32 | m(SideEffect),
33 | ]
34 | });
35 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-hookup",
3 | "version": "0.2.7",
4 | "description": "Hooks for Mithril",
5 | "main": "dist/mithril-hookup",
6 | "module": "dist/mithril-hookup.mjs",
7 | "scripts": {
8 | "lint": "eslint ./src",
9 | "build": "npm run clean && npm run rollup",
10 | "rollup": "../../node_modules/rollup/bin/rollup -c ../../scripts/rollup.umd.js && ../../node_modules/rollup/bin/rollup -c ../../scripts/rollup.es.js",
11 | "clean": "rimraf dist/*",
12 | "size": "gzip -c dist/mithril-hookup.js | wc -c"
13 | },
14 | "files": [
15 | "dist",
16 | "README.md"
17 | ],
18 | "author": "Arthur Clemens (http://visiblearea.com)",
19 | "contributors": [
20 | "Barney Carroll (http://barneycarroll.com/)"
21 | ],
22 | "homepage": "https://github.com/ArthurClemens/mithril-hookup",
23 | "license": "MIT",
24 | "devDependencies": {
25 | "mithril": "2.0.0-rc.4",
26 | "rimraf": "^2.6.3"
27 | },
28 | "peerDependencies": {
29 | "mithril": "2.0.0-rc.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/useState.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("useState", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUseState");
7 | });
8 |
9 | it("should render with the initial value", () => {
10 | cy.get("[data-test-id=InitialValue] [data-test-id=count]").should("contain", "Count: 1");
11 | });
12 |
13 | it("should render with the initial value updated after useEffect", () => {
14 | cy.get("[data-test-id=WithEffect] [data-test-id=count]").should("contain", "Count: 101");
15 | });
16 |
17 | it("should increase the count with setCount", () => {
18 | cy.get("[data-test-id=Interactive] [data-test-id=button]").click();
19 | cy.get("[data-test-id=Interactive] [data-test-id=count]").should("contain", "Count: 1001");
20 | });
21 |
22 | it("should increase the count with setCount as function", () => {
23 | cy.get("[data-test-id=Interactive] [data-test-id=fn-button]").click();
24 | cy.get("[data-test-id=Interactive] [data-test-id=count]").should("contain", "Count: 1002");
25 | });
26 |
27 | });
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/custom-hooks-usereducer/index.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { withHooks } from "mithril-hookup";
3 | import Counter from "./Counter";
4 | import customHooks from "./customHooks";
5 |
6 | const CounterController = ({ useCounter }) => {
7 | const [counters, addCounter, removeCounter] = useCounter();
8 | return [
9 | m(".controls", [
10 | m("button",
11 | {
12 | className: "button is-info",
13 | onclick: () => addCounter()
14 | },
15 | "Add counter"
16 | ),
17 | m(".spacer"),
18 | m("span.info",
19 | m("span",
20 | {
21 | className: "tag is-light is-medium"
22 | },
23 | `Counters: ${counters.length}`
24 | )
25 | )
26 | ]),
27 | counters.map(c => (
28 | m(Counter, {
29 | key: c.id,
30 | id: c.id,
31 | initialCount: c.initialCount,
32 | removeCounter: () => removeCounter(c),
33 | })
34 | ))
35 | ];
36 | };
37 | const HookedCounterController = withHooks(CounterController, customHooks);
38 |
39 | export default HookedCounterController;
40 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/custom-hooks.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("custom hooks", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupCustomHooks");
7 | });
8 |
9 | it("should use a custom hook counter function", () => {
10 | cy.get("[data-test-id=CounterCustomHooks] [data-test-id=count]").should("contain", "0");
11 | cy.get("[data-test-id=CounterCustomHooks] [data-test-id=increment]").click();
12 | cy.get("[data-test-id=CounterCustomHooks] [data-test-id=count]").should("contain", "1");
13 | cy.get("[data-test-id=CounterCustomHooks] [data-test-id=decrement]").click();
14 | cy.get("[data-test-id=CounterCustomHooks] [data-test-id=count]").should("contain", "0");
15 | });
16 |
17 | it("should use a custom hook functions that references another custom hook function", () => {
18 | cy.get("[data-test-id=ItemsCustomHooks] [data-test-id=count]").should("contain", "1");
19 | cy.get("[data-test-id=ItemsCustomHooks] [data-test-id=increment]").click();
20 | cy.get("[data-test-id=ItemsCustomHooks] [data-test-id=count]").should("contain", "2");
21 | cy.get("[data-test-id=ItemsCustomHooks] [data-test-id=decrement]").click();
22 | cy.get("[data-test-id=ItemsCustomHooks] [data-test-id=count]").should("contain", "1");
23 | });
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseCallback.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const someCallback = () => {
5 | return null;
6 | };
7 |
8 | let previousCallback = null;
9 |
10 | const Callback = hookup((vnode, { useCallback, useState }) => {
11 | const [someValue, setSomeValue] = useState(0);
12 |
13 | const memoizedCallback = useCallback(
14 | () => {
15 | return someCallback();
16 | },
17 | [someValue],
18 | );
19 |
20 | return m("[data-test-id=Callback]", [
21 | m("h2", "Callback"),
22 | m("p", [
23 | m("span", "callback reference: "),
24 | m("span[data-test-id=callbackReference]", (previousCallback === memoizedCallback).toString())
25 | ]),
26 | m("button[data-test-id=update]",
27 | { onclick: () => setSomeValue(n => n + 1) },
28 | "Trigger update"
29 | ),
30 | m("button[data-test-id=updatePreviousCallback]",
31 | { onclick: () => {
32 | if (previousCallback !== memoizedCallback) {
33 | previousCallback = memoizedCallback;
34 | }
35 | } },
36 | "Update previousCallback"
37 | ),
38 | m("button[data-test-id=render]",
39 | { onclick: () => {} },
40 | "Trigger render"
41 | ),
42 | ]);
43 | });
44 |
45 | export default ({
46 | view: () => [
47 | m(Callback),
48 | ]
49 | });
50 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestEffectTiming.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const timings = {
5 | useEffect: 0,
6 | useLayoutEffect: 0,
7 | };
8 |
9 | const EffectTimings = hookup((vnode, { useEffect, useLayoutEffect }) => {
10 |
11 | useLayoutEffect(
12 | () => {
13 | timings.useLayoutEffect += new Date().getTime();
14 | },
15 | []
16 | );
17 |
18 | useEffect(
19 | () => {
20 | timings.useEffect += new Date().getTime();
21 | },
22 | []
23 | );
24 |
25 | useEffect(
26 | () => {
27 | timings.useEffect += new Date().getTime();
28 | },
29 | []
30 | );
31 |
32 | useLayoutEffect(
33 | () => {
34 | timings.useLayoutEffect += new Date().getTime();
35 | },
36 | []
37 | );
38 |
39 | return m("[data-test-id=EffectTimings]", [
40 | m("h2", "EffectTimings"),
41 | timings.useEffect
42 | ? m("p", [
43 | m("div", "useEffect: "),
44 | m("span[data-test-id=useEffect]", timings.useEffect.toString())
45 | ])
46 | : null,
47 | timings.useLayoutEffect
48 | ? m("p", [
49 | m("div", "useLayoutEffect: "),
50 | m("span[data-test-id=useLayoutEffect]", timings.useLayoutEffect.toString())
51 | ])
52 | : null,
53 | m("button[data-test-id=button]",
54 | { onclick: () => {} },
55 | "Trigger render"
56 | ),
57 | ]);
58 | });
59 |
60 | export default ({
61 | view: () => [
62 | m(EffectTimings),
63 | ]
64 | });
65 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/test/withHooks.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 | "use strict";
3 |
4 | const assert = require("assert");
5 | require("mithril/test-utils/browserMock")(global);
6 | const m = require("mithril");
7 | global.m = m;
8 | const render = require("mithril-node-render");
9 | const { withHooks } = require("mithril-hookup");
10 | const debug = require("./debug");
11 |
12 | const Counter = ({ useState, initialCount }) => {
13 | const [count, setCount] = useState(initialCount);
14 | return [
15 | m("div", count),
16 | m("button", {
17 | onclick: () => setCount(count + 1)
18 | }, "More")
19 | ];
20 | };
21 |
22 | describe("withHooks", function() {
23 | it("should render", function() {
24 | const HookedCounter = withHooks(Counter);
25 | const expected = "1
";
26 |
27 | return render([
28 | m(HookedCounter, {
29 | initialCount: 1
30 | })
31 | ]).then(actual => {
32 | if (actual !== expected) {
33 | debug(actual, expected);
34 | }
35 | return assert(actual === expected);
36 | });
37 | });
38 |
39 | it("should pass extra arguments", function() {
40 | const HookedCounter = withHooks(Counter, null, { initialCount: 99 });
41 | const expected = "99
";
42 |
43 | return render([
44 | m(HookedCounter)
45 | ]).then(actual => {
46 | if (actual !== expected) {
47 | debug(actual, expected);
48 | }
49 | return assert(actual === expected);
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/css/custom-hooks-usereducer.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --counter-size: 60px;
3 | --counter-block-height: 120px;
4 | --counter-block-padding: 30px;
5 | }
6 | .controls {
7 | font-size: 14px;
8 | display: flex;
9 | align-items: center;
10 | margin: 0 0 15px 0;
11 | }
12 | .controls button + button,
13 | .controls .info {
14 | margin-left: 15px;
15 | }
16 | .controls .spacer {
17 | display: flex;
18 | width: 100%;
19 | }
20 | .counter {
21 | opacity: 0;
22 | height: 0;
23 | max-height: 0;
24 | overflow: hidden;
25 | transition: all .3s ease-in-out;
26 | }
27 | .counter.active {
28 | opacity: 1;
29 | height: var(--counter-block-height);
30 | max-height: var(--counter-block-height);
31 | }
32 | .counter-inner {
33 | display: flex;
34 | align-items: center;
35 | background: #eee;
36 | border-radius: 3px;
37 | padding: var(--counter-block-padding);
38 | border-top: 1px solid #fff;
39 | height: var(--counter-block-height);
40 | width: 100%;
41 | }
42 | .counter-inner .spacer {
43 | display: flex;
44 | width: 100%;
45 | }
46 | .counter-inner button + button,
47 | .counter-inner button + a {
48 | margin-left: 10px;
49 | }
50 | .counter .count {
51 | user-select: none;
52 | flex-shrink: 0;
53 | width: var(--counter-size);
54 | height: var(--counter-size);
55 | margin: 0 20px 0 0;
56 | display: flex;
57 | flex-direction: column;
58 | justify-content: center;
59 | background: #333;
60 | border-radius: 50%;
61 | color: #ddd;
62 | font-size: 24px;
63 | text-align: center;
64 | }
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseMemo.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | // Note that Cypress will kill process that take to long to finish
5 | // so the duration of this process is fairly short.
6 | // If `expensiveCount` suddenly gets "undefined" it may have to do
7 | // with a Cypress optimisation.
8 | const computeExpensiveValue = () => {
9 | let total = [];
10 | const max = 1000 + Math.floor(Math.random() * 40);
11 | for (let i = 0; i < max; i++) {
12 | total.push(new Date().getSeconds());
13 | }
14 | let sum = total.reduce((acc, s) => acc + s);
15 | return sum;
16 | };
17 |
18 | const MemoValue = hookup((vnode, { useMemo, useState }) => {
19 | const [expensiveCount, setExpensiveCount] = useState(0);
20 |
21 | const memoizedValue = useMemo(
22 | () => {
23 | return computeExpensiveValue();
24 | },
25 | [expensiveCount] // only calculate when expensiveCount is updated
26 | );
27 |
28 | return m("[data-test-id=MemoValue]", [
29 | m("h2", "MemoValue"),
30 | m("p", [
31 | m("span", "memoizedValue: "),
32 | m("span[data-test-id=memoizedValue]", memoizedValue.toString())
33 | ]),
34 | m("button[data-test-id=expensive]",
35 | { onclick: () => setExpensiveCount(n => n + 1) },
36 | "Trigger expensive count"
37 | ),
38 | m("button[data-test-id=render]",
39 | { onclick: () => {} },
40 | "Trigger render"
41 | ),
42 | ]);
43 | });
44 |
45 | export default ({
46 | view: () => [
47 | m(MemoValue),
48 | ]
49 | });
50 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestEffectRenderCounts.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 |
5 | const renderCounts = {
6 | useEffectEmptyDeps: 0,
7 | useEffectVariable: 0,
8 | };
9 |
10 | const EffectCountEmpty = hookup((vnode, { useEffect }) => {
11 | renderCounts.useEffectEmptyDeps++;
12 |
13 | useEffect(
14 | () => {
15 | //
16 | },
17 | []
18 | );
19 |
20 | return m("[data-test-id=EffectCountEmpty]", [
21 | m("h2", "EffectCountEmpty"),
22 | m("p[data-test-id=renderCounts]", renderCounts.useEffectEmptyDeps),
23 | m("button[data-test-id=button]",
24 | { onclick: () => {} },
25 | "Trigger render"
26 | ),
27 | ]);
28 | });
29 |
30 | const EffectCountVariable = hookup((vnode, { useState, useEffect }) => {
31 | renderCounts.useEffectVariable++;
32 | const [count, setCount] = useState(0);
33 |
34 | useEffect(
35 | () => {
36 | //
37 | },
38 | [count]
39 | );
40 |
41 | return m("[data-test-id=EffectCountVariable]", [
42 | m("h2", "EffectCountVariable"),
43 | m("p[data-test-id=counts]", count),
44 | m("p[data-test-id=renderCounts]", renderCounts.useEffectVariable),
45 | m("button[data-test-id=button-increment]",
46 | { onclick: () => setCount(count + 1) },
47 | "More"
48 | ),
49 | m("button[data-test-id=button]",
50 | { onclick: () => {} },
51 | "Trigger render"
52 | ),
53 | ]);
54 | });
55 |
56 | export default ({
57 | view: () => [
58 | m(EffectCountEmpty),
59 | m(EffectCountVariable),
60 | ]
61 | });
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-hookup-repo",
3 | "description": "Hooks for Mithril",
4 | "private": true,
5 | "license": "MIT",
6 | "devDependencies": {
7 | "@babel/cli": "^7.2.3",
8 | "@babel/core": "^7.3.4",
9 | "@babel/plugin-external-helpers": "^7.2.0",
10 | "@babel/plugin-proposal-object-rest-spread": "^7.3.4",
11 | "@babel/plugin-transform-arrow-functions": "^7.2.0",
12 | "@babel/plugin-transform-object-assign": "^7.2.0",
13 | "@babel/plugin-transform-runtime": "^7.3.4",
14 | "@babel/preset-env": "^7.3.4",
15 | "@babel/register": "^7.0.0",
16 | "babel-eslint": "^10.0.1",
17 | "babel-loader": "^8.0.5",
18 | "compression-webpack-plugin": "^2.0.0",
19 | "css-loader": "^2.1.1",
20 | "cypress": "^3.2.0",
21 | "http-server": "^0.11.1",
22 | "lerna": "^3.13.1",
23 | "mini-css-extract-plugin": "^0.5.0",
24 | "mithril": "2.0.0-rc.4",
25 | "mithril-node-render": "2.3.1",
26 | "mithril-query": "^2.5.2",
27 | "mocha": "^6.0.2",
28 | "npm-run-all": "4.1.5",
29 | "rimraf": "^2.6.3",
30 | "rollup": "^1.6.0",
31 | "rollup-plugin-babel": "^4.3.2",
32 | "rollup-plugin-commonjs": "^9.2.1",
33 | "rollup-plugin-eslint": "^5.0.0",
34 | "rollup-plugin-node-resolve": "^4.0.1",
35 | "rollup-plugin-pathmodify": "^1.0.1",
36 | "rollup-plugin-terser": "^4.0.4",
37 | "rollup-watch": "^4.3.1",
38 | "start-server-and-test": "^1.7.12",
39 | "style-loader": "^0.23.1",
40 | "terser-webpack-plugin": "1.2.3",
41 | "webpack": "^4.29.6",
42 | "webpack-bundle-analyzer": "^3.1.0",
43 | "webpack-cli": "^3.2.3",
44 | "webpack-dev-server": "^3.2.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-mithril-hookup",
3 | "version": "0.2.7",
4 | "private": true,
5 | "scripts": {
6 | "lint": "eslint ./src",
7 | "dev": "npm-run-all --parallel dev:watch dev:serve",
8 | "dev:serve": "../../node_modules/webpack-dev-server/bin/webpack-dev-server.js --config ../../scripts/webpack.config.dev.js --disableHostCheck true --port 3000 --host 0.0.0.0",
9 | "dev:watch": "../../node_modules/webpack/bin/webpack.js --watch --config ../../scripts/webpack.config.dev.js",
10 | "webpack": "../../node_modules/webpack/bin/webpack.js --config ../../scripts/webpack.config.prod.js",
11 | "build": "npm run clean && npm run webpack",
12 | "serve": "http-server -c-1 -p 8080 dist",
13 | "rollup": "./node_modules/rollup/bin/rollup -c ./scripts/rollup.umd.js && ./node_modules/rollup/bin/rollup -c ./scripts/rollup.es.js",
14 | "clean": "rimraf dist/js/*",
15 | "test": "npm run test:mocha && npm run test:cypress",
16 | "test:mocha": "mocha",
17 | "test:cypress": "npm run build && start-server-and-test serve 8080 cypress:run",
18 | "test:cypress:i": "npm run build && npm-run-all --parallel serve cypress:open",
19 | "cypress:run": "cypress run",
20 | "cypress:open": "cypress open"
21 | },
22 | "license": "MIT",
23 | "devDependencies": {
24 | "cypress": "^3.2.0",
25 | "http-server": "^0.11.1",
26 | "mithril": "2.0.0-rc.4",
27 | "mithril-hookup": "^0.2.7",
28 | "mithril-node-render": "2.3.1",
29 | "mithril-query": "^2.5.2",
30 | "mocha": "^6.0.2",
31 | "npm-run-all": "4.1.5",
32 | "rimraf": "^2.6.3",
33 | "start-server-and-test": "^1.7.12"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseState.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const InitialValue = hookup((vnode, { useState }) => {
5 | const [count, ] = useState(vnode.attrs.initialCount);
6 | return m("[data-test-id=InitialValue]", [
7 | m("h2", "InitialValue"),
8 | m("p[data-test-id=count]",
9 | `Count: ${count}`
10 | )
11 | ]);
12 | });
13 |
14 | const WithEffect = hookup((vnode, { useState, useEffect }) => {
15 | const [count, setCount] = useState(vnode.attrs.initialCount);
16 | // Calling from useEffect will increase the count by 1
17 | useEffect(
18 | () => {
19 | setCount(c => c + 1);
20 | },
21 | [/* empty array: only run at mount */]
22 | );
23 | return m("[data-test-id=WithEffect]", [
24 | m("h2", "WithEffect"),
25 | m("p[data-test-id=count]",
26 | `Count: ${count}`
27 | )
28 | ]);
29 | });
30 |
31 | const Interactive = hookup((vnode, { useState }) => {
32 | const [count, setCount] = useState(vnode.attrs.initialCount);
33 | return m("[data-test-id=Interactive]", [
34 | m("h2", "Interactive"),
35 | m("p[data-test-id=count]",
36 | `Count: ${count}`
37 | ),
38 | m("button[data-test-id=button]",
39 | { onclick: () => setCount(count + 1) },
40 | "Add"
41 | ),
42 | m("button[data-test-id=fn-button]",
43 | { onclick: () => setCount(c => c + 1) },
44 | "Add fn"
45 | )
46 | ]);
47 | });
48 |
49 | export default ({
50 | view: () => [
51 | m(InitialValue, { initialCount: 1 }),
52 | m(WithEffect, { initialCount: 100 }),
53 | m(Interactive, { initialCount: 1000 })
54 | ]
55 | });
56 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/effect-render-counts.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("effect render counts", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestEffectRenderCounts");
7 | });
8 |
9 | it("effect with empty deps: should have 1 render", () => {
10 | cy.get("[data-test-id=EffectCountEmpty] [data-test-id=renderCounts]").invoke("text").then((renderCounts) => {
11 | const count = parseInt(renderCounts, 10);
12 | cy.expect(count).to.be.equal(1);
13 | });
14 | });
15 |
16 | it("effect with empty deps: after click it should have 2 renders", () => {
17 | cy.get("[data-test-id=EffectCountEmpty] [data-test-id=button]").click();
18 | cy.get("[data-test-id=EffectCountEmpty] [data-test-id=renderCounts]").invoke("text").then((renderCounts) => {
19 | const count = parseInt(renderCounts, 10);
20 | cy.expect(count).to.be.equal(2);
21 | });
22 | });
23 |
24 | it("effect with variable deps: should have 2 renders", () => {
25 | cy.get("[data-test-id=EffectCountVariable] [data-test-id=renderCounts]").invoke("text").then((renderCounts) => {
26 | const count = parseInt(renderCounts, 10);
27 | cy.expect(count).to.be.equal(2);
28 | });
29 | });
30 |
31 | it("effect with variable deps: after update count it should have 3 renders", () => {
32 | cy.get("[data-test-id=EffectCountVariable] [data-test-id=button-increment]").click();
33 | cy.get("[data-test-id=EffectCountVariable] [data-test-id=renderCounts]").invoke("text").then((renderCounts) => {
34 | const count = parseInt(renderCounts, 10);
35 | cy.expect(count).to.be.equal(3);
36 | });
37 | });
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUseLayoutEffect.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const DomElementSize = hookup((vnode, { useState, useLayoutEffect, useRef }) => {
5 | const [elementSize, setElementSize] = useState(100);
6 | const [measuredHeight, setMeasuredHeight] = useState(0);
7 | const [inited, setInited] = useState(false);
8 | const domElement = useRef();
9 |
10 | useLayoutEffect(
11 | () => {
12 | domElement.current && setMeasuredHeight(domElement.current.offsetHeight)
13 | },
14 | [elementSize, inited]
15 | );
16 | return m("[data-test-id=DomElementSize]", [
17 | m("h2", "DomElementSize"),
18 | m("p", [
19 | m("span", "element size: "),
20 | m("span[data-test-id=elementSize]", elementSize)
21 | ]),
22 | m("p", [
23 | m("span", "measured height: "),
24 | m("span[data-test-id=measuredHeight]", measuredHeight)
25 | ]),
26 | m("button[data-test-id=clear-button]",
27 | { onclick: () => setMeasuredHeight(0) },
28 | "Clear"
29 | ),
30 | m("button[data-test-id=button]",
31 | { onclick: () => setElementSize(s => s + 10) },
32 | "Grow"
33 | ),
34 | m("button[data-test-id=render]",
35 | { onclick: () => {} },
36 | "Trigger render"
37 | ),
38 | m("div",
39 | {
40 | oncreate: vnode => (
41 | domElement.current = vnode.dom,
42 | setInited(true)
43 | ),
44 | style: {
45 | width: `${elementSize}px`,
46 | height: `${elementSize}px`,
47 | backgroundColor: "#333"
48 | }
49 | }
50 | ),
51 | ]);
52 | });
53 |
54 | export default ({
55 | view: () => m(DomElementSize)
56 | });
57 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestWithHooksExtraArguments.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { withHooks } from "mithril-hookup";
3 |
4 | const myCustomHooks = ({ useState }) => {
5 | // Use a name to access it from hook functions
6 | const hooks = {
7 | useCounter: () => {
8 | // A custom hook that uses another custom hook.
9 | const createNewCounter = () => ({
10 | id: new Date().getTime(),
11 | initialCount: Math.round(Math.random() * 10)
12 | });
13 | const firstCounter = createNewCounter();
14 | const [counters, addCounter, removeCounter] = hooks.useArray([firstCounter]);
15 | return [
16 | counters,
17 | () => addCounter(createNewCounter()),
18 | remove => removeCounter(remove)
19 | ];
20 | },
21 | useArray: (initialValue = []) => {
22 | const [arr, setArr] = useState(initialValue)
23 | return [
24 | arr,
25 | add => setArr(arr.concat(add)),
26 | remove => setArr(arr.filter(item => item !== remove))
27 | ];
28 | },
29 | };
30 | return hooks;
31 | };
32 |
33 | const Counter = ({ initialCount, useState, useCounter, extra }) => {
34 | const [count, setCount] = useState(initialCount);
35 | const [counters, addCounter,] = useCounter();
36 | return m("div[data-test-id=counter]", [
37 | m("div", m("span[data-test-id=extra]", extra)),
38 | m("div", m("span[data-test-id=count]", count)),
39 | m("button[data-test-id=add-count]", {
40 | onclick: () => setCount(count + 1)
41 | }, "More"),
42 | m("div", m("span[data-test-id=counters]", counters.length)),
43 | m("button[data-test-id=add-counter]", {
44 | onclick: () => addCounter()
45 | }, "Add counter")
46 | ]);
47 | };
48 |
49 | const HookedCounter = withHooks(Counter, myCustomHooks, { initialCount: 99, extra: "extra" });
50 |
51 | export default ({
52 | view: () => [
53 | m(HookedCounter),
54 | ]
55 | });
56 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/withHooks.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("withHooks", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestWithHooks");
7 | });
8 |
9 | it("should increase the 'with custom hooks' count with setCount", () => {
10 | cy.get("[data-test-id=counter] [data-test-id=count]").should("contain", "1");
11 | cy.get("[data-test-id=counter] [data-test-id=add-count]").click();
12 | cy.get("[data-test-id=counter] [data-test-id=count]").should("contain", "2");
13 | });
14 |
15 | it("should increase the 'with custom hooks' counters with addCounter", () => {
16 | cy.get("[data-test-id=counter] [data-test-id=counters]").should("contain", "1");
17 | cy.get("[data-test-id=counter] [data-test-id=add-counter]").click();
18 | cy.get("[data-test-id=counter] [data-test-id=counters]").should("contain", "2");
19 | });
20 |
21 | it("should increase the 'simple component' count with setCount", () => {
22 | cy.get("[data-test-id=simple-counter] [data-test-id=count]").should("contain", "10");
23 | cy.get("[data-test-id=simple-counter] [data-test-id=add-count]").click();
24 | cy.get("[data-test-id=simple-counter] [data-test-id=count]").should("contain", "11");
25 | });
26 |
27 | it("should show children", () => {
28 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=count]").should("contain", "10");
29 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=add-count]").click();
30 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=count]").should("contain", "11");
31 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=children]").should("contain", "One");
32 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=children]").should("contain", "Two");
33 | cy.get("[data-test-id=simple-counter-with-children] [data-test-id=children]").should("contain", "Three");
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/cypress/integration/update-rules.spec.js:
--------------------------------------------------------------------------------
1 | /* global cy, describe, before, it */
2 |
3 | describe("update rules", () => {
4 |
5 | before(() => {
6 | cy.visit("/TestHookupUpdateRules");
7 | });
8 |
9 | it("empty array: should run the effect only once with mount only", () => {
10 | cy.get("[data-test-id=RunCountOnMount] [data-test-id=effectRunCount]").should("contain", "effect called: 1");
11 | cy.get("[data-test-id=RunCountOnMount] [data-test-id=button]").click();
12 | cy.get("[data-test-id=RunCountOnMount] [data-test-id=effectRunCount]").should("contain", "effect called: 1");
13 | cy.get("[data-test-id=RunCountOnMount] [data-test-id=renderRunCount]").should("not.contain", "render called: 1");
14 | });
15 |
16 | it("array with variable: should run the effect only after variable change", () => {
17 | cy.get("[data-test-id=RunCountOnChange] [data-test-id=effectRunCount]").should("contain", "effect called: 1");
18 | cy.get("[data-test-id=RunCountOnChange] [data-test-id=button]").click();
19 | cy.get("[data-test-id=RunCountOnChange] [data-test-id=effectRunCount]").should("contain", "effect called: 2");
20 | cy.get("[data-test-id=RunCountOnChange] [data-test-id=renderRunCount]").should("not.contain", "render called: 2");
21 | });
22 |
23 | it("no array: should run the effect at each render", () => {
24 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=effectRunCount]").should("contain", "effect called: 1");
25 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=button]").click();
26 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=effectRunCount]").should("contain", "effect called: 2");
27 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=button]").click();
28 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=effectRunCount]").should("contain", "effect called: 3");
29 | cy.get("[data-test-id=RunCountOnRender] [data-test-id=renderRunCount]").should("not.contain", "render called: 3");
30 | });
31 |
32 | });
33 |
--------------------------------------------------------------------------------
/scripts/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* global process */
2 | const path = require("path");
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4 |
5 | const baseDir = process.cwd();
6 | const env = process.env; // eslint-disable-line no-undef
7 |
8 | const createConfig = isProduction => ({
9 |
10 | context: path.resolve(baseDir, "./src"),
11 |
12 | entry: {
13 | index: path.resolve(baseDir, env.ENTRY || "./src/index.js"),
14 | },
15 |
16 | output: {
17 | path: path.resolve(baseDir, "./dist"),
18 | filename: "js/[name].js"
19 | },
20 |
21 | resolve: {
22 | // Make sure that Mithril is included only once
23 | alias: {
24 | "mithril/stream": path.resolve(baseDir, "node_modules/mithril/stream/stream.js"),
25 | // Keep in this order!
26 | "mithril": path.resolve(baseDir, "node_modules/mithril/mithril.js"),
27 | },
28 | extensions: [".mjs", ".js", ".jsx", ".ts", ".tsx"],
29 | },
30 |
31 | module: {
32 | rules: [
33 | {
34 | test: /\.tsx?$/,
35 | use: [
36 | { loader: "ts-loader" }
37 | ]
38 | },
39 | {
40 | test: /\.m?js$/,
41 | exclude: /node_modules/,
42 | use: [{
43 | loader: "babel-loader",
44 | options: {
45 | configFile: isProduction
46 | ? "../../babel.config.umd.js"
47 | : "../../babel.config.es.js"
48 | }
49 | }]
50 | },
51 | {
52 | test: /\.css$/,
53 | use: [
54 | MiniCssExtractPlugin.loader,
55 | {
56 | loader: "css-loader",
57 | options: {
58 | modules: true,
59 | sourceMap: true,
60 | localIdentName: "[local]"
61 | }
62 | },
63 | ]
64 | }
65 | ]
66 | },
67 |
68 | plugins: [
69 | new MiniCssExtractPlugin({
70 | filename: "css/app.css"
71 | }),
72 | ],
73 |
74 | devtool: "source-map"
75 |
76 | });
77 |
78 | module.exports = createConfig;
79 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestUseReducer.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const counterReducer = (state, action) => {
5 | switch (action.type) {
6 | case "increment":
7 | return { count: state.count + 1 }
8 | case "decrement":
9 | return { count: state.count - 1 }
10 | default:
11 | throw new Error("Unhandled action:", action)
12 | }
13 | };
14 |
15 | const ReducerInitFunction = hookup(
16 | (
17 | { attrs: { initialCount } },
18 | { useReducer }
19 | ) => {
20 | // test setting state using init function
21 | const initState = value => ({ count: value });
22 | const [countState,] = useReducer(counterReducer, initialCount, initState);
23 | const count = countState.count;
24 |
25 | return m("[data-test-id=ReducerInitFunction]", [
26 | m("h2", "ReducerInitFunction"),
27 | m("p", [
28 | m("span", "count: "),
29 | m("span[data-test-id=count]", count)
30 | ]),
31 | m("p", [
32 | m("span", "state: "),
33 | m("span[data-test-id=state]", JSON.stringify(countState))
34 | ]),
35 | ]);
36 | });
37 |
38 | const ReducerCounter = hookup(
39 | (
40 | { attrs: { initialCount } },
41 | { useReducer }
42 | ) => {
43 | const [countState, dispatch] = useReducer(counterReducer, { count: initialCount });
44 | const count = countState.count;
45 |
46 | return m("[data-test-id=ReducerCounter]", [
47 | m("h2", "ReducerCounter"),
48 | m("p", [
49 | m("span", "count: "),
50 | m("span[data-test-id=count]", count)
51 | ]),
52 | m("p", [
53 | m("span", "state: "),
54 | m("span[data-test-id=state]", JSON.stringify(countState))
55 | ]),
56 | m("button[data-test-id=decrement]", {
57 | disabled: count === 0,
58 | onclick: () => dispatch({ type: "decrement" })
59 | }, "Less"),
60 | m("button[data-test-id=increment]", {
61 | onclick: () => dispatch({ type: "increment" })
62 | }, "More")
63 | ]);
64 | });
65 |
66 | export default ({
67 | view: () => [
68 | m(ReducerCounter, { initialCount: 10 }),
69 | m(ReducerInitFunction, { initialCount: 99 }),
70 | ]
71 | });
72 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/custom-hooks-usereducer/Counter.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { withHooks } from "mithril-hookup";
3 | import customHooks from "./customHooks";
4 |
5 | const counterReducer = (state, action) => {
6 | switch (action.type) {
7 | case "increment":
8 | return { count: state.count + 1 };
9 | case "decrement":
10 | return { count: state.count - 1 };
11 | default:
12 | throw new Error("Unhandled action:", action);
13 | }
14 | };
15 |
16 | const Counter = ({ id, initialCount, removeCounter, useEffect, useState, useRef, useReducer }) => {
17 | const [countState, dispatch] = useReducer(counterReducer, { count: initialCount });
18 | const count = countState.count;
19 |
20 | const [inited, setInited] = useState(false);
21 | const dom = useRef();
22 | const domCountElement = useRef();
23 |
24 | const remove = () => {
25 | const removeOnTransitionEnd = () => (
26 | removeCounter(id),
27 | dom.current.removeEventListener("transitionend", removeOnTransitionEnd)
28 | );
29 | dom.current.addEventListener("transitionend", removeOnTransitionEnd);
30 | dom.current.classList.remove("active");
31 | };
32 |
33 | useEffect(() => {
34 | setInited(true);
35 | }, [/* empty array: only run at mount */]);
36 |
37 | return (
38 | m(".counter",
39 | {
40 | className: inited ? "active" : "",
41 | oncreate: vnode => dom.current = vnode.dom,
42 | },
43 | m(".counter-inner", [
44 | m(".count", {
45 | oncreate: vnode => domCountElement.current = vnode.dom
46 | }, count),
47 | m("button",
48 | {
49 | className: "button",
50 | disabled: count === 0,
51 | onclick: () => dispatch({ type: "decrement" })
52 | },
53 | m("span.icon.is-small",
54 | m("i.fas.fa-minus")
55 | )
56 | ),
57 | m("button",
58 | {
59 | className: "button",
60 | onclick: () => dispatch({ type: "increment" })
61 | },
62 | m("span.icon.is-small",
63 | m("i.fas.fa-plus")
64 | )
65 | ),
66 | m(".spacer"),
67 | m("button", {
68 | className: "delete is-large",
69 | onclick: () => remove()
70 | }, "Remove me"),
71 | ])
72 | )
73 | );
74 | };
75 | const HookedCounter = withHooks(Counter, customHooks);
76 |
77 | export default HookedCounter;
78 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupUpdateRules.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | let renderRunCounts = {
5 | mountOnly: 0,
6 | onChange: 0,
7 | render: 0,
8 | };
9 |
10 | const RunCountOnMount = hookup((vnode, { useState, useEffect }) => {
11 | const [effectRunCount, setEffectRunCounts] = useState(0);
12 |
13 | renderRunCounts.mountOnly++;
14 | useEffect(
15 | () => {
16 | setEffectRunCounts(n => n + 1);
17 | },
18 | []
19 | );
20 | return m("[data-test-id=RunCountOnMount]", [
21 | m("h2", "RunCountOnMount"),
22 | m("p[data-test-id=effectRunCount]",
23 | `effect called: ${effectRunCount}`
24 | ),
25 | m("p[data-test-id=renderRunCounts]",
26 | `render called: ${renderRunCounts.mountOnly}`
27 | ),
28 | m("button[data-test-id=button]",
29 | { onclick: () => { } },
30 | "Trigger render"
31 | ),
32 | ]);
33 | });
34 |
35 | const RunCountOnChange = hookup((vnode, { useState, useEffect }) => {
36 | const [effectRunCount, setEffectRunCounts] = useState(0);
37 | const [someValue, setSomeValue] = useState(0);
38 |
39 | renderRunCounts.onChange++;
40 | useEffect(
41 | () => {
42 | setEffectRunCounts(n => n + 1);
43 | },
44 | [someValue]
45 | );
46 | return m("[data-test-id=RunCountOnChange]", [
47 | m("h2", "RunCountOnChange"),
48 | m("p[data-test-id=effectRunCount]",
49 | `effect called: ${effectRunCount}`
50 | ),
51 | m("p[data-test-id=renderRunCounts]",
52 | `render called: ${renderRunCounts.onChange}`
53 | ),
54 | m("button[data-test-id=button]",
55 | { onclick: () => setSomeValue(someValue + 1) },
56 | "Trigger render"
57 | ),
58 | ]);
59 | });
60 |
61 | const RunCountOnRender = hookup((vnode, { useState, useEffect }) => {
62 | const [effectRunCount, setEffectRunCounts] = useState(0);
63 | const [someValue, setSomeValue] = useState(0);
64 |
65 | renderRunCounts.render++;
66 | useEffect(
67 | () => {
68 | setEffectRunCounts(n => n + 1);
69 | },
70 | [someValue]
71 | );
72 | return m("[data-test-id=RunCountOnRender]", [
73 | m("h2", "RunCountOnRender"),
74 | m("p[data-test-id=effectRunCount]",
75 | `effect called: ${effectRunCount}`
76 | ),
77 | m("p[data-test-id=renderRunCounts]",
78 | `render called: ${renderRunCounts.render}`
79 | ),
80 | m("button[data-test-id=button]",
81 | { onclick: () => setSomeValue(someValue + 1) },
82 | "Trigger render"
83 | ),
84 | ]);
85 | });
86 |
87 | export default ({
88 | view: () => [
89 | m(RunCountOnMount),
90 | m(RunCountOnChange),
91 | m(RunCountOnRender),
92 | ]
93 | });
94 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestWithHooks.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { withHooks } from "mithril-hookup";
3 |
4 | const myCustomHooks = ({ useState }) => {
5 | // Use a name to access it from hook functions
6 | const hooks = {
7 | useCounter: () => {
8 | // A custom hook that uses another custom hook.
9 | const createNewCounter = () => ({
10 | id: new Date().getTime(),
11 | initialCount: Math.round(Math.random() * 10)
12 | });
13 | const firstCounter = createNewCounter();
14 | const [counters, addCounter, removeCounter] = hooks.useArray([firstCounter]);
15 | return [
16 | counters,
17 | () => addCounter(createNewCounter()),
18 | remove => removeCounter(remove)
19 | ];
20 | },
21 | useArray: (initialValue = []) => {
22 | const [arr, setArr] = useState(initialValue)
23 | return [
24 | arr,
25 | add => setArr(arr.concat(add)),
26 | remove => setArr(arr.filter(item => item !== remove))
27 | ];
28 | },
29 | };
30 | return hooks;
31 | };
32 |
33 | const Counter = ({ initialCount, useState, useCounter }) => {
34 | const [count, setCount] = useState(initialCount);
35 | const [counters, addCounter,] = useCounter();
36 | return m("div[data-test-id=counter]", [
37 | m("div", m("span[data-test-id=count]", count)),
38 | m("button[data-test-id=add-count]", {
39 | onclick: () => setCount(count + 1)
40 | }, "More"),
41 | m("div", m("span[data-test-id=counters]", counters.length)),
42 | m("button[data-test-id=add-counter]", {
43 | onclick: () => addCounter()
44 | }, "Add counter")
45 | ]);
46 | };
47 |
48 | const SimpleCounter = ({ initialCount, useState }) => {
49 | const [count, setCount] = useState(initialCount);
50 | return m("div[data-test-id=simple-counter]", [
51 | m("div", m("span[data-test-id=count]", count)),
52 | m("button[data-test-id=add-count]", {
53 | onclick: () => setCount(count + 1)
54 | }, "More"),
55 | ]);
56 | };
57 |
58 | const SimpleCounterWithChildren = ({ initialCount, useState, children }) => {
59 | const [count, setCount] = useState(initialCount);
60 | return m("div[data-test-id=simple-counter-with-children]", [
61 | m("div", m("span[data-test-id=count]", count)),
62 | m("button[data-test-id=add-count]", {
63 | onclick: () => setCount(count + 1)
64 | }, "More"),
65 | m("div[data-test-id=children]",
66 | children
67 | )
68 | ]);
69 | };
70 |
71 | const HookedCounter = withHooks(Counter, myCustomHooks);
72 | const HookedSimpleCounter = withHooks(SimpleCounter);
73 | const HookedSimpleCounterWithChildren = withHooks(SimpleCounterWithChildren);
74 |
75 | export default ({
76 | view: () => [
77 | m(HookedCounter, { initialCount: 1 }),
78 | m(HookedSimpleCounter, { initialCount: 10 }),
79 | m(HookedSimpleCounterWithChildren, { initialCount: 10 }, [
80 | m("div", "One"),
81 | m("div", "Two"),
82 | m("div", "Three"),
83 | ]),
84 | ]
85 | });
86 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/cypress-tests/TestHookupCustomHooks.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import { hookup } from "mithril-hookup";
3 |
4 | const customCounterHooks = ({ useState }) => ({
5 | useCount: (initialValue = 0) => {
6 | const [count, setCount] = useState(initialValue);
7 | return [
8 | count, // value
9 | () => setCount(count + 1), // increment
10 | () => setCount(count - 1) // decrement
11 | ];
12 | }
13 | });
14 |
15 | const CounterCustomHooks = hookup((vnode, { useCount }) => {
16 | const [count, increment, decrement] = useCount(0);
17 |
18 | return m("[data-test-id=CounterCustomHooks]", [
19 | m("h2", "CounterCustomHooks"),
20 | m("p", [
21 | m("span", "count: "),
22 | m("span[data-test-id=count]", count)
23 | ]),
24 | m("button[data-test-id=decrement]",
25 | {
26 | disabled: count === 0,
27 | onclick: () => decrement()
28 | },
29 | "Less"
30 | ),
31 | m("button[data-test-id=increment]",
32 | {
33 | onclick: () => increment()
34 | },
35 | "More"
36 | )
37 | ]);
38 | }, customCounterHooks);
39 |
40 | const customItemsHooks = ({ useState }) => {
41 | // Use a name to access it from hook functions
42 | const hooks = {
43 | useCounter: () => {
44 | // A custom hook that uses another custom hook.
45 | const createNewCounter = () => ({
46 | id: new Date().getTime(),
47 | initialCount: Math.round(Math.random() * 1000)
48 | });
49 | const firstCounter = createNewCounter();
50 | const [counters, addCounter, removeCounter] = hooks.useArray([firstCounter]);
51 | return [
52 | counters,
53 | () => addCounter(createNewCounter()),
54 | remove => removeCounter(remove)
55 | ];
56 | },
57 | useArray: (initialValue = []) => {
58 | const [arr, setArr] = useState(initialValue)
59 | return [
60 | arr,
61 | add => setArr(arr.concat(add)),
62 | remove => setArr(arr.filter(item => item !== remove))
63 | ];
64 | },
65 | };
66 | return hooks;
67 | };
68 |
69 | const ItemsCustomHooks = hookup((vnode, { useCounter }) => {
70 | const [counters, addCounter, removeCounter] = useCounter();
71 | const [lastItem, ] = counters.reverse();
72 |
73 | return m("[data-test-id=ItemsCustomHooks]", [
74 | m("h2", "ItemsCustomHooks"),
75 | m("p", [
76 | m("span", "counters: "),
77 | m("span[data-test-id=count]", counters.length)
78 | ]),
79 | m("button[data-test-id=decrement]",
80 | {
81 | disabled: counters.length === 0,
82 | onclick: () => removeCounter(lastItem)
83 | },
84 | "Remove"
85 | ),
86 | m("button[data-test-id=increment]",
87 | {
88 | onclick: () => addCounter()
89 | },
90 | "Add"
91 | )
92 | ]);
93 | }, customItemsHooks);
94 |
95 | export default ({
96 | view: () => [
97 | m(CounterCustomHooks),
98 | m(ItemsCustomHooks),
99 | ]
100 | });
101 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/dist/mithril-hookup.js:
--------------------------------------------------------------------------------
1 | !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("mithril")):"function"==typeof define&&define.amd?define(["exports","mithril"],t):t((n=n||self).mithrilHookup=n.mithrilHookup||{},n.m)}(this,function(n,t){"use strict";function r(n,t,r){return t in n?Object.defineProperty(n,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):n[t]=r,n}function e(n){for(var t=1;t0?!n.every(function(n,t){return n===r[t]}):!i);return l[t]=n,e},d=function(){var n=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return function(t,r){if(h(r)){var e=function(){var n=t();"function"==typeof n&&(p.set(t,n),p.set("_",v))};y.push(n?function(){return new Promise(function(n){return requestAnimationFrame(n)}).then(e)}:e)}}},b=function(n){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(n){return n},r=a++;return i||(f[r]=n),[f[r],function(n){var e=f[r],o=t(n,r);f[r]=o,o!==e&&v()}]},m=function(n,t){var r=h(t),e=o(i?b():b(n()),2),u=e[0],c=e[1];return i&&r&&c(n()),u},w={useState:function(n){return b(n,function(n,t){return"function"==typeof n?n(f[t]):n})},useEffect:d(!0),useLayoutEffect:d(),useReducer:function(n,t,r){var e=!i&&r?r(t):t,u=o(b(e),2),c=u[0],f=u[1];return[c,function(t){return f(n(c,t))}]},useRef:function(n){return o(b({current:n}),1)[0]},useMemo:m,useCallback:function(n,t){return m(function(){return n},t)}},g=e({},w,r&&r(w)),O=function(){y.forEach(c),y.length=0,s=0,a=0};return{view:function(t){return n(t,g)},oncreate:function(){return O(),i=!0},onupdate:O,onremove:function(){u(p.values()).forEach(c)}}}},c=Function.prototype.call.bind(Function.prototype.call);n.hookup=i,n.withHooks=function(n,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return function(n){return i(function(t,r){return n(e({},t.attrs,r,{children:t.children}))})}(function(o){var u=null!=t?t(o):{};return n(e({},o,u,r))})},Object.defineProperty(n,"__esModule",{value:!0})});
2 | //# sourceMappingURL=mithril-hookup.js.map
3 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/src/index.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 | import CounterController from "./custom-hooks-usereducer";
3 | import Toggle from "./toggle";
4 |
5 | import TestHookupCustomHooks from "./cypress-tests/TestHookupCustomHooks";
6 | import TestHookupUseState from "./cypress-tests/TestHookupUseState";
7 | import TestHookupUseRef from "./cypress-tests/TestHookupUseRef";
8 | import TestHookupUseCallback from "./cypress-tests/TestHookupUseCallback";
9 | import TestHookupUseEffect from "./cypress-tests/TestHookupUseEffect";
10 | import TestHookupUseLayoutEffect from "./cypress-tests/TestHookupUseLayoutEffect";
11 | import TestHookupUseMemo from "./cypress-tests/TestHookupUseMemo";
12 | import TestUseReducer from "./cypress-tests/TestUseReducer";
13 | import TestHookupUpdateRules from "./cypress-tests/TestHookupUpdateRules";
14 | import TestWithHooks from "./cypress-tests/TestWithHooks";
15 | import TestEffectTiming from "./cypress-tests/TestEffectTiming";
16 | import TestEffectRenderCounts from "./cypress-tests/TestEffectRenderCounts";
17 | import TestWithHooksExtraArguments from "./cypress-tests/TestWithHooksExtraArguments";
18 |
19 | const links = [
20 | ["Simple toggle", "/toggle", Toggle],
21 | ["Custom hooks with useReducer", "/custom-hooks-usereducer", CounterController],
22 | ];
23 |
24 | const tests = [
25 | ["Test hookup custom hooks", "/TestHookupCustomHooks", TestHookupCustomHooks],
26 | ["Test hookup useState", "/TestHookupUseState", TestHookupUseState],
27 | ["Test hookup useRef", "/TestHookupUseRef", TestHookupUseRef],
28 | ["Test hookup useCallback", "/TestHookupUseCallback", TestHookupUseCallback],
29 | ["Test hookup useEffect", "/TestHookupUseEffect", TestHookupUseEffect],
30 | ["Test hookup useLayoutEffect", "/TestHookupUseLayoutEffect", TestHookupUseLayoutEffect],
31 | ["Test hookup useMemo", "/TestHookupUseMemo", TestHookupUseMemo],
32 | ["Test hookup useReducer", "/TestUseReducer", TestUseReducer],
33 | ["Test hookup update rules", "/TestHookupUpdateRules", TestHookupUpdateRules],
34 | ["Test withHooks", "/TestWithHooks", TestWithHooks],
35 | ["Test withHooks extra arguments", "/TestWithHooksExtraArguments", TestWithHooksExtraArguments],
36 | ["Test effect timing", "/TestEffectTiming", TestEffectTiming],
37 | ["Test effect render counts", "/TestEffectRenderCounts", TestEffectRenderCounts],
38 | ];
39 |
40 | const link = (href, currentRoute, label) =>
41 | m("li",
42 | m("a", {
43 | href,
44 | oncreate: m.route.link,
45 | className: href === currentRoute ? "is-active" : ""
46 | },
47 | label)
48 | );
49 |
50 | const createMenu = currentRoute => (
51 | m("aside.menu", [
52 | m("p.menu-label", "mithril-hooks Demos"),
53 | m("ul.menu-list",
54 | links.map(([label, href]) =>
55 | link(href, currentRoute, label)
56 | )
57 | ),
58 | tests.length
59 | ? (
60 | m("p.menu-label", "Cypress tests"),
61 | m("ul.menu-list",
62 | tests.map(([label, href]) =>
63 | link(href, currentRoute, label)
64 | )
65 | )
66 | )
67 | : null
68 | ])
69 | );
70 |
71 | const Layout = {
72 | view: vnode =>
73 | m(".layout", [
74 | createMenu(m.route.get()),
75 | m(".component", vnode.children)
76 | ])
77 | };
78 |
79 | const root = document.getElementById("root");
80 | const allLinks = links.concat(tests);
81 |
82 | const routes = allLinks.reduce((acc, [, href, Component]) => (
83 | acc[href] = {
84 | render: () =>
85 | m(Layout, { href }, m(Component))
86 | },
87 | acc
88 | ), {});
89 |
90 | const [,firstRoute,] = allLinks[0];
91 | m.route(root, firstRoute, routes);
92 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/src/hookup.js:
--------------------------------------------------------------------------------
1 | import m from "mithril";
2 |
3 | export const hookup = (closure, addHooks) => (/* internal vnode, unused */) => {
4 | let setup = false;
5 |
6 | const states = [];
7 | let statesIndex = 0;
8 |
9 | const depsStates = [];
10 | let depsIndex = 0;
11 |
12 | const updates = [];
13 | const teardowns = new Map; // Keep track of teardowns even when the update was run only once
14 |
15 | const scheduleRender = m.redraw;
16 |
17 | const resetAfterUpdate = () => {
18 | updates.length = 0;
19 | depsIndex = 0;
20 | statesIndex = 0;
21 | };
22 |
23 | const updateDeps = deps => {
24 | const index = depsIndex++;
25 | const prevDeps = depsStates[index] || [];
26 | const shouldRecompute = deps === undefined
27 | ? true // Always compute
28 | : Array.isArray(deps)
29 | ? deps.length > 0
30 | ? !deps.every((x,i) => x === prevDeps[i]) // Only compute when one of the deps has changed
31 | : !setup // Empty array: only compute at mount
32 | : false; // Invalid value, do nothing
33 | depsStates[index] = deps;
34 | return shouldRecompute;
35 | };
36 |
37 | const effect = (isAsync = false) => (fn, deps) => {
38 | const shouldRecompute = updateDeps(deps);
39 | if (shouldRecompute) {
40 | const runCallbackFn = () => {
41 | const teardown = fn();
42 | // A callback may return a function. If any, add it to the teardowns:
43 | if (typeof teardown === "function") {
44 | // Store this this function to be called at unmount
45 | teardowns.set(fn, teardown);
46 | // At unmount, call re-render at least once
47 | teardowns.set("_", scheduleRender);
48 | }
49 | };
50 | updates.push(
51 | isAsync
52 | ? () => new Promise(resolve => requestAnimationFrame(resolve)).then(runCallbackFn)
53 | : runCallbackFn
54 | );
55 | }
56 | };
57 |
58 | const updateState = (initialValue, newValueFn = value => value) => {
59 | const index = statesIndex++;
60 | if (!setup) {
61 | states[index] = initialValue;
62 | }
63 | return [
64 | states[index],
65 | value => {
66 | const previousValue = states[index];
67 | const newValue = newValueFn(value, index);
68 | states[index] = newValue;
69 | if (newValue !== previousValue) {
70 | scheduleRender(); // Calling redraw multiple times: Mithril will drop extraneous redraw calls, so performance should not be an issue
71 | }
72 | }
73 | ];
74 | };
75 |
76 | // Hook functions
77 |
78 | const useState = initialValue => {
79 | const newValueFn = (value, index) =>
80 | typeof value === "function"
81 | ? value(states[index])
82 | : value;
83 | return updateState(initialValue, newValueFn);
84 | };
85 |
86 | const useReducer = (reducer, initialArg, initFn) => {
87 | // From the React docs: You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).
88 | const initialState = !setup && initFn
89 | ? initFn(initialArg)
90 | : initialArg;
91 | const [state, setState] = updateState(initialState);
92 | const dispatch = action =>
93 | setState( // Next state:
94 | reducer(state, action)
95 | );
96 | return [state, dispatch];
97 | };
98 |
99 | const useRef = initialValue => {
100 | // A ref is a persisted object that will not be updated, so it has no setter
101 | const [value] = updateState({ current: initialValue });
102 | return value;
103 | };
104 |
105 | const useMemo = (fn, deps) => {
106 | const shouldRecompute = updateDeps(deps);
107 | const [memoized, setMemoized] = !setup
108 | ? updateState(fn())
109 | : updateState();
110 | if (setup && shouldRecompute) {
111 | setMemoized(fn());
112 | }
113 | return memoized;
114 | };
115 |
116 | const useCallback = (fn, deps) =>
117 | useMemo(() => fn, deps);
118 |
119 | const defaultHooks = {
120 | useState,
121 | useEffect: effect(true),
122 | useLayoutEffect: effect(),
123 | useReducer,
124 | useRef,
125 | useMemo,
126 | useCallback,
127 | };
128 |
129 | const hooks = {
130 | ...defaultHooks,
131 | ...(addHooks && addHooks(defaultHooks))
132 | };
133 |
134 | const update = () => {
135 | updates.forEach(call);
136 | resetAfterUpdate();
137 | };
138 |
139 | const teardown = () => {
140 | [...teardowns.values()].forEach(call);
141 | };
142 |
143 | return {
144 | view: vnode => closure(vnode, hooks),
145 | oncreate: () => (
146 | update(),
147 | setup = true
148 | ),
149 | onupdate: update,
150 | onremove: teardown
151 | };
152 | };
153 |
154 | const call = Function.prototype.call.bind(
155 | Function.prototype.call
156 | );
157 |
--------------------------------------------------------------------------------
/packages/mithril-hookup/dist/mithril-hookup.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"mithril-hookup.js","sources":["../src/hookup.js","../src/utils.js"],"sourcesContent":["import m from \"mithril\";\n\nexport const hookup = (closure, addHooks) => (/* internal vnode, unused */) => {\n let setup = false;\n \n const states = [];\n let statesIndex = 0;\n \n const depsStates = [];\n let depsIndex = 0;\n \n const updates = [];\n const teardowns = new Map; // Keep track of teardowns even when the update was run only once\n \n const scheduleRender = m.redraw;\n \n const resetAfterUpdate = () => {\n updates.length = 0;\n depsIndex = 0;\n statesIndex = 0;\n };\n \n const updateDeps = deps => {\n const index = depsIndex++;\n const prevDeps = depsStates[index] || [];\n const shouldRecompute = deps === undefined\n ? true // Always compute\n : Array.isArray(deps)\n ? deps.length > 0\n ? !deps.every((x,i) => x === prevDeps[i]) // Only compute when one of the deps has changed\n : !setup // Empty array: only compute at mount\n : false; // Invalid value, do nothing\n depsStates[index] = deps;\n return shouldRecompute;\n };\n \n const effect = (isAsync = false) => (fn, deps) => {\n const shouldRecompute = updateDeps(deps);\n if (shouldRecompute) {\n const runCallbackFn = () => {\n const teardown = fn();\n // A callback may return a function. If any, add it to the teardowns:\n if (typeof teardown === \"function\") {\n // Store this this function to be called at unmount\n teardowns.set(fn, teardown);\n // At unmount, call re-render at least once\n teardowns.set(\"_\", scheduleRender);\n }\n };\n updates.push(\n isAsync\n ? () => new Promise(resolve => requestAnimationFrame(resolve)).then(runCallbackFn)\n : runCallbackFn\n );\n }\n };\n \n const updateState = (initialValue, newValueFn = value => value) => {\n const index = statesIndex++;\n if (!setup) {\n states[index] = initialValue;\n }\n return [\n states[index],\n value => {\n const previousValue = states[index];\n const newValue = newValueFn(value, index);\n states[index] = newValue;\n if (newValue !== previousValue) {\n scheduleRender(); // Calling redraw multiple times: Mithril will drop extraneous redraw calls, so performance should not be an issue\n }\n }\n ];\n };\n \n // Hook functions\n\n const useState = initialValue => {\n const newValueFn = (value, index) =>\n typeof value === \"function\"\n ? value(states[index])\n : value;\n return updateState(initialValue, newValueFn);\n };\n \n const useReducer = (reducer, initialArg, initFn) => {\n // From the React docs: You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).\n const initialState = !setup && initFn\n ? initFn(initialArg)\n : initialArg;\n const [state, setState] = updateState(initialState);\n const dispatch = action =>\n setState( // Next state:\n reducer(state, action)\n );\n return [state, dispatch];\n };\n \n const useRef = initialValue => {\n // A ref is a persisted object that will not be updated, so it has no setter\n const [value] = updateState({ current: initialValue });\n return value;\n };\n \n const useMemo = (fn, deps) => {\n const shouldRecompute = updateDeps(deps);\n const [memoized, setMemoized] = !setup\n ? updateState(fn())\n : updateState();\n if (setup && shouldRecompute) {\n setMemoized(fn());\n }\n return memoized;\n };\n \n const useCallback = (fn, deps) =>\n useMemo(() => fn, deps);\n \n const defaultHooks = {\n useState,\n useEffect: effect(true),\n useLayoutEffect: effect(),\n useReducer,\n useRef,\n useMemo,\n useCallback,\n };\n \n const hooks = {\n ...defaultHooks,\n ...(addHooks && addHooks(defaultHooks))\n };\n \n const update = () => {\n updates.forEach(call);\n resetAfterUpdate();\n };\n \n const teardown = () => {\n [...teardowns.values()].forEach(call);\n };\n \n return {\n view: vnode => closure(vnode, hooks),\n oncreate: () => (\n update(),\n setup = true\n ),\n onupdate: update,\n onremove: teardown\n };\n};\n\nconst call = Function.prototype.call.bind(\n Function.prototype.call\n);\n","import { hookup } from \"./hookup\";\n\nconst hookupComponent = component =>\n hookup((vnode, hooks) => (\n component({ ...vnode.attrs, ...hooks, children: vnode.children })\n ));\n\nexport const withHooks = (component, customHooksFn, rest = {}) =>\n hookupComponent(\n hooks => {\n const customHooks = customHooksFn !== undefined && customHooksFn !== null\n ? customHooksFn(hooks)\n : {};\n return component({ ...hooks, ...customHooks, ...rest });\n }\n );\n"],"names":["hookup","closure","addHooks","setup","states","statesIndex","depsStates","depsIndex","updates","teardowns","Map","scheduleRender","m","redraw","updateDeps","deps","index","prevDeps","shouldRecompute","undefined","Array","isArray","length","every","x","i","effect","isAsync","fn","runCallbackFn","teardown","set","push","Promise","resolve","requestAnimationFrame","then","updateState","initialValue","newValueFn","value","previousValue","newValue","useMemo","memoized","setMemoized","defaultHooks","useState","useEffect","useLayoutEffect","useReducer","reducer","initialArg","initFn","initialState","state","setState","action","useRef","current","useCallback","hooks","update","forEach","call","view","vnode","oncreate","onupdate","onremove","values","Function","prototype","bind","component","customHooksFn","rest","attrs","children","hookupComponent","customHooks"],"mappings":"k9CAEaA,EAAS,SAACC,EAASC,UAAa,eACvCC,GAAQ,EAENC,EAAa,GACfC,EAAe,EAEbC,EAAa,GACfC,EAAe,EAEbC,EAAa,GACbC,EAAa,IAAIC,IAEjBC,EAAiBC,EAAEC,OAQnBC,EAAa,SAAAC,OACXC,EAAQT,IACRU,EAAWX,EAAWU,IAAU,GAChCE,OAA2BC,IAATJ,KAEpBK,MAAMC,QAAQN,KACZA,EAAKO,OAAS,GACXP,EAAKQ,MAAM,SAACC,EAAEC,UAAMD,IAAMP,EAASQ,MACnCtB,UAETG,EAAWU,GAASD,EACbG,GAGHQ,EAAS,eAACC,iEAAoB,SAACC,EAAIb,MACfD,EAAWC,GACd,KACbc,EAAgB,eACdC,EAAWF,IAEO,mBAAbE,IAETrB,EAAUsB,IAAIH,EAAIE,GAElBrB,EAAUsB,IAAI,IAAKpB,KAGvBH,EAAQwB,KACNL,EACI,kBAAM,IAAIM,QAAQ,SAAAC,UAAWC,sBAAsBD,KAAUE,KAAKP,IAClEA,MAKJQ,EAAc,SAACC,OAAcC,yDAAa,SAAAC,UAASA,GACjDxB,EAAQX,WACTF,IACHC,EAAOY,GAASsB,GAEX,CACLlC,EAAOY,GACP,SAAAwB,OACQC,EAAgBrC,EAAOY,GACvB0B,EAAWH,EAAWC,EAAOxB,GACnCZ,EAAOY,GAAS0B,EACZA,IAAaD,GACf9B,OAmCFgC,EAAU,SAACf,EAAIb,OACbG,EAAkBJ,EAAWC,OACFZ,EAE7BkC,IADAA,EAAYT,QADTgB,OAAUC,cAGb1C,GAASe,GACX2B,EAAYjB,KAEPgB,GAMHE,EAAe,CACnBC,SA1Ce,SAAAT,UAKRD,EAAYC,EAJA,SAACE,EAAOxB,SACR,mBAAVwB,EACHA,EAAMpC,EAAOY,IACbwB,KAuCNQ,UAAWtB,GAAO,GAClBuB,gBAAiBvB,IACjBwB,WArCiB,SAACC,EAASC,EAAYC,OAEjCC,GAAgBnD,GAASkD,EAC3BA,EAAOD,GACPA,MACsBf,EAAYiB,MAA/BC,OAAOC,aAKP,CAACD,EAJS,SAAAE,UACfD,EACEL,EAAQI,EAAOE,OA8BnBC,OAzBa,SAAApB,YAEGD,EAAY,CAAEsB,QAASrB,WAwBvCK,QAAAA,EACAiB,YAVkB,SAAChC,EAAIb,UACvB4B,EAAQ,kBAAMf,GAAIb,KAYd8C,OACDf,EACC5C,GAAYA,EAAS4C,IAGrBgB,EAAS,WACbtD,EAAQuD,QAAQC,GArHhBxD,EAAQc,OAAS,EACjBf,EAAY,EACZF,EAAc,SA2HT,CACL4D,KAAM,SAAAC,UAASjE,EAAQiE,EAAOL,IAC9BM,SAAU,kBACRL,IACA3D,GAAQ,GAEViE,SAAUN,EACVO,SAXe,aACX5D,EAAU6D,UAAUP,QAAQC,OAc9BA,EAAOO,SAASC,UAAUR,KAAKS,KACnCF,SAASC,UAAUR,6BCnJI,SAACU,EAAWC,OAAeC,yDAAO,UALnC,SAAAF,UACtB1E,EAAO,SAACkE,EAAOL,UACba,OAAeR,EAAMW,MAAUhB,GAAOiB,SAAUZ,EAAMY,cAIxDC,CACE,SAAAlB,OACQmB,EAAcL,MAAAA,EAChBA,EAAcd,GACd,UACGa,OAAeb,EAAUmB,EAAgBJ"}
--------------------------------------------------------------------------------
/packages/mithril-hookup/dist/mithril-hookup.mjs:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 |
3 | function _defineProperty(obj, key, value) {
4 | if (key in obj) {
5 | Object.defineProperty(obj, key, {
6 | value: value,
7 | enumerable: true,
8 | configurable: true,
9 | writable: true
10 | });
11 | } else {
12 | obj[key] = value;
13 | }
14 |
15 | return obj;
16 | }
17 |
18 | function _objectSpread(target) {
19 | for (var i = 1; i < arguments.length; i++) {
20 | var source = arguments[i] != null ? arguments[i] : {};
21 | var ownKeys = Object.keys(source);
22 |
23 | if (typeof Object.getOwnPropertySymbols === 'function') {
24 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
25 | return Object.getOwnPropertyDescriptor(source, sym).enumerable;
26 | }));
27 | }
28 |
29 | ownKeys.forEach(function (key) {
30 | _defineProperty(target, key, source[key]);
31 | });
32 | }
33 |
34 | return target;
35 | }
36 |
37 | function _slicedToArray(arr, i) {
38 | return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
39 | }
40 |
41 | function _arrayWithHoles(arr) {
42 | if (Array.isArray(arr)) return arr;
43 | }
44 |
45 | function _iterableToArrayLimit(arr, i) {
46 | var _arr = [];
47 | var _n = true;
48 | var _d = false;
49 | var _e = undefined;
50 |
51 | try {
52 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
53 | _arr.push(_s.value);
54 |
55 | if (i && _arr.length === i) break;
56 | }
57 | } catch (err) {
58 | _d = true;
59 | _e = err;
60 | } finally {
61 | try {
62 | if (!_n && _i["return"] != null) _i["return"]();
63 | } finally {
64 | if (_d) throw _e;
65 | }
66 | }
67 |
68 | return _arr;
69 | }
70 |
71 | function _nonIterableRest() {
72 | throw new TypeError("Invalid attempt to destructure non-iterable instance");
73 | }
74 |
75 | const hookup = (closure, addHooks) => () =>
76 | /* internal vnode, unused */
77 | {
78 | let setup = false;
79 | const states = [];
80 | let statesIndex = 0;
81 | const depsStates = [];
82 | let depsIndex = 0;
83 | const updates = [];
84 | const teardowns = new Map(); // Keep track of teardowns even when the update was run only once
85 |
86 | const scheduleRender = m.redraw;
87 |
88 | const resetAfterUpdate = () => {
89 | updates.length = 0;
90 | depsIndex = 0;
91 | statesIndex = 0;
92 | };
93 |
94 | const updateDeps = deps => {
95 | const index = depsIndex++;
96 | const prevDeps = depsStates[index] || [];
97 | const shouldRecompute = deps === undefined ? true // Always compute
98 | : Array.isArray(deps) ? deps.length > 0 ? !deps.every((x, i) => x === prevDeps[i]) // Only compute when one of the deps has changed
99 | : !setup // Empty array: only compute at mount
100 | : false; // Invalid value, do nothing
101 |
102 | depsStates[index] = deps;
103 | return shouldRecompute;
104 | };
105 |
106 | const effect = function effect() {
107 | let isAsync = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
108 | return (fn, deps) => {
109 | const shouldRecompute = updateDeps(deps);
110 |
111 | if (shouldRecompute) {
112 | const runCallbackFn = () => {
113 | const teardown = fn(); // A callback may return a function. If any, add it to the teardowns:
114 |
115 | if (typeof teardown === "function") {
116 | // Store this this function to be called at unmount
117 | teardowns.set(fn, teardown); // At unmount, call re-render at least once
118 |
119 | teardowns.set("_", scheduleRender);
120 | }
121 | };
122 |
123 | updates.push(isAsync ? () => new Promise(resolve => requestAnimationFrame(resolve)).then(runCallbackFn) : runCallbackFn);
124 | }
125 | };
126 | };
127 |
128 | const updateState = function updateState(initialValue) {
129 | let newValueFn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : value => value;
130 | const index = statesIndex++;
131 |
132 | if (!setup) {
133 | states[index] = initialValue;
134 | }
135 |
136 | return [states[index], value => {
137 | const previousValue = states[index];
138 | const newValue = newValueFn(value, index);
139 | states[index] = newValue;
140 |
141 | if (newValue !== previousValue) {
142 | scheduleRender(); // Calling redraw multiple times: Mithril will drop extraneous redraw calls, so performance should not be an issue
143 | }
144 | }];
145 | }; // Hook functions
146 |
147 |
148 | const useState = initialValue => {
149 | const newValueFn = (value, index) => typeof value === "function" ? value(states[index]) : value;
150 |
151 | return updateState(initialValue, newValueFn);
152 | };
153 |
154 | const useReducer = (reducer, initialArg, initFn) => {
155 | // From the React docs: You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).
156 | const initialState = !setup && initFn ? initFn(initialArg) : initialArg;
157 |
158 | const _updateState = updateState(initialState),
159 | _updateState2 = _slicedToArray(_updateState, 2),
160 | state = _updateState2[0],
161 | setState = _updateState2[1];
162 |
163 | const dispatch = action => setState( // Next state:
164 | reducer(state, action));
165 |
166 | return [state, dispatch];
167 | };
168 |
169 | const useRef = initialValue => {
170 | // A ref is a persisted object that will not be updated, so it has no setter
171 | const _updateState3 = updateState({
172 | current: initialValue
173 | }),
174 | _updateState4 = _slicedToArray(_updateState3, 1),
175 | value = _updateState4[0];
176 |
177 | return value;
178 | };
179 |
180 | const useMemo = (fn, deps) => {
181 | const shouldRecompute = updateDeps(deps);
182 |
183 | const _ref = !setup ? updateState(fn()) : updateState(),
184 | _ref2 = _slicedToArray(_ref, 2),
185 | memoized = _ref2[0],
186 | setMemoized = _ref2[1];
187 |
188 | if (setup && shouldRecompute) {
189 | setMemoized(fn());
190 | }
191 |
192 | return memoized;
193 | };
194 |
195 | const useCallback = (fn, deps) => useMemo(() => fn, deps);
196 |
197 | const defaultHooks = {
198 | useState,
199 | useEffect: effect(true),
200 | useLayoutEffect: effect(),
201 | useReducer,
202 | useRef,
203 | useMemo,
204 | useCallback
205 | };
206 |
207 | const hooks = _objectSpread({}, defaultHooks, addHooks && addHooks(defaultHooks));
208 |
209 | const update = () => {
210 | updates.forEach(call);
211 | resetAfterUpdate();
212 | };
213 |
214 | const teardown = () => {
215 | [...teardowns.values()].forEach(call);
216 | };
217 |
218 | return {
219 | view: vnode => closure(vnode, hooks),
220 | oncreate: () => (update(), setup = true),
221 | onupdate: update,
222 | onremove: teardown
223 | };
224 | };
225 | const call = Function.prototype.call.bind(Function.prototype.call);
226 |
227 | const hookupComponent = component => hookup((vnode, hooks) => component(_objectSpread({}, vnode.attrs, hooks, {
228 | children: vnode.children
229 | })));
230 |
231 | const withHooks = function withHooks(component, customHooksFn) {
232 | let rest = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
233 | return hookupComponent(hooks => {
234 | const customHooks = customHooksFn !== undefined && customHooksFn !== null ? customHooksFn(hooks) : {};
235 | return component(_objectSpread({}, hooks, customHooks, rest));
236 | });
237 | };
238 |
239 | export { hookup, withHooks };
240 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mithril-hookup
2 |
3 | ## Deprecation
4 |
5 | This project has evolved into [mithril-hooks](https://github.com/ArthurClemens/mithril-hooks)
6 |
7 |
8 | ## Legacy
9 |
10 | Use hooks in Mithril.
11 |
12 | - [Deprecation](#deprecation)
13 | - [Legacy](#legacy)
14 | - [Introduction](#introduction)
15 | - [Online demos](#online-demos)
16 | - [Usage](#usage)
17 | - [Hooks and application logic](#hooks-and-application-logic)
18 | - [Rendering rules](#rendering-rules)
19 | - [With useState](#with-usestate)
20 | - [With other hooks](#with-other-hooks)
21 | - [Cleaning up](#cleaning-up)
22 | - [Default hooks](#default-hooks)
23 | - [useState](#usestate)
24 | - [useEffect](#useeffect)
25 | - [useLayoutEffect](#uselayouteffect)
26 | - [useReducer](#usereducer)
27 | - [useRef](#useref)
28 | - [useMemo](#usememo)
29 | - [useCallback](#usecallback)
30 | - [Omitted hooks](#omitted-hooks)
31 | - [Custom hooks](#custom-hooks)
32 | - [`hookup` function](#hookup-function)
33 | - [Children](#children)
34 | - [Compatibility](#compatibility)
35 | - [Size](#size)
36 | - [Supported browsers](#supported-browsers)
37 | - [History](#history)
38 | - [License](#license)
39 |
40 |
41 | ## Introduction
42 |
43 | Use hook functions from the [React Hooks API](https://reactjs.org/docs/hooks-intro.html) in Mithril:
44 |
45 | * `useState`
46 | * `useEffect`
47 | * `useLayoutEffect`
48 | * `useReducer`
49 | * `useRef`
50 | * `useMemo`
51 | * `useCallback`
52 | * and custom hooks
53 |
54 | ```javascript
55 | import { withHooks } from "mithril-hookup"
56 |
57 | const Counter = ({ useState, initialCount }) => {
58 | const [count, setCount] = useState(initialCount)
59 | return [
60 | m("div", count),
61 | m("button", {
62 | onclick: () => setCount(count + 1)
63 | }, "More")
64 | ]
65 | }
66 |
67 | const HookedCounter = withHooks(Counter)
68 |
69 | m(HookedCounter, { initialCount: 1 })
70 | ```
71 |
72 | ## Online demos
73 |
74 | Editable demos using the [Flems playground](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHgBMIA3AAgjYF4AOiABOtWsWEA+FgHoOnKSAC+FdNlyICAKwRU6DJsTwQsAB1oji3LNzBjbwksTNxEs2QFc0ZgNYBzfDosWSwIYkIRaAABACZ8AAZEgFoRanwAFgB+LFo2T1hhQTRTCytuYG4Ad3DCAAlxXzhuZTsHbmEwiKioZMJGzzMitGKDOGsAYVpzeiNufm4ACkrPOBgAZWIMRhaASnmpCuLubg9uVZhztc3tmGPuERhiTxE0bmR7k6xF4QVhCg6IAAohxrMRaFdLgByC43RhQ4S7e4AXWKylG9HG3AatF8MDYUxmaDmCxqERxTUWhIsxIYSJGaDG1gAgmYzPMjm9uJwIDAqoglvt+FJPjZFhT8dTZnS0cVilh8LlvMQfly2LRqJ4cAx8ABHTwwEQAT3WMFg1HBIh+IAAxGIJIiKPdWUM0LsANzFSggNbm4gQTF4BKIACsAE4VGoQJgcHggnA9DR6IxmFoVMiqFAIGgmkhUNGNHgupFoH0BmZvS9yFpnK53F4fAEgtNQrUemXcYNokl4gBGeQQcat7ql-qditUYhGsyaH3UKJmYzKZHKIA):
75 |
76 | * [Simplest example](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHgBMIA3AAgjYF4AOiABOtWsWEA+FgHoOnKSAC+FdNlyICAKwRU6DJsTwQsAB1oji3LNzBjbwksTNxEs2QFc0ZgNYBzfDosWSwIYkIRaAABACZ8AAZEgFoRanwAFgB+LFo2T1hhQTRTCytuYG4Ad3DCAAlxXzhuZTsHbmEwiKioZMJGzzMitGKDOGsAFVp-f1hufm4ACkrPOBgAZWIMRhaASnmpCuLubjHrZGooCGpfGDYKbjXiAGErm7uAXXnuVY2txkWYAwUDWuwA3MduCIYMRPCI0DZFsIFMIHshIScsEiQAAjTzEYj0VEYk5HBGkin0S7XXyIJb7fiHJ6vGl3RYAQmp7zYuxJJ1UfI6ICmM0KIBJuwoJKxyK4qNOb1ubG4WSFAHkRiBuHThGqwGBhLzyR8jcpRvRxtwGrQlSLZjBvjUItamos7bAjea0JaAIJmMzfYCQzgQGBVOmLBlSaWLF13d0wU3FYpYfC5bzEJHkti0aieHAMfAAR08MBEAE91jBYNRCSJsQBiMQSQ1S8l+oZoXaUEBrGvECAWvAAZkQw4yKjUIEwODwQTgeho9EYzC0Kg+VCuaCaSFQ040eC6kWgfQGZh7cPIWmcrncXh8ASCtBCR56p5tg2iSXiAEZ5BBxlCWo336D9zyoYhyzMTRe2oKIzGMZQN33WctHWUo5hgAAPbAzFgFQgA)
77 | * [Simple form handling with useState](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHgBMIA3AAgjYF4AOiABOtWsWEA+FgHoOnKSAC+FdNlyICAKwRU6DJsTwQsAB1oji3LNzBjbwksTNxEs2QFc0ZgNYBzfDosWSwIYkIRaAABACZ8AAZEgFoRanwAFgB+LFo2T1hhQTRTCytuYG4Ad3DCAAlxXzhuZTsHbmEwiKioZMJGzzMitGKDOGsAYVpzeiNufm4ACkrPOBgAJRgwCm5VmABlYgxGFoBKeakK4u5uMetkaigIal8YNh214gmnl7eAXXmuzWh2OMEWYAwUDWp2ut3o424yD2IkwOA+MGIAFU1iiNACFnsQYxFkUQDC0Dc7oiIHAAAq0cYQND+dHEACSdIZxCZ-nxQIOR2JEKhMHJlPh1kgMCgbE2YEBezli3OHm4xFo3E+dlo1FWxVhVMGbFB2JguJwgJgFyuFJumoxpvNYJg+COIn8GPwnEhnlFsOUAG4DRLuCJ4BiAGKWWwLZXW4Cwm6fR2osGksV2z4c+mM5ngyHQxP24gAFVMMFonmIizj-EuUplcqCnhEYYY+DAOtWyp2AFYEhnA-rbVTCBg0GxYPtPAAjLqW+NFiDyxbI1PcACE-AW6ZtdszGOzXJ5i2IIl9GZurWlaz3++4kLN1eEAFFDCIH-yncJLy0iy6zDDTgjAAES2DACmrQcg1tWEw2IFsKWQIssBJcBo2ECgixuBNbXvW4oAwOA4AAOQ0RAOnQkQsEw7C7XhWcugoscJynRjwjo1Q6NQ4QFEwq4aEI4jhEQYQGzYYRVA6PD7x4kA+MoATHiIuAROEAwz1oKBJJ2Oj9zkoZFIAAxTcjuAAEmANcNG4AAfWzKJIiRi0kozTiwmT91-G53O4tCFJ2BNBJUtTwAgaUJJUXTPLtOSAqUoTVKQdT6E07Souk-D9LQpkzCrWiYvw3Csqy5TiNEkBcvyyg9Ky4gAE8zBgCrGAAD0kGrCqysxCOoGB+hlM0KOEUycAKkr8NTYaQGssbOom+9vSgX0KNmmAPIW-d6Cq4hVrMY1GFG9bavwjg4AwGdYDYCiaRzblmQ2zablVCD1U7XU3DVc91u4VUzouq61Q1CFqGgcJQW4N5wh5NVCBpOxYCwVSuvw+hqDDUEKOWbg2GmM5rXEptdVbOYFlxrBHoW5QTu4by7V8mKGfwuKuH4oKyqSirxJ0zKspZzg2fUxLQo0sR0qkmm5JnKt1RGebNuKp6CJUsicGm6XiFl3g4GSJ40F8calYaprprgdiOspzb-sut4bs5XMWRpq9LZK4R9iYSKabpny6KZ+9buPZluAAMmDpZav5wXguE5KwoinnJf81nFPZ4XY9FrSE5R2K0I12XDaexWleV4jVeayi8-oAujca8vhEruWXc2tGfl8Ciw0+KNqKbkquOz-dhE2T5hie736dqunfwzP5imptAqQaWhXjYKYZjQUnqlqRemkWVeLHXhhyVGEMAEEzDMQFFc4cKqix846xQxZt7ePfZkP2fhywfBcm8Z9bVx3UOB2wAEdfQiHqu7WA1B1QiDQgAYjEBIH8j0z5DDQOSSgIA1hQO5PCPAAAORACQorqBwHgIIxFMEaSMOQzs1E7xmAwGwDgzIKKxASGYVqMFlCYOwTAaBEA8FaASIgXsKg-hUD1k0JAqAQCpjwNLKAWAMDfyZBQvQM0RDkC0M4Vw7hZDUDYGgXQQQoCVjYGAQiYYgjTFkBgbQGBWqyCeDOOAshFHKNkEkAA7JkAxxF3EFGUaotA6jMHG00FghqsASFyI0HgLokRoB9AGGYTBLZtEgF0W4Dw3g-CBGCKEWoPQUlL0GNEJI8QACM8gaTECKd0ZJ-QylpKoBEvAcB0YQDMMYZQEi4lkK0PsUosBtT0JYpOGGNQIj8iJLgZQQA)
78 | * ["Building Your Own Hooks" chat API example](https://flems.io/#0=N4IgtglgJlA2CmIBcBWATAOgOwEYA0IAxgPYB2AzsQskVbAIYAO58UIB5hATncgNoAGPAIC6BAGYQE5fqFL0wiJCAwALAC5hY7WqXXw9NADxQIANwAE0ALwAdED2Lr7APiMB6U2Ze3SOlgiE6hBkMsoCSACcIAC+eHIKSioAVjIEJHoG6jQQYIzEXOoWYBbiPCX2GurMSO7uAK6kjADWAOYYJGDukOqqXFIAApgCGAIAtFyEGAAsAPxgxFD1CPa+ufmFFsAWqsTEzfWMeBYA7hC9ABJ7zeQWMaXlFvY9fVJju-uHq6TrBUXbhHq5HUxDAV32t3uZVBTxU7kBwNB72u5G+v022zKEAMUHIABF6Op6HcHjD7Bh3FiceQxlBCfRvr4MsCLAAxfo4gDKRPUQIs1gsAApMRzSFAAJJQY5A+Ds7Fi7mEvkxACU-JcW18FgszKKEHIAHlSLAIKR4PyLDK5VyeUDBVSxZKVVqLFx4LyuKQrIbjabzdYAxZSMtYC7tbNioLycClTSoE4MI1mqRiCdSPZnV7tRYkN6jSazWHsxGwFGVDHeXGE2QC-AM0XtbnS9HbVX1BhiOJxLX66QYkzQkVwc1WNaFa2LWdLijBWOoIrK8cESCwSjMwOKEU5xbgC6zNiTrnhRZCeouORcyL5VKdvRyABhVSEgCCjAgd1V6s1We1bo9XubFQHSgexjj4BtI3JegzHpLhQIg7Vdx-bMUOBABPBBLwQlCLAAI3oQg2h4RoJTAehWngXMAAN6i4WBBQAEmAYCMGg2DVSo7DtX7ZCUJ4nCLBVPAIMAjB5EUUCHmvMTEiEkSywwchGAI+A4JAOTeNvB8n3UV93wAMn0yDy1bSTS2HUdRXnVtjivHFJVzFjoDuFVMxQkQ3LuXweI3FlH0JAAleBCAgN8sgABQgQjVItY8ZQXeBpRYOcF2VNVrA1JDtV1Cw+DdEKwr0SVjhYdQgoK+V1ElEQLXinl4EFHBPJy-VytCyr8z9Wrkqs1LyEFfL2qyJ0XT-WiAIUjIzyoVF2FyotzOuSzrz62ypPsqBc0GwqqqgFzhOQ0SAmC5x2Hk+xjqCeDNMQriLDIQgn1ICjc39DVSranbJUFAA5eowFw1TBXgDAiS4Cj2xg2B6ngVzsLibDgPxekMDIxh7Ssr8oxu7NAOIRhgjIa6BIErKSYEqGYccqyMGgO7uIO8mUJY8S6xx7VPIEuHNM5wSXQ87zfKHJaoH8srgqGvRIuirhJ3OVRh36sXPsq6WRy4JcgRXRX11IHLX0YWLtjqwlzU-DKtieLMcr4J9tJfN8SvdC47zFvSaoFE39HtehYBYTyxs9ebDsmshpr94mcMA3D6nUEF0zOnGyfJwgGHIcgfsSXM7bdt8LAjewY7jomQBzWEi-j70xgLZpI-Jh6TUIo90ve53XZ0vTBQAQhzju315vjGZJ3uHffAuQDxeACOCGD9B1HT7DL+xnyCcxTfnwlvi5osNJQkfdLzwzIws0WdJViKovV3fcYUpG6+zJGCSJVGmAx68sew0s51su7gKH8n956X-jhVUEFuYoU8iIQW6ZSBgFRsQRo6hsbanjICRQegMAAEcYZcDQpyeAgQQRcDLAAYkcKda+U4FYzgNpmTMOhOhvgQFwGg+FAbaA4AQk6IQKA0AiCgaIcQEiKBoMuJEHwbgYFSAwsOWQch5D+JbJg74oSPHJPCHSYxlH2AANy+F8PAAAHhsIoOUxGrghEbS0LAEpJXgAAUS7CdFyWMyZWl6q2am15JRfmTjqQcuV9SdTNE7KqPpawe2sfABKgpgywFgCqPRMDmaNFXmQW8YoEB9X8s9BqFYgRqj8dqUq4pwl+kFPk8gtMylmkSUWHyyEZSOPECdQUgoW7fgEsoxS9RcKcH6IDAAKsQFKrY34bWOE9OA0TWw5IonUzSgcvTtN8dhbpjRyC9P6RAQG7JQSjNjOMx0N4plZNmU9eZSSBIxCudmVUSSixLLzL6M0tyeL3N8DIvIUhVKsPoOw-wXC0m8OUDgFAqBYjxBAKzURmjunSPSLIwwygcqP3pDuF0EROnZkqVhTSQSXmUQsGeGG9SXQIyzDgPFqFPHYpQgS2suYSVsz4uSoeaBqU4tpUU55jLSi+xYGSrM7yHlmmMYonKyiMVZk2X07gOz4DDIOZWI8wFioZOmdki5sNVnIVRZjAUaKiR8DVVAEQtzsqhCoCDWAxBWiCiorK7ZuFTStAsJU0oBQLBMVNTEKiCzmY00qdU4J-p1rHP5IGNAEEIziAFSynCTKuAwwtf4zcFgoAEPoGhC0aABACAsAAagsDgfNFgABUFgACyhJVAYC4PQMUoJ2mptKoM3I8AEFIJWRbZBOFTkzNjHMhqLFKm8yEhmrNaFPIUu1BsrZ8rdnlGVUCVVVl1UDq1Y2+ZuqUL6vfoaqyyNjWmvNUWUdrYQ2EotHE2AqbmTWowLa+1NEKALoGa691E5xBep9euqAfqA3Zk3ec7dI6g2tiAyKz5nDCE8LCCANA4KkOQuEUkJGtIUYIt0PoZFIAjEmLTSyI1xIBTgSzH46AuYhBFlZrmew4p5D3zYmDejIAqiMFqO4eghRVC0VTvAdBVTWjy16bTYg3H07unIO4fIsA0K9AMPAdwRiFCMGkNx2ePGxg4AwIwZ6kdZ10qsJtEtwC6OwgAFKNrrInFCLGeNsY41xnjvR+MICExgETbncLick6VGTcmFOqCUypwxamNMOa4GMTA+nWiGaHpR0zaBzNZ1hDWmAaFmNaa4E5uOnG6iub45MDzBhhOid8yEfz0nZNUGC6F1TeRIs5bGAAZj0wZuzXlSDmsBXB0INBphIAABzTFQ9CxINAOjpxkZkPDuFFg5rJj+vQYw42QHk7mcgjaaQsH6OIJJPEMDAWxaYJSDA0KOQQIY25vsICtFIGMc4gmLw6iyKpW5+FCKtGImKXMJCuwHZdMpGArrcxgsYDd6Bx3MbFph+-MmC2uCZuiyCTjJbIfuqoM5Eh8A8eHd8KxHL2KzhQF6LmFAAhIe3JC-djQFOqdQ6zEjlHEx6CmFXRYSnABST7BEiIIL+xYXH+OXRfYFyRMY5AIAAC8iUkDMB96HrNsUrfUFL2XRKcAADZqcujIuDU01GLAFoLa1xnBPSCKWUoQGKZMzvqezVdoxtzuxGLGD91M4PLeKQnMtsOGu5fg+mHr62VACj-foFHn3lTaROBJ9AcnJaLculp60enyfQ-ahZ6pNnHPXs89udD2P8Z2w1i6oj-nnuSK5hNIoGPrY49l67LWbF4vq9C7dFABvsYm+JlIMmVMXpK-fd+6ZkhMBu-Q6mjwP2beCis4W8XMA4PMeUBNHtCfMBbkg9MM9cHKAs8Zv1I7y7pRru3OSFrCA4g0JjCmlkZ3hipdg3ULdk0D2nv6DAK923mQuA+4z4zTupAr-D648YiakDG6m4lqCaHZ9bcIDbKCRBIA4CCJiAgA1xhB8BoasLLBkSoymjTZpAgC0TaDKAcYXh1CEBQCkCpAdC2r1BQDdg8YgydDcbJD0CGLuAmh9LuAxywBkTuAjBYAzDwjpz8H4H0CEFW6EAzYEDqBoSMBJDoTUBCITYiLKAvD9CwDIifCMA6BkE0CUFcaNAtDtDsHaFvASKHADAjCYA4CeD6jqDdDyw6F6EHAGEKFKEqHyoEyxAYEwrKD2AABC9QUge+bqAAmggrLAaGmBYIrIvI9ISBYM+OFOKBYI1upogDEEAA) - this example roughly follows the [React documentation on custom hooks](https://reactjs.org/docs/hooks-custom.html)
79 | * [Custom hooks and useReducer](https://flems.io/#0=N4IgtglgJlA2CmIBcBWATAOgOwEYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHlgQeAawAEpeLAC8AHSLiAnp1Hx44k9tF7WxkBPEAHQkgD0XgK6F4DFY5cT4Ad3hCckEMWTAvPQQ+AMIvADcUbAw0L2JCVL4qWPy7DQUAc1IIc2dCUT4AZgAOABYAWlYeMAB5AGEARwAjABVYAHUASVgAKyaAGTNh6fIsHoBVAGVxacIAaV6Wt263AEVB-rMILAAlUlEAOSxfKAgATmuw0V3NAFkACTcaTmgz+AHFXhtSHYyOR8uQquUNM4+Dw5GYwOR-CYAHxGHh4lQvNLaaDOCjkWwgbEqLxE3E8EAEAIIYjiCByLjCHBIOggAC+eF4AiEIh2jJAsnkinEyggYDc8PE2jA2lYFBVJlcHm8fh4bk05Vi0S8kHEDg4AAFMHQMHQ2qRiBgWgB+DFQXwIEx4uUK0hK4DaMLVUR-cjkTSEbR81Xq7QmU3m2BtURhzS+NwmADc3vliu0AeI-nE0VD4cj0bV0TjIlyReiydThCzeLxksIStkvilpGu8HdxHgpG0Bm0AAp23wFHhtHxWeyeABKYfY-N47TaQhB8TEURj2dsuQYcxueBL4Br9faYjJeDVjRkeCCeQmJAXy+6Gy+Ug8fNXzHyJAN1CBQjS7JUAGptBwKM3yvG9qygeAHyfSlXx-d8P3EL8fwLf9xEAicQM7eRtDaKCYPQ9dENYPgPXw2D1zNCgwm0Hh4BYgBRUgKFIUcTDWHh6h4OA+xnOc5FfRkxIPRcLz5PE+WzfEeDbJVejwwdhzHANoGnDRqggQp1LA6c9AxNJ4GM7tp38eAONYVgkPEGyAjmPgzExcR7Mc1kXPgLZJ3gPze1YYK+18AdSD8iYeGqUT5MXZdV3Q1TtBoYjxACqdtBeQg3EnHcmC02ze37QdRwywdSoiwdp1wsDAP0tkjLwqMFyU9dUoyrSMqywIMqUi9UpoJq+2nAJxBiuKoCKkdbL60caNgAJ2qGjklSgKs5oCELR1WlL1py6IrK8hAUOKnb4FYPaOrjA6eHbD9zNvEc9qS89KL-B6lTM8gLO6HhhlIFFCAMuQOOErS3oMFc+M+9dfosk7yugBc8AYo6wFiL89HkDBEbsiz5DmCB20UcqTHEYGHrBtjhJMUzHz++AAaBkHaYhqAF1g7nPs2rHC246UMD4GAOKJ8QSbJtjeMp6nQZkxQoAZp7mdZ+WOeE3n3357Ghbx4hYGSQgpfEfGmYsviQH3CALJMbWKPXC9bO8pzR2hlcPvfCapoUKBRyp3xT1ugU0q8AAqbRHzccwZ249zALkWAzF0LsZyVDEwO0cOvCYB2Lz0LDvzHWCwCt0DuwZjGvYwzqjfye5hUa2K-e0Z1qxtu2QG0QCvUZDH1zkB9AsAtJUUQ96B8vXXBdxpURzH8hEIwfnMzu2v1wS2uBQxsuTArhRSDaDQZZVmh143veRAylWa437Qh70EftEXieYcxk6OLO4XZ+lLTX8CPzKeocMpoynlfQYvhxDFnxP3eGGE773yvPXQgjdBC9xAJA6Bcgq7wNrrlPggwEBQEAt1Aw5DtAMCnu+Ie6hiCaEAh7HKpN8rblEKOAMx54AYMQshaUdg+QO3vjvPBl4r55RRBgCAkopGEDaIQMAhRYAq2oWIq2EAgjJE0W0SAPAsQgCERvQx74wGiIgVAmBuCkHJWsXXY2aDuHViwZYuBtiH4qToQwscS5365VYTuDh2guEYPvGZfh3dBGqJEdY8R+UeBSJkaTeRiiqAqNEe+K+GiaKEG0W4WA+jjEYUKeuUx98r4YAkZFe26MzFW2cTgqSiDa6G3sU3BC+gbC3iSUbUg5R4BWKQbQ6RXimEEz2sA6cJhezPWVP0gxNTa55x5heXmCVUqlk0H2ZGQ4RxbhDI2Uc2zpyFnbCWRs+1WyHQAIJuDcFDAMtltltUnvdR66UNKkEINOUWUAjmqyRh82a2hHkfJugXT8xcaCl3LpKKmlAmxSShfAsuAByepPAUULI3k0jCLSG5tJMOikkciNDBAGffIZ9DGE+JXD87Z4zRHRI3iiq5MA-xgUHCijGpSMLlMqYOapu8rYSPiaS8g5LeXCriRK2uOLmkoIcRg0I5RiVtHUOUCQqrBAvF8GAL06SoxYvvgAA22Z4bQAASYAlVPkYAQDwcoZo+TGqnoUwxecjXsu7DkxRbgKpJThrXMuGytkfLqlPTZZhSFSKgJ6980Bo26Snk1QysATqJpbqmk6cbLwE22dSpKebQXEB5e+SJn0FwOyYEpRSLYugYEzvIQNR1CwoQwP0IOpAzAbA6ayeEVsADE5JKQ8r2Rswgo4bluGOXWMA47eb7XFHENwHBBzKEGIQ-Q4pmROXnJyEAaA0BIAaCgfkgoQD8EEMoE5xYwANjLBgMUMgQjSmUPAAAHr6Dsh0b1nLLPc4FAQ+rPPfjXHw2g1gBBnKxYUQTyBiQHPkEkSpKwqhTOGVUXZxIPTWt9ewjYtKIJBRy0gBbQMY3A1cq8s78MYbNJOQDkQZyojNJpX9aHUwYAxl1J+Ch7jsSea9Gl2lk0kNYuxbQAARQKe0MB9PEMMOU8A9o5vXCmlqDVtA-EnKIfG-5-babNPjFE-M3qRxwHQQpgjbrvlSuwT54hBNXl4-AfjYR6X7WaYdd5JGvkzhgH8otJGgXoYjBgWyVz45mFHDQez7Ztl5xs7miFP4kX3xtV8qeTC6XFpc25jzpaMIE0LRbSyoKCaFOrbBJl64ItRcYep2AAA1QoQctI0Dzi8rzeGaB8G4uNGwkXSBAvmsBZTjWWv5NPBjQu2E0pTz61FBbbL34TSG6ORbRoVKTg2zAStqn-kvRXGt7iG3uJBA4IfUccUVTvxu9oAAhBQirlXqtYtrehWbxdQuEBrZmJd0QV0IFIOuzd0giC9pkvuugSA0DtBhzgBoZ6hRXuEMULgz6pRKGEEgYdNjSJtBtfIiAAAvRxAA2Ogbh323TaITj5bQiHkHocmeAEANX4SgmgKnNOLx06J0zln+UYAaHKIBHAKBqc1rxIMJeKca7BHkB0AQHAo0bhBvIwcEBWDS-ibCigy18fC5eA68XkvefoUV+IYnZPxeHAt1RFhRs1esAQA7mc6qeDHwUGAc1A5uy6623Cw3RLILopqUHg3kYpE8GCPjxRvSNBqqupziXUuFJ4kj-C7QFT8qRXx3453gFXcftukGKAZpxd0DoAAUkDza-H5A8-VDV3QW6Vh2cSEAm3i8ij32s875znv6FmakFd+QMIgFRDQEQjwW6VN2YyUAkonPDRIzwBvMfL3nl68fJFnOCyjfm-mHF+3tnHPR59dHPzhngvNAD45555UfB+8d4vy-K-N+SOM9gMz+-b+JB9oEoD5Bwt8ZYC8ncE5VQ3dbpCh2cvcbs-dpRBxboN16FKh9NAIB1rB4BbpjdRdL9eIv9uwf8-82h8CHUn9ZdSBEIj5iw3BxdqcNxKBoBtAB0HIdcLwADOc0hP96dv878H9ACy9oBK8oJq868M89cGcT5NJc9ZxNIa5C8oCS93dy8xCLNa9d9v9ZChxQ9tBw9M8iddCDCLE5BtBII+AbEE9EQvcEBWBU8edtDuwc9uoa5bIj4d1WRAJUQ2JbpVD5FzQtBu8RCK9RBCDr9+CSDQYycn9uCIjiDD4bdg5e8+tbDu9tBucmC6BKFbplCXcYCLwAiXg9BsNSFKBdU58LxpgixtczB6csdOd-dD5UDZwDQKAuwxMB0GgejUD4RaD7RRYIB-BAIUAtC1pf9SM2CYAoB-CQhkjAI4d090IFB31rc4DygeBSFkDSBdc61t1Ic91lAcAWhYdyd+QWAQB1AtBOQaAUcRRIFYBFEG0NB0dxQvxqBhAtRPAfBiAoAeAdhYhf9nhXc+t+pjQ+BpgX8vB1BBhUhHjFEvAbQsAnRch8gvAES+AXi9d8hxQuFlB2wLBEABR7jlAEwqgkxQt0x3jSBPiXBoFtQfAux9RDQ4gTRgwKT700w3ALQbRMBydaRSZxB2SmIOAuTqSCB8ThASAqgY4LiCBL0RRegaMftmMoBGNqpIp+QgA)
80 | * [Custom hooks to search iTunes with a debounce function](https://flems.io/#0=N4Igxg9gdgzhA2BTEAucD4EMAONEBMQAaEGMAJw1QG0AGI2gXRIDMBLJGG0KTAW2RoAdAAsALn3jF0UMYlmoQAHnxsAbgAI2+ALwAdEJQhiDAPiUB6VWtMgAvkR79BIIQCsuJSLPljFFgCo9KAAVETYYLUjMDXJEPkwAa0QNCBYNcTFcFAsLAFc8EQgIRJghSD58vAARRAAjCDyoMEQLDTqATw1qAHFMOpSAJUwAcywofEYACkzs3LEAdzYxOXJyiEqR-sQAfXJR8fwASmCAi2C2PmwIcjENPg0WSgeDWZgc-KhsRJH1yr5liJyBwAAIAJiEtEhAFpyGAhAAWAD8fAg+DySAMFyuNzuwA0SzEIgAEsVSho7I9nhoDACicD4NCiiU8tgsVBLtdbhp8WACmINqSSpFKU8NjTXBY+TABXwmWSYOzObieRo8Jg4SIAILYNgUqnigxCCw4Njs4LeGVqxAasAiACybCgABl5CMiRodBoAMwW6BW-D1RrNRD2yJegCstFoftgd0QAA9+NgkABlG2axDh7oGABCNrEnAMRAlAGlEPA8F1nXlixL8+QYBqnXWDKmxOQ8mwliMRAZGLGrYnk0hnU7Ep6NKsHjpTMENPcpgZMMX5wueWv1xk4iwUBKAMSrqBbhfQMDwNhgRJ7lKzjfHk8Ly3xpACWST-AQPlvsRCACOeSIOQHTpkgYACuQS4gPu6qagYJwPo+FbxL4QhqJg8CAZO06bieyE-kIqgwNgmBiHaACiai+FMUCIAsGiUdRBh2pgUAjIg8EIY+di4Q4uE4Q+CGDvGSZXKO47ZtO4amPeT7+ncF5Wl6UlCAk2BTMOYmIGOUCJFxsSIGIeTkMe1CMOszSkVMQg2Ypv5qRpnoydQrbEBoiCMEcRxCDAF4tFMACMCE8Q+wkaAAwhs1y0e+XpTPiBSIO2pGICWiXkSwLCIOBaV4IMiAsLliC1A0TQtBSRxObJGi5BoyVyBorH4NaKxAZELA3BoNlCGutXpraIhTkBfBrs+3SwXaITDSWeBiP1mpTeQfCMJOiX1YgUHwb1bRagACgAktaA0GTAGJiIqD5jdQcSnfA50zYZ+W3edK1emtYgpVMZn6X1GZ2k6Ixqh9RmRFMCwiIZEPkFOUMpBEGjYPIqhsRoe2HXEAFZmI+lXRE83-WxD1iPtMD4+EbGvRo72fSwGF4D9bQlcG5UTYN05qhAMOkVodzQPAXQjOoWZU5EWByFa6GYSk3XbV1NlaOkrOLQ8IiYJEUDGO0iDyFT2D4ClzWEuTGhYFaUa0HwF0LrVWrRCdZ0wykaMaGAGHwGqRQYs1fNdOwcSpCGItAUDEC4FOHS6mxo3yRogalSG+Bk8rq01EGZUbUr02x2nIZhjjMem2I5E+HEzVvXlBU0Ri8D6Wu6WZdlYhLohGhTJVd7ALhC5sOkUxx8zBBJ8NGgAGQj9n8ctInf0iMrQhIGxHoyazjoum6RKVZ3LdbrNJNkwDUwdoB+mPkgdwa-RXqF8XqwEOUxlxLFGjVClQgX23NVtAkyRqsZKREtzBYKREqOxNmreMJcCBDSWl3dcrMdRsD7jnKeQ8lreSJPIKYN0zrSWqo+buvcL6eh0FfcBN8gJ3z5OQR+2M8H4IXLvUmM8D600rIgE+9CWpPRwVgrMOChDYLujADh3FYELjsCI-UFY8B0J3o9PhQivqeVgSFLcfEW7UH7unaeA1lYrVqgAeSgPzF2bt3IN3AgrCeA9mqs2gQ8FibEsxrlrg+OIRkTKLiNDgNkxBcIuW3l4kA1hWyBIXHwKCTpsB5F-KzI8nDZH4O0HuAwcS-FhJPCmTALQijwEDOQFJIAyZaBCE0Zx6SEmng5F8GJN4qqzVQRExAQgPrkHYr+SWx8iBiJPGeVWTi6l3gaTPZWGkWkanaWhDCXSeniJ6ZIo43TAkRKNE6DqdYAn0JWa4TSKZyklk2ZwgwIRgKFKWZU9yok9k6VKBpK5aYZ5ZkkQuTy5z8HbJ8sDAodY8bMKjhkhcSIJT7xRjLEAsyNB7gAAbcKEXuAAJMAQR5157rxEHYKF8zYGvNgcisoC93R9gyUCj5eL4mcLxapHAvDnpOQheEqCAiFEmDcviZIHQ9zIpafsK8+1moOBzACk82zLgjDrPiGAcJOXMqEBqRYNxEgAFVyDwACtGCqbyLkfP1h9DZ9LhWMo7NkxI0JeACDrFyo1V4AByzhFn6q3B8uVEQxCmucBamVzqZS2oEPaoVj4PnYDiGoNgdFQkXIDVBTAeRVAQHJRGk8W8E30MlWAaVz0hBBsQCGuiyr4CauTVuMQEdECFOjbGiwCZoR8ARCuCphaTzeA7Agd4U5OypQdZw6NAoskcseHTDt-rOHqIbeubZIS3IGAAJqNGhnUSgCw8DQ0-FmKAAByO4p1sBcjuOWtgnMPojEVCAZ5lTT30PPSeZRQ7PILghae09e4oDV1wgOQSd6oAhTGkKZI+BIo4hincL0Rsf0wCmP+6KvgSzSllKBoSzQY46mwJOJNGgc0LD3B-WcuEIk-oIBB6AvhgrBGCHwVSwYm5rk-N+VCmNgKgUbjcKC+4jAsr9QuJDwQELSDwGBMQ+7YCKG9CgCMABOewjgQBmpcKadwngZByAUGgQIFxSm0UiMUtGwQ3gfEwJlDgbBX7LDKWUHxSA-gWBurOloMArBfjyD+UiAmLDGfU9CGUNxEDQiAXUdzQEQ0tD8wNaEppzhQDOMERMO6XYx3gbqbCw8O5rnccZY8WUyIiGbluKFOnciuazLK7d5mKgWFZkiacOhEXTjsCPAQqhMA6D4AUS8UKC2ocdZDNEhSejkRCOyNRLiWkQygFgqqawPDQDbsRqA0gKi6iQOQRQdRthSBILxxuAmuBoDVSgME3oJNOAEIoGDGx5TCjk7N6Aim-BoEiwmaLY0Tt8FA5OeKQd1pFQyllSxEiqrxTrqnSepbW6dNStnLAHR24yXa5-OqwMUhNRaqsdqnUtEJzQ9MjiLcrpo6ngANUx0TJm2iCdS0ptTOQUxQfwa3PXb7lHAlYehz02qiq9YpWsdojHUtGosFWODzAHQeljX6fgBbk5ZohEuIgRoTcmeJLkWIYnCdSeASp5j09ArAwQ5p-g2q4VWItHdhgqc0vZdWNBy7fp7FQZ005tAAXXRHHsVSNDJoqImjYxZ20MI8N4ZFHokAhGwbfCc-R5bsUDxWQ6oBhbzHUQresXYs1GWGTaryyNk6UB2vBcIyAvu-AQgNBS4EOb9pkRzwZigYjm6rS5CF56Slzx8uYePkrxqEvMuYkzCagtzXABuFRBaFzUFByWHPHR9FtCMSYuI0JXbwHdgVenceeeo4rLn53ziW5HEH1AXCTfjy44IKrrHczP278u1cDgQElsrZ48hcCm3FB7d2xGA7UnnCKHKDAeTTbfBLZohdCoYJBtJOh7i0B74LgdSyDQi0wAj8x7hNiwB+bAgsB74hSnDVQNBVowBsAABeAMe4DQ5A+S0IOBGBwQmAJYmAKAIaeB9e1UcgCYrqgYkA+w-G0AT6hGUBMW8ANwe4+4YIAUomAAbAAGLeiUFQCFbIaoYkT4DIwjB7hgi0DYAJjSE+QzzVRLD4BEh7hqq0AACkvBMBrqeB+BwOYIcQfAvBChShEBQgEY8QvBJBZB+wqgBQe4oh6hrhNw+SBh6hHMF4zU+4YA4RmhrMKAHU0osksuF4tE3BtEkRM8KAKA3m9QiQyw0IUSMS0IWSOSCA+SqogQEUQIGwrQBiiM+wFgqYemzYGgEWD4Zh7mxaSAe4ywGEl4vBkA-BBSGg+4mAQx0hwQnypEBQJYQguynA1ULRFhwOAUCIvh0cfRghom6xmhaynMIBEy4BGgAUEYQRtAGgxxkBwQIUYxIM1UoBgsUAe4CIahCYJxzxZxn6JGMheK1xuxdx+xhxTxpxJxfhpBQE5BxgsogRTxcAIRAx2siAmhyK1UREvae4LASAGha49hRB+xjxgJa4bhIJPaEJwR2gMJ2sphV27mBBCxAAHMsW8Q+EyrSqKnQrofodiY8bwQuBDGwL2GIAYdGHSefguIyQ7IRKRDEDDjcU6NCEgHzvyYKRSGuCKXdF1FaiatJjyKol1AibKrcC6m6gIHQnMVSQYUseidjggAIQMesaJrwVqcqXcJmsGqGvRJKd8dCISdiQqfaTqVmuho1DGvunQtybyfcRyWuHYA-nxs-ttqJigAFNSfYMwCAAkaUNwJ-kdmgHSECBwGdokKyNIMZFIGgLlp8N8L8CVtmQyHmayCCFCBCAFFYC6hYFWbmcyPmdgNIMWojIoGQMCNgH4HYMmdJooIqngSjHTo3JEEbI1GHuVCwGVJwTNnYEAA)
81 |
82 | ## Usage
83 |
84 | ```bash
85 | npm install mithril-hookup
86 | ```
87 |
88 | Use in code:
89 |
90 | ```javascript
91 | import { withHooks } from "mithril-hookup"
92 | ```
93 |
94 |
95 | ### Hooks and application logic
96 |
97 | Hooks can be defined outside of the component, imported from other files. This makes it possible to define utility functions to be shared across the application.
98 |
99 | [Custom hooks](#custom-hooks) shows how to define and incorporate these hooks.
100 |
101 |
102 |
103 | ### Rendering rules
104 |
105 | #### With useState
106 |
107 | Mithril's `redraw` is called when the state is initially set, and every time a state changes value.
108 |
109 |
110 | #### With other hooks
111 |
112 | Hook functions are always called at the first render.
113 |
114 | For subsequent renders, an optional second parameter can be passed to define if it should rerun:
115 |
116 | ```javascript
117 | useEffect(
118 | () => {
119 | document.title = `You clicked ${count} times`
120 | },
121 | [count] // Only re-run the effect if count changes
122 | )
123 | ```
124 |
125 | mithril-hookup follows the React Hooks API:
126 |
127 | * Without a second argument: will run every render (Mithril lifecycle function [view](https://mithril.js.org/index.html#components)).
128 | * With an empty array: will only run at mount (Mithril lifecycle function [oncreate](https://mithril.js.org/lifecycle-methods.html#oncreate)).
129 | * With an array with variables: will only run whenever one of the variables has changed value (Mithril lifecycle function [onupdate](https://mithril.js.org/lifecycle-methods.html#onupdate)).
130 |
131 |
132 | Note that effect hooks do not cause a re-render themselves.
133 |
134 |
135 | #### Cleaning up
136 |
137 | If a hook function returns a function, that function is called at unmount (Mithril lifecycle function [onremove](https://mithril.js.org/lifecycle-methods.html#onremove)).
138 |
139 | ```javascript
140 | useEffect(
141 | () => {
142 | const subscription = subscribe()
143 |
144 | // Cleanup function:
145 | return () => {
146 | unsubscribe()
147 | }
148 | }
149 | )
150 | ```
151 |
152 | At cleanup Mithril's `redraw` is called.
153 |
154 |
155 | ### Default hooks
156 |
157 | The [React Hooks documentation](https://reactjs.org/docs/hooks-intro.html) provides excellent usage examples for default hooks. Let us suffice here with shorter descriptions.
158 |
159 |
160 | #### useState
161 |
162 | Provides the state value and a setter function:
163 |
164 | ```javascript
165 | const [count, setCount] = useState(0)
166 | ```
167 |
168 | The setter function itself can pass a function - useful when values might otherwise be cached:
169 |
170 | ```javascript
171 | setTicks(ticks => ticks + 1)
172 | ```
173 |
174 | A setter function can be called from another hook:
175 |
176 | ```javascript
177 | const [inited, setInited] = useState(false)
178 |
179 | useEffect(
180 | () => {
181 | setInited(true)
182 | },
183 | [/* empty array: only run at mount */]
184 | )
185 | ```
186 |
187 |
188 | #### useEffect
189 |
190 | Lets you perform side effects:
191 |
192 | ```javascript
193 | useEffect(
194 | () => {
195 | const className = "dark-mode"
196 | const element = window.document.body
197 | if (darkModeEnabled) {
198 | element.classList.add(className)
199 | } else {
200 | element.classList.remove(className)
201 | }
202 | },
203 | [darkModeEnabled] // Only re-run when value has changed
204 | )
205 | ```
206 |
207 |
208 | #### useLayoutEffect
209 |
210 | Similar to `useEffect`, but fires synchronously after all DOM mutations. Use this when calculations must be done on DOM objects.
211 |
212 | ```javascript
213 | useLayoutEffect(
214 | () => {
215 | setMeasuredHeight(domElement.offsetHeight)
216 | },
217 | [screenSize]
218 | )
219 | ```
220 |
221 | #### useReducer
222 |
223 | From the [React docs](https://reactjs.org/docs/hooks-reference.html#usereducer):
224 |
225 | > An alternative to useState. Accepts a reducer of type `(state, action) => newState`, and returns the current state paired with a `dispatch` method. (If you’re familiar with Redux, you already know how this works.)
226 | >
227 | > `useReducer` is usually preferable to `useState` when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
228 |
229 | Example:
230 |
231 | ```javascript
232 | const counterReducer = (state, action) => {
233 | switch (action.type) {
234 | case "increment":
235 | return { count: state.count + 1 }
236 | case "decrement":
237 | return { count: state.count - 1 }
238 | default:
239 | throw new Error("Unhandled action:", action)
240 | }
241 | }
242 |
243 | const Counter = ({ initialCount, useReducer }) => {
244 | const initialState = { count: initialCount }
245 | const [countState, dispatch] = useReducer(counterReducer, initialState)
246 | const count = countState.count
247 |
248 | return [
249 | m("div", count),
250 | m("button", {
251 | disabled: count === 0,
252 | onclick: () => dispatch({ type: "decrement" })
253 | }, "Less"),
254 | m("button", {
255 | onclick: () => dispatch({ type: "increment" })
256 | }, "More")
257 | ]
258 | }
259 |
260 | const HookedCounter = withHooks(Counter)
261 |
262 | m(HookedCounter, { initialCount: 0 })
263 | ```
264 |
265 |
266 | #### useRef
267 |
268 | The "ref" object is a generic container whose `current` property is mutable and can hold any value.
269 |
270 | ```javascript
271 | const dom = useRef(null)
272 |
273 | return [
274 | m("div",
275 | {
276 | oncreate: vnode => dom.current = vnode.dom
277 | },
278 | count
279 | )
280 | ]
281 | ```
282 |
283 | To keep track of a value:
284 |
285 | ```javascript
286 | const Timer = ({ useState, useEffect, useRef }) => {
287 | const [ticks, setTicks] = useState(0)
288 | const intervalRef = useRef()
289 |
290 | const handleCancelClick = () => {
291 | clearInterval(intervalRef.current)
292 | intervalRef.current = undefined
293 | }
294 |
295 | useEffect(
296 | () => {
297 | const intervalId = setInterval(() => {
298 | setTicks(ticks => ticks + 1)
299 | }, 1000)
300 | intervalRef.current = intervalId
301 | // Cleanup:
302 | return () => {
303 | clearInterval(intervalRef.current)
304 | }
305 | },
306 | [/* empty array: only run at mount */]
307 | )
308 |
309 | return [
310 | m("span", `Ticks: ${ticks}`),
311 | m("button",
312 | {
313 | disabled: intervalRef.current === undefined,
314 | onclick: handleCancelClick
315 | },
316 | "Cancel"
317 | )
318 | ]
319 | }
320 |
321 | const HookedTimer = withHooks(Timer)
322 | ```
323 |
324 |
325 | #### useMemo
326 |
327 | Returns a memoized value.
328 |
329 | ```javascript
330 | const Counter = ({ count, useMemo }) => {
331 | const memoizedValue = useMemo(
332 | () => {
333 | return computeExpensiveValue(count)
334 | },
335 | [count] // only recalculate when count is updated
336 | )
337 | // ...
338 | }
339 | ```
340 |
341 |
342 | #### useCallback
343 |
344 | Returns a memoized callback.
345 |
346 | The function reference is unchanged in next renders (which makes a difference in performance expecially in React), but its return value will not be memoized.
347 |
348 | ```javascript
349 | let previousCallback = null
350 |
351 | const memoizedCallback = useCallback(
352 | () => {
353 | doSomething(a, b)
354 | },
355 | [a, b]
356 | )
357 |
358 | // Testing for reference equality:
359 | if (previousCallback !== memoizedCallback) {
360 | // New callback function created
361 | previousCallback = memoizedCallback
362 | memoizedCallback()
363 | } else {
364 | // Callback function is identical to the previous render
365 | }
366 | ```
367 |
368 | #### Omitted hooks
369 |
370 | These React hooks make little sense with Mithril and are not included:
371 |
372 | * `useContext`
373 | * `useImperativeHandle`
374 | * `useDebugValue`
375 |
376 | ### Custom hooks
377 |
378 | Custom hooks are created with a factory function. The function receives the default hooks (automatically), and should return an object with custom hook functions:
379 |
380 | ```javascript
381 | const customHooks = ({ useState /* or other default hooks required here */ }) => ({
382 | useCount: (initialValue = 0) => {
383 | const [count, setCount] = useState(initialValue)
384 | return [
385 | count, // value
386 | () => setCount(count + 1), // increment
387 | () => setCount(count - 1) // decrement
388 | ]
389 | }
390 | })
391 | ```
392 |
393 | Pass the custom hooks function as second parameter to `withHooks`:
394 |
395 | ```javascript
396 | const HookedCounter = withHooks(Counter, customHooks)
397 | ```
398 |
399 | The custom hooks can now be used from the component:
400 |
401 | ```javascript
402 | const Counter = ({ useCount }) => {
403 | const [count, increment, decrement] = useCount(0)
404 | // ...
405 | }
406 | ```
407 |
408 | The complete code:
409 |
410 | ```javascript
411 | const customHooks = ({ useState }) => ({
412 | useCount: (initialValue = 0) => {
413 | const [count, setCount] = useState(initialValue)
414 | return [
415 | count, // value
416 | () => setCount(count + 1), // increment
417 | () => setCount(count - 1) // decrement
418 | ]
419 | }
420 | })
421 |
422 | const Counter = ({ initialCount, useCount }) => {
423 |
424 | const [count, increment, decrement] = useCount(initialCount)
425 |
426 | return m("div", [
427 | m("p",
428 | `Count: ${count}`
429 | ),
430 | m("button",
431 | {
432 | disabled: count === 0,
433 | onclick: () => decrement()
434 | },
435 | "Less"
436 | ),
437 | m("button",
438 | {
439 | onclick: () => increment()
440 | },
441 | "More"
442 | )
443 | ])
444 | }
445 |
446 | const HookedCounter = withHooks(Counter, customHooks)
447 |
448 | m(HookedCounter, { initialCount: 0 })
449 | ```
450 |
451 |
452 |
453 | ### `hookup` function
454 |
455 | `withHooks` is a wrapper function around the function `hookup`. It may be useful to know how this function works.
456 |
457 | ```javascript
458 | import { hookup } from "mithril-hookup"
459 |
460 | const HookedCounter = hookup((vnode, { useState }) => {
461 |
462 | const [count, setCount] = useState(vnode.attrs.initialCount)
463 |
464 | return [
465 | m("div", count),
466 | m("button", {
467 | onclick: () => setCount(count + 1)
468 | }, "More")
469 | ]
470 | })
471 |
472 | m(HookedCounter, { initialCount: 1 })
473 | ```
474 |
475 | The first parameter passed to `hookup` is a wrapper function - also called a closure - that provides access to the original component vnode and the hook functions:
476 |
477 | ```javascript
478 | hookup(
479 | (vnode, hookFunctions) => { /* returns a view */ }
480 | )
481 | ```
482 |
483 | Attributes passed to the component can be accessed through `vnode`.
484 |
485 | `hookFunctions` is an object that contains the default hooks: `useState`, `useEffect`, `useReducer`, etcetera, plus [custom hooks](#custom-hooks):
486 |
487 | ```javascript
488 | const Counter = hookup((vnode, { useState }) => {
489 |
490 | const initialCount = vnode.attrs.initialCount
491 | const [count, setCount] = useState(initialCount)
492 |
493 | return [
494 | m("div", count),
495 | m("button", {
496 | onclick: () => setCount(count + 1)
497 | }, "More")
498 | ]
499 | })
500 |
501 | m(Counter, { initialCount: 0 })
502 | ```
503 |
504 | The custom hooks function is passed as second parameter to `hookup`:
505 |
506 | ```javascript
507 | const Counter = hookup(
508 | (
509 | vnode,
510 | { useCount }
511 | ) => {
512 | const [count, increment, decrement] = useCount(0)
513 | // ...
514 | },
515 | customHooks
516 | )
517 | ```
518 |
519 | ### Children
520 |
521 | Child elements are accessed through the variable `children`:
522 |
523 | ```javascript
524 | import { withHooks } from "mithril-hookup"
525 |
526 | const Counter = ({ useState, initialCount, children }) => {
527 | const [count, setCount] = useState(initialCount)
528 | return [
529 | m("div", count),
530 | children
531 | ]
532 | }
533 |
534 | const HookedCounter = withHooks(Counter)
535 |
536 | m(HookedCounter,
537 | { initialCount: 1 },
538 | [
539 | m("div", "This is a child element")
540 | ]
541 | )
542 | ```
543 |
544 |
545 | ## Compatibility
546 |
547 | Tested with Mithril 1.1.6 and Mithril 2.x.
548 |
549 |
550 | ## Size
551 |
552 | 1.4 Kb gzipped
553 |
554 |
555 | ## Supported browsers
556 |
557 | Output from `npx browserslist`:
558 |
559 | ```
560 | and_chr 71
561 | and_ff 64
562 | and_qq 1.2
563 | and_uc 11.8
564 | android 67
565 | baidu 7.12
566 | chrome 72
567 | chrome 71
568 | edge 18
569 | edge 17
570 | firefox 65
571 | firefox 64
572 | ie 11
573 | ie_mob 11
574 | ios_saf 12.0-12.1
575 | ios_saf 11.3-11.4
576 | op_mini all
577 | op_mob 46
578 | opera 57
579 | safari 12
580 | samsung 8.2
581 | ```
582 |
583 | ## History
584 |
585 | * Initial implementation: Barney Carroll (https://twitter.com/barneycarroll/status/1059865107679928320)
586 | * Updated and enhanced by Arthur Clemens
587 |
588 |
589 | ## License
590 |
591 | MIT
592 |
--------------------------------------------------------------------------------
/packages/test-mithril-hookup/dist/js/index.js:
--------------------------------------------------------------------------------
1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=5)}([function(t,e,n){(function(e,n){!function(){"use strict";function r(t,e,n,r,o,i){return{tag:t,key:e,attrs:n,children:r,text:o,dom:i,domSize:void 0,state:void 0,events:void 0,instance:void 0}}r.normalize=function(t){return Array.isArray(t)?r("[",void 0,void 0,r.normalizeChildren(t),void 0,void 0):null!=t&&"object"!=typeof t?r("#",void 0,void 0,!1===t?"":t,void 0,void 0):t},r.normalizeChildren=function(t){for(var e=[],n=0;n0&&(o.className=r.join(" ")),u[t]={tag:n,attrs:o}}(t),e):(e.tag=t,e)}l.trust=function(t){return null==t&&(t=""),r("<",void 0,void 0,t,void 0,void 0)},l.fragment=function(){var t=o.apply(0,arguments);return t.tag="[",t.children=r.normalizeChildren(t.children),t};var f=function(){return l.apply(this,arguments)};if(f.m=l,f.trust=l.trust,f.fragment=l.fragment,(s=function(t){if(!(this instanceof s))throw new Error("Promise must be called with `new`");if("function"!=typeof t)throw new TypeError("executor must be a function");var n=this,r=[],o=[],i=l(r,!0),u=l(o,!1),a=n._instance={resolvers:r,rejectors:o},c="function"==typeof e?e:setTimeout;function l(t,e){return function i(l){var s;try{if(!e||null==l||"object"!=typeof l&&"function"!=typeof l||"function"!=typeof(s=l.then))c(function(){e||0!==t.length||console.error("Possible unhandled promise rejection:",l);for(var n=0;n0||t(n)}}var r=n(u);try{t(n(i),r)}catch(t){r(t)}}f(t)}).prototype.then=function(t,e){var n,r,o=this._instance;function i(t,e,i,u){e.push(function(e){if("function"!=typeof t)i(e);else try{n(t(e))}catch(t){r&&r(t)}}),"function"==typeof o.retry&&u===o.state&&o.retry()}var u=new s(function(t,e){n=t,r=e});return i(t,o.resolvers,n,!0),i(e,o.rejectors,r,!1),u},s.prototype.catch=function(t){return this.then(null,t)},s.prototype.finally=function(t){return this.then(function(e){return s.resolve(t()).then(function(){return e})},function(e){return s.resolve(t()).then(function(){return s.reject(e)})})},s.resolve=function(t){return t instanceof s?t:new s(function(e){e(t)})},s.reject=function(t){return new s(function(e,n){n(t)})},s.all=function(t){return new s(function(e,n){var r=t.length,o=0,i=[];if(0===t.length)e([]);else for(var u=0;u=200&&s.status<300||304===s.status||/^file:\/\//i.test(e),i=s.responseText;if("function"==typeof n.extract)i=n.extract(s,n),t=!0;else if("function"==typeof n.deserialize)i=n.deserialize(i);else try{i=i?JSON.parse(i):null}catch(t){throw new Error("Invalid JSON: "+i)}if(t)r(i);else{var u=new Error(s.responseText);u.code=s.status,u.response=i,o(u)}}catch(t){o(t)}},c&&null!=l?s.send(l):s.send()}),jsonp:o(function(e,n,o,i){var a=n.callbackName||"_mithril_"+Math.round(1e16*Math.random())+"_"+r++,c=t.document.createElement("script");t[a]=function(e){c.parentNode.removeChild(c),o(e),delete t[a]},c.onerror=function(){c.parentNode.removeChild(c),i(new Error("JSONP request failed")),delete t[a]},e=u(e,n.data,!0),c.src=e+(e.indexOf("?")<0?"?":"&")+encodeURIComponent(n.callbackKey||"callback")+"="+encodeURIComponent(a),t.document.documentElement.appendChild(c)}),setCompletionCallback:function(t){n=t}}}(window,s),p=function(t){var e,n=t.document,o={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function i(t){return t.attrs&&t.attrs.xmlns||o[t.tag]}function u(t,e){if(t.state!==e)throw new Error("`vnode.state` must not be modified")}function a(t){var e=t.state;try{return this.apply(e,arguments)}finally{u(t,e)}}function c(){try{return n.activeElement}catch(t){return null}}function l(t,e,n,r,o,i,u){for(var a=n;a'+e.children+"",u=u.firstChild):u.innerHTML=e.children,e.dom=u.firstChild,e.domSize=u.childNodes.length;for(var a,c=n.createDocumentFragment();a=u.firstChild;)c.appendChild(a);g(t,c,o)}function v(t,e,n,r,o,i){if(e!==n&&(null!=e||null!=n))if(null==e||0===e.length)l(t,n,0,n.length,r,o,i);else if(null==n||0===n.length)w(e,0,e.length);else{for(var u=0,a=0,c=null,s=null;a=a&&x>=u;)if(b=e[T],C=n[x],null==b)T--;else if(null==C)x--;else{if(b.key!==C.key)break;b!==C&&p(t,b,C,r,o,i),null!=C.dom&&(o=C.dom),T--,x--}for(;T>=a&&x>=u;)if(d=e[a],v=n[u],null==d)a++;else if(null==v)u++;else{if(d.key!==v.key)break;a++,u++,d!==v&&p(t,d,v,r,y(e,a,o),i)}for(;T>=a&&x>=u;){if(null==d)a++;else if(null==v)u++;else if(null==b)T--;else if(null==C)x--;else{if(u===x)break;if(d.key!==C.key||b.key!==v.key)break;E=y(e,a,o),g(t,m(b),E),b!==v&&p(t,b,v,r,E,i),++u<=--x&&g(t,m(d),o),d!==C&&p(t,d,C,r,o,i),null!=C.dom&&(o=C.dom),a++,T--}b=e[T],C=n[x],d=e[a],v=n[u]}for(;T>=a&&x>=u;){if(null==b)T--;else if(null==C)x--;else{if(b.key!==C.key)break;b!==C&&p(t,b,C,r,o,i),null!=C.dom&&(o=C.dom),T--,x--}b=e[T],C=n[x]}if(u>x)w(e,a,T+1);else if(a>T)l(t,n,u,x+1,r,o,i);else{var S,A,I=o,O=x-u+1,R=new Array(O),j=0,M=0,z=2147483647,L=0;for(M=0;M=u;M--)if(null==S&&(S=h(e,a,T+1)),null!=(C=n[M])){var N=S[C.key];null!=N&&(z=N0&&(r[i]=o[e-1]),o[e]=i)}}e=o.length,n=o[e-1];for(;e-- >0;)o[e]=n,n=r[n];return o}(R)).length-1,M=x;M>=u;M--)v=n[M],-1===R[M-u]?f(t,v,r,i,o):A[j]===M-u?j--:g(t,m(v),o),null!=v.dom&&(o=n[M].dom);else for(M=x;M>=u;M--)v=n[M],-1===R[M-u]&&f(t,v,r,i,o),null!=v.dom&&(o=n[M].dom)}}else{var P=e.lengthP&&w(e,u,e.length),n.length>P&&l(t,n,u,n.length,r,o,i)}}}function p(t,e,n,o,u,c){var l=e.tag;if(l===n.tag){if(n.state=e.state,n.events=e.events,function(t,e){do{if(null!=t.attrs&&"function"==typeof t.attrs.onbeforeupdate){var n=a.call(t.attrs.onbeforeupdate,t,e);if(void 0!==n&&!n)break}if("string"!=typeof t.tag&&"function"==typeof t.state.onbeforeupdate){var n=a.call(t.state.onbeforeupdate,t,e);if(void 0!==n&&!n)break}return!1}while(0);return t.dom=e.dom,t.domSize=e.domSize,t.instance=e.instance,!0}(n,e))return;if("string"==typeof l)switch(null!=n.attrs&&z(n.attrs,n,o),l){case"#":!function(t,e){t.children.toString()!==e.children.toString()&&(t.dom.nodeValue=e.children);e.dom=t.dom}(e,n);break;case"<":!function(t,e,n,r,o){e.children!==n.children?(m(e),d(t,n,r,o)):(n.dom=e.dom,n.domSize=e.domSize)}(t,e,n,c,u);break;case"[":!function(t,e,n,r,o,i){v(t,e.children,n.children,r,o,i);var u=0,a=n.children;if(n.dom=null,null!=a){for(var c=0;c0){for(var o=t.dom;--e;)r.appendChild(o.nextSibling);r.insertBefore(o,r.firstChild)}return r}return t.dom}function y(t,e,n){for(;e-1||null!=t.attrs&&t.attrs.is||"href"!==e&&"list"!==e&&"form"!==e&&"width"!==e&&"height"!==e)&&e in t.dom}var S=/[A-Z]/g;function A(t){return"-"+t.toLowerCase()}function I(t){return"-"===t[0]&&"-"===t[1]?t:"cssFloat"===t?"float":t.replace(S,A)}function O(t,e,n){if(e===n);else if(null==n)t.style.cssText="";else if("object"!=typeof n)t.style.cssText=n;else if(null==e||"object"!=typeof e)for(var r in t.style.cssText="",n){null!=(o=n[r])&&t.style.setProperty(I(r),String(o))}else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(e[r])&&t.style.setProperty(I(r),o)}for(var r in e)null!=e[r]&&null==n[r]&&t.style.removeProperty(I(r))}}function R(){}function j(t,e,n){if(null!=t.events){if(t.events[e]===n)return;null==n||"function"!=typeof n&&"object"!=typeof n?(null!=t.events[e]&&t.dom.removeEventListener(e.slice(2),t.events,!1),t.events[e]=void 0):(null==t.events[e]&&t.dom.addEventListener(e.slice(2),t.events,!1),t.events[e]=n)}else null==n||"function"!=typeof n&&"object"!=typeof n||(t.events=new R,t.dom.addEventListener(e.slice(2),t.events,!1),t.events[e]=n)}function M(t,e,n){"function"==typeof t.oninit&&a.call(t.oninit,e),"function"==typeof t.oncreate&&n.push(a.bind(t.oncreate,e))}function z(t,e,n){"function"==typeof t.onupdate&&n.push(a.bind(t.onupdate,e))}return R.prototype=Object.create(null),R.prototype.handleEvent=function(t){var n,r=this["on"+t.type];"function"==typeof r?n=r.call(t.currentTarget,t):"function"==typeof r.handleEvent&&r.handleEvent(t),!1===t.redraw?t.redraw=void 0:"function"==typeof e&&e(),!1===n&&(t.preventDefault(),t.stopPropagation())},{render:function(t,e){if(!t)throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");var n=[],o=c(),i=t.namespaceURI;null==t.vnodes&&(t.textContent=""),e=r.normalizeChildren(Array.isArray(e)?e:[e]),v(t,t.vnodes,e,n,null,"http://www.w3.org/1999/xhtml"===i?void 0:i),t.vnodes=e,null!=o&&c()!==o&&"function"==typeof o.focus&&o.focus();for(var u=0;u-1&&r.splice(e,2)}function u(){if(o)throw new Error("Nested m.redraw.sync() call");o=!0;for(var t=1;t-1&&c.pop();for(var f=0;f-1?r:o>-1?o:t.length;if(r>-1){var u=o>-1?o:t.length,a=g(t.slice(r+1,u));for(var c in a)e[c]=a[c]}if(o>-1){var l=g(t.slice(o+1));for(var c in l)n[c]=l[c]}return t.slice(0,i)}var a={prefix:"#!",getPath:function(){switch(a.prefix.charAt(0)){case"#":return i("hash").slice(a.prefix.length);case"?":return i("search").slice(a.prefix.length)+i("hash");default:return i("pathname").slice(a.prefix.length)+i("search")+i("hash")}},setPath:function(e,n,o){var i={},c={};if(e=u(e,i,c),null!=n){for(var l in n)i[l]=n[l];e=e.replace(/:([^\/]+)/g,function(t,e){return delete i[e],n[e]})}var f=d(i);f&&(e+="?"+f);var s=d(c);if(s&&(e+="#"+s),r){var v=o?o.state:null,p=o?o.title:null;t.onpopstate(),o&&o.replace?t.history.replaceState(v,p,a.prefix+e):t.history.pushState(v,p,a.prefix+e)}else t.location.href=a.prefix+e}};return a.defineRoutes=function(e,i,c){function l(){var n=a.getPath(),r={},o=u(n,r,r),l=t.history.state;if(null!=l)for(var f in l)r[f]=l[f];for(var s in e){var d=new RegExp("^"+s.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(d.test(o))return void o.replace(d,function(){for(var t=s.match(/:[^\/]+/g)||[],o=[].slice.call(arguments,1,-2),u=0;u=0&&(t._idleTimeoutId=setTimeout(function(){t._onTimeout&&t._onTimeout()},e))},n(3),e.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==t&&t.setImmediate||this&&this.setImmediate,e.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==t&&t.clearImmediate||this&&this.clearImmediate}).call(this,n(1))},function(t,e,n){(function(t,e){!function(t,n){"use strict";if(!t.setImmediate){var r,o,i,u,a,c=1,l={},f=!1,s=t.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(t);d=d&&d.setTimeout?d:t,"[object process]"==={}.toString.call(t.process)?r=function(t){e.nextTick(function(){p(t)})}:!function(){if(t.postMessage&&!t.importScripts){var e=!0,n=t.onmessage;return t.onmessage=function(){e=!1},t.postMessage("","*"),t.onmessage=n,e}}()?t.MessageChannel?((i=new MessageChannel).port1.onmessage=function(t){p(t.data)},r=function(t){i.port2.postMessage(t)}):s&&"onreadystatechange"in s.createElement("script")?(o=s.documentElement,r=function(t){var e=s.createElement("script");e.onreadystatechange=function(){p(t),e.onreadystatechange=null,o.removeChild(e),e=null},o.appendChild(e)}):r=function(t){setTimeout(p,0,t)}:(u="setImmediate$"+Math.random()+"$",a=function(e){e.source===t&&"string"==typeof e.data&&0===e.data.indexOf(u)&&p(+e.data.slice(u.length))},t.addEventListener?t.addEventListener("message",a,!1):t.attachEvent("onmessage",a),r=function(e){t.postMessage(u+e,"*")}),d.setImmediate=function(t){"function"!=typeof t&&(t=new Function(""+t));for(var e=new Array(arguments.length-1),n=0;n1)for(var n=1;n0?!t.every(function(t,e){return t===r[e]}):!n);return l[e]=t,o},m=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return function(e,n){if(h(n)){var r=function(){var t=e();"function"==typeof t&&(v.set(e,t),v.set("_",p))};d.push(t?function(){return new Promise(function(t){return requestAnimationFrame(t)}).then(r)}:r)}}},y=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(t){return t},r=u++;return n||(o[r]=t),[o[r],function(t){var n=o[r],i=e(t,r);o[r]=i,i!==n&&p()}]},g=function(t,e){var r=h(e),o=c(n?y():y(t()),2),i=o[0],u=o[1];return n&&r&&u(t()),i},b={useState:function(t){return y(t,function(t,e){return"function"==typeof t?t(o[e]):t})},useEffect:m(!0),useLayoutEffect:m(),useReducer:function(t,e,r){var o=!n&&r?r(e):e,i=c(y(o),2),u=i[0],a=i[1];return[u,function(e){return a(t(u,e))}]},useRef:function(t){return c(y({current:t}),1)[0]},useMemo:g,useCallback:function(t,e){return g(function(){return t},e)}},w=a({},b,e&&e(b)),k=function(){d.forEach(f),d.length=0,s=0,u=0};return{view:function(e){return t(e,w)},oncreate:function(){return k(),n=!0},onupdate:k,onremove:function(){i(v.values()).forEach(f)}}}},f=Function.prototype.call.bind(Function.prototype.call),s=function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return function(t){return l(function(e,n){return t(a({},e.attrs,n,{children:e.children}))})}(function(r){var o=null!=e?e(r):{};return t(a({},r,o,n))})};function d(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var v=function(t){var e=t.useState,n={useCounter:function(){var t=function(){return{id:(new Date).getTime(),initialCount:Math.round(10*Math.random())}},e=t(),r=d(n.useArray([e]),3),o=r[0],i=r[1],u=r[2];return[o,function(){return i(t())},function(t){return u(t)}]},useArray:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=d(e(t),2),r=n[0],o=n[1];return[r,function(t){return o(r.concat(t))},function(t){return o(r.filter(function(e){return e!==t}))}]}};return n};function p(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var h=function(t,e){switch(e.type){case"increment":return{count:t.count+1};case"decrement":return{count:t.count-1};default:throw new Error("Unhandled action:",e)}},m=s(function(t){var e=t.id,n=t.initialCount,r=t.removeCounter,i=t.useEffect,u=t.useState,a=t.useRef,c=p((0,t.useReducer)(h,{count:n}),2),l=c[0],f=c[1],s=l.count,d=p(u(!1),2),v=d[0],m=d[1],y=a(),g=a();return i(function(){m(!0)},[]),o()(".counter",{className:v?"active":"",oncreate:function(t){return y.current=t.dom}},o()(".counter-inner",[o()(".count",{oncreate:function(t){return g.current=t.dom}},s),o()("button",{className:"button",disabled:0===s,onclick:function(){return f({type:"decrement"})}},o()("span.icon.is-small",o()("i.fas.fa-minus"))),o()("button",{className:"button",onclick:function(){return f({type:"increment"})}},o()("span.icon.is-small",o()("i.fas.fa-plus"))),o()(".spacer"),o()("button",{className:"delete is-large",onclick:function(){return y.current.addEventListener("transitionend",function t(){return r(e),y.current.removeEventListener("transitionend",t)}),void y.current.classList.remove("active")}},"Remove me")]))},v);function y(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var g=s(function(t){var e=y((0,t.useCounter)(),3),n=e[0],r=e[1],i=e[2];return[o()(".controls",[o()("button",{className:"button is-info",onclick:function(){return r()}},"Add counter"),o()(".spacer"),o()("span.info",o()("span",{className:"tag is-light is-medium"},"Counters: ".concat(n.length)))]),n.map(function(t){return o()(m,{key:t.id,id:t.id,initialCount:t.initialCount,removeCounter:function(){return i(t)}})})]},v);function b(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var w=s(function(t){var e=b((0,t.useState)(!1),2),n=e[0],r=e[1];return o()(".toggle",[o()("button",{className:"button ".concat(n?"is-info":""),onclick:function(){return r(!n)}},"Toggle"),o()(".info",n?"On":"Off")])});function k(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var C=l(function(t,e){var n=k((0,e.useCount)(0),3),r=n[0],i=n[1],u=n[2];return o()("[data-test-id=CounterCustomHooks]",[o()("h2","CounterCustomHooks"),o()("p",[o()("span","count: "),o()("span[data-test-id=count]",r)]),o()("button[data-test-id=decrement]",{disabled:0===r,onclick:function(){return u()}},"Less"),o()("button[data-test-id=increment]",{onclick:function(){return i()}},"More")])},function(t){var e=t.useState;return{useCount:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,n=k(e(t),2),r=n[0],o=n[1];return[r,function(){return o(r+1)},function(){return o(r-1)}]}}}),E=l(function(t,e){var n=k((0,e.useCounter)(),3),r=n[0],i=n[1],u=n[2],a=k(r.reverse(),1)[0];return o()("[data-test-id=ItemsCustomHooks]",[o()("h2","ItemsCustomHooks"),o()("p",[o()("span","counters: "),o()("span[data-test-id=count]",r.length)]),o()("button[data-test-id=decrement]",{disabled:0===r.length,onclick:function(){return u(a)}},"Remove"),o()("button[data-test-id=increment]",{onclick:function(){return i()}},"Add")])},function(t){var e=t.useState,n={useCounter:function(){var t=function(){return{id:(new Date).getTime(),initialCount:Math.round(1e3*Math.random())}},e=t(),r=k(n.useArray([e]),3),o=r[0],i=r[1],u=r[2];return[o,function(){return i(t())},function(t){return u(t)}]},useArray:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=k(e(t),2),r=n[0],o=n[1];return[r,function(t){return o(r.concat(t))},function(t){return o(r.filter(function(e){return e!==t}))}]}};return n}),T={view:function(){return[o()(C),o()(E)]}};function x(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var S=l(function(t,e){var n=x((0,e.useState)(t.attrs.initialCount),1)[0];return o()("[data-test-id=InitialValue]",[o()("h2","InitialValue"),o()("p[data-test-id=count]","Count: ".concat(n))])}),A=l(function(t,e){var n=e.useState,r=e.useEffect,i=x(n(t.attrs.initialCount),2),u=i[0],a=i[1];return r(function(){a(function(t){return t+1})},[]),o()("[data-test-id=WithEffect]",[o()("h2","WithEffect"),o()("p[data-test-id=count]","Count: ".concat(u))])}),I=l(function(t,e){var n=x((0,e.useState)(t.attrs.initialCount),2),r=n[0],i=n[1];return o()("[data-test-id=Interactive]",[o()("h2","Interactive"),o()("p[data-test-id=count]","Count: ".concat(r)),o()("button[data-test-id=button]",{onclick:function(){return i(r+1)}},"Add"),o()("button[data-test-id=fn-button]",{onclick:function(){return i(function(t){return t+1})}},"Add fn")])}),O={view:function(){return[o()(S,{initialCount:1}),o()(A,{initialCount:100}),o()(I,{initialCount:1e3})]}},R=l(function(t,e){var n=(0,e.useRef)();return o()("[data-test-id=DomElementRef]",[o()("h2","DomElementRef"),o()("div",{oncreate:function(t){return n.current=t.dom}},"QWERTY"),o()("p",[o()("span","element text: "),o()("span[data-test-id=textContent]",n.current&&n.current.textContent)]),o()("button[data-test-id=render]",{onclick:function(){}},"Trigger render")])}),j={view:function(){return[o()(R)]}};function M(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var z=null,L=l(function(t,e){var n=e.useCallback,r=M((0,e.useState)(0),2),i=r[0],u=r[1],a=n(function(){return null},[i]);return o()("[data-test-id=Callback]",[o()("h2","Callback"),o()("p",[o()("span","callback reference: "),o()("span[data-test-id=callbackReference]",(z===a).toString())]),o()("button[data-test-id=update]",{onclick:function(){return u(function(t){return t+1})}},"Trigger update"),o()("button[data-test-id=updatePreviousCallback]",{onclick:function(){z!==a&&(z=a)}},"Update previousCallback"),o()("button[data-test-id=render]",{onclick:function(){}},"Trigger render")])}),N={view:function(){return[o()(L)]}};function P(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var D=l(function(t,e){var n=e.useState,r=e.useEffect,i=P(n(!1),2),u=i[0],a=i[1];return r(function(){var t=document.querySelector("#root");u?t.classList.add("dark-mode"):t.classList.remove("dark-mode")},[u]),o()("[data-test-id=dark]",[o()("h2","SideEffect"),o()("p[data-test-id=darkModeEnabled]","SideEffect mode enabled: ".concat(u)),o()("button[data-test-id=button]",{onclick:function(){return a(!0)}},"Set dark mode")])}),H={view:function(){return[o()(D)]}};function $(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var _=l(function(t,e){var n=e.useState,r=e.useLayoutEffect,i=e.useRef,u=$(n(100),2),a=u[0],c=u[1],l=$(n(0),2),f=l[0],s=l[1],d=$(n(!1),2),v=d[0],p=d[1],h=i();return r(function(){h.current&&s(h.current.offsetHeight)},[a,v]),o()("[data-test-id=DomElementSize]",[o()("h2","DomElementSize"),o()("p",[o()("span","element size: "),o()("span[data-test-id=elementSize]",a)]),o()("p",[o()("span","measured height: "),o()("span[data-test-id=measuredHeight]",f)]),o()("button[data-test-id=clear-button]",{onclick:function(){return s(0)}},"Clear"),o()("button[data-test-id=button]",{onclick:function(){return c(function(t){return t+10})}},"Grow"),o()("button[data-test-id=render]",{onclick:function(){}},"Trigger render"),o()("div",{oncreate:function(t){return h.current=t.dom,p(!0)},style:{width:"".concat(a,"px"),height:"".concat(a,"px"),backgroundColor:"#333"}})])}),U={view:function(){return o()(_)}};function F(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var V=l(function(t,e){var n=e.useMemo,r=F((0,e.useState)(0),2),i=r[0],u=r[1],a=n(function(){return function(){for(var t=[],e=1e3+Math.floor(40*Math.random()),n=0;n0&&void 0!==arguments[0]?arguments[0]:[],n=nt(e(t),2),r=n[0],o=n[1];return[r,function(t){return o(r.concat(t))},function(t){return o(r.filter(function(e){return e!==t}))}]}};return n}),ot=s(function(t){var e=t.initialCount,n=nt((0,t.useState)(e),2),r=n[0],i=n[1];return o()("div[data-test-id=simple-counter]",[o()("div",o()("span[data-test-id=count]",r)),o()("button[data-test-id=add-count]",{onclick:function(){return i(r+1)}},"More")])}),it=s(function(t){var e=t.initialCount,n=t.useState,r=t.children,i=nt(n(e),2),u=i[0],a=i[1];return o()("div[data-test-id=simple-counter-with-children]",[o()("div",o()("span[data-test-id=count]",u)),o()("button[data-test-id=add-count]",{onclick:function(){return a(u+1)}},"More"),o()("div[data-test-id=children]",r)])}),ut={view:function(){return[o()(rt,{initialCount:1}),o()(ot,{initialCount:10}),o()(it,{initialCount:10},[o()("div","One"),o()("div","Two"),o()("div","Three")])]}},at={useEffect:0,useLayoutEffect:0},ct=l(function(t,e){var n=e.useEffect,r=e.useLayoutEffect;return r(function(){at.useLayoutEffect+=(new Date).getTime()},[]),n(function(){at.useEffect+=(new Date).getTime()},[]),n(function(){at.useEffect+=(new Date).getTime()},[]),r(function(){at.useLayoutEffect+=(new Date).getTime()},[]),o()("[data-test-id=EffectTimings]",[o()("h2","EffectTimings"),at.useEffect?o()("p",[o()("div","useEffect: "),o()("span[data-test-id=useEffect]",at.useEffect.toString())]):null,at.useLayoutEffect?o()("p",[o()("div","useLayoutEffect: "),o()("span[data-test-id=useLayoutEffect]",at.useLayoutEffect.toString())]):null,o()("button[data-test-id=button]",{onclick:function(){}},"Trigger render")])}),lt={view:function(){return[o()(ct)]}};function ft(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var st={useEffectEmptyDeps:0,useEffectVariable:0},dt=l(function(t,e){var n=e.useEffect;return st.useEffectEmptyDeps++,n(function(){},[]),o()("[data-test-id=EffectCountEmpty]",[o()("h2","EffectCountEmpty"),o()("p[data-test-id=renderCounts]",st.useEffectEmptyDeps),o()("button[data-test-id=button]",{onclick:function(){}},"Trigger render")])}),vt=l(function(t,e){var n=e.useState,r=e.useEffect;st.useEffectVariable++;var i=ft(n(0),2),u=i[0],a=i[1];return r(function(){},[u]),o()("[data-test-id=EffectCountVariable]",[o()("h2","EffectCountVariable"),o()("p[data-test-id=counts]",u),o()("p[data-test-id=renderCounts]",st.useEffectVariable),o()("button[data-test-id=button-increment]",{onclick:function(){return a(u+1)}},"More"),o()("button[data-test-id=button]",{onclick:function(){}},"Trigger render")])}),pt={view:function(){return[o()(dt),o()(vt)]}};function ht(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var mt=s(function(t){var e=t.initialCount,n=t.useState,r=t.useCounter,i=t.extra,u=ht(n(e),2),a=u[0],c=u[1],l=ht(r(),2),f=l[0],s=l[1];return o()("div[data-test-id=counter]",[o()("div",o()("span[data-test-id=extra]",i)),o()("div",o()("span[data-test-id=count]",a)),o()("button[data-test-id=add-count]",{onclick:function(){return c(a+1)}},"More"),o()("div",o()("span[data-test-id=counters]",f.length)),o()("button[data-test-id=add-counter]",{onclick:function(){return s()}},"Add counter")])},function(t){var e=t.useState,n={useCounter:function(){var t=function(){return{id:(new Date).getTime(),initialCount:Math.round(10*Math.random())}},e=t(),r=ht(n.useArray([e]),3),o=r[0],i=r[1],u=r[2];return[o,function(){return i(t())},function(t){return u(t)}]},useArray:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=ht(e(t),2),r=n[0],o=n[1];return[r,function(t){return o(r.concat(t))},function(t){return o(r.filter(function(e){return e!==t}))}]}};return n},{initialCount:99,extra:"extra"});function yt(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var u,a=t[Symbol.iterator]();!(r=(u=a.next()).done)&&(n.push(u.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw i}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var gt=[["Simple toggle","/toggle",w],["Custom hooks with useReducer","/custom-hooks-usereducer",g]],bt=[["Test hookup custom hooks","/TestHookupCustomHooks",T],["Test hookup useState","/TestHookupUseState",O],["Test hookup useRef","/TestHookupUseRef",j],["Test hookup useCallback","/TestHookupUseCallback",N],["Test hookup useEffect","/TestHookupUseEffect",H],["Test hookup useLayoutEffect","/TestHookupUseLayoutEffect",U],["Test hookup useMemo","/TestHookupUseMemo",q],["Test hookup useReducer","/TestUseReducer",G],["Test hookup update rules","/TestHookupUpdateRules",et],["Test withHooks","/TestWithHooks",ut],["Test withHooks extra arguments","/TestWithHooksExtraArguments",{view:function(){return[o()(mt)]}}],["Test effect timing","/TestEffectTiming",lt],["Test effect render counts","/TestEffectRenderCounts",pt]],wt=function(t,e,n){return o()("li",o()("a",{href:t,oncreate:o.a.route.link,className:t===e?"is-active":""},n))},kt={view:function(t){return o()(".layout",[(e=o.a.route.get(),o()("aside.menu",[o()("p.menu-label","mithril-hooks Demos"),o()("ul.menu-list",gt.map(function(t){var n=yt(t,2),r=n[0],o=n[1];return wt(o,e,r)})),bt.length?(o()("p.menu-label","Cypress tests"),o()("ul.menu-list",bt.map(function(t){var n=yt(t,2),r=n[0],o=n[1];return wt(o,e,r)}))):null])),o()(".component",t.children)]);var e}},Ct=document.getElementById("root"),Et=gt.concat(bt),Tt=Et.reduce(function(t,e){var n=yt(e,3),r=n[1],i=n[2];return t[r]={render:function(){return o()(kt,{href:r},o()(i))}},t},{}),xt=yt(Et[0],2)[1];o.a.route(Ct,xt,Tt)}]);
2 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------