├── .browserslistrc ├── .eslintrc ├── .gitignore ├── README.md ├── babel.config.base.js ├── babel.config.es.js ├── babel.config.umd.js ├── lerna.json ├── package.json ├── packages ├── mithril-hookup │ ├── README.md │ ├── dist │ │ ├── mithril-hookup.js │ │ ├── mithril-hookup.js.map │ │ └── mithril-hookup.mjs │ ├── package.json │ └── src │ │ ├── hookup.js │ │ ├── index.js │ │ └── utils.js └── test-mithril-hookup │ ├── cypress.json │ ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ ├── custom-hooks.spec.js │ │ ├── effect-render-counts.spec.js │ │ ├── effect-timing.spec.js │ │ ├── extra-arguments.spec.js │ │ ├── update-rules.spec.js │ │ ├── useCallback.spec.js │ │ ├── useEffect.spec.js │ │ ├── useLayoutEffect.spec.js │ │ ├── useMemo.spec.js │ │ ├── useReducer.spec.js │ │ ├── useRef.spec.js │ │ ├── useState.spec.js │ │ └── withHooks.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js │ ├── dist │ ├── css │ │ ├── app.css │ │ ├── custom-hooks-usereducer.css │ │ └── toggle.css │ ├── index.html │ └── js │ │ ├── index.js │ │ ├── index.js.gz │ │ ├── index.js.map │ │ └── index.js.map.gz │ ├── package.json │ ├── src │ ├── custom-hooks-usereducer │ │ ├── Counter.js │ │ ├── customHooks.js │ │ └── index.js │ ├── cypress-tests │ │ ├── TestEffectRenderCounts.js │ │ ├── TestEffectTiming.js │ │ ├── TestHookupCustomHooks.js │ │ ├── TestHookupUpdateRules.js │ │ ├── TestHookupUseCallback.js │ │ ├── TestHookupUseEffect.js │ │ ├── TestHookupUseLayoutEffect.js │ │ ├── TestHookupUseMemo.js │ │ ├── TestHookupUseRef.js │ │ ├── TestHookupUseState.js │ │ ├── TestUseReducer.js │ │ ├── TestWithHooks.js │ │ └── TestWithHooksExtraArguments.js │ ├── index.js │ └── toggle │ │ └── index.js │ └── test │ ├── debug.js │ └── withHooks.js └── scripts ├── rollup.base.js ├── rollup.es.js ├── rollup.umd.js ├── webpack.config.dev.js ├── webpack.config.js └── webpack.config.prod.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 version 2 | > 1% 3 | not dead 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log* 4 | *.log 5 | *.lock 6 | package-lock.json -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.2.7" 6 | } 7 | -------------------------------------------------------------------------------- /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/mithril-hookup/README.md: -------------------------------------------------------------------------------- 1 | # mithril-hookup 2 | 3 | Use hooks in Mithril. 4 | 5 | [Documentation](../..//README.md) 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/src/index.js: -------------------------------------------------------------------------------- 1 | export * from "./hookup"; 2 | export * from "./utils"; 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/test-mithril-hookup/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080/#!", 3 | "video": false 4 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/dist/css/toggle.css: -------------------------------------------------------------------------------- 1 | .toggle .info { 2 | color: #666; 3 | margin: .5em 0 0 0; 4 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /packages/test-mithril-hookup/dist/js/index.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArthurClemens/mithril-hookup/5e925c03b7dc8ad611b6b90bc5ee38992ed7f7d4/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/5e925c03b7dc8ad611b6b90bc5ee38992ed7f7d4/packages/test-mithril-hookup/dist/js/index.js.map.gz -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------