├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── index.md └── modules │ ├── Cmd.ts.md │ ├── Debug │ ├── Html.ts.md │ ├── Navigation.ts.md │ ├── commons.ts.md │ ├── console.ts.md │ ├── index.ts.md │ └── redux-devtool.ts.md │ ├── Decode.ts.md │ ├── Html.ts.md │ ├── Http.ts.md │ ├── Navigation.ts.md │ ├── Platform.ts.md │ ├── React.ts.md │ ├── Sub.ts.md │ ├── Task.ts.md │ ├── Time.ts.md │ ├── index.md │ └── index.ts.md ├── examples ├── Blessed.tsx ├── ComposeModules │ ├── App.tsx │ ├── Counter.tsx │ ├── StringBuilder.tsx │ ├── helpers.ts │ └── index.tsx ├── Counter.tsx ├── DebuggerHtml.tsx ├── Http.tsx ├── LabeledCheckboxes.tsx ├── Navigation.tsx ├── Task.tsx └── react-blessed.d.ts ├── package-lock.json ├── package.json ├── scripts ├── docs-index.ts ├── helpers │ ├── fs.ts │ ├── logger.ts │ └── program.ts └── rewrite-es6-paths.ts ├── src ├── Cmd.ts ├── Debug │ ├── Html.ts │ ├── Navigation.ts │ ├── commons.ts │ ├── console.ts │ ├── index.ts │ └── redux-devtool.ts ├── Decode.ts ├── Html.ts ├── Http.ts ├── Navigation.ts ├── Platform.ts ├── React.ts ├── Sub.ts ├── Task.ts ├── Time.ts └── index.ts ├── test ├── Cmd.test.ts ├── Debug │ ├── Html.test.ts │ ├── Navigation.test.ts │ ├── _helpers.ts │ ├── commons.test.ts │ ├── console.test.ts │ └── redux-devtool.test.ts ├── Decode.test.ts ├── Html.test.ts ├── Http.test.ts ├── Navigation.test.ts ├── Platform.test.ts ├── React.test.tsx ├── Sub.test.ts ├── Task.test.ts ├── Time.test.ts └── helpers │ ├── app.ts │ ├── mock-history.ts │ └── utils.ts ├── tsconfig.build-es6.json ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | *.log 3 | node_modules 4 | lib 5 | es6 6 | dist 7 | dev 8 | coverage 9 | tmp 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /docs -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "prettier.printWidth": 120, 4 | "prettier.semi": false, 5 | "prettier.singleQuote": true, 6 | "tslint.enable": true 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > 5 | > - [New Feature] 6 | > - [Bug Fix] 7 | > - [Breaking Change] 8 | > - [Documentation] 9 | > - [Internal] 10 | > - [Polish] 11 | > - [Experimental] 12 | 13 | **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a 14 | high state of flux, you're at risk of it changing without notice. 15 | 16 | ## 0.6.0 17 | 18 | - **Bug Fix** 19 | 20 | - `XMLHttpRequest`'s response headers in `Response` object (@StefanoMagrassi) 21 | 22 | - **New Feature** 23 | - new `sendBy` function in `Http` module which carries the full `Response` object (@StefanoMagrassi) 24 | 25 | ## 0.5.8 26 | 27 | - **Bug Fix** 28 | - fix issue with empty response bodies, now they are converted to `{}`, fix #46 (@bmazzarol) 29 | 30 | ## 0.5.7 31 | 32 | - **Bug Fix** 33 | - fix execution of initial commands when React view runs effect on mount (@StefanoMagrassi) 34 | 35 | ## 0.5.6 36 | 37 | - **Internal** 38 | - add a namespace to `Decoder`'s `URI` in order to avoid name collision (@StefanoMagrassi) 39 | 40 | ## 0.5.5 41 | 42 | - **Bug Fix** 43 | - fix `response` for `BadStatus` error in order to comply with `Response` type (@StefanoMagrassi) 44 | 45 | ## 0.5.4 46 | 47 | - **New Feature** 48 | - Stop/unsubscribe application (@StefanoMagrassi) 49 | 50 | ## 0.5.3 51 | 52 | - **New Feature** 53 | - `Debugger` specialization for `Navigation` programs (@StefanoMagrassi) 54 | 55 | ## 0.5.2 56 | 57 | - **Bug Fix:** 58 | - fix return type of `programWithDebugger` in order to comply to `Html`'s `program` (@StefanoMagrassi) 59 | 60 | ## 0.5.1 61 | 62 | - **Breaking Change** 63 | - upgrade to `fp-ts@2.x` (@gcanti) 64 | - upgrade to `rxjs@6.x` (@gcanti) 65 | - by default do not export `Navigation` from `index`, resolves #14 (@gcanti) 66 | - `Cmd` 67 | - make `map` data-last (@gcanti) 68 | - add `of` function - it lifts a `Msg` into a command (@StefanoMagrassi) 69 | - `Decode` 70 | - remove dependency on `io-ts` (@gcanti) 71 | - refactor `Decoder` definition (@gcanti) 72 | - `Html` 73 | - make `map` data-last (@gcanti) 74 | - `Http` 75 | - remove `Expect` type (@gcanti) 76 | - remove class encoding for `BadUrl`, `Timeout`, `NetworkError`, `BadStatus`, `BadPayload` (@gcanti) 77 | - make `send` data-last (@gcanti) 78 | - `React` 79 | - make `map` data-last (@gcanti) 80 | - `Sub` 81 | - make `map` data-last (@gcanti) 82 | - `Task` 83 | - make `perform` data-last (@gcanti) 84 | - make `attempt` data-last (@gcanti) 85 | - remove `sequence` (@gcanti) 86 | - **New Feature** 87 | - Debugger support, resolves #3 (@StefanoMagrassi, @minedeljkovic) 88 | - **Internal** 89 | - Remove `axios` as dependency, resolves #4 (@StefanoMagrassi) 90 | - Full tests coverage (@StefanoMagrassi) 91 | - switch from `Mocha` to `Jest` as test runner (@StefanoMagrassi) 92 | 93 | ## 0.4.3 94 | 95 | - **Bug Fix** 96 | - don't rely on `instanceof Error` while matching axios errors, closes #23 (@minedeljkovic) 97 | 98 | ## 0.4.1 99 | 100 | - **New Feature** 101 | - expose `http.requestToTask` as `toTask` (@minedeljkovic) 102 | - **Polish** 103 | - remove `react-dom` dependency (@gcanti) 104 | 105 | ## 0.4.0 106 | 107 | - **Breaking Change** 108 | - remove `Reader` from `Html` signature (@minedeljkovic) 109 | - swap `Cmd.map` arguments (@minedeljkovic) 110 | - swap `Decoder.map` arguments (@minedeljkovic) 111 | - swap `Http.send` arguments (@minedeljkovic) 112 | - swap `Task.perform` arguments (@minedeljkovic) 113 | - swap `Task.attempt` arguments (@minedeljkovic) 114 | - **New Feature** 115 | - add `Html.map` function (@minedeljkovic) 116 | - add `React.map` function (@minedeljkovic) 117 | - add `Sub.map` function (@minedeljkovic) 118 | 119 | ## 0.3.1 120 | 121 | - **Bug Fix** 122 | - call subscriptions with initial model, fix #10 (@minedeljkovic) 123 | 124 | ## 0.3.0 125 | 126 | - **Breaking Change** 127 | - upgrade to `fp-ts@1.x.x`, `io-ts@1.x.x` (@gcanti) 128 | 129 | ## 0.2.0 130 | 131 | Refactoring 132 | 133 | ## 0.1.0 134 | 135 | Initial release 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-ts 2 | 3 | A porting of [_The Elm Architecture_](https://guide.elm-lang.org/architecture/) to TypeScript featuring `fp-ts`, `RxJS` and `React`. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm i elm-ts fp-ts rxjs react 9 | ``` 10 | 11 | Note: `fp-ts`, `rxjs` and `react` are peer dependencies 12 | 13 | ## Differences from Elm 14 | 15 | - no ports 16 | - `React` instead of `virtual-dom` (pluggable) 17 | - `Navigation` is based on [history](https://github.com/ReactTraining/history) 18 | 19 | ## React 20 | 21 | ```ts 22 | import * as React from 'elm-ts/lib/React' 23 | import { render } from 'react-dom' 24 | import * as component from './examples/Counter' 25 | 26 | const main = React.program(component.init, component.update, component.view) 27 | 28 | React.run(main, dom => render(dom, document.getElementById('app')!)) 29 | ``` 30 | 31 | ## How to derive decoders from [io-ts](https://github.com/gcanti/io-ts) codecs 32 | 33 | ```ts 34 | import * as t from 'io-ts' 35 | import { failure } from 'io-ts/lib/PathReporter' 36 | 37 | function fromCodec(codec: t.Decoder): Decoder { 38 | return flow( 39 | codec.decode, 40 | E.mapLeft(errors => failure(errors).join('\n')) 41 | ) 42 | } 43 | ``` 44 | 45 | ## Enable debugger in development mode 46 | 47 | For `Html` (and its specializations) programs: 48 | 49 | ```ts 50 | import { programWithDebugger } from 'elm-ts/lib/Debug/Html' 51 | import * as React from 'elm-ts/lib/React' 52 | import { render } from 'react-dom' 53 | import * as component from './examples/Counter' 54 | 55 | const program = process.env.NODE_ENV === 'production' ? React.program : programWithDebugger 56 | 57 | const main = program(component.init, component.update, component.view) 58 | 59 | React.run(main, dom => render(dom, document.getElementById('app')!)) 60 | ``` 61 | 62 | For `Navigation` (and its specializations) programs: 63 | 64 | ```ts 65 | import { programWithDebuggerWithFlags } from 'elm-ts/lib/Debug/Navigation' 66 | import * as Navigation from 'elm-ts/lib/Navigation' 67 | import * as React from 'elm-ts/lib/React' 68 | import { render } from 'react-dom' 69 | import * as component from './examples/Navigation' 70 | 71 | const program = process.env.NODE_ENV === 'production' ? Navigation.programWithFlags : programWithDebuggerWithFlags 72 | 73 | const main = program(component.locationToMsg, component.init, component.update, component.view) 74 | 75 | React.run(main(component.flags), dom => render(dom, document.getElementById('app')!)) 76 | ``` 77 | 78 | ## Stop the application 79 | 80 | If you need to stop the application for any reason, you can use the `withStop` combinator: 81 | 82 | ```ts 83 | import { withStop } from 'elm-ts/lib/Html' 84 | import * as React from 'elm-ts/lib/React' 85 | import { render } from 'react-dom' 86 | import { fromEvent } from 'rxjs' 87 | import * as component from './examples/Counter' 88 | 89 | const stopSignal$ = fromEvent(document.getElementById('stop-btn'), 'click') 90 | 91 | const program = React.program(component.init, component.update, component.view) 92 | 93 | const main = withStop(stopSignal$)(program) 94 | 95 | React.run(main, dom => render(dom, document.getElementById('app')!)) 96 | ``` 97 | 98 | The combinator takes a `Program` and stops consuming it when the provided `Observable` emits a value. 99 | 100 | In case you want to enable the debugger, you have to use some specific functions from the `Debug` sub-module: 101 | 102 | ```ts 103 | // instead of `programWithDebuggerWithFlags` 104 | import { programWithDebuggerWithFlagsWithStop } from 'elm-ts/lib/Debug/Navigation' 105 | import { withStop } from 'elm-ts/lib/Html' 106 | import * as Navigation from 'elm-ts/lib/Navigation' 107 | import * as React from 'elm-ts/lib/React' 108 | import { render } from 'react-dom' 109 | import * as component from './examples/Navigation' 110 | 111 | const stopSignal$ = fromEvent(document.getElementById('stop-btn'), 'click') 112 | 113 | const program = 114 | process.env.NODE_ENV === 'production' 115 | ? Navigation.programWithFlags 116 | : programWithDebuggerWithFlagsWithStop(stopSignal$) 117 | 118 | const main = withStop(stopSignal$)(program(component.locationToMsg, component.init, component.update, component.view)) 119 | 120 | React.run(main(component.flags), dom => render(dom, document.getElementById('app')!)) 121 | ``` 122 | 123 | ## Examples 124 | 125 | - [Counter](examples/Counter.tsx) 126 | - [Labeled Checkboxes (with a sprinkle of functional optics)](examples/LabeledCheckboxes.tsx) 127 | - [Task, Time and Option](examples/Task.tsx) 128 | - [Http and Either](examples/Http.tsx) 129 | - [Navigation](examples/Navigation.tsx) 130 | - [Compose Modules](examples/ComposeModules/index.tsx) 131 | 132 | ## Documentation 133 | 134 | - [API Reference](https://gcanti.github.io/elm-ts) 135 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | 3 | # Enable or disable the site search 4 | search_enabled: true 5 | 6 | # Aux links for the upper right navigation 7 | aux_links: 8 | 'elm-ts on GitHub': 9 | - 'https://github.com/gcanti/elm-ts' 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | nav_order: 1 4 | --- 5 | 6 | # elm-ts 7 | 8 | A porting of [_The Elm Architecture_](https://guide.elm-lang.org/architecture/) to TypeScript featuring `fp-ts`, `RxJS` and `React`. 9 | 10 | ## Installation 11 | 12 | ```sh 13 | npm i elm-ts fp-ts rxjs react 14 | ``` 15 | 16 | Note: `fp-ts`, `rxjs` and `react` are peer dependencies 17 | 18 | ## Differences from Elm 19 | 20 | - no ports 21 | - `React` instead of `virtual-dom` (pluggable) 22 | - `Navigation` is based on [history](https://github.com/ReactTraining/history) 23 | 24 | ## React 25 | 26 | ```ts 27 | import * as React from 'elm-ts/lib/React' 28 | import { render } from 'react-dom' 29 | import * as component from './examples/Counter' 30 | 31 | const main = React.program(component.init, component.update, component.view) 32 | 33 | React.run(main, dom => render(dom, document.getElementById('app')!)) 34 | ``` 35 | 36 | ## How to derive decoders from [io-ts](https://github.com/gcanti/io-ts) codecs 37 | 38 | ```ts 39 | import * as t from 'io-ts' 40 | import { failure } from 'io-ts/lib/PathReporter' 41 | 42 | function fromCodec(codec: t.Decoder): Decoder { 43 | return flow( 44 | codec.decode, 45 | E.mapLeft(errors => failure(errors).join('\n')) 46 | ) 47 | } 48 | ``` 49 | 50 | ## Enable debugger in development mode 51 | 52 | For `Html` (and its specializations) programs: 53 | 54 | ```ts 55 | import { programWithDebugger } from 'elm-ts/lib/Debug/Html' 56 | import * as React from 'elm-ts/lib/React' 57 | import { render } from 'react-dom' 58 | import * as component from './examples/Counter' 59 | 60 | const program = process.env.NODE_ENV === 'production' ? React.program : programWithDebugger 61 | 62 | const main = program(component.init, component.update, component.view) 63 | 64 | React.run(main, dom => render(dom, document.getElementById('app')!)) 65 | ``` 66 | 67 | For `Navigation` (and its specializations) programs: 68 | 69 | ```ts 70 | import { programWithDebuggerWithFlags } from 'elm-ts/lib/Debug/Navigation' 71 | import * as Navigation from 'elm-ts/lib/Navigation' 72 | import * as React from 'elm-ts/lib/React' 73 | import { render } from 'react-dom' 74 | import * as component from './examples/Navigation' 75 | 76 | const program = process.env.NODE_ENV === 'production' ? Navigation.programWithFlags : programWithDebuggerWithFlags 77 | 78 | const main = program(component.locationToMsg, component.init, component.update, component.view) 79 | 80 | React.run(main(component.flags), dom => render(dom, document.getElementById('app')!)) 81 | ``` 82 | 83 | ## Stop the application 84 | 85 | If you need to stop the application for any reason, you can use the `withStop` combinator: 86 | 87 | ```ts 88 | import { withStop } from 'elm-ts/lib/Html' 89 | import * as React from 'elm-ts/lib/React' 90 | import { render } from 'react-dom' 91 | import { fromEvent } from 'rxjs' 92 | import * as component from './examples/Counter' 93 | 94 | const stopSignal$ = fromEvent(document.getElementById('stop-btn'), 'click') 95 | 96 | const program = React.program(component.init, component.update, component.view) 97 | 98 | const main = withStop(stopSignal$)(program) 99 | 100 | React.run(main, dom => render(dom, document.getElementById('app')!)) 101 | ``` 102 | 103 | The combinator takes a `Program` and stops consuming it when the provided `Observable` emits a value. 104 | 105 | In case you want to enable the debugger, you have to use some specific functions from the `Debug` sub-module: 106 | 107 | ```ts 108 | // instead of `programWithDebuggerWithFlags` 109 | import { programWithDebuggerWithFlagsWithStop } from 'elm-ts/lib/Debug/Navigation' 110 | import { withStop } from 'elm-ts/lib/Html' 111 | import * as Navigation from 'elm-ts/lib/Navigation' 112 | import * as React from 'elm-ts/lib/React' 113 | import { render } from 'react-dom' 114 | import * as component from './examples/Navigation' 115 | 116 | const stopSignal$ = fromEvent(document.getElementById('stop-btn'), 'click') 117 | 118 | const program = 119 | process.env.NODE_ENV === 'production' 120 | ? Navigation.programWithFlags 121 | : programWithDebuggerWithFlagsWithStop(stopSignal$) 122 | 123 | const main = withStop(stopSignal$)(program(component.locationToMsg, component.init, component.update, component.view)) 124 | 125 | React.run(main(component.flags), dom => render(dom, document.getElementById('app')!)) 126 | ``` 127 | 128 | ## Examples 129 | 130 | - [Counter](examples/Counter.tsx) 131 | - [Labeled Checkboxes (with a sprinkle of functional optics)](examples/LabeledCheckboxes.tsx) 132 | - [Task, Time and Option](examples/Task.tsx) 133 | - [Http and Either](examples/Http.tsx) 134 | - [Navigation](examples/Navigation.tsx) 135 | - [Compose Modules](examples/ComposeModules/index.tsx) 136 | 137 | ## Documentation 138 | 139 | - [API Reference](https://gcanti.github.io/elm-ts) 140 | -------------------------------------------------------------------------------- /docs/modules/Cmd.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cmd.ts 3 | nav_order: 1 4 | parent: Modules 5 | --- 6 | 7 | ## Cmd overview 8 | 9 | Defines `Cmd`s as streams of asynchronous operations which can not fail and that can optionally carry a message. 10 | 11 | See the [Platform.Cmd](https://package.elm-lang.org/packages/elm/core/latest/Platform-Cmd) Elm package. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [Applicative](#applicative) 20 | - [of](#of) 21 | - [Functor](#functor) 22 | - [map](#map) 23 | - [constructors](#constructors) 24 | - [none](#none) 25 | - [model](#model) 26 | - [Cmd (interface)](#cmd-interface) 27 | - [utils](#utils) 28 | - [batch](#batch) 29 | 30 | --- 31 | 32 | # Applicative 33 | 34 | ## of 35 | 36 | Creates a new `Cmd` that carries the provided `Msg`. 37 | 38 | **Signature** 39 | 40 | ```ts 41 | export declare function of(m: Msg): Cmd 42 | ``` 43 | 44 | Added in v0.5.0 45 | 46 | # Functor 47 | 48 | ## map 49 | 50 | Maps the carried `Msg` of a `Cmd` into another `Msg`. 51 | 52 | **Signature** 53 | 54 | ```ts 55 | export declare function map(f: (a: A) => Msg): (cmd: Cmd
) => Cmd 56 | ``` 57 | 58 | Added in v0.5.0 59 | 60 | # constructors 61 | 62 | ## none 63 | 64 | A `none` command is an empty stream. 65 | 66 | **Signature** 67 | 68 | ```ts 69 | export declare const none: Cmd 70 | ``` 71 | 72 | Added in v0.5.0 73 | 74 | # model 75 | 76 | ## Cmd (interface) 77 | 78 | **Signature** 79 | 80 | ```ts 81 | export interface Cmd extends Observable>> {} 82 | ``` 83 | 84 | Added in v0.5.0 85 | 86 | # utils 87 | 88 | ## batch 89 | 90 | Batches the execution of a list of commands. 91 | 92 | **Signature** 93 | 94 | ```ts 95 | export declare function batch(arr: Array>): Cmd 96 | ``` 97 | 98 | Added in v0.5.0 99 | -------------------------------------------------------------------------------- /docs/modules/Debug/Html.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/Html.ts 3 | nav_order: 4 4 | parent: Modules 5 | --- 6 | 7 | ## Html overview 8 | 9 | This module makes available a debugging utility for `elm-ts` applications running `Html` programs. 10 | 11 | `elm-ts` ships with a [Redux DevTool Extension](https://github.com/zalmoxisus/redux-devtools-extension) integration, falling back to a simple debugger via standard browser's [`console`](https://developer.mozilla.org/en-US/docs/Web/API/Console) in case the extension is not available. 12 | 13 | **Note:** debugging is to be considered unsafe by design so it should be used only in **development**. 14 | 15 | This is an example of usage: 16 | 17 | ```ts 18 | import { react, cmd } from 'elm-ts' 19 | import { programWithDebugger } from 'elm-ts/lib/Debug/Html' 20 | import { render } from 'react-dom' 21 | 22 | type Model = number 23 | type Msg = 'INCREMENT' | 'DECREMENT' 24 | 25 | declare const init: [Model, cmd.none] 26 | declare function update(msg: Msg, model: Model): [Model, cmd.Cmd] 27 | declare function view(model: Model): react.Html 28 | 29 | const program = process.NODE_ENV === 'production' ? react.program : programWithDebugger 30 | 31 | const main = program(init, update, view) 32 | 33 | react.run(main, (dom) => render(dom, document.getElementById('app'))) 34 | ``` 35 | 36 | Added in v0.5.3 37 | 38 | --- 39 | 40 |

Table of contents

41 | 42 | - [constructors](#constructors) 43 | - [programWithDebugger](#programwithdebugger) 44 | - [programWithDebuggerWithFlags](#programwithdebuggerwithflags) 45 | - [programWithDebuggerWithFlagsWithStop](#programwithdebuggerwithflagswithstop) 46 | - [programWithDebuggerWithStop](#programwithdebuggerwithstop) 47 | 48 | --- 49 | 50 | # constructors 51 | 52 | ## programWithDebugger 53 | 54 | Adds a debugging capability to a generic `Html` `Program`. 55 | 56 | It tracks every `Message` dispatched and resulting `Model` update. 57 | 58 | It also lets directly updating the application's state with a special `Message` of type: 59 | 60 | ```ts 61 | { 62 | type: '__DebugUpdateModel__' 63 | payload: Model 64 | } 65 | ``` 66 | 67 | or applying a message with: 68 | 69 | ```ts 70 | { 71 | type: '__DebugApplyMsg__' 72 | payload: Msg 73 | } 74 | ``` 75 | 76 | **Signature** 77 | 78 | ```ts 79 | export declare function programWithDebugger( 80 | init: [Model, Cmd], 81 | update: (msg: Msg, model: Model) => [Model, Cmd], 82 | view: (model: Model) => Html, 83 | subscriptions?: (model: Model) => Sub 84 | ): Program 85 | ``` 86 | 87 | Added in v0.5.3 88 | 89 | ## programWithDebuggerWithFlags 90 | 91 | Same as `programWithDebugger()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export declare function programWithDebuggerWithFlags( 97 | init: (flags: Flags) => [Model, Cmd], 98 | update: (msg: Msg, model: Model) => [Model, Cmd], 99 | view: (model: Model) => Html, 100 | subscriptions?: (model: Model) => Sub 101 | ): (flags: Flags) => Program 102 | ``` 103 | 104 | Added in v0.5.3 105 | 106 | ## programWithDebuggerWithFlagsWithStop 107 | 108 | Same as `programWithDebuggerWithStop()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 109 | 110 | **Signature** 111 | 112 | ```ts 113 | export declare function programWithDebuggerWithFlagsWithStop( 114 | stopDebuggerOn: Observable 115 | ): ( 116 | init: (flags: Flags) => [S, Cmd], 117 | update: (msg: M, model: S) => [S, Cmd], 118 | view: (model: S) => Html, 119 | subscriptions?: (model: S) => Sub 120 | ) => (flags: Flags) => Program 121 | ``` 122 | 123 | Added in v0.5.4 124 | 125 | ## programWithDebuggerWithStop 126 | 127 | A function that requires an `Observable` and returns a `programWithDebugger()` function: the underlying debugger will stop when the `Observable` emits a value. 128 | 129 | **Signature** 130 | 131 | ```ts 132 | export declare function programWithDebuggerWithStop( 133 | stopDebuggerOn: Observable 134 | ): ( 135 | init: [S, Cmd], 136 | update: (msg: M, model: S) => [S, Cmd], 137 | view: (model: S) => Html, 138 | subscriptions?: (model: S) => Sub 139 | ) => Program 140 | ``` 141 | 142 | Added in v0.5.4 143 | -------------------------------------------------------------------------------- /docs/modules/Debug/Navigation.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/Navigation.ts 3 | nav_order: 6 4 | parent: Modules 5 | --- 6 | 7 | ## Navigation overview 8 | 9 | This module makes available a debugging utility for `elm-ts` applications running `Navigation` programs. 10 | 11 | `elm-ts` ships with a [Redux DevTool Extension](https://github.com/zalmoxisus/redux-devtools-extension) integration, falling back to a simple debugger via standard browser's [`console`](https://developer.mozilla.org/en-US/docs/Web/API/Console) in case the extension is not available. 12 | 13 | **Note:** debugging is to be considered unsafe by design so it should be used only in **development**. 14 | 15 | This is an example of usage: 16 | 17 | ```ts 18 | import { react, cmd } from 'elm-ts' 19 | import { programWithDebugger } from 'elm-ts/lib/Debug/Navigation' 20 | import { Location, program } from 'elm-ts/lib/Navigation' 21 | import { render } from 'react-dom' 22 | 23 | type Model = number 24 | type Msg = 'INCREMENT' | 'DECREMENT' 25 | 26 | declare function locationToMsg(location: Location): Msg 27 | declare function init(location: Location): [Model, cmd.none] 28 | declare function update(msg: Msg, model: Model): [Model, cmd.Cmd] 29 | declare function view(model: Model): react.Html 30 | 31 | const program = process.NODE_ENV === 'production' ? program : programWithDebugger 32 | 33 | const main = program(locationToMsg, init, update, view) 34 | 35 | react.run(main, (dom) => render(dom, document.getElementById('app'))) 36 | ``` 37 | 38 | Added in v0.5.3 39 | 40 | --- 41 | 42 |

Table of contents

43 | 44 | - [constructors](#constructors) 45 | - [programWithDebugger](#programwithdebugger) 46 | - [programWithDebuggerWithFlags](#programwithdebuggerwithflags) 47 | - [programWithDebuggerWithFlagsWithStop](#programwithdebuggerwithflagswithstop) 48 | - [programWithDebuggerWithStop](#programwithdebuggerwithstop) 49 | 50 | --- 51 | 52 | # constructors 53 | 54 | ## programWithDebugger 55 | 56 | Adds a debugging capability to a generic `Navigation` `Program`. 57 | 58 | It tracks every `Message` dispatched and resulting `Model` update. 59 | 60 | It also lets directly updating the application's state with a special `Message` of type: 61 | 62 | ```ts 63 | { 64 | type: '__DebugUpdateModel__' 65 | payload: Model 66 | } 67 | ``` 68 | 69 | or applying a message with: 70 | 71 | ```ts 72 | { 73 | type: '__DebugApplyMsg__' 74 | payload: Msg 75 | } 76 | ``` 77 | 78 | **Signature** 79 | 80 | ```ts 81 | export declare function programWithDebugger( 82 | locationToMessage: (location: Location) => Msg, 83 | init: (location: Location) => [Model, Cmd], 84 | update: (msg: Msg, model: Model) => [Model, Cmd], 85 | view: (model: Model) => Html, 86 | subscriptions?: (model: Model) => Sub 87 | ): Program 88 | ``` 89 | 90 | Added in v0.5.3 91 | 92 | ## programWithDebuggerWithFlags 93 | 94 | Same as `programWithDebugger()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 95 | 96 | **Signature** 97 | 98 | ```ts 99 | export declare function programWithDebuggerWithFlags( 100 | locationToMessage: (location: Location) => Msg, 101 | init: (flags: Flags) => (location: Location) => [Model, Cmd], 102 | update: (msg: Msg, model: Model) => [Model, Cmd], 103 | view: (model: Model) => Html, 104 | subscriptions?: (model: Model) => Sub 105 | ): (flags: Flags) => Program 106 | ``` 107 | 108 | Added in v0.5.3 109 | 110 | ## programWithDebuggerWithFlagsWithStop 111 | 112 | Same as `programWithDebuggerWithStop()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 113 | 114 | **Signature** 115 | 116 | ```ts 117 | export declare function programWithDebuggerWithFlagsWithStop( 118 | stopDebuggerOn: Observable 119 | ): ( 120 | locationToMessage: (location: Location) => M, 121 | init: (flags: Flags) => (location: Location) => [S, Cmd], 122 | update: (msg: M, model: S) => [S, Cmd], 123 | view: (model: S) => Html, 124 | subscriptions?: (model: S) => Sub 125 | ) => (flags: Flags) => Program 126 | ``` 127 | 128 | Added in v0.5.4 129 | 130 | ## programWithDebuggerWithStop 131 | 132 | A function that requires an `Observable` and returns a `programWithDebugger()` function: the underlying debugger will stop when the `Observable` emits a value. 133 | 134 | **Signature** 135 | 136 | ```ts 137 | export declare function programWithDebuggerWithStop( 138 | stopDebuggerOn: Observable 139 | ): ( 140 | locationToMessage: (location: Location) => M, 141 | init: (location: Location) => [S, Cmd], 142 | update: (msg: M, model: S) => [S, Cmd], 143 | view: (model: S) => Html, 144 | subscriptions?: (model: S) => Sub 145 | ) => Program 146 | ``` 147 | 148 | Added in v0.5.4 149 | -------------------------------------------------------------------------------- /docs/modules/Debug/commons.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/commons.ts 3 | nav_order: 2 4 | parent: Modules 5 | --- 6 | 7 | ## commons overview 8 | 9 | Common utilities and type definitions for the `Debug` module. 10 | 11 | Added in v0.5.0 12 | 13 | --- 14 | 15 |

Table of contents

16 | 17 | - [constructors](#constructors) 18 | - [debugMsg](#debugmsg) 19 | - [constructos](#constructos) 20 | - [debugInit](#debuginit) 21 | - [model](#model) 22 | - [Debug (interface)](#debug-interface) 23 | - [DebugAction (type alias)](#debugaction-type-alias) 24 | - [DebugData (type alias)](#debugdata-type-alias) 25 | - [DebugInit (interface)](#debuginit-interface) 26 | - [DebugMsg (interface)](#debugmsg-interface) 27 | - [Debugger (interface)](#debugger-interface) 28 | - [DebuggerR (interface)](#debuggerr-interface) 29 | - [Global (type alias)](#global-type-alias) 30 | - [MsgWithDebug (type alias)](#msgwithdebug-type-alias) 31 | - [utils](#utils) 32 | - [runDebugger](#rundebugger) 33 | - [updateWithDebug](#updatewithdebug) 34 | 35 | --- 36 | 37 | # constructors 38 | 39 | ## debugMsg 40 | 41 | Creates a `DebugMsg` 42 | 43 | **Signature** 44 | 45 | ```ts 46 | export declare const debugMsg: (payload: Msg) => DebugMsg 47 | ``` 48 | 49 | Added in v0.5.0 50 | 51 | # constructos 52 | 53 | ## debugInit 54 | 55 | Creates a `DebugInit` 56 | 57 | **Signature** 58 | 59 | ```ts 60 | export declare const debugInit: () => DebugInit 61 | ``` 62 | 63 | Added in v0.5.0 64 | 65 | # model 66 | 67 | ## Debug (interface) 68 | 69 | Defines a generic debugging function 70 | 71 | **Signature** 72 | 73 | ```ts 74 | export interface Debug { 75 | (data: DebugData): void 76 | } 77 | ``` 78 | 79 | Added in v0.5.0 80 | 81 | ## DebugAction (type alias) 82 | 83 | **Signature** 84 | 85 | ```ts 86 | export type DebugAction = DebugInit | DebugMsg 87 | ``` 88 | 89 | Added in v0.5.0 90 | 91 | ## DebugData (type alias) 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export type DebugData = [DebugAction, Model] 97 | ``` 98 | 99 | Added in v0.5.0 100 | 101 | ## DebugInit (interface) 102 | 103 | **Signature** 104 | 105 | ```ts 106 | export interface DebugInit { 107 | type: 'INIT' 108 | } 109 | ``` 110 | 111 | Added in v0.5.0 112 | 113 | ## DebugMsg (interface) 114 | 115 | **Signature** 116 | 117 | ```ts 118 | export interface DebugMsg { 119 | type: 'MESSAGE' 120 | payload: Msg 121 | } 122 | ``` 123 | 124 | Added in v0.5.0 125 | 126 | ## Debugger (interface) 127 | 128 | Defines a generic `Debugger` 129 | 130 | **Signature** 131 | 132 | ```ts 133 | export interface Debugger { 134 | (d: DebuggerR): { 135 | debug: Debug 136 | stop: () => void 137 | } 138 | } 139 | ``` 140 | 141 | Added in v0.5.4 142 | 143 | ## DebuggerR (interface) 144 | 145 | Defines the dependencies for a `Debugger` function. 146 | 147 | **Signature** 148 | 149 | ```ts 150 | export interface DebuggerR { 151 | init: Model 152 | debug$: BehaviorSubject> 153 | dispatch: Dispatch> 154 | } 155 | ``` 156 | 157 | Added in v0.5.0 158 | 159 | ## Global (type alias) 160 | 161 | **Signature** 162 | 163 | ```ts 164 | export type Global = typeof window 165 | ``` 166 | 167 | Added in v0.5.0 168 | 169 | ## MsgWithDebug (type alias) 170 | 171 | Extends `Msg` with a special kind of message from Debugger 172 | 173 | **Signature** 174 | 175 | ```ts 176 | export type MsgWithDebug = 177 | | Msg 178 | | { type: '__DebugUpdateModel__'; payload: Model } 179 | | { type: '__DebugApplyMsg__'; payload: Msg } 180 | ``` 181 | 182 | Added in v0.5.0 183 | 184 | # utils 185 | 186 | ## runDebugger 187 | 188 | Checks which type of debugger can be used (standard `console` or _Redux DevTool Extension_) based on provided `window` and prepares the subscription to the "debug" stream 189 | 190 | **Warning:** this function **SHOULD** be considered as an internal method; using it in your application **SHOULD** be avoided. 191 | 192 | **Signature** 193 | 194 | ```ts 195 | export declare function runDebugger( 196 | win: Global, 197 | stop$: Observable 198 | ): (deps: DebuggerR) => IO 199 | ``` 200 | 201 | Added in v0.5.4 202 | 203 | ## updateWithDebug 204 | 205 | Adds debugging capability to the provided `update` function. 206 | 207 | It tracks through the `debug$` stream every `Message` dispatched and resulting `Model` update. 208 | 209 | It also lets directly updating the application's state with a special `Message` of type: 210 | 211 | ```ts 212 | { 213 | type: '__DebugUpdateModel__' 214 | payload: Model 215 | } 216 | ``` 217 | 218 | or applying a message with: 219 | 220 | ```ts 221 | { 222 | type: '__DebugApplyMsg__' 223 | payload: Msg 224 | } 225 | ``` 226 | 227 | **Signature** 228 | 229 | ```ts 230 | export declare function updateWithDebug( 231 | debug$: BehaviorSubject>, 232 | update: (msg: Msg, model: Model) => [Model, Cmd] 233 | ): (msg: MsgWithDebug, model: Model) => [Model, Cmd] 234 | ``` 235 | 236 | Added in v0.5.3 237 | -------------------------------------------------------------------------------- /docs/modules/Debug/console.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/console.ts 3 | nav_order: 3 4 | parent: Modules 5 | --- 6 | 7 | ## console overview 8 | 9 | Debug via standard browser's `console`. 10 | 11 | Added in v0.5.0 12 | 13 | --- 14 | 15 |

Table of contents

16 | 17 | - [constructors](#constructors) 18 | - [consoleDebugger](#consoledebugger) 19 | 20 | --- 21 | 22 | # constructors 23 | 24 | ## consoleDebugger 25 | 26 | **[UNSAFE]** Simple debugger that uses the standard browser's `console` 27 | 28 | **Signature** 29 | 30 | ```ts 31 | export declare function consoleDebugger(): Debugger 32 | ``` 33 | 34 | Added in v0.5.4 35 | -------------------------------------------------------------------------------- /docs/modules/Debug/index.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/index.ts 3 | nav_order: 5 4 | parent: Modules 5 | --- 6 | 7 | ## index overview 8 | 9 | This module makes available a debugging utility for `elm-ts` applications. 10 | 11 | Use of the functions directly exported from this module is **deprecated**. 12 | 13 | Please use the specialized versions that you can find under `Debug/`. 14 | 15 | Added in v0.5.0 16 | 17 | --- 18 | 19 |

Table of contents

20 | 21 | - [constructors](#constructors) 22 | - [~~programWithDebuggerWithFlags~~](#programwithdebuggerwithflags) 23 | - [~~programWithDebugger~~](#programwithdebugger) 24 | 25 | --- 26 | 27 | # constructors 28 | 29 | ## ~~programWithDebuggerWithFlags~~ 30 | 31 | **Signature** 32 | 33 | ```ts 34 | export declare const programWithDebuggerWithFlags: typeof HtmlDebugger.programWithDebuggerWithFlags 35 | ``` 36 | 37 | Added in v0.5.0 38 | 39 | ## ~~programWithDebugger~~ 40 | 41 | **Signature** 42 | 43 | ```ts 44 | export declare const programWithDebugger: typeof HtmlDebugger.programWithDebugger 45 | ``` 46 | 47 | Added in v0.5.0 48 | -------------------------------------------------------------------------------- /docs/modules/Debug/redux-devtool.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debug/redux-devtool.ts 3 | nav_order: 7 4 | parent: Modules 5 | --- 6 | 7 | ## redux-devtool overview 8 | 9 | Integration with _Redux DevTool Extension_. 10 | 11 | Please check the [docs](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API) fur further information. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [constructors](#constructors) 20 | - [reduxDevToolDebugger](#reduxdevtooldebugger) 21 | - [model](#model) 22 | - [Connection (interface)](#connection-interface) 23 | - [Extension (interface)](#extension-interface) 24 | - [utils](#utils) 25 | - [getConnection](#getconnection) 26 | 27 | --- 28 | 29 | # constructors 30 | 31 | ## reduxDevToolDebugger 32 | 33 | **[UNSAFE]** Debug through _Redux DevTool Extension_ 34 | 35 | **Signature** 36 | 37 | ```ts 38 | export declare function reduxDevToolDebugger(connection: Connection): Debugger 39 | ``` 40 | 41 | Added in v0.5.4 42 | 43 | # model 44 | 45 | ## Connection (interface) 46 | 47 | Defines a _Redux DevTool Extension_ connection object. 48 | 49 | **Signature** 50 | 51 | ```ts 52 | export interface Connection { 53 | subscribe: (listener?: Dispatch) => Unsubscription 54 | send(action: null, state: LiftedState): void 55 | send(action: Msg, state: Model): void 56 | init: (state: Model) => void 57 | error: (message: unknown) => void 58 | unsubscribe: () => void 59 | } 60 | ``` 61 | 62 | Added in v0.5.0 63 | 64 | ## Extension (interface) 65 | 66 | Defines a _Redux DevTool Extension_ object. 67 | 68 | **Signature** 69 | 70 | ```ts 71 | export interface Extension { 72 | connect: () => Connection 73 | } 74 | ``` 75 | 76 | Added in v0.5.0 77 | 78 | # utils 79 | 80 | ## getConnection 81 | 82 | Gets a _Redux DevTool Extension_ connection in case the extension is available 83 | 84 | **Signature** 85 | 86 | ```ts 87 | export declare function getConnection(global: Global): IO>> 88 | ``` 89 | 90 | Added in v0.5.0 91 | -------------------------------------------------------------------------------- /docs/modules/Decode.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Decode.ts 3 | nav_order: 8 4 | parent: Modules 5 | --- 6 | 7 | ## Decode overview 8 | 9 | Defines a `Decoder`, namely a function that receives an `unknown` value and tries to decodes it in an `A` value. 10 | 11 | It returns an `Either` with a `string` as `Left` when decoding fails or an `A` as `Right` when decoding succeeds. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [Alt](#alt) 20 | - [alt](#alt) 21 | - [Apply](#apply) 22 | - [ap](#ap) 23 | - [apFirst](#apfirst) 24 | - [apSecond](#apsecond) 25 | - [Functor](#functor) 26 | - [map](#map) 27 | - [Monad](#monad) 28 | - [chain](#chain) 29 | - [chainFirst](#chainfirst) 30 | - [flatten](#flatten) 31 | - [combinators](#combinators) 32 | - [orElse](#orelse) 33 | - [constructors](#constructors) 34 | - [left](#left) 35 | - [right](#right) 36 | - [instances](#instances) 37 | - [URI](#uri) 38 | - [URI (type alias)](#uri-type-alias) 39 | - [decoder](#decoder) 40 | - [model](#model) 41 | - [Decoder (interface)](#decoder-interface) 42 | 43 | --- 44 | 45 | # Alt 46 | 47 | ## alt 48 | 49 | **Signature** 50 | 51 | ```ts 52 | export declare const alt:
(that: () => Decoder) => (fa: Decoder) => Decoder 53 | ``` 54 | 55 | Added in v0.5.0 56 | 57 | # Apply 58 | 59 | ## ap 60 | 61 | **Signature** 62 | 63 | ```ts 64 | export declare const ap: (fa: Decoder) => (fab: Decoder<(a: A) => B>) => Decoder 65 | ``` 66 | 67 | Added in v0.5.0 68 | 69 | ## apFirst 70 | 71 | **Signature** 72 | 73 | ```ts 74 | export declare const apFirst: (fb: Decoder) => (fa: Decoder) => Decoder 75 | ``` 76 | 77 | Added in v0.5.0 78 | 79 | ## apSecond 80 | 81 | **Signature** 82 | 83 | ```ts 84 | export declare const apSecond: (fb: Decoder) => (fa: Decoder) => Decoder 85 | ``` 86 | 87 | Added in v0.5.0 88 | 89 | # Functor 90 | 91 | ## map 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export declare const map: (f: (a: A) => B) => (fa: Decoder) => Decoder 97 | ``` 98 | 99 | Added in v0.5.0 100 | 101 | # Monad 102 | 103 | ## chain 104 | 105 | **Signature** 106 | 107 | ```ts 108 | export declare const chain: (f: (a: A) => Decoder) => (ma: Decoder) => Decoder 109 | ``` 110 | 111 | Added in v0.5.0 112 | 113 | ## chainFirst 114 | 115 | **Signature** 116 | 117 | ```ts 118 | export declare const chainFirst: (f: (a: A) => Decoder) => (ma: Decoder) => Decoder 119 | ``` 120 | 121 | Added in v0.5.0 122 | 123 | ## flatten 124 | 125 | **Signature** 126 | 127 | ```ts 128 | export declare const flatten: (mma: Decoder>) => Decoder 129 | ``` 130 | 131 | Added in v0.5.0 132 | 133 | # combinators 134 | 135 | ## orElse 136 | 137 | **Signature** 138 | 139 | ```ts 140 | export declare const orElse: (f: (e: string) => Decoder) => (ma: Decoder) => Decoder 141 | ``` 142 | 143 | Added in v0.5.0 144 | 145 | # constructors 146 | 147 | ## left 148 | 149 | **Signature** 150 | 151 | ```ts 152 | export declare const left: (e: string) => Decoder 153 | ``` 154 | 155 | Added in v0.5.0 156 | 157 | ## right 158 | 159 | **Signature** 160 | 161 | ```ts 162 | export declare const right: (a: A) => Decoder 163 | ``` 164 | 165 | Added in v0.5.0 166 | 167 | # instances 168 | 169 | ## URI 170 | 171 | **Signature** 172 | 173 | ```ts 174 | export declare const URI: 'elm-ts/Decoder' 175 | ``` 176 | 177 | Added in v0.5.0 178 | 179 | ## URI (type alias) 180 | 181 | **Signature** 182 | 183 | ```ts 184 | export type URI = typeof URI 185 | ``` 186 | 187 | Added in v0.5.0 188 | 189 | ## decoder 190 | 191 | **Signature** 192 | 193 | ```ts 194 | export declare const decoder: Monad1<'elm-ts/Decoder'> & Alternative1<'elm-ts/Decoder'> 195 | ``` 196 | 197 | Added in v0.5.0 198 | 199 | # model 200 | 201 | ## Decoder (interface) 202 | 203 | **Signature** 204 | 205 | ```ts 206 | export interface Decoder extends ReaderEither {} 207 | ``` 208 | 209 | Added in v0.5.0 210 | -------------------------------------------------------------------------------- /docs/modules/Html.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Html.ts 3 | nav_order: 9 4 | parent: Modules 5 | --- 6 | 7 | ## Html overview 8 | 9 | A specialization of `Program` with the capability of mapping `Model` to `View` 10 | and rendering it into a DOM node. 11 | 12 | `Html` is a base abstraction in order to work with any library that renders html. 13 | 14 | Added in v0.5.0 15 | 16 | --- 17 | 18 |

Table of contents

19 | 20 | - [Functor](#functor) 21 | - [map](#map) 22 | - [combinators](#combinators) 23 | - [withStop](#withstop) 24 | - [constructors](#constructors) 25 | - [program](#program) 26 | - [programWithFlags](#programwithflags) 27 | - [model](#model) 28 | - [Html (interface)](#html-interface) 29 | - [Program (interface)](#program-interface) 30 | - [Renderer (interface)](#renderer-interface) 31 | - [utils](#utils) 32 | - [run](#run) 33 | 34 | --- 35 | 36 | # Functor 37 | 38 | ## map 39 | 40 | Maps a view which carries a message of type `A` into a view which carries a message of type `B`. 41 | 42 | **Signature** 43 | 44 | ```ts 45 | export declare function map(f: (a: A) => Msg): (ha: Html) => Html 46 | ``` 47 | 48 | Added in v0.5.0 49 | 50 | # combinators 51 | 52 | ## withStop 53 | 54 | Stops the `program` when `signal` Observable emits a value. 55 | 56 | **Signature** 57 | 58 | ```ts 59 | export declare function withStop( 60 | signal: Observable 61 | ): (program: Program) => Program 62 | ``` 63 | 64 | Added in v0.5.4 65 | 66 | # constructors 67 | 68 | ## program 69 | 70 | Returns a `Program` specialized for `Html`. 71 | 72 | It needs a `view()` function that maps `Model` to `Html`. 73 | 74 | Underneath it uses `Platform.program()`. 75 | 76 | **Signature** 77 | 78 | ```ts 79 | export declare function program( 80 | init: [Model, Cmd], 81 | update: (msg: Msg, model: Model) => [Model, Cmd], 82 | view: (model: Model) => Html, 83 | subscriptions: (model: Model) => Sub = () => none 84 | ): Program 85 | ``` 86 | 87 | Added in v0.5.0 88 | 89 | ## programWithFlags 90 | 91 | Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export declare function programWithFlags( 97 | init: (flags: Flags) => [Model, Cmd], 98 | update: (msg: Msg, model: Model) => [Model, Cmd], 99 | view: (model: Model) => Html, 100 | subscriptions?: (model: Model) => Sub 101 | ): (flags: Flags) => Program 102 | ``` 103 | 104 | Added in v0.5.0 105 | 106 | # model 107 | 108 | ## Html (interface) 109 | 110 | It is defined as a function that takes a `dispatch()` function as input and returns a `Dom` as output, 111 | with DOM and messages types constrained. 112 | 113 | **Signature** 114 | 115 | ```ts 116 | export interface Html { 117 | (dispatch: platform.Dispatch): Dom 118 | } 119 | ``` 120 | 121 | Added in v0.5.0 122 | 123 | ## Program (interface) 124 | 125 | The `Program` interface is extended with a `html$` stream (an `Observable` of views) and a `Dom` type constraint. 126 | 127 | **Signature** 128 | 129 | ```ts 130 | export interface Program extends platform.Program { 131 | html$: Observable> 132 | } 133 | ``` 134 | 135 | Added in v0.5.0 136 | 137 | ## Renderer (interface) 138 | 139 | Defines the generalized `Renderer` as a function that takes a `Dom` as input and returns a `void`. 140 | 141 | It suggests an effectful computation. 142 | 143 | **Signature** 144 | 145 | ```ts 146 | export interface Renderer { 147 | (dom: Dom): void 148 | } 149 | ``` 150 | 151 | Added in v0.5.0 152 | 153 | # utils 154 | 155 | ## run 156 | 157 | Runs the `Program`. 158 | 159 | Underneath it uses `Platform.run()`. 160 | 161 | It subscribes to the views stream (`html$`) and runs `Renderer` for each new value. 162 | 163 | **Signature** 164 | 165 | ```ts 166 | export declare function run( 167 | program: Program, 168 | renderer: Renderer 169 | ): Observable 170 | ``` 171 | 172 | Added in v0.5.0 173 | -------------------------------------------------------------------------------- /docs/modules/Http.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Http.ts 3 | nav_order: 10 4 | parent: Modules 5 | --- 6 | 7 | ## Http overview 8 | 9 | Makes http calls to remote resources as `Cmd`s. 10 | 11 | See [Http](https://package.elm-lang.org/packages/elm/http/latest/Http) Elm package. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [constructors](#constructors) 20 | - [get](#get) 21 | - [post](#post) 22 | - [destructors](#destructors) 23 | - [toTask](#totask) 24 | - [model](#model) 25 | - [Headers (type alias)](#headers-type-alias) 26 | - [HttpError (type alias)](#httperror-type-alias) 27 | - [Method (type alias)](#method-type-alias) 28 | - [Request (interface)](#request-interface) 29 | - [Response (interface)](#response-interface) 30 | - [utils](#utils) 31 | - [send](#send) 32 | - [sendBy](#sendby) 33 | 34 | --- 35 | 36 | # constructors 37 | 38 | ## get 39 | 40 | **Signature** 41 | 42 | ```ts 43 | export declare function get
(url: string, decoder: Decoder): Request 44 | ``` 45 | 46 | Added in v0.5.0 47 | 48 | ## post 49 | 50 | **Signature** 51 | 52 | ```ts 53 | export declare function post(url: string, body: unknown, decoder: Decoder): Request 54 | ``` 55 | 56 | Added in v0.5.0 57 | 58 | # destructors 59 | 60 | ## toTask 61 | 62 | **Signature** 63 | 64 | ```ts 65 | export declare function toTask(req: Request): TaskEither 66 | ``` 67 | 68 | Added in v0.5.0 69 | 70 | # model 71 | 72 | ## Headers (type alias) 73 | 74 | **Signature** 75 | 76 | ```ts 77 | export type Headers = Record 78 | ``` 79 | 80 | Added in v0.6.0 81 | 82 | ## HttpError (type alias) 83 | 84 | **Signature** 85 | 86 | ```ts 87 | export type HttpError = 88 | | { readonly _tag: 'BadUrl'; readonly value: string } 89 | | { readonly _tag: 'Timeout' } 90 | | { readonly _tag: 'NetworkError'; readonly value: string } 91 | | { readonly _tag: 'BadStatus'; readonly response: Response } 92 | | { readonly _tag: 'BadPayload'; readonly value: string; readonly response: Response } 93 | ``` 94 | 95 | Added in v0.5.0 96 | 97 | ## Method (type alias) 98 | 99 | **Signature** 100 | 101 | ```ts 102 | export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' 103 | ``` 104 | 105 | Added in v0.5.0 106 | 107 | ## Request (interface) 108 | 109 | **Signature** 110 | 111 | ```ts 112 | export interface Request { 113 | expect: Decoder 114 | url: string 115 | method: Method 116 | headers: Headers 117 | body?: unknown 118 | timeout: Option 119 | withCredentials: boolean 120 | } 121 | ``` 122 | 123 | Added in v0.5.0 124 | 125 | ## Response (interface) 126 | 127 | **Signature** 128 | 129 | ```ts 130 | export interface Response { 131 | url: string 132 | status: { 133 | code: number 134 | message: string 135 | } 136 | headers: Headers 137 | body: Body 138 | } 139 | ``` 140 | 141 | Added in v0.5.0 142 | 143 | # utils 144 | 145 | ## send 146 | 147 | Executes as `Cmd` the provided call to remote resource, mapping the result to a `Msg`. 148 | 149 | Derived from [`sendBy`](#sendBy). 150 | 151 | **Signature** 152 | 153 | ```ts 154 | export declare function send(f: (e: Either) => Msg): (req: Request) => Cmd 155 | ``` 156 | 157 | Added in v0.5.0 158 | 159 | ## sendBy 160 | 161 | Executes as `Cmd` the provided call to remote resource, mapping the full Response to a `Msg`. 162 | 163 | **Signature** 164 | 165 | ```ts 166 | export declare function sendBy(f: (e: Either>) => Msg): (req: Request) => Cmd 167 | ``` 168 | 169 | Added in v0.6.0 170 | -------------------------------------------------------------------------------- /docs/modules/Navigation.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Navigation.ts 3 | nav_order: 12 4 | parent: Modules 5 | --- 6 | 7 | ## Navigation overview 8 | 9 | A specialization of `Program` that handles application navigation via location's hash. 10 | 11 | It uses [`history`](https://github.com/ReactTraining/history) package. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [constructors](#constructors) 20 | - [program](#program) 21 | - [programWithFlags](#programwithflags) 22 | - [model](#model) 23 | - [Location (type alias)](#location-type-alias) 24 | - [utils](#utils) 25 | - [push](#push) 26 | 27 | --- 28 | 29 | # constructors 30 | 31 | ## program 32 | 33 | Returns a `Program` specialized for `Navigation`. 34 | 35 | The `Program` is a `Html.Program` but it needs a `locationToMsg()` function which converts location changes to messages. 36 | 37 | Underneath it consumes `location$` stream (applying `locationToMsg()` on its values). 38 | 39 | **Signature** 40 | 41 | ```ts 42 | export declare function program( 43 | locationToMessage: (location: Location) => Msg, 44 | init: (location: Location) => [Model, Cmd], 45 | update: (msg: Msg, model: Model) => [Model, Cmd], 46 | view: (model: Model) => html.Html, 47 | subscriptions: (model: Model) => Sub = () => none 48 | ): html.Program 49 | ``` 50 | 51 | Added in v0.5.0 52 | 53 | ## programWithFlags 54 | 55 | Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 56 | 57 | **Signature** 58 | 59 | ```ts 60 | export declare function programWithFlags( 61 | locationToMessage: (location: Location) => Msg, 62 | init: (flags: Flags) => (location: Location) => [Model, Cmd], 63 | update: (msg: Msg, model: Model) => [Model, Cmd], 64 | view: (model: Model) => html.Html, 65 | subscriptions: (model: Model) => Sub = () => none 66 | ): (flags: Flags) => html.Program 67 | ``` 68 | 69 | Added in v0.5.0 70 | 71 | # model 72 | 73 | ## Location (type alias) 74 | 75 | **Signature** 76 | 77 | ```ts 78 | export type Location = H.Location 79 | ``` 80 | 81 | Added in v0.5.0 82 | 83 | # utils 84 | 85 | ## push 86 | 87 | Generates a `Cmd` that adds a new location to the history's list. 88 | 89 | **Signature** 90 | 91 | ```ts 92 | export declare function push(url: string): Cmd 93 | ``` 94 | 95 | Added in v0.5.0 96 | -------------------------------------------------------------------------------- /docs/modules/Platform.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Platform.ts 3 | nav_order: 13 4 | parent: Modules 5 | --- 6 | 7 | ## Platform overview 8 | 9 | The `Platform` module is the backbone of `elm-ts`. 10 | It defines the base `program()` and `run()` functions which will be extended by more specialized modules. 11 | _The Elm Architecture_ is implemented via **RxJS** `Observables`. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [combinators](#combinators) 20 | - [withStop](#withstop) 21 | - [constructors](#constructors) 22 | - [program](#program) 23 | - [programWithFlags](#programwithflags) 24 | - [model](#model) 25 | - [Dispatch (interface)](#dispatch-interface) 26 | - [Program (interface)](#program-interface) 27 | - [utils](#utils) 28 | - [run](#run) 29 | 30 | --- 31 | 32 | # combinators 33 | 34 | ## withStop 35 | 36 | Stops the `program` when `signal` Observable emits a value. 37 | 38 | **Signature** 39 | 40 | ```ts 41 | export declare function withStop( 42 | signal: Observable 43 | ): (program: Program) => Program 44 | ``` 45 | 46 | Added in v0.5.4 47 | 48 | # constructors 49 | 50 | ## program 51 | 52 | `program()` is the real core of `elm-ts`. 53 | 54 | When a new `Program` is defined, a `BehaviorSubject` is created (because an initial value is needed) that will track every change to the `Model` and every `Cmd` executed. 55 | 56 | Every time `dispatch()` is called a new value, computed by the `update()` function, is added to the the stream. 57 | 58 | **Signature** 59 | 60 | ```ts 61 | export declare function program( 62 | init: [Model, Cmd], 63 | update: (msg: Msg, model: Model) => [Model, Cmd], 64 | subscriptions: (model: Model) => Sub = () => none 65 | ): Program 66 | ``` 67 | 68 | Added in v0.5.0 69 | 70 | ## programWithFlags 71 | 72 | Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 73 | 74 | **Signature** 75 | 76 | ```ts 77 | export declare function programWithFlags( 78 | init: (flags: Flags) => [Model, Cmd], 79 | update: (msg: Msg, model: Model) => [Model, Cmd], 80 | subscriptions: (model: Model) => Sub = () => none 81 | ): (flags: Flags) => Program 82 | ``` 83 | 84 | Added in v0.5.0 85 | 86 | # model 87 | 88 | ## Dispatch (interface) 89 | 90 | **Signature** 91 | 92 | ```ts 93 | export interface Dispatch { 94 | (msg: Msg): void 95 | } 96 | ``` 97 | 98 | Added in v0.5.0 99 | 100 | ## Program (interface) 101 | 102 | `Program` is just an object that exposes the underlying streams which compose _The Elm Architecture_. 103 | Even **Commands** and **Subscriptions** are expressed as `Observables` in order to mix them with ease. 104 | 105 | **Signature** 106 | 107 | ```ts 108 | export interface Program { 109 | dispatch: Dispatch 110 | cmd$: Cmd 111 | sub$: Sub 112 | model$: Observable 113 | } 114 | ``` 115 | 116 | Added in v0.5.0 117 | 118 | # utils 119 | 120 | ## run 121 | 122 | Runs the `Program`. 123 | 124 | Because the program essentially is an object of streams, "running it" means subscribing to these streams and starting to consume values. 125 | 126 | **Signature** 127 | 128 | ```ts 129 | export declare function run(program: Program): Observable 130 | ``` 131 | 132 | Added in v0.5.0 133 | -------------------------------------------------------------------------------- /docs/modules/React.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React.ts 3 | nav_order: 14 4 | parent: Modules 5 | --- 6 | 7 | ## React overview 8 | 9 | A specialization of `Html` that uses `React` as renderer. 10 | 11 | Added in v0.5.0 12 | 13 | --- 14 | 15 |

Table of contents

16 | 17 | - [Functor](#functor) 18 | - [map](#map) 19 | - [constructors](#constructors) 20 | - [program](#program) 21 | - [programWithFlags](#programwithflags) 22 | - [model](#model) 23 | - [Dom (interface)](#dom-interface) 24 | - [Html (interface)](#html-interface) 25 | - [Program (interface)](#program-interface) 26 | - [utils](#utils) 27 | - [run](#run) 28 | 29 | --- 30 | 31 | # Functor 32 | 33 | ## map 34 | 35 | `map()` is `Html.map()` with `Html` type constrained to the specialized version for `React`. 36 | 37 | **Signature** 38 | 39 | ```ts 40 | export declare function map(f: (a: A) => Msg): (ha: Html
) => Html 41 | ``` 42 | 43 | Added in v0.5.0 44 | 45 | # constructors 46 | 47 | ## program 48 | 49 | `program()` is `Html.program()` with `Html` type constrained to the specialized version for `React`. 50 | 51 | **Signature** 52 | 53 | ```ts 54 | export declare function program( 55 | init: [Model, Cmd], 56 | update: (msg: Msg, model: Model) => [Model, Cmd], 57 | view: (model: Model) => html.Html, 58 | subscriptions?: (model: Model) => Sub 59 | ): Program 60 | ``` 61 | 62 | Added in v0.5.0 63 | 64 | ## programWithFlags 65 | 66 | Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 67 | 68 | **Signature** 69 | 70 | ```ts 71 | export declare function programWithFlags( 72 | init: (flags: Flags) => [Model, Cmd], 73 | update: (msg: Msg, model: Model) => [Model, Cmd], 74 | view: (model: Model) => html.Html, 75 | subscriptions?: (model: Model) => Sub 76 | ): (flags: Flags) => Program 77 | ``` 78 | 79 | Added in v0.5.0 80 | 81 | # model 82 | 83 | ## Dom (interface) 84 | 85 | `Dom` is a `ReactElement`. 86 | 87 | **Signature** 88 | 89 | ```ts 90 | export interface Dom extends ReactElement {} 91 | ``` 92 | 93 | Added in v0.5.0 94 | 95 | ## Html (interface) 96 | 97 | `Html` has `Dom` type constrained to the specialized version for `React`. 98 | 99 | **Signature** 100 | 101 | ```ts 102 | export interface Html extends html.Html {} 103 | ``` 104 | 105 | Added in v0.5.0 106 | 107 | ## Program (interface) 108 | 109 | **Signature** 110 | 111 | ```ts 112 | export interface Program extends html.Program {} 113 | ``` 114 | 115 | Added in v0.5.0 116 | 117 | # utils 118 | 119 | ## run 120 | 121 | `run()` is `Html.run()` with `dom` type constrained to the specialized version for `React`. 122 | 123 | **Signature** 124 | 125 | ```ts 126 | export declare function run(program: Program, renderer: html.Renderer): Observable 127 | ``` 128 | 129 | Added in v0.5.0 130 | -------------------------------------------------------------------------------- /docs/modules/Sub.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sub.ts 3 | nav_order: 15 4 | parent: Modules 5 | --- 6 | 7 | ## Sub overview 8 | 9 | Defines `Sub`s as streams of messages. 10 | 11 | Added in v0.5.0 12 | 13 | --- 14 | 15 |

Table of contents

16 | 17 | - [Functor](#functor) 18 | - [map](#map) 19 | - [constructors](#constructors) 20 | - [none](#none) 21 | - [model](#model) 22 | - [Sub (interface)](#sub-interface) 23 | - [utils](#utils) 24 | - [batch](#batch) 25 | 26 | --- 27 | 28 | # Functor 29 | 30 | ## map 31 | 32 | Maps `Msg` of a `Sub` into another `Msg`. 33 | 34 | **Signature** 35 | 36 | ```ts 37 | export declare function map(f: (a: A) => Msg): (sub: Sub
) => Sub 38 | ``` 39 | 40 | Added in v0.5.0 41 | 42 | # constructors 43 | 44 | ## none 45 | 46 | A `none` subscription is an empty stream. 47 | 48 | **Signature** 49 | 50 | ```ts 51 | export declare const none: Sub 52 | ``` 53 | 54 | Added in v0.5.0 55 | 56 | # model 57 | 58 | ## Sub (interface) 59 | 60 | **Signature** 61 | 62 | ```ts 63 | export interface Sub extends Observable {} 64 | ``` 65 | 66 | Added in v0.5.0 67 | 68 | # utils 69 | 70 | ## batch 71 | 72 | Merges subscriptions streams into one stream. 73 | 74 | **Signature** 75 | 76 | ```ts 77 | export declare function batch(arr: Array>): Sub 78 | ``` 79 | 80 | Added in v0.5.0 81 | -------------------------------------------------------------------------------- /docs/modules/Task.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Task.ts 3 | nav_order: 16 4 | parent: Modules 5 | --- 6 | 7 | ## Task overview 8 | 9 | Handles the execution of asynchronous effectful operations. 10 | 11 | See the [Task](https://package.elm-lang.org/packages/elm/core/latest/Task) Elm package. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [utils](#utils) 20 | - [attempt](#attempt) 21 | - [perform](#perform) 22 | 23 | --- 24 | 25 | # utils 26 | 27 | ## attempt 28 | 29 | Executes a `Task` that can fail as a `Cmd` mapping the result (`Either`) to a `Msg`. 30 | 31 | **Signature** 32 | 33 | ```ts 34 | export declare function attempt(f: (e: Either) => Msg): (task: Task>) => Cmd 35 | ``` 36 | 37 | Added in v0.5.0 38 | 39 | ## perform 40 | 41 | Executes a `Task` as a `Cmd` mapping the result to a `Msg`. 42 | 43 | **Signature** 44 | 45 | ```ts 46 | export declare function perform(f: (a: A) => Msg): (t: Task
) => Cmd 47 | ``` 48 | 49 | Added in v0.5.0 50 | -------------------------------------------------------------------------------- /docs/modules/Time.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Time.ts 3 | nav_order: 17 4 | parent: Modules 5 | --- 6 | 7 | ## Time overview 8 | 9 | Exposes some utilities to work with unix time. 10 | 11 | See [Time](https://package.elm-lang.org/packages/elm/time/latest/Time) Elm package. 12 | 13 | Added in v0.5.0 14 | 15 | --- 16 | 17 |

Table of contents

18 | 19 | - [constructors](#constructors) 20 | - [now](#now) 21 | - [utils](#utils) 22 | - [every](#every) 23 | 24 | --- 25 | 26 | # constructors 27 | 28 | ## now 29 | 30 | Get the current unix time as a `Task`. 31 | 32 | **Signature** 33 | 34 | ```ts 35 | export declare function now(): Task 36 | ``` 37 | 38 | Added in v0.5.0 39 | 40 | # utils 41 | 42 | ## every 43 | 44 | Get the current unix time periodically. 45 | 46 | **Signature** 47 | 48 | ```ts 49 | export declare function every(time: number, f: (time: number) => Msg): Sub 50 | ``` 51 | 52 | Added in v0.5.0 53 | -------------------------------------------------------------------------------- /docs/modules/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | has_children: true 4 | permalink: /docs/modules 5 | nav_order: 2 6 | --- 7 | -------------------------------------------------------------------------------- /docs/modules/index.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: index.ts 3 | nav_order: 11 4 | parent: Modules 5 | --- 6 | 7 | ## index overview 8 | 9 | Added in v0.5.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [instances](#instances) 16 | - [cmd](#cmd) 17 | - [decode](#decode) 18 | - [html](#html) 19 | - [http](#http) 20 | - [platform](#platform) 21 | - [react](#react) 22 | - [sub](#sub) 23 | - [task](#task) 24 | - [time](#time) 25 | 26 | --- 27 | 28 | # instances 29 | 30 | ## cmd 31 | 32 | **Signature** 33 | 34 | ```ts 35 | export declare const cmd: typeof cmd 36 | ``` 37 | 38 | Added in v0.5.0 39 | 40 | ## decode 41 | 42 | **Signature** 43 | 44 | ```ts 45 | export declare const decode: typeof decode 46 | ``` 47 | 48 | Added in v0.5.0 49 | 50 | ## html 51 | 52 | **Signature** 53 | 54 | ```ts 55 | export declare const html: typeof html 56 | ``` 57 | 58 | Added in v0.5.0 59 | 60 | ## http 61 | 62 | **Signature** 63 | 64 | ```ts 65 | export declare const http: typeof http 66 | ``` 67 | 68 | Added in v0.5.0 69 | 70 | ## platform 71 | 72 | **Signature** 73 | 74 | ```ts 75 | export declare const platform: typeof platform 76 | ``` 77 | 78 | Added in v0.5.0 79 | 80 | ## react 81 | 82 | **Signature** 83 | 84 | ```ts 85 | export declare const react: typeof react 86 | ``` 87 | 88 | Added in v0.5.0 89 | 90 | ## sub 91 | 92 | **Signature** 93 | 94 | ```ts 95 | export declare const sub: typeof sub 96 | ``` 97 | 98 | Added in v0.5.0 99 | 100 | ## task 101 | 102 | **Signature** 103 | 104 | ```ts 105 | export declare const task: typeof task 106 | ``` 107 | 108 | Added in v0.5.0 109 | 110 | ## time 111 | 112 | **Signature** 113 | 114 | ```ts 115 | export declare const time: typeof time 116 | ``` 117 | 118 | Added in v0.5.0 119 | -------------------------------------------------------------------------------- /examples/Blessed.tsx: -------------------------------------------------------------------------------- 1 | import * as blessed from 'blessed' 2 | import * as React from 'react' 3 | import { render } from 'react-blessed' 4 | import * as cmd from '../src/Cmd' 5 | import { Html, program, run } from '../src/React' 6 | 7 | // --- Blessed configuration 8 | // Creating our screen 9 | const screen = blessed.screen({ 10 | autoPadding: true, 11 | smartCSR: true, 12 | title: 'react-blessed hello world' 13 | }) 14 | 15 | // Adding a way to quit the program 16 | screen.key(['escape', 'q', 'C-c'], function() { 17 | return process.exit(0) 18 | }) 19 | 20 | // --- Model 21 | export type Model = undefined 22 | 23 | export const init: [Model, cmd.Cmd] = [undefined, cmd.none] 24 | 25 | // --- Messages 26 | export type Msg = { type: 'NoOp' } 27 | 28 | // --- Update 29 | export function update(msg: Msg, model: Model): [Model, cmd.Cmd] { 30 | switch (msg.type) { 31 | case 'NoOp': 32 | return [model, cmd.none] 33 | } 34 | } 35 | 36 | // --- View 37 | export function view(_: Model): Html { 38 | return _ => 39 | } 40 | 41 | function App() { 42 | return ( 43 | 51 | Hello World! 52 | 53 | ) 54 | } 55 | 56 | // --- Main 57 | export const main = () => run(program(init, update, view), dom => render(dom, screen)) 58 | -------------------------------------------------------------------------------- /examples/ComposeModules/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Cmd, batch } from '../../src/Cmd' 4 | import { Html } from '../../src/React' 5 | import * as Counter from './Counter' 6 | import * as StringBuilder from './StringBuilder' 7 | import { withEffect } from './helpers' 8 | 9 | export type Flags = StringBuilder.Flags // Only `StringBuilder` has flags 10 | 11 | export interface Model { 12 | counter: Counter.Model 13 | stringBuilder: StringBuilder.Model 14 | } 15 | 16 | export type Msg = Counter.Msg | StringBuilder.Msg 17 | 18 | export const init = (flags: Flags): [Model, Cmd] => { 19 | const [counter, counterCmd] = Counter.init 20 | const [stringBuilder, stringBuilderCmd] = StringBuilder.init(flags) 21 | 22 | return [{ counter, stringBuilder }, batch([counterCmd, stringBuilderCmd])] 23 | } 24 | 25 | export const update = (msg: Msg, model: Model): [Model, Cmd] => { 26 | switch (msg.group) { 27 | case 'Counter': 28 | const [counter, counterCmd] = Counter.update(msg, model.counter) 29 | return withEffect({ ...model, counter }, counterCmd) 30 | 31 | case 'StringBuilder': 32 | const [stringBuilder, stringBuilderCmd] = StringBuilder.update(msg, model.stringBuilder) 33 | return withEffect({ ...model, stringBuilder }, stringBuilderCmd) 34 | } 35 | } 36 | 37 | export const view = (model: Model): Html => dispatch => ( 38 |
39 |

My Application

40 | {Counter.view(model.counter)(dispatch)} 41 | {StringBuilder.view(model.stringBuilder)(dispatch)} 42 |
43 | ) 44 | -------------------------------------------------------------------------------- /examples/ComposeModules/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Cmd, none } from '../../src/Cmd' 4 | import { Html } from '../../src/React' 5 | import { withModel } from './helpers' 6 | 7 | export type Model = number 8 | 9 | interface Up { 10 | type: 'Up' 11 | group: 'Counter' 12 | } 13 | 14 | const Up: Up = { type: 'Up', group: 'Counter' } 15 | 16 | interface Down { 17 | type: 'Down' 18 | group: 'Counter' 19 | } 20 | 21 | const Down: Down = { type: 'Down', group: 'Counter' } 22 | 23 | export type Msg = Up | Down 24 | 25 | export const init: [Model, Cmd] = [0, none] 26 | 27 | export const update = (msg: Msg, model: Model): [Model, Cmd] => { 28 | switch (msg.type) { 29 | case 'Up': 30 | return withModel(model + 1) 31 | 32 | case 'Down': 33 | return withModel(model - 1) 34 | } 35 | } 36 | 37 | export const view = (model: Model): Html => dispatch => ( 38 |
39 | 40 | {model} 41 | 42 |
43 | ) 44 | -------------------------------------------------------------------------------- /examples/ComposeModules/StringBuilder.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Cmd, none } from '../../src/Cmd' 4 | import { Html } from '../../src/React' 5 | import { withModel } from './helpers' 6 | 7 | export interface Flags { 8 | prefix: string 9 | } 10 | 11 | export type Model = string 12 | 13 | interface AddChar { 14 | type: 'AddChar' 15 | group: 'StringBuilder' 16 | char: string 17 | } 18 | 19 | const AddChar = (char: string): AddChar => ({ type: 'AddChar', group: 'StringBuilder', char }) 20 | 21 | interface Reset { 22 | type: 'Reset' 23 | group: 'StringBuilder' 24 | } 25 | 26 | const Reset: Reset = { type: 'Reset', group: 'StringBuilder' } 27 | 28 | export type Msg = AddChar | Reset 29 | 30 | export const init = (flags: Flags): [Model, Cmd] => [flags.prefix, none] 31 | 32 | export const update = (msg: Msg, model: Model): [Model, Cmd] => { 33 | switch (msg.type) { 34 | case 'AddChar': 35 | return withModel(`${model}${msg.char}`) 36 | 37 | case 'Reset': 38 | return withModel('') 39 | } 40 | } 41 | 42 | export const view = (model: Model): Html => dispatch => ( 43 |
44 |

{model}

45 | 46 | 47 |
48 | ) 49 | -------------------------------------------------------------------------------- /examples/ComposeModules/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Cmd, none } from '../../src/Cmd' 2 | 3 | export const withEffect = (m: Model, c: Cmd): [Model, Cmd] => [m, c] 4 | 5 | export const withModel = (m: Model): [Model, Cmd] => [m, none] 6 | -------------------------------------------------------------------------------- /examples/ComposeModules/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom' 2 | 3 | import * as DebugHtml from '../../src/Debug/Html' 4 | import * as React from '../../src/React' 5 | 6 | import * as App from './App' 7 | 8 | const program = process.env.NODE_ENV === 'production' ? React.programWithFlags : DebugHtml.programWithDebuggerWithFlags 9 | 10 | const main = program(App.init, App.update, App.view) 11 | 12 | React.run(main({ prefix: 'String: ' }), dom => { 13 | ReactDOM.render(dom, document.getElementById('root')) 14 | }) 15 | -------------------------------------------------------------------------------- /examples/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cmd } from '../src' 3 | import { Html } from '../src/React' 4 | 5 | // --- Model 6 | export type Model = number 7 | 8 | export const init: [Model, cmd.Cmd] = [0, cmd.none] 9 | 10 | // --- Messages 11 | export type Msg = { type: 'Increment' } | { type: 'Decrement' } 12 | 13 | // --- Update 14 | export function update(msg: Msg, model: Model): [Model, cmd.Cmd] { 15 | switch (msg.type) { 16 | case 'Increment': 17 | return [model + 1, cmd.none] 18 | 19 | case 'Decrement': 20 | return [model - 1, cmd.none] 21 | } 22 | } 23 | 24 | // --- View 25 | export function view(model: Model): Html { 26 | return dispatch => ( 27 |
28 | Count: {model} 29 | 30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /examples/DebuggerHtml.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom' 2 | import { programWithDebugger } from '../src/Debug/Html' 3 | import * as React from '../src/React' 4 | import * as component from './Counter' 5 | 6 | const program = process.env.NODE_ENV === 'production' ? React.program : programWithDebugger 7 | 8 | const main = program(component.init, component.update, component.view) 9 | 10 | React.run(main, dom => render(dom, document.getElementById('app'))) 11 | -------------------------------------------------------------------------------- /examples/Http.tsx: -------------------------------------------------------------------------------- 1 | import * as E from 'fp-ts/lib/Either' 2 | import * as O from 'fp-ts/lib/Option' 3 | import { flow } from 'fp-ts/lib/function' 4 | import { pipe } from 'fp-ts/lib/pipeable' 5 | import * as t from 'io-ts' 6 | import { failure } from 'io-ts/lib/PathReporter' 7 | import { Lens } from 'monocle-ts' 8 | import * as React from 'react' 9 | import { cmd, http } from '../src' 10 | import { Html } from '../src/React' 11 | 12 | // original: https://guide.elm-lang.org/architecture/effects/http.html 13 | 14 | export type Result = E.Either 15 | 16 | // --- Flags 17 | export type Flags = Model 18 | 19 | export const flags: Flags = { 20 | topic: 'cats', 21 | gifUrl: O.none 22 | } 23 | 24 | // --- Model 25 | export type Model = { 26 | topic: string 27 | gifUrl: O.Option 28 | } 29 | 30 | export function init(flags: Flags): [Model, cmd.Cmd] { 31 | return [flags, getRandomGif(flags.topic)] 32 | } 33 | 34 | // --- Messages 35 | export type Msg = MorePlease | NewGif 36 | 37 | export type MorePlease = { type: 'MorePlease' } 38 | export type NewGif = { type: 'NewGif'; result: Result } 39 | 40 | function newGif(result: Result): NewGif { 41 | return { type: 'NewGif', result } 42 | } 43 | 44 | // --- Get random gif 45 | const ApiPayloadSchema = t.interface({ 46 | data: t.interface({ 47 | image_url: t.string 48 | }) 49 | }) 50 | 51 | const decoder = flow( 52 | ApiPayloadSchema.decode, 53 | E.mapLeft(errors => failure(errors).join('\n')) 54 | ) 55 | 56 | function getRandomGif(topic: string): cmd.Cmd { 57 | const url = `https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}` 58 | 59 | return pipe( 60 | http.get(url, decoder), 61 | http.send(e => newGif(E.either.map(e, a => a.data.image_url))) 62 | ) 63 | } 64 | 65 | // --- Update 66 | const gifUrlLens = Lens.fromProp()('gifUrl') 67 | 68 | export function update(msg: Msg, model: Model): [Model, cmd.Cmd] { 69 | switch (msg.type) { 70 | case 'MorePlease': 71 | return [gifUrlLens.set(O.none)(model), getRandomGif(model.topic)] 72 | 73 | case 'NewGif': 74 | return [gifUrlLens.set(O.some(msg.result))(model), cmd.none] 75 | } 76 | throw new Error('err') 77 | } 78 | 79 | // --- View 80 | export function view(model: Model): Html { 81 | return dispatch => ( 82 |
83 |

{model.topic}

84 | {pipe( 85 | model.gifUrl, 86 | O.fold( 87 | () => loading..., 88 | E.fold(error => Error: {error._tag}, gifUrl => ) 89 | ) 90 | )} 91 | 92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /examples/LabeledCheckboxes.tsx: -------------------------------------------------------------------------------- 1 | import { Lens } from 'monocle-ts' 2 | import * as React from 'react' 3 | import { cmd } from '../src' 4 | import { Html } from '../src/React' 5 | 6 | // --- Flags 7 | export type Flags = Model 8 | 9 | export const flags: Flags = { 10 | notifications: false, 11 | autoplay: false, 12 | location: false 13 | } 14 | 15 | // --- Model 16 | export type Model = { 17 | notifications: boolean 18 | autoplay: boolean 19 | location: boolean 20 | } 21 | 22 | export function init(flags: Flags): [Model, cmd.Cmd] { 23 | return [flags, cmd.none] 24 | } 25 | 26 | // --- Messages 27 | export type Msg = { type: 'ToggleNotifications' } | { type: 'ToggleAutoplay' } | { type: 'ToggleLocation' } 28 | 29 | // --- Update 30 | const notificationsLens = Lens.fromProp()('notifications') 31 | const autoplayLens = Lens.fromProp()('autoplay') 32 | const locationLens = Lens.fromProp()('location') 33 | 34 | const toggle = (b: boolean): boolean => !b 35 | 36 | export function update(msg: Msg, model: Model): [Model, cmd.Cmd] { 37 | switch (msg.type) { 38 | case 'ToggleNotifications': 39 | return [notificationsLens.modify(toggle)(model), cmd.none] 40 | 41 | case 'ToggleAutoplay': 42 | return [autoplayLens.modify(toggle)(model), cmd.none] 43 | 44 | case 'ToggleLocation': 45 | return [locationLens.modify(toggle)(model), cmd.none] 46 | } 47 | } 48 | 49 | // --- View 50 | export function view(_: Model): Html { 51 | return dispatch => ( 52 |
53 | {checkbox({ type: 'ToggleNotifications' as const }, 'Email Notifications')(dispatch)} 54 | {checkbox({ type: 'ToggleAutoplay' as const }, 'Video Autoplay')(dispatch)} 55 | {checkbox({ type: 'ToggleLocation' as const }, 'Use Location')(dispatch)} 56 |
57 | ) 58 | } 59 | 60 | function checkbox(msg: msg, label: string): Html { 61 | return dispatch => ( 62 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /examples/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cmd } from '../src' 3 | import { Location, push } from '../src/Navigation' 4 | import { Html } from '../src/React' 5 | 6 | // --- Routes 7 | const routes = { 8 | RouteA: true, 9 | RouteB: true 10 | } 11 | 12 | type Route = keyof typeof routes 13 | 14 | // --- Flags 15 | export type Flags = Model 16 | 17 | const defaultRoute: Route = 'RouteA' 18 | 19 | export const flags: Flags = defaultRoute 20 | 21 | // --- Model 22 | export type Model = Route 23 | 24 | function isRoute(route: string): route is Route { 25 | return routes.hasOwnProperty(route) 26 | } 27 | 28 | function getRoute(location: Location): Route { 29 | const route = location.pathname.substring(1) 30 | return isRoute(route) ? route : defaultRoute 31 | } 32 | 33 | export function locationToMsg(location: Location): Msg { 34 | return { type: getRoute(location) } as Msg 35 | } 36 | 37 | export function init(_: Flags): (location: Location) => [Model, cmd.Cmd] { 38 | return location => [getRoute(location), cmd.none] 39 | } 40 | 41 | // --- Messages 42 | export type Msg = { type: 'RouteA' } | { type: 'RouteB' } | { type: 'Push'; url: Route } 43 | 44 | // --- Update 45 | export function update(msg: Msg, model: Model): [Model, cmd.Cmd] { 46 | switch (msg.type) { 47 | case 'RouteA': 48 | return ['RouteA', cmd.none] 49 | 50 | case 'RouteB': 51 | return ['RouteB', cmd.none] 52 | 53 | case 'Push': 54 | return [model, push(msg.url)] 55 | } 56 | } 57 | 58 | // --- View 59 | export function view(model: Model): Html { 60 | return dispatch =>
{model === 'RouteA' ? RouteA(dispatch) : RouteB(dispatch)}
61 | } 62 | 63 | const RouteA: Html = dispatch => ( 64 |
65 | RouteA 66 |
67 | ) 68 | 69 | const RouteB: Html = dispatch => ( 70 |
71 | RouteB 72 |
73 | ) 74 | -------------------------------------------------------------------------------- /examples/Task.tsx: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option' 2 | import * as T from 'fp-ts/lib/Task' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | import * as React from 'react' 5 | import { cmd } from '../src' 6 | import { Html } from '../src/React' 7 | import { perform } from '../src/Task' 8 | import { now } from '../src/Time' 9 | 10 | type Time = number 11 | 12 | // --- Flags 13 | export type Flags = void 14 | 15 | export const flags: Flags = undefined 16 | 17 | // --- Model 18 | export type Model = O.Option
(n: number, task: T.Task): T.Task { 41 | return () => 42 | new Promise(resolve => { 43 | setTimeout(() => task().then(resolve), n) 44 | }) 45 | } 46 | 47 | export function update(msg: Msg, _: Model): [Model, cmd.Cmd] { 48 | switch (msg.type) { 49 | case 'Click': 50 | return [ 51 | O.none, 52 | pipe( 53 | delay(1000, now()), 54 | perform(newTime) 55 | ) 56 | ] 57 | 58 | case 'NewTime': 59 | return [O.some(msg.time), cmd.none] 60 | } 61 | } 62 | 63 | // --- View 64 | export function view(model: Model): Html { 65 | return dispatch => ( 66 |
67 | Time:{' '} 68 | {pipe( 69 | model, 70 | O.fold(displayLoading, displayTime) 71 | )} 72 | 73 |
74 | ) 75 | } 76 | 77 | function displayTime(time: Time): string { 78 | return new Date(time).toISOString() 79 | } 80 | 81 | const displayLoading = () => 'loading...' 82 | -------------------------------------------------------------------------------- /examples/react-blessed.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-blessed' 2 | 3 | declare namespace JSX { 4 | interface IntrinsicElements { 5 | box: { 6 | top: string 7 | left: string 8 | width: string 9 | height: string 10 | border: object 11 | style: object 12 | children: string 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-ts", 3 | "version": "0.6.0", 4 | "description": "A porting of TEA to TypeScript featuring fp-ts, rxjs6 and React", 5 | "files": [ 6 | "lib", 7 | "es6" 8 | ], 9 | "main": "lib/index.js", 10 | "module": "es6/index.js", 11 | "typings": "lib/index.d.ts", 12 | "sideEffects": false, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/gcanti/elm-ts.git" 16 | }, 17 | "author": "Giulio Canti ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/gcanti/elm-ts/issues" 21 | }, 22 | "homepage": "https://github.com/gcanti/elm-ts", 23 | "tags": [ 24 | "typescript", 25 | "elm", 26 | "fp-ts" 27 | ], 28 | "keywords": [ 29 | "typescript", 30 | "elm", 31 | "fp-ts" 32 | ], 33 | "scripts": { 34 | "check": "tsc -p .", 35 | "lint": "tslint -p . -t verbose", 36 | "pretest": "npm run check && npm run lint", 37 | "test": "jest", 38 | "posttest": "npm run docs", 39 | "prebuild": "rm -rf ./lib ./es6", 40 | "build": "tsc -p ./tsconfig.build.json && tsc -p ./tsconfig.build-es6.json", 41 | "postbuild": "ts-node scripts/rewrite-es6-paths", 42 | "docs": "docs-ts", 43 | "postdocs": "ts-node scripts/docs-index" 44 | }, 45 | "dependencies": { 46 | "history": "^4.7.2" 47 | }, 48 | "peerDependencies": { 49 | "@types/history": "^4.6.2", 50 | "fp-ts": "^2.0.2", 51 | "rxjs": "^6.5.2", 52 | "react": "^16.8.6" 53 | }, 54 | "devDependencies": { 55 | "@types/blessed": "^0.1.17", 56 | "@types/glob": "^7.1.2", 57 | "@types/history": "^4.6.2", 58 | "@types/jest": "^26.0.3", 59 | "@types/node": "^10.17.9", 60 | "@types/react": "^16.0.27", 61 | "@types/react-dom": "^16.9.3", 62 | "@types/sinon": "^9.0.4", 63 | "blessed": "^0.1.81", 64 | "chalk": "^4.1.0", 65 | "docs-ts": "^0.5.1", 66 | "fp-ts": "^2.0.2", 67 | "glob": "^7.1.6", 68 | "husky": "^4.2.5", 69 | "io-ts": "^2.0.0", 70 | "jest": "^26.1.0", 71 | "monocle-ts": "^2.0.0", 72 | "prettier": "^2.0.5", 73 | "pretty-quick": "^2.0.1", 74 | "react": "^16.8.6", 75 | "react-blessed": "^0.6.2", 76 | "react-dom": "^16.11.0", 77 | "rxjs": "^6.5.2", 78 | "sinon": "^9.0.2", 79 | "ts-jest": "^26.1.1", 80 | "ts-node": "^8.10.2", 81 | "tslint": "^6.1.2", 82 | "tslint-config-standard": "^9.0.0", 83 | "typescript": "^3.5.3" 84 | }, 85 | "jest": { 86 | "preset": "ts-jest", 87 | "globals": { 88 | "ts-jest": { 89 | "diagnostics": true 90 | } 91 | }, 92 | "bail": true, 93 | "collectCoverage": true, 94 | "coveragePathIgnorePatterns": [ 95 | "/test/", 96 | "/node_modules/" 97 | ], 98 | "coverageReporters": [ 99 | "text" 100 | ], 101 | "roots": [ 102 | "/test/" 103 | ], 104 | "testMatch": null, 105 | "testRegex": "(\\.|/)(test|spec)\\.tsx?$" 106 | }, 107 | "husky": { 108 | "hooks": { 109 | "pre-commit": "pretty-quick --staged" 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /scripts/docs-index.ts: -------------------------------------------------------------------------------- 1 | import * as TE from 'fp-ts/lib/TaskEither' 2 | import { pipe } from 'fp-ts/lib/pipeable' 3 | import { FileSystem, fileSystemNode } from './helpers/fs' 4 | import { Logger, loggerConsole } from './helpers/logger' 5 | import { Program, run } from './helpers/program' 6 | 7 | const README_FILE = 'README.md' 8 | const DOCS_INDEX_FILE = 'docs/index.md' 9 | const HEADLINE = `--- 10 | title: Home 11 | nav_order: 1 12 | --- 13 | 14 | ` 15 | 16 | interface Capabilities extends FileSystem, Logger {} 17 | 18 | interface AppEff
extends Program {} 19 | 20 | const withHeadline = (content: string): string => `${HEADLINE}${content}` 21 | 22 | const main: AppEff = C => 23 | pipe( 24 | C.info(`Copy content of ${README_FILE} into ${DOCS_INDEX_FILE}...`), 25 | TE.chain(() => C.readFile(README_FILE)), 26 | TE.map(withHeadline), 27 | TE.chain(content => C.writeFile(DOCS_INDEX_FILE, content)), 28 | TE.chainFirst(() => C.log('Docs index updated')) 29 | ) 30 | 31 | // --- Run the program 32 | run( 33 | main({ 34 | ...fileSystemNode, 35 | ...loggerConsole 36 | }) 37 | ) 38 | -------------------------------------------------------------------------------- /scripts/helpers/fs.ts: -------------------------------------------------------------------------------- 1 | import { mapLeft, taskify } from 'fp-ts/lib/TaskEither' 2 | import { flow } from 'fp-ts/lib/function' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | import * as fs from 'fs' 5 | import Glob from 'glob' 6 | import { Eff } from './program' 7 | 8 | export interface FileSystem { 9 | readonly readFile: (path: string) => Eff 10 | readonly writeFile: (path: string, content: string) => Eff 11 | readonly glob: (pattern: string) => Eff 12 | } 13 | 14 | const readFileTE = taskify(fs.readFile) 15 | const writeFileTE = taskify(fs.writeFile) 16 | const globTE = taskify(Glob) 17 | const toError = (e: Error): string => e.message 18 | 19 | export const fileSystemNode: FileSystem = { 20 | readFile: path => 21 | pipe( 22 | readFileTE(path, 'utf8'), 23 | mapLeft(toError) 24 | ), 25 | 26 | writeFile: flow( 27 | writeFileTE, 28 | mapLeft(toError) 29 | ), 30 | 31 | glob: pattern => 32 | pipe( 33 | globTE(pattern), 34 | mapLeft(toError) 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /scripts/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { info, log } from 'fp-ts/lib/Console' 3 | import { rightIO } from 'fp-ts/lib/TaskEither' 4 | import { Eff } from './program' 5 | 6 | export interface Logger { 7 | readonly debug: (s: string) => Eff 8 | readonly info: (s: string) => Eff 9 | readonly log: (s: string) => Eff 10 | } 11 | 12 | export const loggerConsole: Logger = { 13 | debug: s => rightIO(log(chalk.gray(s))), 14 | info: s => rightIO(info(chalk.bold.magenta(s))), 15 | log: s => rightIO(log(chalk.bold.green(s))) 16 | } 17 | -------------------------------------------------------------------------------- /scripts/helpers/program.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { fold } from 'fp-ts/lib/Either' 3 | import * as RTE from 'fp-ts/lib/ReaderTaskEither' 4 | import * as TE from 'fp-ts/lib/TaskEither' 5 | 6 | export interface Eff extends TE.TaskEither {} 7 | 8 | export interface Program extends RTE.ReaderTaskEither {} 9 | 10 | export function run(eff: Eff): void { 11 | eff() 12 | .then( 13 | fold( 14 | e => { 15 | throw e 16 | }, 17 | _ => { 18 | process.exitCode = 0 19 | } 20 | ) 21 | ) 22 | .catch(e => { 23 | console.error(chalk.red('[ERROR]', e)) 24 | 25 | process.exitCode = 1 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /scripts/rewrite-es6-paths.ts: -------------------------------------------------------------------------------- 1 | import { array } from 'fp-ts/lib/Array' 2 | import * as RTE from 'fp-ts/lib/ReaderTaskEither' 3 | import * as TE from 'fp-ts/lib/TaskEither' 4 | import { pipe } from 'fp-ts/lib/pipeable' 5 | import { FileSystem, fileSystemNode } from './helpers/fs' 6 | import { Logger, loggerConsole } from './helpers/logger' 7 | import { Program, run } from './helpers/program' 8 | 9 | const PATH_REGEXP = /(\s(?:from|module)\s['|"]fp-ts)\/lib\/([\w-\/]+['|"])/gm 10 | const ES6_GLOB_PATTERN = 'es6/**/*.@(ts|js)' 11 | 12 | const traverseRTE = array.traverse(RTE.readerTaskEither) 13 | 14 | interface Capabilities extends FileSystem, Logger {} 15 | 16 | interface AppEff extends Program {} 17 | 18 | const getES6Paths: AppEff = C => C.glob(ES6_GLOB_PATTERN) 19 | 20 | const replacePath = (content: string): string => content.replace(PATH_REGEXP, '$1/es6/$2') 21 | 22 | const rewritePaths = (file: string): AppEff => C => 23 | pipe( 24 | C.debug(`Rewriting file ${file}`), 25 | TE.chain(() => C.readFile(file)), 26 | TE.map(replacePath), 27 | TE.chain(content => C.writeFile(file, content)) 28 | ) 29 | 30 | const log = (s: string): AppEff => C => C.log(s) 31 | 32 | const main: AppEff = pipe( 33 | getES6Paths, 34 | RTE.chain(files => traverseRTE(files, rewritePaths)), 35 | RTE.chainFirst(() => log('ES6 import paths rewritten')) 36 | ) 37 | 38 | // --- Run the program 39 | run( 40 | main({ 41 | ...fileSystemNode, 42 | ...loggerConsole 43 | }) 44 | ) 45 | -------------------------------------------------------------------------------- /src/Cmd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines `Cmd`s as streams of asynchronous operations which can not fail and that can optionally carry a message. 3 | * 4 | * See the [Platform.Cmd](https://package.elm-lang.org/packages/elm/core/latest/Platform-Cmd) Elm package. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import { Option, option, some } from 'fp-ts/lib/Option' 10 | import { Task, task } from 'fp-ts/lib/Task' 11 | import { EMPTY, Observable, merge, of as RxOf } from 'rxjs' 12 | import { map as RxMap } from 'rxjs/operators' 13 | 14 | /** 15 | * @category model 16 | * @since 0.5.0 17 | */ 18 | export interface Cmd extends Observable>> {} 19 | 20 | /** 21 | * Creates a new `Cmd` that carries the provided `Msg`. 22 | * @category Applicative 23 | * @since 0.5.0 24 | */ 25 | export function of(m: Msg): Cmd { 26 | return RxOf(task.of(some(m))) 27 | } 28 | 29 | /** 30 | * Maps the carried `Msg` of a `Cmd` into another `Msg`. 31 | * @category Functor 32 | * @since 0.5.0 33 | */ 34 | export function map(f: (a: A) => Msg): (cmd: Cmd) => Cmd { 35 | return cmd => cmd.pipe(RxMap(t => task.map(t, o => option.map(o, f)))) 36 | } 37 | 38 | /** 39 | * Batches the execution of a list of commands. 40 | * @category utils 41 | * @since 0.5.0 42 | */ 43 | export function batch(arr: Array>): Cmd { 44 | return merge(...arr) 45 | } 46 | 47 | /** 48 | * A `none` command is an empty stream. 49 | * @category constructors 50 | * @since 0.5.0 51 | */ 52 | export const none: Cmd = EMPTY 53 | -------------------------------------------------------------------------------- /src/Debug/Html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module makes available a debugging utility for `elm-ts` applications running `Html` programs. 3 | * 4 | * `elm-ts` ships with a [Redux DevTool Extension](https://github.com/zalmoxisus/redux-devtools-extension) integration, falling back to a simple debugger via standard browser's [`console`](https://developer.mozilla.org/en-US/docs/Web/API/Console) in case the extension is not available. 5 | * 6 | * **Note:** debugging is to be considered unsafe by design so it should be used only in **development**. 7 | * 8 | * This is an example of usage: 9 | * ```ts 10 | * import {react, cmd} from 'elm-ts' 11 | * import {programWithDebugger} from 'elm-ts/lib/Debug/Html' 12 | * import { render } from 'react-dom' 13 | * 14 | * type Model = number 15 | * type Msg = 'INCREMENT' | 'DECREMENT' 16 | * 17 | * declare const init: [Model, cmd.none] 18 | * declare function update(msg: Msg, model: Model): [Model, cmd.Cmd] 19 | * declare function view(model: Model): react.Html 20 | * 21 | * const program = process.NODE_ENV === 'production' ? react.program : programWithDebugger 22 | * 23 | * const main = program(init, update, view) 24 | * 25 | * react.run(main, dom => render(dom, document.getElementById('app'))) 26 | * ``` 27 | * 28 | * @since 0.5.3 29 | */ 30 | 31 | import { BehaviorSubject, EMPTY, Observable } from 'rxjs' 32 | import { Cmd } from '../Cmd' 33 | import { Html, Program, program } from '../Html' 34 | import { Sub } from '../Sub' 35 | import { DebugData, DebuggerR, debugInit, runDebugger, updateWithDebug } from './commons' 36 | 37 | /** 38 | * Adds a debugging capability to a generic `Html` `Program`. 39 | * 40 | * It tracks every `Message` dispatched and resulting `Model` update. 41 | * 42 | * It also lets directly updating the application's state with a special `Message` of type: 43 | * 44 | * ```ts 45 | * { 46 | * type: '__DebugUpdateModel__' 47 | * payload: Model 48 | * } 49 | * ``` 50 | * 51 | * or applying a message with: 52 | * ```ts 53 | * { 54 | * type: '__DebugApplyMsg__'; 55 | * payload: Msg 56 | * } 57 | * ``` 58 | * @category constructors 59 | * @since 0.5.3 60 | */ 61 | export function programWithDebugger( 62 | init: [Model, Cmd], 63 | update: (msg: Msg, model: Model) => [Model, Cmd], 64 | view: (model: Model) => Html, 65 | subscriptions?: (model: Model) => Sub 66 | ): Program { 67 | return createProgram(EMPTY, init, update, view, subscriptions) 68 | } 69 | 70 | /** 71 | * A function that requires an `Observable` and returns a `programWithDebugger()` function: the underlying debugger will stop when the `Observable` emits a value. 72 | * @category constructors 73 | * @since 0.5.4 74 | */ 75 | export function programWithDebuggerWithStop( 76 | stopDebuggerOn: Observable 77 | ): ( 78 | init: [S, Cmd], 79 | update: (msg: M, model: S) => [S, Cmd], 80 | view: (model: S) => Html, 81 | subscriptions?: (model: S) => Sub 82 | ) => Program { 83 | return (init, update, view, subscriptions) => createProgram(stopDebuggerOn, init, update, view, subscriptions) 84 | } 85 | 86 | /** 87 | * Same as `programWithDebugger()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 88 | * @category constructors 89 | * @since 0.5.3 90 | */ 91 | export function programWithDebuggerWithFlags( 92 | init: (flags: Flags) => [Model, Cmd], 93 | update: (msg: Msg, model: Model) => [Model, Cmd], 94 | view: (model: Model) => Html, 95 | subscriptions?: (model: Model) => Sub 96 | ): (flags: Flags) => Program { 97 | return flags => programWithDebugger(init(flags), update, view, subscriptions) 98 | } 99 | 100 | /** 101 | * Same as `programWithDebuggerWithStop()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 102 | * @category constructors 103 | * @since 0.5.4 104 | */ 105 | export function programWithDebuggerWithFlagsWithStop( 106 | stopDebuggerOn: Observable 107 | ): ( 108 | init: (flags: Flags) => [S, Cmd], 109 | update: (msg: M, model: S) => [S, Cmd], 110 | view: (model: S) => Html, 111 | subscriptions?: (model: S) => Sub 112 | ) => (flags: Flags) => Program { 113 | return (init, update, view, subscriptions) => flags => 114 | createProgram(stopDebuggerOn, init(flags), update, view, subscriptions) 115 | } 116 | 117 | // --- Internal 118 | function createProgram( 119 | stopDebuggerOn: Observable, 120 | init: [Model, Cmd], 121 | update: (msg: Msg, model: Model) => [Model, Cmd], 122 | view: (model: Model) => Html, 123 | subscriptions?: (model: Model) => Sub 124 | ): Program { 125 | const Debugger = runDebugger(window, stopDebuggerOn) 126 | 127 | const initModel = init[0] 128 | 129 | const debug$ = new BehaviorSubject>([debugInit(), initModel]) 130 | 131 | const p = program(init, updateWithDebug(debug$, update), view, subscriptions) 132 | 133 | // --- Run the debugger 134 | // --- we need to make a type assertion for `dispatch` because we cannot change the intrinsic `msg` type of `program`; 135 | // --- otherwise `programWithDebugger` won't be usable as a transparent extension/substitution of `Html`'s programs 136 | Debugger({ 137 | debug$, 138 | init: initModel, 139 | dispatch: p.dispatch as DebuggerR['dispatch'] 140 | })() 141 | 142 | return p 143 | } 144 | -------------------------------------------------------------------------------- /src/Debug/Navigation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module makes available a debugging utility for `elm-ts` applications running `Navigation` programs. 3 | * 4 | * `elm-ts` ships with a [Redux DevTool Extension](https://github.com/zalmoxisus/redux-devtools-extension) integration, falling back to a simple debugger via standard browser's [`console`](https://developer.mozilla.org/en-US/docs/Web/API/Console) in case the extension is not available. 5 | * 6 | * **Note:** debugging is to be considered unsafe by design so it should be used only in **development**. 7 | * 8 | * This is an example of usage: 9 | * ```ts 10 | * import {react, cmd} from 'elm-ts' 11 | * import {programWithDebugger} from 'elm-ts/lib/Debug/Navigation' 12 | * import {Location, program} from 'elm-ts/lib/Navigation' 13 | * import {render} from 'react-dom' 14 | * 15 | * type Model = number 16 | * type Msg = 'INCREMENT' | 'DECREMENT' 17 | * 18 | * declare function locationToMsg(location: Location): Msg 19 | * declare function init(location: Location): [Model, cmd.none] 20 | * declare function update(msg: Msg, model: Model): [Model, cmd.Cmd] 21 | * declare function view(model: Model): react.Html 22 | * 23 | * const program = process.NODE_ENV === 'production' ? program : programWithDebugger 24 | * 25 | * const main = program(locationToMsg, init, update, view) 26 | * 27 | * react.run(main, dom => render(dom, document.getElementById('app'))) 28 | * ``` 29 | * 30 | * @since 0.5.3 31 | */ 32 | 33 | import * as H from 'history' 34 | import { BehaviorSubject, EMPTY, Observable } from 'rxjs' 35 | import { Cmd } from '../Cmd' 36 | import { Html, Program } from '../Html' 37 | import { Location, program } from '../Navigation' 38 | import { Sub } from '../Sub' 39 | import { DebugData, DebuggerR, debugInit, runDebugger, updateWithDebug } from './commons' 40 | 41 | /** 42 | * Adds a debugging capability to a generic `Navigation` `Program`. 43 | * 44 | * It tracks every `Message` dispatched and resulting `Model` update. 45 | * 46 | * It also lets directly updating the application's state with a special `Message` of type: 47 | * 48 | * ```ts 49 | * { 50 | * type: '__DebugUpdateModel__' 51 | * payload: Model 52 | * } 53 | * ``` 54 | * 55 | * or applying a message with: 56 | * ```ts 57 | * { 58 | * type: '__DebugApplyMsg__'; 59 | * payload: Msg 60 | * } 61 | * ``` 62 | * @category constructors 63 | * @since 0.5.3 64 | */ 65 | export function programWithDebugger( 66 | locationToMessage: (location: Location) => Msg, 67 | init: (location: Location) => [Model, Cmd], 68 | update: (msg: Msg, model: Model) => [Model, Cmd], 69 | view: (model: Model) => Html, 70 | subscriptions?: (model: Model) => Sub 71 | ): Program { 72 | return createProgram(EMPTY, locationToMessage, init, update, view, subscriptions) 73 | } 74 | 75 | /** 76 | * A function that requires an `Observable` and returns a `programWithDebugger()` function: the underlying debugger will stop when the `Observable` emits a value. 77 | * @category constructors 78 | * @since 0.5.4 79 | */ 80 | export function programWithDebuggerWithStop( 81 | stopDebuggerOn: Observable 82 | ): ( 83 | locationToMessage: (location: Location) => M, 84 | init: (location: Location) => [S, Cmd], 85 | update: (msg: M, model: S) => [S, Cmd], 86 | view: (model: S) => Html, 87 | subscriptions?: (model: S) => Sub 88 | ) => Program { 89 | return (locationToMessage, init, update, view, subscriptions) => 90 | createProgram(stopDebuggerOn, locationToMessage, init, update, view, subscriptions) 91 | } 92 | 93 | /** 94 | * Same as `programWithDebugger()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 95 | * @category constructors 96 | * @since 0.5.3 97 | */ 98 | export function programWithDebuggerWithFlags( 99 | locationToMessage: (location: Location) => Msg, 100 | init: (flags: Flags) => (location: Location) => [Model, Cmd], 101 | update: (msg: Msg, model: Model) => [Model, Cmd], 102 | view: (model: Model) => Html, 103 | subscriptions?: (model: Model) => Sub 104 | ): (flags: Flags) => Program { 105 | return flags => programWithDebugger(locationToMessage, init(flags), update, view, subscriptions) 106 | } 107 | 108 | /** 109 | * Same as `programWithDebuggerWithStop()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 110 | * @category constructors 111 | * @since 0.5.4 112 | */ 113 | export function programWithDebuggerWithFlagsWithStop( 114 | stopDebuggerOn: Observable 115 | ): ( 116 | locationToMessage: (location: Location) => M, 117 | init: (flags: Flags) => (location: Location) => [S, Cmd], 118 | update: (msg: M, model: S) => [S, Cmd], 119 | view: (model: S) => Html, 120 | subscriptions?: (model: S) => Sub 121 | ) => (flags: Flags) => Program { 122 | return (locationToMessage, init, update, view, subscriptions) => flags => 123 | createProgram(stopDebuggerOn, locationToMessage, init(flags), update, view, subscriptions) 124 | } 125 | 126 | // --- Internal 127 | function createProgram( 128 | stopDebuggerOn: Observable, 129 | locationToMessage: (location: Location) => Msg, 130 | init: (location: Location) => [Model, Cmd], 131 | update: (msg: Msg, model: Model) => [Model, Cmd], 132 | view: (model: Model) => Html, 133 | subscriptions?: (model: Model) => Sub 134 | ): Program { 135 | const history = H.createHashHistory() // this is needed only to generate init model for debug$ :S 136 | 137 | const Debugger = runDebugger(window, stopDebuggerOn) 138 | 139 | const initModel = init(history.location)[0] 140 | 141 | const debug$ = new BehaviorSubject>([debugInit(), initModel]) 142 | 143 | const p = program(locationToMessage, init, updateWithDebug(debug$, update), view, subscriptions) 144 | 145 | // --- Run the debugger 146 | // --- we need to make a type assertion for `dispatch` because we cannot change the intrinsic `msg` type of `program`; 147 | // --- otherwise `programWithDebugger` won't be usable as a transparent extension/substitution of `Html`'s programs 148 | Debugger({ 149 | debug$, 150 | init: initModel, 151 | dispatch: p.dispatch as DebuggerR['dispatch'] 152 | })() 153 | 154 | return p 155 | } 156 | -------------------------------------------------------------------------------- /src/Debug/commons.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common utilities and type definitions for the `Debug` module. 3 | * 4 | * @since 0.5.0 5 | */ 6 | import { IO, chain, map } from 'fp-ts/lib/IO' 7 | import { fold } from 'fp-ts/lib/Option' 8 | import { pipe } from 'fp-ts/lib/pipeable' 9 | import { BehaviorSubject, Observable } from 'rxjs' 10 | import { takeUntil } from 'rxjs/operators' 11 | import { Cmd, none } from '../Cmd' 12 | import { Dispatch } from '../Platform' 13 | import { consoleDebugger } from './console' 14 | import { getConnection, reduxDevToolDebugger } from './redux-devtool' 15 | 16 | /** 17 | * @category model 18 | * @since 0.5.0 19 | */ 20 | export type Global = typeof window 21 | 22 | /** 23 | * @category model 24 | * @since 0.5.0 25 | */ 26 | export type DebugData = [DebugAction, Model] 27 | 28 | /** 29 | * @category model 30 | * @since 0.5.0 31 | */ 32 | export type DebugAction = DebugInit | DebugMsg 33 | 34 | /** 35 | * @category model 36 | * @since 0.5.0 37 | */ 38 | export interface DebugInit { 39 | type: 'INIT' 40 | } 41 | /** 42 | * Creates a `DebugInit` 43 | * @category constructos 44 | * @since 0.5.0 45 | */ 46 | export const debugInit = (): DebugInit => ({ type: 'INIT' }) 47 | 48 | /** 49 | * @category model 50 | * @since 0.5.0 51 | */ 52 | export interface DebugMsg { 53 | type: 'MESSAGE' 54 | payload: Msg 55 | } 56 | /** 57 | * Creates a `DebugMsg` 58 | * @category constructors 59 | * @since 0.5.0 60 | */ 61 | export const debugMsg = (payload: Msg): DebugMsg => ({ type: 'MESSAGE', payload }) 62 | 63 | /** 64 | * Extends `Msg` with a special kind of message from Debugger 65 | * @category model 66 | * @since 0.5.0 67 | */ 68 | export type MsgWithDebug = 69 | | Msg 70 | | { type: '__DebugUpdateModel__'; payload: Model } 71 | | { type: '__DebugApplyMsg__'; payload: Msg } 72 | 73 | /** 74 | * Defines a generic debugging function 75 | * @category model 76 | * @since 0.5.0 77 | */ 78 | export interface Debug { 79 | (data: DebugData): void 80 | } 81 | 82 | /** 83 | * Defines a generic `Debugger` 84 | * @category model 85 | * @since 0.5.4 86 | */ 87 | export interface Debugger { 88 | (d: DebuggerR): { 89 | debug: Debug 90 | stop: () => void 91 | } 92 | } 93 | 94 | /** 95 | * Defines the dependencies for a `Debugger` function. 96 | * @category model 97 | * @since 0.5.0 98 | */ 99 | export interface DebuggerR { 100 | init: Model 101 | debug$: BehaviorSubject> 102 | dispatch: Dispatch> 103 | } 104 | 105 | /** 106 | * Adds debugging capability to the provided `update` function. 107 | * 108 | * It tracks through the `debug$` stream every `Message` dispatched and resulting `Model` update. 109 | * 110 | * It also lets directly updating the application's state with a special `Message` of type: 111 | * 112 | * ```ts 113 | * { 114 | * type: '__DebugUpdateModel__' 115 | * payload: Model 116 | * } 117 | * ``` 118 | * 119 | * or applying a message with: 120 | * ```ts 121 | * { 122 | * type: '__DebugApplyMsg__'; 123 | * payload: Msg 124 | * } 125 | * ``` 126 | * @category utils 127 | * @since 0.5.3 128 | */ 129 | export function updateWithDebug( 130 | debug$: BehaviorSubject>, 131 | update: (msg: Msg, model: Model) => [Model, Cmd] 132 | ): (msg: MsgWithDebug, model: Model) => [Model, Cmd] { 133 | return (msg, model) => { 134 | if ('type' in msg) { 135 | switch (msg.type) { 136 | case '__DebugUpdateModel__': 137 | return [msg.payload, none] 138 | 139 | case '__DebugApplyMsg__': 140 | return [update(msg.payload, model)[0], none] 141 | } 142 | } 143 | 144 | const result = update(msg, model) 145 | 146 | debug$.next([debugMsg(msg), result[0]]) 147 | 148 | return result 149 | } 150 | } 151 | 152 | /** 153 | * Checks which type of debugger can be used (standard `console` or _Redux DevTool Extension_) based on provided `window` and prepares the subscription to the "debug" stream 154 | * 155 | * **Warning:** this function **SHOULD** be considered as an internal method; using it in your application **SHOULD** be avoided. 156 | * @category utils 157 | * @since 0.5.4 158 | */ 159 | export function runDebugger( 160 | win: Global, 161 | stop$: Observable 162 | ): (deps: DebuggerR) => IO { 163 | return deps => 164 | pipe( 165 | getConnection(win), 166 | map(fold(() => consoleDebugger(), reduxDevToolDebugger)), 167 | chain(Debugger => () => { 168 | const { debug, stop } = Debugger(deps) 169 | 170 | deps.debug$.pipe(takeUntil(stop$)).subscribe({ 171 | next: debug, 172 | complete: stop 173 | }) 174 | }) 175 | ) 176 | } 177 | -------------------------------------------------------------------------------- /src/Debug/console.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Debug via standard browser's `console`. 3 | * 4 | * @since 0.5.0 5 | */ 6 | 7 | import { Option, alt, fromNullable, getOrElse } from 'fp-ts/lib/Option' 8 | import { pipe } from 'fp-ts/lib/pipeable' 9 | import { DebugMsg, Debugger } from './commons' 10 | 11 | /** 12 | * **[UNSAFE]** Simple debugger that uses the standard browser's `console` 13 | * @category constructors 14 | * @since 0.5.4 15 | */ 16 | export function consoleDebugger(): Debugger { 17 | return () => { 18 | return { 19 | debug: data => { 20 | const [action, model] = data 21 | 22 | console.group('%cELM-TS', 'background-color: green; color: black') 23 | 24 | // --- Action 25 | if (action.type === 'INIT') { 26 | console.log('[INIT]') 27 | } 28 | 29 | if (action.type === 'MESSAGE') { 30 | const showType = getOrElse(() => '')(getMsgType(action)) 31 | 32 | console.groupCollapsed(`[MESSAGE] %c${showType}`, 'font-weight: bold') 33 | console.dir(action.payload) 34 | console.groupEnd() 35 | } 36 | 37 | // --- Model 38 | console.groupCollapsed('[MODEL]') 39 | console.dir(model) 40 | console.groupEnd() 41 | console.groupEnd() 42 | }, 43 | 44 | stop: () => { 45 | console.group('%cELM-TS', 'background-color: green; color: black') 46 | console.log('--- stop debugger ---') 47 | console.groupEnd() 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Tries to extract the "type" of a `Message` reading the value of the [_discriminant_ property](http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) (iterating through a list of commonly used names - e.g. "tag", "type", "kind" etc.) 55 | * @since 0.5.0 56 | */ 57 | function getMsgType(m: DebugMsg): Option { 58 | const { payload } = m as any 59 | 60 | return pipe( 61 | fromNullable(payload['tag']), 62 | alt(() => fromNullable(payload['_tag'])), 63 | alt(() => fromNullable(payload['type'])), 64 | alt(() => fromNullable(payload['_type'])), 65 | alt(() => fromNullable(payload['kind'])), 66 | alt(() => fromNullable(payload['_kind'])) 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/Debug/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module makes available a debugging utility for `elm-ts` applications. 3 | * 4 | * Use of the functions directly exported from this module is **deprecated**. 5 | * 6 | * Please use the specialized versions that you can find under `Debug/`. 7 | * 8 | * @since 0.5.0 9 | */ 10 | 11 | import * as HtmlDebugger from './Html' 12 | 13 | /** 14 | * @deprecated Please use the specialized version exposed by `Debug/Html` module 15 | * @category constructors 16 | * @since 0.5.0 17 | */ 18 | export const programWithDebugger = HtmlDebugger.programWithDebugger 19 | 20 | /** 21 | * @deprecated Please use the specialized version exposed by `Debug/Html` module 22 | * @category constructors 23 | * @since 0.5.0 24 | */ 25 | export const programWithDebuggerWithFlags = HtmlDebugger.programWithDebuggerWithFlags 26 | -------------------------------------------------------------------------------- /src/Debug/redux-devtool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Integration with _Redux DevTool Extension_. 3 | * 4 | * Please check the [docs](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/docs/API) fur further information. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import { sequenceT } from 'fp-ts/lib/Apply' 10 | import * as E from 'fp-ts/lib/Either' 11 | import * as IO_ from 'fp-ts/lib/IO' 12 | import * as O from 'fp-ts/lib/Option' 13 | import { pipe } from 'fp-ts/lib/pipeable' 14 | import { Dispatch } from '../Platform' 15 | import { Debug, Debugger, DebuggerR, Global, MsgWithDebug } from './commons' 16 | 17 | // --- Aliases for docs 18 | import Option = O.Option 19 | import Either = E.Either 20 | import IO = IO_.IO 21 | 22 | const sequenceTEither = sequenceT(E.either) 23 | 24 | type Unsubscription = () => void 25 | 26 | /** 27 | * Defines a _Redux DevTool Extension_ object. 28 | * @category model 29 | * @since 0.5.0 30 | */ 31 | export interface Extension { 32 | connect: () => Connection 33 | } 34 | 35 | /** 36 | * Defines a _Redux DevTool Extension_ connection object. 37 | * @category model 38 | * @since 0.5.0 39 | */ 40 | export interface Connection { 41 | subscribe: (listener?: Dispatch) => Unsubscription 42 | send(action: null, state: LiftedState): void 43 | send(action: Msg, state: Model): void 44 | init: (state: Model) => void 45 | error: (message: unknown) => void 46 | unsubscribe: () => void 47 | } 48 | 49 | type DevToolMsg = Start | Action | Monitor 50 | 51 | interface Start { 52 | type: 'START' 53 | } 54 | 55 | interface Action { 56 | type: 'ACTION' 57 | payload: unknown 58 | } 59 | 60 | interface Monitor { 61 | type: 'DISPATCH' 62 | payload: { 63 | type: 'JUMP_TO_STATE' | 'JUMP_TO_ACTION' | 'RESET' | 'ROLLBACK' | 'COMMIT' | 'IMPORT_STATE' | 'TOGGLE_ACTION' 64 | [k: string]: unknown 65 | } 66 | [k: string]: unknown 67 | } 68 | 69 | interface LiftedState { 70 | actionsById: Record 71 | computedStates: Array<{ state: Model }> 72 | currentStateIndex: number 73 | nextActionId: number 74 | skippedActionIds: number[] 75 | stagedActionIds: number[] 76 | isPaused: boolean 77 | } 78 | 79 | interface DevToolHandlerR extends DebuggerR { 80 | connection: Connection 81 | } 82 | 83 | /** 84 | * Gets a _Redux DevTool Extension_ connection in case the extension is available 85 | * @category utils 86 | * @since 0.5.0 87 | */ 88 | export function getConnection(global: Global): IO>> { 89 | return () => (hasExtension(global) ? O.some(global.__REDUX_DEVTOOLS_EXTENSION__.connect()) : O.none) 90 | } 91 | 92 | /** 93 | * **[UNSAFE]** Type guard to check if _Redux DevTool Extension_ is available. 94 | * 95 | * This is "tagged" as unsafe because the check is really loose. 96 | * @since 0.5.0 97 | */ 98 | function hasExtension(global: Global): global is Global & { __REDUX_DEVTOOLS_EXTENSION__: Extension } { 99 | return '__REDUX_DEVTOOLS_EXTENSION__' in global 100 | } 101 | 102 | /** 103 | * **[UNSAFE]** Debug through _Redux DevTool Extension_ 104 | * @category constructors 105 | * @since 0.5.4 106 | */ 107 | export function reduxDevToolDebugger(connection: Connection): Debugger { 108 | return d => { 109 | const deps = { ...d, connection } 110 | 111 | // --- Subscribe to extension in order to receive messages from monitor 112 | connection.subscribe(handleSubscription(deps)) 113 | 114 | return { 115 | debug: handleActions(deps), 116 | stop: connection.unsubscribe 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * **[UNSAFE]** Handles the execution of an effect related to an incoming messages sent from extension monitor. 123 | * @since 0.5.0 124 | */ 125 | function handleSubscription(deps: DevToolHandlerR): (msg: DevToolMsg) => void { 126 | const handler = handleIncomingMsg(deps) 127 | 128 | return msg => 129 | pipe( 130 | handler(msg), 131 | E.fold( 132 | err => console.warn('[REDUX DEV TOOL]', err), 133 | eff => eff() 134 | ) 135 | ) 136 | } 137 | 138 | /** 139 | * **[UNSAFE]** Handles incoming messages sent from extension monitor. 140 | * 141 | * This is largely inspired by https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js 142 | * 143 | * **Note:** the monitor can dispatch messages of any shape that will be re-dispatched into the application; these messages **are not validated** and can lead to unexpected behaviours. 144 | * @since 0.5.0 145 | */ 146 | function handleIncomingMsg({ 147 | connection, 148 | init, 149 | debug$, 150 | dispatch 151 | }: DevToolHandlerR): (msg: DevToolMsg) => Either> { 152 | const dispatchToApp = (m: unknown): IO => () => dispatch(m as Msg) 153 | const reinit: IO = () => connection.init(init) 154 | const update = (payload: Model): IO => dispatchToApp({ type: '__DebugUpdateModel__', payload }) 155 | const restart = (model: Model): IO => () => connection.init(model) 156 | const liftState = (state: LiftedState): IO => () => connection.send(null, state) 157 | const toggle = toggleAction(dispatch) 158 | 159 | return msg => { 160 | switch (msg.type) { 161 | case 'START': 162 | return E.right(reinit) 163 | 164 | case 'ACTION': 165 | return pipe( 166 | E.parseJSON(String(msg.payload), E.toError), 167 | E.bimap(e => e.message, dispatchToApp) 168 | ) 169 | 170 | case 'DISPATCH': 171 | switch (msg.payload.type) { 172 | case 'JUMP_TO_STATE': 173 | case 'JUMP_TO_ACTION': 174 | return pipe(parseJump(msg), E.map(update)) 175 | 176 | case 'RESET': 177 | return E.right( 178 | pipe( 179 | update(init), 180 | IO_.chain(() => reinit) 181 | ) 182 | ) 183 | 184 | case 'ROLLBACK': 185 | return pipe( 186 | parseRollback(msg), 187 | E.map(m => 188 | pipe( 189 | update(m), 190 | IO_.chain(() => restart(m)) 191 | ) 192 | ) 193 | ) 194 | 195 | case 'COMMIT': 196 | return E.right(restart(debug$.getValue()[1])) 197 | 198 | case 'IMPORT_STATE': 199 | return pipe( 200 | parseImportState(msg), 201 | E.map(liftedState => 202 | pipe( 203 | update(liftedState.computedStates[liftedState.computedStates.length - 1].state), 204 | IO_.chain(() => liftState(liftedState)) 205 | ) 206 | ) 207 | ) 208 | 209 | case 'TOGGLE_ACTION': 210 | return pipe( 211 | parseToggleAction(msg), 212 | E.map(([id, liftedState]) => pipe(toggle(id, liftedState), IO_.chain(liftState))) 213 | ) 214 | 215 | default: 216 | return E.left(msg.payload.type) 217 | } 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Handles debugging actions. 224 | * 225 | * The `MESSAGE` action will send the message payload and state to the connected extension (`connection`). 226 | * @since 0.5.0 227 | */ 228 | function handleActions({ connection }: DevToolHandlerR): Debug { 229 | return ([action, model]) => (action.type === 'MESSAGE' ? connection.send(action.payload, model) : undefined) 230 | } 231 | 232 | /** 233 | * Parses a `JUMP` message. 234 | * 235 | * @since 0.5.0 236 | */ 237 | function parseJump(msg: Monitor): Either { 238 | return pipe( 239 | E.parseJSON(String(msg.state), E.toError), 240 | E.bimap( 241 | e => e.message, 242 | u => u as Model 243 | ) 244 | ) 245 | } 246 | 247 | /** 248 | * Parses a `ROLLBACK` message. 249 | * 250 | * @since 0.5.0 251 | */ 252 | function parseRollback(msg: Monitor): Either { 253 | return pipe( 254 | E.parseJSON(String(msg.state), E.toError), 255 | E.bimap( 256 | e => e.message, 257 | u => u as Model 258 | ) 259 | ) 260 | } 261 | 262 | /** 263 | * Parses an `IMPORT_STATE` message. 264 | * 265 | * @since 0.5.0 266 | */ 267 | function parseImportState(msg: Monitor): Either> { 268 | return typeof msg.payload.nextLiftedState === 'object' && msg.payload.nextLiftedState !== null 269 | ? E.right(msg.payload.nextLiftedState as LiftedState) 270 | : E.left('IMPORT_STATE message has some bad payload...') 271 | } 272 | 273 | /** 274 | * Parses a `TOGGLE_ACTION` message. 275 | * 276 | * @since 0.5.0 277 | */ 278 | function parseToggleAction(msg: Monitor): Either]> { 279 | const getId = pipe( 280 | msg.payload.id, 281 | E.fromNullable('TOGGLE_ACTION message has some bad payload...'), 282 | E.map(x => x as number) 283 | ) 284 | 285 | const parseState = pipe( 286 | E.parseJSON(String(msg.state), E.toError), 287 | E.bimap( 288 | e => e.message, 289 | u => u as LiftedState 290 | ) 291 | ) 292 | 293 | return sequenceTEither(getId, parseState) 294 | } 295 | 296 | /** 297 | * Handles toggling a specific action (identified by `id` parameter). 298 | * 299 | * It re-executes all the actions (and updates the store) excluding the toggled one. 300 | * 301 | * The implementation is taken from [MobX dev tool integration](https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js#L22) 302 | * 303 | * @since 0.5.0 304 | */ 305 | function toggleAction( 306 | dispatch: Dispatch> 307 | ): (id: number, liftedState: LiftedState) => IO> { 308 | return (id, liftedState) => () => { 309 | const state = JSON.parse(JSON.stringify(liftedState)) // poor man deep clone... 310 | 311 | const { skippedActionIds, stagedActionIds, computedStates, actionsById } = state 312 | 313 | const skippedIndex = skippedActionIds.indexOf(id) 314 | const skipped = skippedIndex !== -1 315 | const actionIndex = stagedActionIds.indexOf(id) 316 | 317 | if (actionIndex === -1) { 318 | return state 319 | } 320 | 321 | dispatch({ type: '__DebugUpdateModel__', payload: computedStates[actionIndex - 1].state }) 322 | 323 | const start = skipped ? actionIndex : actionIndex + 1 324 | const end = stagedActionIds.length 325 | 326 | for (let i = start; i < end; i++) { 327 | const currentActionId = stagedActionIds[i] 328 | 329 | if (i !== actionIndex && skippedActionIds.indexOf(currentActionId) !== -1) { 330 | continue // it's already skipped 331 | } 332 | 333 | dispatch({ type: '__DebugApplyMsg__', payload: actionsById[currentActionId].action }) 334 | } 335 | 336 | if (skipped) { 337 | skippedActionIds.splice(skippedIndex, 1) 338 | } else { 339 | skippedActionIds.push(id) 340 | } 341 | 342 | return state 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Decode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines a `Decoder`, namely a function that receives an `unknown` value and tries to decodes it in an `A` value. 3 | * 4 | * It returns an `Either` with a `string` as `Left` when decoding fails or an `A` as `Right` when decoding succeeds. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import { Alternative1 } from 'fp-ts/lib/Alternative' 10 | import * as E from 'fp-ts/lib/Either' 11 | import { Monad1 } from 'fp-ts/lib/Monad' 12 | import * as RE from 'fp-ts/lib/ReaderEither' 13 | import { pipeable } from 'fp-ts/lib/pipeable' 14 | 15 | // --- Aliases for docs 16 | import ReaderEither = RE.ReaderEither 17 | 18 | /** 19 | * @category instances 20 | * @since 0.5.0 21 | */ 22 | export const URI = 'elm-ts/Decoder' 23 | 24 | /** 25 | * @category instances 26 | * @since 0.5.0 27 | */ 28 | export type URI = typeof URI 29 | 30 | declare module 'fp-ts/lib/HKT' { 31 | interface URItoKind { 32 | readonly [URI]: Decoder 33 | } 34 | } 35 | 36 | /** 37 | * @category model 38 | * @since 0.5.0 39 | */ 40 | export interface Decoder extends ReaderEither {} 41 | 42 | /** 43 | * @category constructors 44 | * @since 0.5.0 45 | */ 46 | export const left: (e: string) => Decoder = RE.left 47 | 48 | /** 49 | * @category constructors 50 | * @since 0.5.0 51 | */ 52 | export const right: (a: A) => Decoder = RE.readerEither.of 53 | 54 | /** 55 | * @category combinators 56 | * @since 0.5.0 57 | */ 58 | export const orElse: (f: (e: string) => Decoder) => (ma: Decoder) => Decoder = RE.orElse 59 | 60 | /** 61 | * @category instances 62 | * @since 0.5.0 63 | */ 64 | export const decoder: Monad1 & Alternative1 = { 65 | URI, 66 | map: RE.readerEither.map, 67 | of: right, 68 | ap: RE.readerEither.ap, 69 | chain: RE.readerEither.chain, 70 | alt: RE.readerEither.alt, 71 | zero: () => () => E.left('zero') 72 | } 73 | 74 | const { alt, ap, apFirst, apSecond, chain, chainFirst, flatten, map } = pipeable(decoder) 75 | 76 | export { 77 | /** 78 | * @category Alt 79 | * @since 0.5.0 80 | */ 81 | alt, 82 | /** 83 | * @category Apply 84 | * @since 0.5.0 85 | */ 86 | ap, 87 | /** 88 | * @category Apply 89 | * @since 0.5.0 90 | */ 91 | apFirst, 92 | /** 93 | * @category Apply 94 | * @since 0.5.0 95 | */ 96 | apSecond, 97 | /** 98 | * @category Monad 99 | * @since 0.5.0 100 | */ 101 | chain, 102 | /** 103 | * @category Monad 104 | * @since 0.5.0 105 | */ 106 | chainFirst, 107 | /** 108 | * @category Monad 109 | * @since 0.5.0 110 | */ 111 | flatten, 112 | /** 113 | * @category Functor 114 | * @since 0.5.0 115 | */ 116 | map 117 | } 118 | -------------------------------------------------------------------------------- /src/Html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A specialization of `Program` with the capability of mapping `Model` to `View` 3 | * and rendering it into a DOM node. 4 | * 5 | * `Html` is a base abstraction in order to work with any library that renders html. 6 | * 7 | * @since 0.5.0 8 | */ 9 | 10 | import { Observable } from 'rxjs' 11 | import { map as RxMap, takeUntil } from 'rxjs/operators' 12 | import { Cmd } from './Cmd' 13 | import * as platform from './Platform' 14 | import { Sub, none } from './Sub' 15 | 16 | /** 17 | * It is defined as a function that takes a `dispatch()` function as input and returns a `Dom` as output, 18 | * with DOM and messages types constrained. 19 | * @category model 20 | * @since 0.5.0 21 | */ 22 | export interface Html { 23 | (dispatch: platform.Dispatch): Dom 24 | } 25 | 26 | /** 27 | * Defines the generalized `Renderer` as a function that takes a `Dom` as input and returns a `void`. 28 | * 29 | * It suggests an effectful computation. 30 | * @category model 31 | * @since 0.5.0 32 | */ 33 | export interface Renderer { 34 | (dom: Dom): void 35 | } 36 | 37 | /** 38 | * The `Program` interface is extended with a `html$` stream (an `Observable` of views) and a `Dom` type constraint. 39 | * @category model 40 | * @since 0.5.0 41 | */ 42 | export interface Program extends platform.Program { 43 | html$: Observable> 44 | } 45 | 46 | /** 47 | * Maps a view which carries a message of type `A` into a view which carries a message of type `B`. 48 | * @category Functor 49 | * @since 0.5.0 50 | */ 51 | export function map(f: (a: A) => Msg): (ha: Html) => Html { 52 | return ha => dispatch => ha(a => dispatch(f(a))) 53 | } 54 | 55 | /** 56 | * Returns a `Program` specialized for `Html`. 57 | * 58 | * It needs a `view()` function that maps `Model` to `Html`. 59 | * 60 | * Underneath it uses `Platform.program()`. 61 | * @category constructors 62 | * @since 0.5.0 63 | */ 64 | export function program( 65 | init: [Model, Cmd], 66 | update: (msg: Msg, model: Model) => [Model, Cmd], 67 | view: (model: Model) => Html, 68 | subscriptions: (model: Model) => Sub = () => none 69 | ): Program { 70 | const { dispatch, cmd$, sub$, model$ } = platform.program(init, update, subscriptions) 71 | 72 | const html$ = model$.pipe(RxMap(view)) 73 | 74 | return { dispatch, cmd$, sub$, model$, html$ } 75 | } 76 | 77 | /** 78 | * Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 79 | * @category constructors 80 | * @since 0.5.0 81 | */ 82 | export function programWithFlags( 83 | init: (flags: Flags) => [Model, Cmd], 84 | update: (msg: Msg, model: Model) => [Model, Cmd], 85 | view: (model: Model) => Html, 86 | subscriptions?: (model: Model) => Sub 87 | ): (flags: Flags) => Program { 88 | return flags => program(init(flags), update, view, subscriptions) 89 | } 90 | 91 | /** 92 | * Stops the `program` when `signal` Observable emits a value. 93 | * @category combinators 94 | * @since 0.5.4 95 | */ 96 | export function withStop( 97 | signal: Observable 98 | ): (program: Program) => Program { 99 | return program => { 100 | const platformProgram = platform.withStop(signal)(program) 101 | 102 | return { 103 | ...platformProgram, 104 | 105 | html$: program.html$.pipe(takeUntil(signal)) 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Runs the `Program`. 112 | * 113 | * Underneath it uses `Platform.run()`. 114 | * 115 | * It subscribes to the views stream (`html$`) and runs `Renderer` for each new value. 116 | * @category utils 117 | * @since 0.5.0 118 | */ 119 | export function run(program: Program, renderer: Renderer): Observable { 120 | const { dispatch, html$ } = program 121 | 122 | html$.subscribe(html => renderer(html(dispatch))) 123 | 124 | return platform.run(program) 125 | } 126 | -------------------------------------------------------------------------------- /src/Http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes http calls to remote resources as `Cmd`s. 3 | * 4 | * See [Http](https://package.elm-lang.org/packages/elm/http/latest/Http) Elm package. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import * as Arr from 'fp-ts/lib/Array' 10 | import * as E from 'fp-ts/lib/Either' 11 | import * as O from 'fp-ts/lib/Option' 12 | import * as Rec from 'fp-ts/lib/Record' 13 | import { getLastSemigroup } from 'fp-ts/lib/Semigroup' 14 | import * as T from 'fp-ts/lib/Task' 15 | import * as TE from 'fp-ts/lib/TaskEither' 16 | import { flow } from 'fp-ts/lib/function' 17 | import { pipe } from 'fp-ts/lib/pipeable' 18 | import { Observable, OperatorFunction, of } from 'rxjs' 19 | import { AjaxError, AjaxRequest, AjaxResponse, AjaxTimeoutError, ajax } from 'rxjs/ajax' 20 | import { catchError, map } from 'rxjs/operators' 21 | import { Cmd } from './Cmd' 22 | import { Decoder } from './Decode' 23 | 24 | // --- Aliases for docs 25 | import Option = O.Option 26 | import Either = E.Either 27 | import TaskEither = TE.TaskEither 28 | // --- 29 | 30 | /** 31 | * @category model 32 | * @since 0.5.0 33 | */ 34 | export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' 35 | 36 | /** 37 | * @category model 38 | * @since 0.6.0 39 | */ 40 | export type Headers = Record 41 | 42 | /** 43 | * @category model 44 | * @since 0.5.0 45 | */ 46 | export interface Request { 47 | expect: Decoder 48 | url: string 49 | method: Method 50 | headers: Headers 51 | body?: unknown 52 | timeout: Option 53 | withCredentials: boolean 54 | } 55 | 56 | /** 57 | * @category model 58 | * @since 0.5.0 59 | */ 60 | export interface Response { 61 | url: string 62 | status: { 63 | code: number 64 | message: string 65 | } 66 | headers: Headers 67 | body: Body 68 | } 69 | 70 | /** 71 | * @category model 72 | * @since 0.5.0 73 | */ 74 | export type HttpError = 75 | | { readonly _tag: 'BadUrl'; readonly value: string } 76 | | { readonly _tag: 'Timeout' } 77 | | { readonly _tag: 'NetworkError'; readonly value: string } 78 | | { readonly _tag: 'BadStatus'; readonly response: Response } 79 | | { readonly _tag: 'BadPayload'; readonly value: string; readonly response: Response } 80 | 81 | /** 82 | * @category destructors 83 | * @since 0.5.0 84 | */ 85 | export function toTask(req: Request): TaskEither { 86 | return () => xhrOnlyBody(req).toPromise() 87 | } 88 | 89 | /** 90 | * Executes as `Cmd` the provided call to remote resource, mapping the result to a `Msg`. 91 | * 92 | * Derived from [`sendBy`](#sendBy). 93 | * 94 | * @since 0.5.0 95 | */ 96 | export function send(f: (e: Either) => Msg): (req: Request) => Cmd { 97 | return sendBy( 98 | flow( 99 | E.map(r => r.body), 100 | f 101 | ) 102 | ) 103 | } 104 | 105 | /** 106 | * Executes as `Cmd` the provided call to remote resource, mapping the full Response to a `Msg`. 107 | * 108 | * @since 0.6.0 109 | */ 110 | export function sendBy(f: (e: Either>) => Msg): (req: Request) => Cmd { 111 | return flow(xhr, toMsg(f)) 112 | } 113 | 114 | /** 115 | * @category constructors 116 | * @since 0.5.0 117 | */ 118 | export function get(url: string, decoder: Decoder): Request { 119 | return { 120 | method: 'GET', 121 | headers: {}, 122 | url, 123 | body: undefined, 124 | expect: decoder, 125 | timeout: O.none, 126 | withCredentials: false 127 | } 128 | } 129 | 130 | /** 131 | * @category constructors 132 | * @since 0.5.0 133 | */ 134 | export function post(url: string, body: unknown, decoder: Decoder): Request { 135 | return { 136 | method: 'POST', 137 | headers: {}, 138 | url, 139 | body, 140 | expect: decoder, 141 | timeout: O.none, 142 | withCredentials: false 143 | } 144 | } 145 | 146 | // ----------- 147 | // --- Helpers 148 | // ----------- 149 | type Result = Either 150 | type ResultResponse = Result> 151 | type TO = T.Task> 152 | 153 | const fromStrArr = Rec.fromFoldableMap(getLastSemigroup(), Arr.array) 154 | 155 | const xhrOnlyBody = flow(xhr, extractBody()) 156 | 157 | function toMsg(project: (e: Result) => Msg): OperatorFunction, TO> { 158 | return map(flow(project, O.some, T.of)) 159 | } 160 | 161 | function extractBody(): OperatorFunction, Result> { 162 | return map(E.map(response => response.body)) 163 | } 164 | 165 | type ResultResponse$ = Observable> 166 | 167 | function xhr(req: Request): ResultResponse$ { 168 | return pipe( 169 | toXHRRequest(req), 170 | ajax, 171 | map(flow(toResponse, decodeWith(req.expect))), 172 | catchError((e: unknown): ResultResponse$ => of(E.left(toHttpError(req, e)))) 173 | ) 174 | } 175 | 176 | function toXHRRequest(req: Request): AjaxRequest { 177 | return { 178 | ...req, 179 | timeout: O.getOrElse(() => 0)(req.timeout), 180 | async: true, 181 | responseType: 'text' 182 | } 183 | } 184 | 185 | function toResponse(resp: AjaxResponse): Response { 186 | return { 187 | url: resp.request.url!, // url in Request is always defined 188 | status: { code: resp.status, message: resp.xhr.statusText }, 189 | headers: toResponseHeaders(resp.xhr), 190 | body: typeof resp.response === 'string' && resp.response.length > 0 ? resp.response : '{}' 191 | } 192 | } 193 | 194 | function decodeWith(decoder: Decoder): (response: Response) => ResultResponse { 195 | return response => 196 | pipe( 197 | // By spec parsing json can only throw `SyntaxError` 198 | E.parseJSON(response.body, e => (e as SyntaxError).message), 199 | E.chain(decoder), 200 | E.bimap( 201 | value => ({ _tag: 'BadPayload', value, response }), 202 | body => ({ ...response, body }) 203 | ) 204 | ) 205 | } 206 | 207 | function toHttpError(req: Request, e: unknown): HttpError { 208 | if (e instanceof AjaxTimeoutError) { 209 | return { _tag: 'Timeout' } 210 | } 211 | 212 | if (e instanceof AjaxError && e.status === 404) { 213 | return { _tag: 'BadUrl', value: req.url } 214 | } 215 | 216 | if (e instanceof AjaxError && e.status !== 0) { 217 | return { 218 | _tag: 'BadStatus', 219 | response: { 220 | url: e.request.url!, // url in Request is always defined 221 | status: { code: e.status, message: e.xhr.statusText }, 222 | headers: toResponseHeaders(e.xhr), 223 | body: e.response 224 | } 225 | } 226 | } 227 | 228 | return { _tag: 'NetworkError', value: e instanceof Error ? e.message : '' } 229 | } 230 | 231 | function toResponseHeaders(xhr: XMLHttpRequest): Headers { 232 | return pipe( 233 | xhr.getAllResponseHeaders(), 234 | O.fromPredicate((x: string) => x.length > 0), 235 | O.map(hs => hs.trim().split(/[\r\n]+/)), 236 | O.map(Arr.map(line => line.split(': '))), 237 | O.map(kvs => fromStrArr(kvs, ([k, v]) => [k, v])), 238 | O.getOrElse(() => ({})) 239 | ) 240 | } 241 | -------------------------------------------------------------------------------- /src/Navigation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A specialization of `Program` that handles application navigation via location's hash. 3 | * 4 | * It uses [`history`](https://github.com/ReactTraining/history) package. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import * as O from 'fp-ts/lib/Option' 10 | import * as H from 'history' 11 | import { Subject, of } from 'rxjs' 12 | import { map } from 'rxjs/operators' 13 | import { Cmd } from './Cmd' 14 | import * as html from './Html' 15 | import { Sub, batch, none } from './Sub' 16 | 17 | /** 18 | * @category model 19 | * @since 0.5.0 20 | */ 21 | export type Location = H.Location 22 | 23 | const history = H.createHashHistory() 24 | 25 | /** 26 | * Location changes are expressed as a stream 27 | */ 28 | const location$ = new Subject() 29 | 30 | history.listen(location => { 31 | location$.next(location) 32 | }) 33 | 34 | /** 35 | * Generates a `Cmd` that adds a new location to the history's list. 36 | * @category utils 37 | * @since 0.5.0 38 | */ 39 | export function push(url: string): Cmd { 40 | return of(() => { 41 | history.push(url) 42 | 43 | return Promise.resolve(O.none) 44 | }) 45 | } 46 | 47 | /** 48 | * Returns a `Program` specialized for `Navigation`. 49 | * 50 | * The `Program` is a `Html.Program` but it needs a `locationToMsg()` function which converts location changes to messages. 51 | * 52 | * Underneath it consumes `location$` stream (applying `locationToMsg()` on its values). 53 | * @category constructors 54 | * @since 0.5.0 55 | */ 56 | export function program( 57 | locationToMessage: (location: Location) => Msg, 58 | init: (location: Location) => [Model, Cmd], 59 | update: (msg: Msg, model: Model) => [Model, Cmd], 60 | view: (model: Model) => html.Html, 61 | subscriptions: (model: Model) => Sub = () => none 62 | ): html.Program { 63 | const onChangeLocation$ = location$.pipe(map(location => locationToMessage(location))) 64 | 65 | const subs = (model: Model): Sub => batch([subscriptions(model), onChangeLocation$]) 66 | 67 | return html.program(init(history.location), update, view, subs) 68 | } 69 | 70 | /** 71 | * Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 72 | * @category constructors 73 | * @since 0.5.0 74 | */ 75 | export function programWithFlags( 76 | locationToMessage: (location: Location) => Msg, 77 | init: (flags: Flags) => (location: Location) => [Model, Cmd], 78 | update: (msg: Msg, model: Model) => [Model, Cmd], 79 | view: (model: Model) => html.Html, 80 | subscriptions: (model: Model) => Sub = () => none 81 | ): (flags: Flags) => html.Program { 82 | return flags => program(locationToMessage, init(flags), update, view, subscriptions) 83 | } 84 | -------------------------------------------------------------------------------- /src/Platform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The `Platform` module is the backbone of `elm-ts`. 3 | * It defines the base `program()` and `run()` functions which will be extended by more specialized modules. 4 | * _The Elm Architecture_ is implemented via **RxJS** `Observables`. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import * as O from 'fp-ts/lib/Option' 10 | import { BehaviorSubject, Observable } from 'rxjs' 11 | import { distinctUntilChanged, map, mergeAll, startWith, switchMap, takeUntil } from 'rxjs/operators' 12 | import { Cmd } from './Cmd' 13 | import { Sub, none } from './Sub' 14 | 15 | /** 16 | * @category model 17 | * @since 0.5.0 18 | */ 19 | export interface Dispatch { 20 | (msg: Msg): void 21 | } 22 | 23 | /** 24 | * `Program` is just an object that exposes the underlying streams which compose _The Elm Architecture_. 25 | * Even **Commands** and **Subscriptions** are expressed as `Observables` in order to mix them with ease. 26 | * @category model 27 | * @since 0.5.0 28 | */ 29 | export interface Program { 30 | dispatch: Dispatch 31 | cmd$: Cmd 32 | sub$: Sub 33 | model$: Observable 34 | } 35 | 36 | /** 37 | * `program()` is the real core of `elm-ts`. 38 | * 39 | * When a new `Program` is defined, a `BehaviorSubject` is created (because an initial value is needed) that will track every change to the `Model` and every `Cmd` executed. 40 | * 41 | * Every time `dispatch()` is called a new value, computed by the `update()` function, is added to the the stream. 42 | * @category constructors 43 | * @since 0.5.0 44 | */ 45 | export function program( 46 | init: [Model, Cmd], 47 | update: (msg: Msg, model: Model) => [Model, Cmd], 48 | subscriptions: (model: Model) => Sub = () => none 49 | ): Program { 50 | const state$ = new BehaviorSubject(init) 51 | const dispatch: Dispatch = msg => state$.next(update(msg, state$.getValue()[0])) 52 | 53 | const cmd$ = state$.pipe( 54 | startWith(init), 55 | map(state => state[1]), 56 | distinctUntilChanged(), 57 | mergeAll() 58 | ) 59 | 60 | const model$ = state$.pipe( 61 | map(state => state[0]), 62 | distinctUntilChanged() 63 | ) 64 | 65 | const sub$ = model$.pipe(switchMap(model => subscriptions(model))) 66 | 67 | return { dispatch, cmd$, sub$, model$ } 68 | } 69 | 70 | /** 71 | * Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 72 | * @category constructors 73 | * @since 0.5.0 74 | */ 75 | export function programWithFlags( 76 | init: (flags: Flags) => [Model, Cmd], 77 | update: (msg: Msg, model: Model) => [Model, Cmd], 78 | subscriptions: (model: Model) => Sub = () => none 79 | ): (flags: Flags) => Program { 80 | return flags => program(init(flags), update, subscriptions) 81 | } 82 | 83 | /** 84 | * Stops the `program` when `signal` Observable emits a value. 85 | * @category combinators 86 | * @since 0.5.4 87 | */ 88 | export function withStop( 89 | signal: Observable 90 | ): (program: Program) => Program { 91 | return program => { 92 | const { cmd$, model$, sub$, ...rest } = program 93 | 94 | return { 95 | ...rest, 96 | model$: model$.pipe(takeUntil(signal)), 97 | cmd$: cmd$.pipe(takeUntil(signal)), 98 | sub$: sub$.pipe(takeUntil(signal)) 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Runs the `Program`. 105 | * 106 | * Because the program essentially is an object of streams, "running it" means subscribing to these streams and starting to consume values. 107 | * @category utils 108 | * @since 0.5.0 109 | */ 110 | export function run(program: Program): Observable { 111 | const { dispatch, cmd$, sub$, model$ } = program 112 | 113 | cmd$.subscribe(task => task().then(O.map(dispatch))) 114 | 115 | sub$.subscribe(dispatch) 116 | 117 | return model$ 118 | } 119 | -------------------------------------------------------------------------------- /src/React.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A specialization of `Html` that uses `React` as renderer. 3 | * 4 | * @since 0.5.0 5 | */ 6 | 7 | import { ReactElement } from 'react' 8 | import { Observable } from 'rxjs' 9 | import { Cmd } from './Cmd' 10 | import * as html from './Html' 11 | import { Sub } from './Sub' 12 | 13 | /** 14 | * `Dom` is a `ReactElement`. 15 | * @category model 16 | * @since 0.5.0 17 | */ 18 | export interface Dom extends ReactElement {} 19 | 20 | /** 21 | * `Html` has `Dom` type constrained to the specialized version for `React`. 22 | * @category model 23 | * @since 0.5.0 24 | */ 25 | export interface Html extends html.Html {} 26 | 27 | /** 28 | * @category model 29 | * @since 0.5.0 30 | */ 31 | export interface Program extends html.Program {} 32 | 33 | /** 34 | * `map()` is `Html.map()` with `Html` type constrained to the specialized version for `React`. 35 | * @category Functor 36 | * @since 0.5.0 37 | */ 38 | export function map(f: (a: A) => Msg): (ha: Html) => Html { 39 | return html.map(f) 40 | } 41 | 42 | /** 43 | * `program()` is `Html.program()` with `Html` type constrained to the specialized version for `React`. 44 | * @category constructors 45 | * @since 0.5.0 46 | */ 47 | export function program( 48 | init: [Model, Cmd], 49 | update: (msg: Msg, model: Model) => [Model, Cmd], 50 | view: (model: Model) => html.Html, 51 | subscriptions?: (model: Model) => Sub 52 | ): Program { 53 | return html.program(init, update, view, subscriptions) 54 | } 55 | 56 | /** 57 | * Same as `program()` but with `Flags` that can be passed when the `Program` is created in order to manage initial values. 58 | * @category constructors 59 | * @since 0.5.0 60 | */ 61 | export function programWithFlags( 62 | init: (flags: Flags) => [Model, Cmd], 63 | update: (msg: Msg, model: Model) => [Model, Cmd], 64 | view: (model: Model) => html.Html, 65 | subscriptions?: (model: Model) => Sub 66 | ): (flags: Flags) => Program { 67 | return flags => program(init(flags), update, view, subscriptions) 68 | } 69 | 70 | /** 71 | * `run()` is `Html.run()` with `dom` type constrained to the specialized version for `React`. 72 | * @category utils 73 | * @since 0.5.0 74 | */ 75 | export function run(program: Program, renderer: html.Renderer): Observable { 76 | return html.run(program, renderer) 77 | } 78 | -------------------------------------------------------------------------------- /src/Sub.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines `Sub`s as streams of messages. 3 | * 4 | * @since 0.5.0 5 | */ 6 | 7 | import { EMPTY, Observable, merge } from 'rxjs' 8 | import { map as RxMap } from 'rxjs/operators' 9 | 10 | /** 11 | * @category model 12 | * @since 0.5.0 13 | */ 14 | export interface Sub extends Observable {} 15 | 16 | /** 17 | * Maps `Msg` of a `Sub` into another `Msg`. 18 | * @category Functor 19 | * @since 0.5.0 20 | */ 21 | export function map(f: (a: A) => Msg): (sub: Sub) => Sub { 22 | return sub => sub.pipe(RxMap(f)) 23 | } 24 | 25 | /** 26 | * Merges subscriptions streams into one stream. 27 | * @category utils 28 | * @since 0.5.0 29 | */ 30 | export function batch(arr: Array>): Sub { 31 | return merge(...arr) 32 | } 33 | 34 | /** 35 | * A `none` subscription is an empty stream. 36 | * @category constructors 37 | * @since 0.5.0 38 | */ 39 | export const none: Sub = EMPTY 40 | -------------------------------------------------------------------------------- /src/Task.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles the execution of asynchronous effectful operations. 3 | * 4 | * See the [Task](https://package.elm-lang.org/packages/elm/core/latest/Task) Elm package. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import { Either } from 'fp-ts/lib/Either' 10 | import { some } from 'fp-ts/lib/Option' 11 | import { Task, task } from 'fp-ts/lib/Task' 12 | import { of } from 'rxjs' 13 | import { Cmd } from './Cmd' 14 | 15 | /** 16 | * Executes a `Task` as a `Cmd` mapping the result to a `Msg`. 17 | * @category utils 18 | * @since 0.5.0 19 | */ 20 | export function perform(f: (a: A) => Msg): (t: Task) => Cmd { 21 | return t => of(task.map(t, a => some(f(a)))) 22 | } 23 | 24 | /** 25 | * Executes a `Task` that can fail as a `Cmd` mapping the result (`Either`) to a `Msg`. 26 | * @category utils 27 | * @since 0.5.0 28 | */ 29 | export function attempt(f: (e: Either) => Msg): (task: Task>) => Cmd { 30 | return perform(f) 31 | } 32 | -------------------------------------------------------------------------------- /src/Time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exposes some utilities to work with unix time. 3 | * 4 | * See [Time](https://package.elm-lang.org/packages/elm/time/latest/Time) Elm package. 5 | * 6 | * @since 0.5.0 7 | */ 8 | 9 | import { Task } from 'fp-ts/lib/Task' 10 | import { interval } from 'rxjs' 11 | import { map } from 'rxjs/operators' 12 | import { Sub } from './Sub' 13 | 14 | /** 15 | * Get the current unix time as a `Task`. 16 | * @category constructors 17 | * @since 0.5.0 18 | */ 19 | export function now(): Task { 20 | return () => Promise.resolve(new Date().getTime()) 21 | } 22 | 23 | /** 24 | * Get the current unix time periodically. 25 | * @category utils 26 | * @since 0.5.0 27 | */ 28 | export function every(time: number, f: (time: number) => Msg): Sub { 29 | return interval(time).pipe(map(() => f(new Date().getTime()))) 30 | } 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.5.0 3 | */ 4 | import * as cmd from './Cmd' 5 | import * as decode from './Decode' 6 | import * as html from './Html' 7 | import * as http from './Http' 8 | import * as platform from './Platform' 9 | import * as react from './React' 10 | import * as sub from './Sub' 11 | import * as task from './Task' 12 | import * as time from './Time' 13 | 14 | export { 15 | /** 16 | * @category instances 17 | * @since 0.5.0 18 | */ 19 | cmd, 20 | /** 21 | * @category instances 22 | * @since 0.5.0 23 | */ 24 | decode, 25 | /** 26 | * @category instances 27 | * @since 0.5.0 28 | */ 29 | html, 30 | /** 31 | * @category instances 32 | * @since 0.5.0 33 | */ 34 | http, 35 | /** 36 | * @category instances 37 | * @since 0.5.0 38 | */ 39 | platform, 40 | /** 41 | * @category instances 42 | * @since 0.5.0 43 | */ 44 | react, 45 | /** 46 | * @category instances 47 | * @since 0.5.0 48 | */ 49 | sub, 50 | /** 51 | * @category instances 52 | * @since 0.5.0 53 | */ 54 | task, 55 | /** 56 | * @category instances 57 | * @since 0.5.0 58 | */ 59 | time 60 | } 61 | -------------------------------------------------------------------------------- /test/Cmd.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as A from 'fp-ts/lib/Array' 3 | import * as O from 'fp-ts/lib/Option' 4 | import * as T from 'fp-ts/lib/Task' 5 | import * as Rx from 'rxjs' 6 | import { batch, map, of } from '../src/Cmd' 7 | 8 | const sequenceTask = A.array.sequence(T.task) 9 | 10 | describe('Cmd', () => { 11 | it('of() should lift a Msg into a Cmd', done => { 12 | const input = of('TEST') 13 | 14 | return input.subscribe(async to => { 15 | const result = await to() 16 | 17 | assert.deepStrictEqual(result, O.some('TEST')) 18 | 19 | done() 20 | }) 21 | }) 22 | 23 | it('map() should transform a Cmd into a Cmd', done => { 24 | const cmdA = Rx.of(T.of(O.some('a'))) 25 | 26 | return map(a => a + 'b')(cmdA).subscribe(async to => { 27 | const result = await to() 28 | 29 | assert.deepStrictEqual(result, O.some('ab')) 30 | 31 | done() 32 | }) 33 | }) 34 | 35 | it('batch() should batch the execution of an Array of Cmd', done => { 36 | const log: Array>> = [] 37 | 38 | const commands = [Rx.of(T.of(O.some('a'))), Rx.of(T.of(O.some('b'))), Rx.of(T.of(O.some('c')))] 39 | 40 | // Use `subscribe` and `done()` callbacks when dealing with async Observables 41 | return batch(commands).subscribe({ 42 | next: v => log.push(v), 43 | 44 | complete: async () => { 45 | const result = await sequenceTask(log)() 46 | 47 | assert.deepStrictEqual(result, [O.some('a'), O.some('b'), O.some('c')]) 48 | 49 | done() 50 | } 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/Debug/Html.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { Subject } from 'rxjs' 3 | import { take } from 'rxjs/operators' 4 | import { Cmd, none } from '../../src/Cmd' 5 | import { 6 | programWithDebugger, 7 | programWithDebuggerWithFlags, 8 | programWithDebuggerWithFlagsWithStop, 9 | programWithDebuggerWithStop 10 | } from '../../src/Debug/Html' 11 | import { DebugData } from '../../src/Debug/commons' 12 | import * as ConsoleDebugger from '../../src/Debug/console' 13 | import * as DevToolDebugger from '../../src/Debug/redux-devtool' 14 | import { Html, run } from '../../src/Html' 15 | import * as Sub from '../../src/Sub' 16 | import { Model, Msg, mockDebugger } from './_helpers' 17 | 18 | describe('Debug', () => { 19 | describe('programWithDebugger()', () => { 20 | it('should return a Program with a Debugger (console)', done => { 21 | const log: Array> = [] 22 | 23 | // --- Trace only console debugger 24 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 25 | 26 | const program = programWithDebugger(init, update, view) 27 | const updates = run(program, _ => undefined) 28 | 29 | // the difference between the number of Model and the length of the debug log 30 | // is due to `__DebugUpdateModel__` and `__DebugApplyMsg__` messages 31 | // that generate a new Model update but are not tracked by the debugger 32 | updates.pipe(take(7)).subscribe({ 33 | complete: () => { 34 | assert.strictEqual(log.length, 5) 35 | assert.deepStrictEqual(log, [ 36 | [{ type: 'INIT' }, 0], 37 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 1], 38 | [{ type: 'MESSAGE', payload: { type: 'Dec' } }, 9], 39 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 10], 40 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 12] 41 | ]) 42 | 43 | done() 44 | } 45 | }) 46 | 47 | program.dispatch({ type: 'Inc' }) 48 | // we need to cast as any to test internal handling of this "special" message 49 | program.dispatch({ type: '__DebugUpdateModel__', payload: 10 } as any) 50 | program.dispatch({ type: 'Dec' }) 51 | program.dispatch({ type: 'Inc' }) 52 | // we need to cast as any to test internal handling of this "special" message 53 | program.dispatch({ type: '__DebugApplyMsg__', payload: { type: 'Inc' } } as any) 54 | program.dispatch({ type: 'Inc' }) 55 | }) 56 | 57 | it('should return a Program with a Debugger (redux devtool)', done => { 58 | const log: Array> = [] 59 | const win = window as any 60 | 61 | // --- Mock Redux DevTool Extension 62 | win.__REDUX_DEVTOOLS_EXTENSION__ = { 63 | connect: () => ({}) 64 | } 65 | 66 | // --- Trace only devtool debugger 67 | jest.spyOn(DevToolDebugger, 'reduxDevToolDebugger').mockReturnValueOnce(mockDebugger(log)) 68 | 69 | const program = programWithDebugger(init, update, view, () => Sub.none) 70 | const updates = run(program, _ => undefined) 71 | 72 | // the difference between the number of Model and the length of the debug log 73 | // is due to `__DebugUpdateModel__` and `__DebugApplyMsg__` messages 74 | // that generate a new Model update but are not tracked by the debugger 75 | updates.pipe(take(6)).subscribe({ 76 | complete: () => { 77 | assert.strictEqual(log.length, 5) 78 | assert.deepStrictEqual(log, [ 79 | [{ type: 'INIT' }, 0], 80 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 1], 81 | [{ type: 'MESSAGE', payload: { type: 'Dec' } }, 9], 82 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 10], 83 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 11] 84 | ]) 85 | 86 | delete win.__REDUX_DEVTOOLS_EXTENSION__ 87 | 88 | done() 89 | } 90 | }) 91 | 92 | program.dispatch({ type: 'Inc' }) 93 | // we need to cast as any to test internal handling of this "special" message 94 | program.dispatch({ type: '__DebugUpdateModel__', payload: 10 } as any) 95 | program.dispatch({ type: 'Dec' }) 96 | program.dispatch({ type: 'Inc' }) 97 | program.dispatch({ type: 'Inc' }) 98 | }) 99 | }) 100 | 101 | it('programWithDebuggerWithStop() should stop debugger when `stopDebuggerOn` emits a value', done => { 102 | const signal = new Subject() 103 | const log: Array> = [] 104 | 105 | // --- Trace only console debugger 106 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 107 | 108 | const withStop = programWithDebuggerWithStop(signal) 109 | const program = withStop(init, update, view) 110 | const updates = run(program, _ => undefined) 111 | 112 | updates.pipe(take(5)).subscribe({ 113 | complete: () => { 114 | assert.strictEqual(log.length, 3) 115 | assert.deepStrictEqual(log, [ 116 | [{ type: 'INIT' }, 0], 117 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 1], 118 | [{ type: 'MESSAGE', payload: { type: 'Dec' } }, 0] 119 | ]) 120 | 121 | done() 122 | } 123 | }) 124 | 125 | program.dispatch({ type: 'Inc' }) 126 | program.dispatch({ type: 'Dec' }) 127 | 128 | // Emit stop signal and the other changes are bypassed 129 | signal.next('stop me!') 130 | 131 | program.dispatch({ type: 'Inc' }) 132 | program.dispatch({ type: 'Inc' }) 133 | }) 134 | 135 | it('programWithDebuggerWithFlags() should return a function that returns a Program with a Debugger and flags on `init`', done => { 136 | const log: Array> = [] 137 | 138 | // --- Trace only console debugger 139 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 140 | 141 | const initWithFlags = (v: number): [Model, Cmd] => [v, none] 142 | const program = programWithDebuggerWithFlags(initWithFlags, update, view)(10) 143 | const updates = run(program, _ => undefined) 144 | 145 | updates.pipe(take(2)).subscribe({ 146 | complete: () => { 147 | assert.strictEqual(log.length, 2) 148 | assert.deepStrictEqual(log, [[{ type: 'INIT' }, 10], [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 11]]) 149 | 150 | done() 151 | } 152 | }) 153 | 154 | program.dispatch({ type: 'Inc' }) 155 | }) 156 | 157 | it('programWithDebuggerWithStopWithFlags() should stop debugger when `stopDebuggerOn` emits a value', done => { 158 | const signal = new Subject() 159 | const log: Array> = [] 160 | 161 | // --- Trace only console debugger 162 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 163 | 164 | const initWithFlags = (v: number): [Model, Cmd] => [v, none] 165 | const withStop = programWithDebuggerWithFlagsWithStop(signal) 166 | const program = withStop(initWithFlags, update, view)(10) 167 | const updates = run(program, _ => undefined) 168 | 169 | updates.pipe(take(5)).subscribe({ 170 | complete: () => { 171 | assert.strictEqual(log.length, 3) 172 | assert.deepStrictEqual(log, [ 173 | [{ type: 'INIT' }, 10], 174 | [{ type: 'MESSAGE', payload: { type: 'Inc' } }, 11], 175 | [{ type: 'MESSAGE', payload: { type: 'Dec' } }, 10] 176 | ]) 177 | 178 | done() 179 | } 180 | }) 181 | 182 | program.dispatch({ type: 'Inc' }) 183 | program.dispatch({ type: 'Dec' }) 184 | 185 | // Emit stop signal and the other changes are bypassed 186 | signal.next('stop me!') 187 | 188 | program.dispatch({ type: 'Inc' }) 189 | program.dispatch({ type: 'Inc' }) 190 | }) 191 | }) 192 | 193 | // --- Fake application 194 | const init: [Model, Cmd] = [0, none] 195 | 196 | const update = (msg: Msg, model: Model): [Model, Cmd] => { 197 | switch (msg.type) { 198 | case 'Inc': 199 | return [model + 1, none] 200 | case 'Dec': 201 | return [model - 1, none] 202 | } 203 | } 204 | 205 | const view = (_: Model): Html => _dispatch => undefined 206 | -------------------------------------------------------------------------------- /test/Debug/Navigation.test.ts: -------------------------------------------------------------------------------- 1 | // --- Mocking `History` module - super tricky... 2 | import * as history from 'history' 3 | import { mocked } from 'ts-jest/utils' 4 | import { createMockHistory } from '../helpers/mock-history' 5 | 6 | jest.mock('history') 7 | 8 | const historyM = mocked(history) 9 | const historyLog: string[] = [] 10 | 11 | historyM.createHashHistory.mockImplementation(createMockHistory(historyLog)) 12 | // --- /Mocking 13 | 14 | import * as assert from 'assert' 15 | import { Subject } from 'rxjs' 16 | import { take } from 'rxjs/operators' 17 | import { Cmd, none } from '../../src/Cmd' 18 | import { 19 | programWithDebugger, 20 | programWithDebuggerWithFlags, 21 | programWithDebuggerWithFlagsWithStop, 22 | programWithDebuggerWithStop 23 | } from '../../src/Debug/Navigation' 24 | import { DebugData } from '../../src/Debug/commons' 25 | import * as ConsoleDebugger from '../../src/Debug/console' 26 | import * as DevToolDebugger from '../../src/Debug/redux-devtool' 27 | import { Html, run } from '../../src/Html' 28 | import { push } from '../../src/Navigation' 29 | import * as Sub from '../../src/Sub' 30 | import { mockDebugger } from './_helpers' 31 | 32 | beforeEach(() => { 33 | historyLog.splice(0, historyLog.length) 34 | }) 35 | 36 | describe('Debug', () => { 37 | describe('programWithDebugger()', () => { 38 | it('should return a Program with a Debugger (console)', done => { 39 | const log: Array> = [] 40 | 41 | // --- Trace only console debugger 42 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 43 | 44 | const program = programWithDebugger(locationToMsg, init, update, view) 45 | const updates = run(program, _ => undefined) 46 | 47 | // the difference between the number of Model and the length of the debug log 48 | // is due to `__DebugUpdateModel__` and `__DebugApplyMsg__` messages 49 | // that generate a new Model update but are not tracked by the debugger 50 | updates.pipe(take(9)).subscribe({ 51 | complete: () => { 52 | assert.strictEqual(log.length, 7) 53 | assert.deepStrictEqual(log, [ 54 | [{ type: 'INIT' }, ''], 55 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/a' } }, ''], 56 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/a' } }, '/a'], 57 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/c' } }, '/b'], 58 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/c' } }, '/c'], 59 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/e' } }, '/d'], 60 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/e' } }, '/e'] 61 | ]) 62 | 63 | done() 64 | } 65 | }) 66 | 67 | program.dispatch({ type: 'GoTo', path: '/a' }) 68 | // we need to cast as any to test internal handling of this "special" message 69 | program.dispatch({ type: '__DebugUpdateModel__', payload: '/b' } as any) 70 | program.dispatch({ type: 'GoTo', path: '/c' }) 71 | // we need to cast as any to test internal handling of this "special" message 72 | program.dispatch({ type: '__DebugApplyMsg__', payload: { type: 'Route', path: '/d' } } as any) 73 | program.dispatch({ type: 'GoTo', path: '/e' }) 74 | }) 75 | 76 | it('should return a Program with a Debugger (redux devtool)', done => { 77 | const log: Array> = [] 78 | const win = window as any 79 | 80 | // --- Mock Redux DevTool Extension 81 | win.__REDUX_DEVTOOLS_EXTENSION__ = { 82 | connect: () => ({}) 83 | } 84 | 85 | // --- Trace only devtool debugger 86 | jest.spyOn(DevToolDebugger, 'reduxDevToolDebugger').mockReturnValueOnce(mockDebugger(log)) 87 | 88 | const program = programWithDebugger(locationToMsg, init, update, view, () => Sub.none) 89 | const updates = run(program, _ => undefined) 90 | 91 | // the difference between the number of Model and the length of the debug log 92 | // is due to `__DebugUpdateModel__` and `__DebugApplyMsg__` messages 93 | // that generate a new Model update but are not tracked by the debugger 94 | updates.pipe(take(8)).subscribe({ 95 | complete: () => { 96 | assert.strictEqual(log.length, 7) 97 | assert.deepStrictEqual(log, [ 98 | [{ type: 'INIT' }, ''], 99 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/a' } }, ''], 100 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/a' } }, '/a'], 101 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/c' } }, '/b'], 102 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/c' } }, '/c'], 103 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/d' } }, '/c'], 104 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/d' } }, '/d'] 105 | ]) 106 | 107 | delete win.__REDUX_DEVTOOLS_EXTENSION__ 108 | 109 | done() 110 | } 111 | }) 112 | 113 | program.dispatch({ type: 'GoTo', path: '/a' }) 114 | // we need to cast as any to test internal handling of this "special" message 115 | program.dispatch({ type: '__DebugUpdateModel__', payload: '/b' } as any) 116 | program.dispatch({ type: 'GoTo', path: '/c' }) 117 | program.dispatch({ type: 'GoTo', path: '/d' }) 118 | }) 119 | }) 120 | 121 | it('programWithDebuggerWithStop() should stop debugger when `stopDebuggerOn` emits a value', done => { 122 | const signal = new Subject() 123 | const log: Array> = [] 124 | 125 | // --- Trace only console debugger 126 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 127 | 128 | const withStop = programWithDebuggerWithStop(signal) 129 | const program = withStop(locationToMsg, init, update, view) 130 | const updates = run(program, _ => undefined) 131 | 132 | updates.pipe(take(9)).subscribe({ 133 | complete: () => { 134 | assert.strictEqual(log.length, 5) 135 | assert.deepStrictEqual(log, [ 136 | [{ type: 'INIT' }, ''], 137 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/a' } }, ''], 138 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/a' } }, '/a'], 139 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/b' } }, '/a'], 140 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/b' } }, '/b'] 141 | ]) 142 | 143 | done() 144 | } 145 | }) 146 | 147 | program.dispatch({ type: 'GoTo', path: '/a' }) 148 | program.dispatch({ type: 'GoTo', path: '/b' }) 149 | 150 | // Emit stop signal and the other changes are bypassed 151 | signal.next('stop me!') 152 | 153 | program.dispatch({ type: 'GoTo', path: '/c' }) 154 | program.dispatch({ type: 'GoTo', path: '/d' }) 155 | }) 156 | 157 | it('programWithDebuggerWithFlags() should return a function that returns a Program with a Debugger and flags on `init`', () => { 158 | const log: Array> = [] 159 | 160 | // --- Trace only console debugger 161 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 162 | 163 | const initWithFlags = (flag: string) => (_: history.Location): [Model, Cmd] => [flag, none] 164 | const program = programWithDebuggerWithFlags(locationToMsg, initWithFlags, update, view)('/start') 165 | const runs = run(program, _ => undefined) 166 | 167 | runs.pipe(take(3)).subscribe({ 168 | complete: () => { 169 | assert.strictEqual(log.length, 3) 170 | assert.deepStrictEqual(log, [ 171 | [{ type: 'INIT' }, '/start'], 172 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/a' } }, '/start'], 173 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/a' } }, '/a'] 174 | ]) 175 | } 176 | }) 177 | 178 | program.dispatch({ type: 'GoTo', path: '/a' }) 179 | }) 180 | 181 | it('programWithDebuggerWithFlagsWithStop() should stop debugger when `stopDebuggerOn` emits a value', done => { 182 | const signal = new Subject() 183 | const log: Array> = [] 184 | 185 | // --- Trace only console debugger 186 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 187 | 188 | const initWithFlags = (flag: string) => (_: history.Location): [Model, Cmd] => [flag, none] 189 | const withStop = programWithDebuggerWithFlagsWithStop(signal) 190 | const program = withStop(locationToMsg, initWithFlags, update, view)('/start') 191 | const updates = run(program, _ => undefined) 192 | 193 | updates.pipe(take(9)).subscribe({ 194 | complete: () => { 195 | assert.strictEqual(log.length, 5) 196 | assert.deepStrictEqual(log, [ 197 | [{ type: 'INIT' }, '/start'], 198 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/a' } }, '/start'], 199 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/a' } }, '/a'], 200 | [{ type: 'MESSAGE', payload: { type: 'GoTo', path: '/b' } }, '/a'], 201 | [{ type: 'MESSAGE', payload: { type: 'Route', path: '/b' } }, '/b'] 202 | ]) 203 | 204 | done() 205 | } 206 | }) 207 | 208 | program.dispatch({ type: 'GoTo', path: '/a' }) 209 | program.dispatch({ type: 'GoTo', path: '/b' }) 210 | 211 | // Emit stop signal and the other changes are bypassed 212 | signal.next('stop me!') 213 | 214 | program.dispatch({ type: 'GoTo', path: '/c' }) 215 | program.dispatch({ type: 'GoTo', path: '/d' }) 216 | }) 217 | }) 218 | 219 | // --- Fake application 220 | type Model = string 221 | type Msg = { type: 'GoTo'; path: string } | { type: 'Route'; path: string } 222 | 223 | function locationToMsg(l: history.Location): Msg { 224 | return { 225 | type: 'Route', 226 | path: l.pathname 227 | } 228 | } 229 | 230 | const init = (l: history.Location): [Model, Cmd] => [l.pathname, none] 231 | 232 | const update = (msg: Msg, model: Model): [Model, Cmd] => { 233 | switch (msg.type) { 234 | case 'GoTo': 235 | return [model, push(msg.path)] 236 | case 'Route': 237 | return [msg.path, none] 238 | } 239 | } 240 | 241 | const view = (_: Model): Html => _dispatch => undefined 242 | -------------------------------------------------------------------------------- /test/Debug/_helpers.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs' 2 | import { DebugData, DebuggerR } from '../../src/Debug/commons' 3 | 4 | export type Model = number 5 | export type Msg = { type: 'Inc' } | { type: 'Dec' } 6 | 7 | export const DATA$ = new BehaviorSubject>([{ type: 'INIT' }, 0]) 8 | 9 | export const STD_DEPS: DebuggerR = { 10 | init: 0, 11 | debug$: DATA$, 12 | dispatch: () => undefined 13 | } 14 | 15 | export const mockDebugger = (log: DebugData[]) => () => ({ 16 | debug: (data: unknown) => log.push(data as DebugData), 17 | stop: () => undefined 18 | }) 19 | 20 | export const disableDebugger = () => ({ debug: () => undefined, stop: () => undefined }) 21 | -------------------------------------------------------------------------------- /test/Debug/commons.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { BehaviorSubject, EMPTY, Subject } from 'rxjs' 3 | import { Cmd, none } from '../../src/Cmd' 4 | import { DebugData, debugInit, debugMsg, runDebugger, updateWithDebug } from '../../src/Debug/commons' 5 | import * as ConsoleDebugger from '../../src/Debug/console' 6 | import * as DevToolDebugger from '../../src/Debug/redux-devtool' 7 | import { disableDebugger, mockDebugger } from './_helpers' 8 | 9 | afterEach(() => { 10 | jest.restoreAllMocks() 11 | 12 | delete (window as any).__REDUX_DEVTOOLS_EXTENSION__ 13 | }) 14 | 15 | describe('Debug/commons', () => { 16 | it('debugInit() should return a `DebugInit` object', () => { 17 | assert.deepStrictEqual(debugInit(), { type: 'INIT' }) 18 | }) 19 | 20 | it('debugMsg() should return a `DebugMsg` object', () => { 21 | assert.deepStrictEqual(debugMsg({ type: 'MyMessage', some: 'data' }), { 22 | type: 'MESSAGE', 23 | payload: { 24 | type: 'MyMessage', 25 | some: 'data' 26 | } 27 | }) 28 | }) 29 | 30 | it('updateWithDebug() should run the provided update tracking messages and model updates through debug$', () => { 31 | const init = 0 32 | const debug$ = new BehaviorSubject>([debugInit(), init]) 33 | const updateWD = updateWithDebug(debug$, update) 34 | 35 | const step1 = updateWD({ tag: 'Inc' }, init) 36 | assert.deepStrictEqual(step1, [1, none]) 37 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Inc' } }, 1]) 38 | 39 | // we need to cast as any to test internal handling of this "special" message 40 | const step2 = updateWD({ type: '__DebugUpdateModel__', payload: 10 } as any, step1[0]) 41 | assert.deepStrictEqual(step2, [10, none]) 42 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Inc' } }, 1]) 43 | 44 | const step3 = updateWD({ tag: 'Dec' }, step2[0]) 45 | assert.deepStrictEqual(step3, [9, none]) 46 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Dec' } }, 9]) 47 | 48 | const step4 = updateWD({ tag: 'Inc' }, step3[0]) 49 | assert.deepStrictEqual(step4, [10, none]) 50 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Inc' } }, 10]) 51 | 52 | // we need to cast as any to test internal handling of this "special" message 53 | const step5 = updateWD({ type: '__DebugApplyMsg__', payload: { tag: 'Inc' } } as any, step4[0]) 54 | assert.deepStrictEqual(step5, [11, none]) 55 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Inc' } }, 10]) 56 | 57 | const step6 = updateWD({ tag: 'Inc' }, step5[0]) 58 | assert.deepStrictEqual(step6, [12, none]) 59 | assert.deepStrictEqual(debug$.getValue(), [{ type: 'MESSAGE', payload: { tag: 'Inc' } }, 12]) 60 | }) 61 | 62 | describe('runDebugger()', () => { 63 | it('should run a standard console debugger when redux dev-tool is not available', () => { 64 | const log: Array> = [] 65 | 66 | // --- Mock debuggers 67 | jest.spyOn(DevToolDebugger, 'reduxDevToolDebugger').mockReturnValueOnce(disableDebugger) 68 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(mockDebugger(log)) 69 | // --- 70 | 71 | const Debugger = runDebugger(window, EMPTY) 72 | const init = 0 73 | const debug$ = new BehaviorSubject>([debugInit(), init]) 74 | const dispatch = () => undefined 75 | 76 | Debugger({ init, debug$, dispatch })() 77 | 78 | debug$.next([debugMsg({ tag: 'Inc' }), 1]) 79 | 80 | assert.deepStrictEqual(log, [[debugInit(), 0], [debugMsg({ tag: 'Inc' }), 1]]) 81 | 82 | assert.strictEqual((DevToolDebugger.reduxDevToolDebugger as any).mock.calls.length, 0) 83 | }) 84 | 85 | it('should run a redux dev-tool debugger when is available', () => { 86 | const log: Array> = [] 87 | 88 | // --- Mock debuggers 89 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(disableDebugger) 90 | jest.spyOn(DevToolDebugger, 'reduxDevToolDebugger').mockReturnValueOnce(mockDebugger(log)) 91 | // --- 92 | 93 | const win = window as any 94 | 95 | // --- Mock Redux DevTool Extension 96 | win.__REDUX_DEVTOOLS_EXTENSION__ = { 97 | connect: () => ({}) 98 | } 99 | 100 | const Debugger = runDebugger(win, EMPTY) 101 | const init = 0 102 | const debug$ = new BehaviorSubject>([debugInit(), init]) 103 | const dispatch = () => undefined 104 | 105 | Debugger({ init, debug$, dispatch })() 106 | 107 | debug$.next([debugMsg({ tag: 'Inc' }), 1]) 108 | 109 | assert.deepStrictEqual(log, [[debugInit(), 0], [debugMsg({ tag: 'Inc' }), 1]]) 110 | 111 | assert.strictEqual((ConsoleDebugger.consoleDebugger as any).mock.calls.length, 0) 112 | }) 113 | 114 | it('should stop debugger when signal is emitted', () => { 115 | const signal = new Subject() 116 | const log: Array> = [] 117 | const spyStop = jest.fn() 118 | 119 | // --- Mock debuggers 120 | jest.spyOn(ConsoleDebugger, 'consoleDebugger').mockReturnValueOnce(() => ({ 121 | debug: data => log.push(data as DebugData), 122 | stop: spyStop 123 | })) 124 | // --- 125 | 126 | const Debugger = runDebugger(window, signal) 127 | const init = 0 128 | const debug$ = new BehaviorSubject>([debugInit(), init]) 129 | const dispatch = () => undefined 130 | 131 | Debugger({ init, debug$, dispatch })() 132 | 133 | debug$.next([debugMsg({ tag: 'Inc' }), 1]) 134 | debug$.next([debugMsg({ tag: 'Dec' }), 0]) 135 | 136 | // Emit stop signal and the other changes are bypassed 137 | signal.next('stop me!') 138 | 139 | debug$.next([debugMsg({ tag: 'Inc' }), 1]) 140 | debug$.next([debugMsg({ tag: 'Inc' }), 1]) 141 | 142 | assert.deepStrictEqual(log, [[debugInit(), 0], [debugMsg({ tag: 'Inc' }), 1], [debugMsg({ tag: 'Dec' }), 0]]) 143 | assert.strictEqual(spyStop.mock.calls.length, 1) 144 | }) 145 | }) 146 | }) 147 | 148 | // --- Helpers 149 | type Model = number 150 | type Msg = { tag: 'Inc' } | { tag: 'Dec' } 151 | 152 | const update = (msg: Msg, model: Model): [Model, Cmd] => { 153 | switch (msg.tag) { 154 | case 'Inc': 155 | return [model + 1, none] 156 | case 'Dec': 157 | return [model - 1, none] 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /test/Debug/console.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { consoleDebugger } from '../../src/Debug/console' 3 | import { Model, Msg, STD_DEPS } from './_helpers' 4 | 5 | describe('Debug/console', () => { 6 | const oriConsoleLog = console.log 7 | const oriConsoleDir = console.dir 8 | const oriConsoleGroup = console.group 9 | const oriConsoleGroupCollapsed = console.groupCollapsed 10 | const oriConsoleGroupEnd = console.groupEnd 11 | 12 | let log: Array<{ type: string; value: any }> 13 | const logger = (type: string) => (...values: any[]) => log.push({ type, value: values }) 14 | 15 | // --- Setup 16 | beforeAll(() => { 17 | console.log = logger('log') 18 | console.dir = logger('dir') 19 | console.group = logger('group') 20 | console.groupCollapsed = logger('groupCollapsed') 21 | console.groupEnd = logger('groupEnd') 22 | }) 23 | 24 | beforeEach(() => { 25 | log = [] 26 | }) 27 | 28 | // --- Teardown 29 | afterAll(() => { 30 | console.log = oriConsoleLog 31 | console.dir = oriConsoleDir 32 | console.group = oriConsoleGroup 33 | console.groupCollapsed = oriConsoleGroupCollapsed 34 | console.groupEnd = oriConsoleGroupEnd 35 | }) 36 | 37 | // --- Tests 38 | it('consoleDebugger() should debug to console', () => { 39 | const D = consoleDebugger()(STD_DEPS) 40 | 41 | D.debug([{ type: 'INIT' }, 0]) 42 | D.debug([{ type: 'MESSAGE', payload: { type: 'Inc' } }, 1]) 43 | 44 | assert.deepStrictEqual(log, [ 45 | // --- INIT 46 | { type: 'group', value: ['%cELM-TS', 'background-color: green; color: black'] }, 47 | { type: 'log', value: ['[INIT]'] }, 48 | { type: 'groupCollapsed', value: ['[MODEL]'] }, 49 | { type: 'dir', value: [0] }, 50 | { type: 'groupEnd', value: [] }, 51 | { type: 'groupEnd', value: [] }, 52 | // --- MESSAGE 53 | { type: 'group', value: ['%cELM-TS', 'background-color: green; color: black'] }, 54 | { type: 'groupCollapsed', value: [`[MESSAGE] %cInc`, 'font-weight: bold'] }, 55 | { type: 'dir', value: [{ type: 'Inc' }] }, 56 | { type: 'groupEnd', value: [] }, 57 | { type: 'groupCollapsed', value: ['[MODEL]'] }, 58 | { type: 'dir', value: [1] }, 59 | { type: 'groupEnd', value: [] }, 60 | { type: 'groupEnd', value: [] } 61 | ]) 62 | }) 63 | 64 | it('consoleDebugger() should handle messages with different tag property names', () => { 65 | type MsgTag = { tag: 'FooTag' } 66 | type Msg_Tag = { _tag: 'Foo_Tag' } 67 | type MsgType = { type: 'FooType' } 68 | type Msg_Type = { _type: 'Foo_Type' } 69 | type MsgKind = { kind: 'FooKind' } 70 | type Msg_Kind = { _kind: 'Foo_Kind' } 71 | type MsgOther = { other: 'FooOther' } 72 | type MsgDetect = MsgTag | Msg_Tag | MsgType | Msg_Type | MsgKind | Msg_Kind | MsgOther 73 | 74 | const D = consoleDebugger()(STD_DEPS as any) 75 | 76 | D.debug([{ type: 'MESSAGE', payload: { tag: 'FooTag' } }, 1]) 77 | D.debug([{ type: 'MESSAGE', payload: { _tag: 'Foo_Tag' } }, 1]) 78 | D.debug([{ type: 'MESSAGE', payload: { type: 'FooType' } }, 1]) 79 | D.debug([{ type: 'MESSAGE', payload: { _type: 'Foo_Type' } }, 1]) 80 | D.debug([{ type: 'MESSAGE', payload: { kind: 'FooKind' } }, 1]) 81 | D.debug([{ type: 'MESSAGE', payload: { _kind: 'Foo_Kind' } }, 1]) 82 | D.debug([{ type: 'MESSAGE', payload: { other: 'FooOther' } }, 1]) 83 | 84 | const result = log.filter(x => x.type === 'groupCollapsed' && x.value[0] !== '[MODEL]') 85 | 86 | assert.deepStrictEqual(result, [ 87 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFooTag`, 'font-weight: bold'] }, 88 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFoo_Tag`, 'font-weight: bold'] }, 89 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFooType`, 'font-weight: bold'] }, 90 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFoo_Type`, 'font-weight: bold'] }, 91 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFooKind`, 'font-weight: bold'] }, 92 | { type: 'groupCollapsed', value: [`[MESSAGE] %cFoo_Kind`, 'font-weight: bold'] }, 93 | { type: 'groupCollapsed', value: [`[MESSAGE] %c`, 'font-weight: bold'] } 94 | ]) 95 | }) 96 | 97 | it('consoleDebugger() should log "stop" message on stop', () => { 98 | const D = consoleDebugger()(STD_DEPS) 99 | 100 | D.stop() 101 | 102 | assert.deepStrictEqual(log, [ 103 | { type: 'group', value: ['%cELM-TS', 'background-color: green; color: black'] }, 104 | { type: 'log', value: ['--- stop debugger ---'] }, 105 | { type: 'groupEnd', value: [] } 106 | ]) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /test/Decode.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as E from 'fp-ts/lib/Either' 3 | import { decoder } from '../src/Decode' 4 | 5 | describe('Decode', () => { 6 | it('decoder.zero()', () => { 7 | const anyValue: unknown = {} 8 | 9 | assert.deepStrictEqual(decoder.zero()(anyValue), E.left('zero')) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/Html.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { array } from 'fp-ts/lib/Array' 3 | import { Option, some } from 'fp-ts/lib/Option' 4 | import { Task, task } from 'fp-ts/lib/Task' 5 | import { Subject } from 'rxjs' 6 | import { Cmd, none } from '../src/Cmd' 7 | import { map, program, programWithFlags, run, withStop } from '../src/Html' 8 | import * as App from './helpers/app' 9 | import { delayedAssert } from './helpers/utils' 10 | 11 | const sequenceTask = array.sequence(task) 12 | 13 | describe('Html', () => { 14 | describe('map()', () => { 15 | it('should map an `Html` into an `Html`', () => { 16 | const state: string[] = [] 17 | const btn = App.button('A button') 18 | const dispatch = (msg: App.Msg) => state.push(msg.type) 19 | const m = map(() => ({ type: 'BAR' as const })) 20 | 21 | btn(dispatch).onclick() 22 | m(btn)(dispatch).onclick() 23 | 24 | assert.deepStrictEqual(state, ['FOO', 'BAR']) 25 | }) 26 | }) 27 | 28 | describe('program()', () => { 29 | it('should return the Model/Cmd/Sub/Html streams and Dispatch function - no subscription', () => { 30 | const views: App.View[] = [] 31 | const subs: App.Msg[] = [] 32 | const { dispatch, html$, sub$ } = program(App.init, App.update, App.view) 33 | 34 | html$.subscribe(v => views.push(v)) 35 | sub$.subscribe(v => subs.push(v)) 36 | 37 | dispatch({ type: 'FOO' }) 38 | dispatch({ type: 'BAR' }) 39 | dispatch({ type: 'DO-THE-THING!' }) 40 | 41 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['', 'foo', 'bar']) 42 | assert.deepStrictEqual(subs, []) 43 | }) 44 | 45 | it('should return the Model/Cmd/Sub/Html streams and Dispatch function - with subscription', () => { 46 | const views: App.View[] = [] 47 | const subs: App.Msg[] = [] 48 | const { sub$, html$, dispatch } = program(App.init, App.update, App.view, App.subscriptions) 49 | 50 | html$.subscribe(v => views.push(v)) 51 | sub$.subscribe(v => subs.push(v)) 52 | 53 | dispatch({ type: 'FOO' }) 54 | dispatch({ type: 'BAR' }) 55 | dispatch({ type: 'SUB' }) 56 | 57 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['', 'foo', 'bar', 'sub']) 58 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 59 | }) 60 | }) 61 | 62 | describe('programWithFlags()', () => { 63 | it('should return a function that returns a program() with flags on `init`', () => { 64 | const views: App.View[] = [] 65 | const initWithFlags = (f: string): [App.Model, Cmd] => [{ x: f }, none] 66 | const withFlags = programWithFlags(initWithFlags, App.update, App.view, App.subscriptions) 67 | const { dispatch, html$ } = withFlags('start!') 68 | 69 | html$.subscribe(v => views.push(v)) 70 | 71 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['start!']) 72 | }) 73 | }) 74 | 75 | describe('withStop()', () => { 76 | it('should stop the Program when a signal is emitted', async () => { 77 | const signal = new Subject() 78 | 79 | const cmds: Array>> = [] 80 | const views: App.View[] = [] 81 | const subs: App.Msg[] = [] 82 | const { sub$, html$, cmd$, dispatch } = withStop(signal)( 83 | program(App.init, App.update, App.view, App.subscriptions) 84 | ) 85 | 86 | cmd$.subscribe(v => cmds.push(v)) 87 | html$.subscribe(v => views.push(v)) 88 | sub$.subscribe(v => subs.push(v)) 89 | 90 | dispatch({ type: 'FOO' }) 91 | dispatch({ type: 'BAR' }) 92 | dispatch({ type: 'DO-THE-THING!' }) 93 | dispatch({ type: 'SUB' }) 94 | 95 | // Emit stop signal and the other changes are bypassed 96 | signal.next('stop me!') 97 | 98 | dispatch({ type: 'FOO' }) 99 | dispatch({ type: 'BAR' }) 100 | dispatch({ type: 'DO-THE-THING!' }) 101 | dispatch({ type: 'SUB' }) 102 | 103 | const commands = await sequenceTask(cmds)() 104 | 105 | assert.deepStrictEqual(commands, [some({ type: 'FOO' })]) 106 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['', 'foo', 'bar', 'sub']) 107 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 108 | }) 109 | }) 110 | 111 | describe('run()', () => { 112 | it('should run the Program', () => { 113 | const renderings: string[] = [] 114 | const renderer = (dom: App.Dom) => { 115 | renderings.push(`<${dom.tag}>${dom.text}`) 116 | } 117 | const view = (model: App.Model) => App.span(model.x) 118 | const p = program(App.init, App.update, view, App.subscriptions) 119 | 120 | run(p, renderer) 121 | 122 | p.dispatch({ type: 'FOO' }) 123 | p.dispatch({ type: 'SUB' }) 124 | p.dispatch({ type: 'BAR' }) 125 | p.dispatch({ type: 'DO-THE-THING!' }) 126 | 127 | return delayedAssert(() => { 128 | assert.deepStrictEqual(renderings, [ 129 | '', 130 | 'foo', 131 | 'sub', 132 | 'listen', 133 | 'bar', 134 | 'foo' 135 | ]) 136 | }) 137 | }) 138 | 139 | it('should stop the Program when signal is emitted', () => { 140 | const signal = new Subject() 141 | 142 | const renderings: string[] = [] 143 | const renderer = (dom: App.Dom) => { 144 | renderings.push(`<${dom.tag}>${dom.text}`) 145 | } 146 | const view = (model: App.Model) => App.span(model.x) 147 | const p = withStop(signal)(program(App.init, App.update, view, App.subscriptions)) 148 | 149 | run(p, renderer) 150 | 151 | p.dispatch({ type: 'FOO' }) 152 | p.dispatch({ type: 'SUB' }) 153 | p.dispatch({ type: 'BAR' }) 154 | p.dispatch({ type: 'DO-THE-THING!' }) 155 | 156 | // Emit stop signal and the other changes are bypassed 157 | signal.next('stop me!') 158 | 159 | p.dispatch({ type: 'FOO' }) 160 | p.dispatch({ type: 'SUB' }) 161 | p.dispatch({ type: 'BAR' }) 162 | p.dispatch({ type: 'DO-THE-THING!' }) 163 | 164 | return delayedAssert(() => { 165 | assert.deepStrictEqual(renderings, [ 166 | '', 167 | 'foo', 168 | 'sub', 169 | 'listen', 170 | 'bar' 171 | // the last "foo" would be generated by the "DO-THE-THING!" command 172 | // but is bypassed because the "stop" signal is emitted before the command's message is consumed 173 | // 'foo' 174 | ]) 175 | }) 176 | }) 177 | }) 178 | }) 179 | -------------------------------------------------------------------------------- /test/Navigation.test.ts: -------------------------------------------------------------------------------- 1 | // --- Mocking `History` module - super tricky... 2 | import * as history from 'history' 3 | import { mocked } from 'ts-jest/utils' 4 | import { createMockHistory } from './helpers/mock-history' 5 | 6 | jest.mock('history') 7 | 8 | const historyM = mocked(history) 9 | const log: string[] = [] 10 | 11 | historyM.createHashHistory.mockImplementation(createMockHistory(log)) 12 | // --- /Mocking 13 | 14 | import * as assert from 'assert' 15 | import * as O from 'fp-ts/lib/Option' 16 | import * as T from 'fp-ts/lib/Task' 17 | import { EMPTY, of } from 'rxjs' 18 | import { Cmd, none } from '../src/Cmd' 19 | import { Html } from '../src/Html' 20 | import { Location, program, programWithFlags, push } from '../src/Navigation' 21 | import { Sub } from '../src/Sub' 22 | import * as App from './helpers/app' 23 | 24 | beforeEach(() => { 25 | log.splice(0, log.length) 26 | }) 27 | 28 | afterAll(() => { 29 | jest.restoreAllMocks() 30 | }) 31 | 32 | describe('Navigation', () => { 33 | describe('push()', () => { 34 | it('should push a new path into history and return a Cmd', done => { 35 | return push('/a-path').subscribe(async to => { 36 | const result = await to() 37 | 38 | assert.deepStrictEqual(result, O.none) 39 | assert.deepStrictEqual(log, ['/a-path']) 40 | 41 | done() 42 | }) 43 | }) 44 | }) 45 | 46 | describe('program()', () => { 47 | it('should return a Program and listen to location changes - no subscription', () => { 48 | const cmds: Array>> = [] 49 | const views: NavView[] = [] 50 | const subs: NavMsg[] = [] 51 | 52 | const { dispatch, html$, cmd$, sub$ } = program(locationToMsg, initWithLocation, navUpdate, App.view) 53 | 54 | cmd$.subscribe(v => cmds.push(v)) 55 | html$.subscribe(v => views.push(v)) 56 | sub$.subscribe(v => { 57 | subs.push(v) 58 | dispatch(v) // simulate `run()` 59 | }) 60 | 61 | dispatch({ type: 'FOO' }) 62 | dispatch({ type: 'GO_TO', path: '/bar' }) 63 | 64 | // Assert that there is just one command 65 | assert.strictEqual(cmds.length, 1) 66 | 67 | return cmds[0]().then(() => { 68 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['', 'foo', 'bar']) 69 | assert.deepStrictEqual(subs, [{ type: 'BAR' }]) 70 | }) 71 | }) 72 | 73 | it('should return a Program and listen to location changes - with subscription', () => { 74 | const cmds: Array>> = [] 75 | const views: NavView[] = [] 76 | const subs: NavMsg[] = [] 77 | 78 | const { dispatch, html$, cmd$, sub$ } = program( 79 | locationToMsg, 80 | initWithLocation, 81 | navUpdate, 82 | App.view, 83 | subscriptions 84 | ) 85 | 86 | cmd$.subscribe(v => cmds.push(v)) 87 | html$.subscribe(v => views.push(v)) 88 | sub$.subscribe(v => { 89 | subs.push(v) 90 | dispatch(v) // simulate `run()` 91 | }) 92 | 93 | dispatch({ type: 'FOO' }) 94 | dispatch({ type: 'GO_TO', path: '/bar' }) 95 | dispatch({ type: 'SUB' }) 96 | 97 | // Assert that there is just one command 98 | assert.strictEqual(cmds.length, 1) 99 | 100 | return cmds[0]().then(() => { 101 | // "BAR" is dispatch after "LISTEN" because it's dependent of `cmd()` 102 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['', 'foo', 'sub', 'listen', 'bar']) 103 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }, { type: 'BAR' }]) 104 | }) 105 | }) 106 | }) 107 | 108 | describe('programWithFlags()', () => { 109 | it('programWithFlags() should return a function which returns a program() with flags on `init` - no subscription', () => { 110 | const views: NavView[] = [] 111 | const subs: NavMsg[] = [] 112 | const initWithFlags = (f: string) => (_: Location): [App.Model, Cmd] => [{ x: f }, none] 113 | const withFlags = programWithFlags(locationToMsg, initWithFlags, navUpdate, App.view) 114 | const { dispatch, html$, sub$ } = withFlags('start!') 115 | 116 | html$.subscribe(v => views.push(v)) 117 | sub$.subscribe(v => subs.push(v)) 118 | 119 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['start!']) 120 | assert.deepStrictEqual(subs, []) 121 | }) 122 | 123 | it('programWithFlags() should return a function which returns a program() with flags on `init` - with subscription', () => { 124 | const views: NavView[] = [] 125 | const subs: NavMsg[] = [] 126 | const initWithFlags = (f: string) => (_: Location): [App.Model, Cmd] => [{ x: f }, none] 127 | const withFlags = programWithFlags(locationToMsg, initWithFlags, navUpdate, App.view, subscriptions) 128 | const { dispatch, html$, sub$ } = withFlags('start!') 129 | 130 | html$.subscribe(v => views.push(v)) 131 | sub$.subscribe(v => subs.push(v)) 132 | 133 | dispatch({ type: 'SUB' }) 134 | 135 | assert.deepStrictEqual(views.map(x => x(dispatch).text), ['start!', 'sub']) 136 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 137 | }) 138 | }) 139 | }) 140 | 141 | // --- Utilities 142 | type NavMsg = App.Msg | { type: 'GO_TO'; path: string } 143 | type NavView = Html 144 | 145 | function navUpdate(msg: NavMsg, model: App.Model): [App.Model, Cmd] { 146 | if (msg.type === 'GO_TO') { 147 | return [model, push(msg.path)] 148 | } 149 | 150 | return App.update(msg, model) 151 | } 152 | 153 | function locationToMsg(l: Location): NavMsg { 154 | return { 155 | type: l.pathname.substring(1).toUpperCase() 156 | } as NavMsg 157 | } 158 | 159 | function initWithLocation(l: Location): [App.Model, Cmd] { 160 | return [{ x: l.pathname }, none] 161 | } 162 | 163 | function subscriptions(m: App.Model): Sub { 164 | return m.x === 'sub' ? of({ type: 'LISTEN' }) : EMPTY 165 | } 166 | -------------------------------------------------------------------------------- /test/Platform.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { array } from 'fp-ts/lib/Array' 3 | import { Option, some } from 'fp-ts/lib/Option' 4 | import { Task, task } from 'fp-ts/lib/Task' 5 | import { Subject } from 'rxjs' 6 | import { Cmd, none } from '../src/Cmd' 7 | import { program, programWithFlags, run, withStop } from '../src/Platform' 8 | import * as App from './helpers/app' 9 | import { delayedAssert } from './helpers/utils' 10 | 11 | const sequenceTask = array.sequence(task) 12 | 13 | describe('Platform', () => { 14 | describe('program()', () => { 15 | it('should return the Model/Cmd/Sub streams and Dispatch function - no subscription', async () => { 16 | const cmds: Array>> = [] 17 | const models: App.Model[] = [] 18 | const subs: App.Msg[] = [] 19 | const { model$, cmd$, sub$, dispatch } = program(App.init, App.update) 20 | 21 | cmd$.subscribe(v => cmds.push(v)) 22 | model$.subscribe(v => models.push(v)) 23 | sub$.subscribe(v => subs.push(v)) 24 | 25 | dispatch({ type: 'FOO' }) 26 | dispatch({ type: 'BAR' }) 27 | dispatch({ type: 'DO-THE-THING!' }) 28 | 29 | assert.deepStrictEqual(models, [{ x: '' }, { x: 'foo' }, { x: 'bar' }]) 30 | assert.deepStrictEqual(subs, []) 31 | 32 | const commands = await sequenceTask(cmds)() 33 | 34 | assert.deepStrictEqual(commands, [some({ type: 'FOO' })]) 35 | }) 36 | 37 | it('should return the Model/Cmd/Sub streams and Dispatch function - with subscription', () => { 38 | const models: App.Model[] = [] 39 | const subs: App.Msg[] = [] 40 | const { model$, sub$, dispatch } = program(App.init, App.update, App.subscriptions) 41 | 42 | model$.subscribe(v => models.push(v)) 43 | sub$.subscribe(v => subs.push(v)) 44 | 45 | dispatch({ type: 'FOO' }) 46 | dispatch({ type: 'BAR' }) 47 | dispatch({ type: 'SUB' }) 48 | 49 | assert.deepStrictEqual(models, [{ x: '' }, { x: 'foo' }, { x: 'bar' }, { x: 'sub' }]) 50 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 51 | }) 52 | }) 53 | 54 | describe('programWithFlags()', () => { 55 | it('should return a function which returns a program() with flags on `init` - no subscription', () => { 56 | const models: App.Model[] = [] 57 | const subs: App.Msg[] = [] 58 | 59 | const initWithFlags = (f: string): [App.Model, Cmd] => [{ x: f }, none] 60 | const withFlags = programWithFlags(initWithFlags, App.update) 61 | const { model$, sub$ } = withFlags('start!') 62 | 63 | model$.subscribe(v => models.push(v)) 64 | sub$.subscribe(v => subs.push(v)) 65 | 66 | assert.deepStrictEqual(models, [{ x: 'start!' }]) 67 | assert.deepStrictEqual(subs, []) 68 | }) 69 | 70 | it('should return a function which returns a program() with flags on `init` - with subscription', () => { 71 | const models: App.Model[] = [] 72 | const subs: App.Msg[] = [] 73 | 74 | const initWithFlags = (f: string): [App.Model, Cmd] => [{ x: f }, none] 75 | const withFlags = programWithFlags(initWithFlags, App.update, App.subscriptions) 76 | const { model$, sub$, dispatch } = withFlags('start!') 77 | 78 | model$.subscribe(v => models.push(v)) 79 | sub$.subscribe(v => subs.push(v)) 80 | 81 | dispatch({ type: 'SUB' }) 82 | 83 | assert.deepStrictEqual(models, [{ x: 'start!' }, { x: 'sub' }]) 84 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 85 | }) 86 | }) 87 | 88 | describe('withStop()', () => { 89 | it('should stop the Program when a signal is emitted', async () => { 90 | const signal = new Subject() 91 | 92 | const cmds: Array>> = [] 93 | const models: App.Model[] = [] 94 | const subs: App.Msg[] = [] 95 | const { model$, cmd$, sub$, dispatch } = withStop(signal)(program(App.init, App.update, App.subscriptions)) 96 | 97 | cmd$.subscribe(v => cmds.push(v)) 98 | model$.subscribe(v => models.push(v)) 99 | sub$.subscribe(v => subs.push(v)) 100 | 101 | dispatch({ type: 'FOO' }) 102 | dispatch({ type: 'BAR' }) 103 | dispatch({ type: 'DO-THE-THING!' }) 104 | dispatch({ type: 'SUB' }) 105 | 106 | // Emit stop signal and the other changes are bypassed 107 | signal.next('stop me!') 108 | 109 | dispatch({ type: 'FOO' }) 110 | dispatch({ type: 'BAR' }) 111 | dispatch({ type: 'DO-THE-THING!' }) 112 | dispatch({ type: 'SUB' }) 113 | 114 | const commands = await sequenceTask(cmds)() 115 | 116 | assert.deepStrictEqual(commands, [some({ type: 'FOO' })]) 117 | assert.deepStrictEqual(models, [{ x: '' }, { x: 'foo' }, { x: 'bar' }, { x: 'sub' }]) 118 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 119 | }) 120 | }) 121 | 122 | describe('run()', () => { 123 | it('should run the Program', () => { 124 | // setup 125 | const models: App.Model[] = [] 126 | const p = program(App.init, App.update, App.subscriptions) 127 | p.model$.subscribe(model => models.push(model)) 128 | 129 | // run 130 | run(p) 131 | 132 | // dispatch 133 | p.dispatch({ type: 'FOO' }) 134 | p.dispatch({ type: 'SUB' }) 135 | p.dispatch({ type: 'BAR' }) 136 | p.dispatch({ type: 'DO-THE-THING!' }) 137 | 138 | // assert 139 | return delayedAssert(() => { 140 | assert.deepStrictEqual(models, [ 141 | { x: '' }, 142 | { x: 'foo' }, 143 | { x: 'sub' }, 144 | { x: 'listen' }, 145 | { x: 'bar' }, 146 | { x: 'foo' } 147 | ]) 148 | }) 149 | }) 150 | 151 | it('should stop the Program when signal is emitted', () => { 152 | // setup 153 | const signal = new Subject() 154 | const models: App.Model[] = [] 155 | const p = withStop(signal)(program(App.init, App.update, App.subscriptions)) 156 | p.model$.subscribe(model => models.push(model)) 157 | 158 | // run 159 | run(p) 160 | 161 | // dispatch 162 | p.dispatch({ type: 'FOO' }) 163 | p.dispatch({ type: 'SUB' }) 164 | p.dispatch({ type: 'BAR' }) 165 | p.dispatch({ type: 'DO-THE-THING!' }) 166 | 167 | // Emit stop signal and the other changes are bypassed 168 | signal.next('stop me!') 169 | 170 | p.dispatch({ type: 'FOO' }) 171 | p.dispatch({ type: 'SUB' }) 172 | p.dispatch({ type: 'BAR' }) 173 | p.dispatch({ type: 'DO-THE-THING!' }) 174 | 175 | // assert 176 | return delayedAssert(() => { 177 | assert.deepStrictEqual(models, [ 178 | { x: '' }, 179 | { x: 'foo' }, 180 | { x: 'sub' }, 181 | { x: 'listen' }, 182 | { x: 'bar' } 183 | // the last "foo" would be generated by the "DO-THE-THING!" command 184 | // but is bypassed because the "stop" signal is emitted before the command's message is consumed 185 | // { x: 'foo' } 186 | ]) 187 | }) 188 | }) 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /test/React.test.tsx: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as React from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import { act } from 'react-dom/test-utils' 5 | import { Cmd, none } from '../src/Cmd' 6 | import { Html, map, program, programWithFlags, run } from '../src/React' 7 | import * as App from './helpers/app' 8 | import { delayedAssert } from './helpers/utils' 9 | 10 | describe('React', () => { 11 | describe('map()', () => { 12 | it('should map an Html into an Html', () => { 13 | const log: string[] = [] 14 | const dispatch = (msg: App.Msg) => log.push(msg.type) 15 | 16 | const btn01: Html = dispatch => 17 | const btn02 = map(() => ({ type: 'BAR' }))(btn01) 18 | 19 | btn01(dispatch).props.onClick() 20 | btn02(dispatch).props.onClick() 21 | 22 | assert.deepStrictEqual(log, ['FOO', 'BAR']) 23 | }) 24 | }) 25 | 26 | describe('program()', () => { 27 | it('should return the Model/Cmd/Sub/Html streams and Dispatch function for React - no subscription', () => { 28 | const [container, teardown] = makeEntryPoint() 29 | const [renderings, renderer] = makeRenderer(container) 30 | 31 | const { dispatch, html$ } = program(App.init, App.update, viewWithMount) 32 | 33 | html$.subscribe(v => renderer(v(dispatch))) 34 | 35 | dispatch({ type: 'FOO' }) 36 | dispatch({ type: 'BAR' }) 37 | dispatch({ type: 'DO-THE-THING!' }) 38 | 39 | assert.deepStrictEqual(renderings, ['', 'foo', 'bar']) 40 | 41 | teardown() 42 | }) 43 | 44 | it('should return the Model/Cmd/Sub/Html streams and Dispatch function for React - with subscription', () => { 45 | const [container, teardown] = makeEntryPoint() 46 | const [renderings, renderer] = makeRenderer(container) 47 | 48 | const subs: App.Msg[] = [] 49 | const { sub$, html$, dispatch } = program(App.init, App.update, viewWithMount, App.subscriptions) 50 | 51 | html$.subscribe(v => renderer(v(dispatch))) 52 | sub$.subscribe(v => subs.push(v)) 53 | 54 | dispatch({ type: 'FOO' }) 55 | dispatch({ type: 'BAR' }) 56 | dispatch({ type: 'SUB' }) 57 | 58 | assert.deepStrictEqual(subs, [{ type: 'LISTEN' }]) 59 | assert.deepStrictEqual(renderings, ['', 'foo', 'bar', 'sub']) 60 | 61 | teardown() 62 | }) 63 | }) 64 | 65 | describe('programWithFlags()', () => { 66 | it('should return a function which returns a program() with flags on `init` for React', () => { 67 | const [container, teardown] = makeEntryPoint() 68 | const [renderings, renderer] = makeRenderer(container) 69 | 70 | const initWithFlags = (f: string): [App.Model, Cmd] => [{ x: f }, none] 71 | const withFlags = programWithFlags(initWithFlags, App.update, viewWithMount, App.subscriptions) 72 | const { dispatch, html$ } = withFlags('start!') 73 | 74 | html$.subscribe(v => renderer(v(dispatch))) 75 | 76 | assert.deepStrictEqual(renderings, ['start!']) 77 | 78 | teardown() 79 | }) 80 | }) 81 | 82 | describe('run()', () => { 83 | it('should run the React Program', () => { 84 | const [container, teardown] = makeEntryPoint() 85 | const [renderings, renderer] = makeRenderer(container) 86 | 87 | const p = program(App.init, App.update, view, App.subscriptions) 88 | 89 | run(p, renderer) 90 | 91 | p.dispatch({ type: 'FOO' }) 92 | p.dispatch({ type: 'SUB' }) 93 | p.dispatch({ type: 'BAR' }) 94 | p.dispatch({ type: 'DO-THE-THING!' }) 95 | 96 | return delayedAssert(() => { 97 | assert.deepStrictEqual(renderings, ['', 'foo', 'sub', 'listen', 'bar', 'foo']) 98 | 99 | teardown() 100 | }) 101 | }) 102 | 103 | it('should run the React Program - with commands in init and on component mount', () => { 104 | const [container, teardown] = makeEntryPoint() 105 | const [renderings, renderer] = makeRenderer(container) 106 | 107 | const p = program(App.initWithCmd, App.update, viewWithMount, App.subscriptions) 108 | 109 | run(p, renderer) 110 | 111 | // use timeout in order to better simulate a real case 112 | const to = setTimeout(() => { 113 | p.dispatch({ type: 'FOO' }) 114 | p.dispatch({ type: 'SUB' }) 115 | p.dispatch({ type: 'BAR' }) 116 | p.dispatch({ type: 'DO-THE-THING!' }) 117 | 118 | clearTimeout(to) 119 | }, 250) 120 | 121 | return delayedAssert(() => { 122 | assert.deepStrictEqual(renderings, ['', 'baz', 'foo', 'foo', 'sub', 'listen', 'bar', 'foo']) 123 | 124 | teardown() 125 | }, 500) 126 | }) 127 | }) 128 | }) 129 | 130 | // --- Utilities 131 | function view(model: App.Model): Html { 132 | return dispatch => dispatch({ type: 'FOO' })} /> 133 | } 134 | 135 | function viewWithMount(model: App.Model): Html { 136 | return dispatch => ( 137 | dispatch({ type: 'FOO' })} 140 | onMount={() => dispatch({ type: 'DO-THE-THING!' })} 141 | /> 142 | ) 143 | } 144 | 145 | interface ComponentProps { 146 | value: string 147 | onClick: () => void 148 | onMount?: () => void 149 | } 150 | 151 | function Component({ value, onClick, onMount }: ComponentProps): JSX.Element { 152 | // the component would dispatch a message on mount which will execute a command 153 | // in case `onMount` callback is defined 154 | React.useEffect(() => { 155 | if (typeof onMount !== 'undefined') { 156 | return onMount() 157 | } 158 | }, []) 159 | 160 | return 161 | } 162 | 163 | function makeEntryPoint(): [HTMLDivElement, () => void] { 164 | const container = document.createElement('div') 165 | document.body.appendChild(container) 166 | 167 | return [ 168 | container, 169 | () => { 170 | unmountComponentAtNode(container) 171 | container.remove() 172 | } 173 | ] 174 | } 175 | 176 | type Contents = Array 177 | 178 | function makeRenderer(container: HTMLDivElement): [Contents, (dom: React.ReactElement) => void] { 179 | const log: Contents = [] 180 | 181 | return [ 182 | log, 183 | dom => { 184 | act(() => { 185 | render(dom, container) 186 | }) 187 | 188 | log.push(container.textContent) 189 | } 190 | ] 191 | } 192 | -------------------------------------------------------------------------------- /test/Sub.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { of } from 'rxjs' 3 | import { batch, map } from '../src/Sub' 4 | 5 | describe('Sub', () => { 6 | it('map() should transform a Sub into Sub', done => { 7 | const subA = of('a') 8 | 9 | return map(a => a + 'b')(subA).subscribe(v => { 10 | assert.strictEqual(v, 'ab') 11 | 12 | done() 13 | }) 14 | }) 15 | 16 | it('batch() should batch an array of Sub', done => { 17 | const log: string[] = [] 18 | const subs = [of('a'), of('b'), of('c')] 19 | 20 | return batch(subs).subscribe({ 21 | next: v => log.push(v), 22 | 23 | complete: () => { 24 | assert.deepStrictEqual(log, ['a', 'b', 'c']) 25 | 26 | done() 27 | } 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/Task.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { fold, right } from 'fp-ts/lib/Either' 3 | import { some } from 'fp-ts/lib/Option' 4 | import { task } from 'fp-ts/lib/Task' 5 | import { attempt, perform } from '../src/Task' 6 | 7 | describe('Task', () => { 8 | it('perform() should "perform" at runtime a Task and give back a Cmd', done => { 9 | const t = task.of('foo') 10 | const p = perform(_ => ({ type: 'FOO' })) 11 | 12 | return p(t).subscribe(async to => { 13 | const result = await to() 14 | 15 | assert.deepStrictEqual(result, some({ type: 'FOO' })) 16 | 17 | done() 18 | }) 19 | }) 20 | 21 | it('attempt() should "perform" at runtime a Task that can fail', done => { 22 | const te = task.of(right('foo')) 23 | const a = attempt(fold(_ => ({ type: 'BAR' }), _ => ({ type: 'FOO' }))) 24 | 25 | return a(te).subscribe(async to => { 26 | const result = await to() 27 | 28 | assert.deepStrictEqual(result, some({ type: 'FOO' })) 29 | 30 | done() 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/Time.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { take } from 'rxjs/operators' 3 | import { every, now } from '../src/Time' 4 | 5 | interface Msg { 6 | type: 'CURRENT_TIME' 7 | time: number 8 | } 9 | 10 | // --- Mocks current time in order to have deterministic tests 11 | const TIME = 1531313966894 12 | 13 | beforeAll(() => { 14 | global.Date.prototype.getTime = jest.fn(() => TIME) 15 | }) 16 | 17 | afterAll(() => { 18 | jest.restoreAllMocks() 19 | }) 20 | // --- 21 | 22 | describe('Time', () => { 23 | it('now() should return a Task which resolves to current Date time', () => 24 | now()().then(v => assert.strictEqual(v, TIME))) 25 | 26 | it('every() should return a Sub which dispatch a Msg every "n" Time', done => { 27 | const log: Msg[] = [] 28 | const toMsg = (time: number): Msg => ({ type: 'CURRENT_TIME', time }) 29 | const sub$ = every(250, toMsg) 30 | 31 | sub$.pipe(take(3)).subscribe({ 32 | next: v => log.push(v), 33 | 34 | complete: () => { 35 | assert.deepStrictEqual(log, [ 36 | { 37 | type: 'CURRENT_TIME', 38 | time: TIME 39 | }, 40 | { 41 | type: 'CURRENT_TIME', 42 | time: TIME 43 | }, 44 | { 45 | type: 'CURRENT_TIME', 46 | time: TIME 47 | } 48 | ]) 49 | 50 | done() 51 | } 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/helpers/app.ts: -------------------------------------------------------------------------------- 1 | import { some } from 'fp-ts/lib/Option' 2 | import { task } from 'fp-ts/lib/Task' 3 | import { EMPTY, Observable, of } from 'rxjs' 4 | import * as cmd from '../../src/Cmd' 5 | import { Html } from '../../src/Html' 6 | 7 | type State = [Model, cmd.Cmd] 8 | 9 | const withModel = (model: Model): State => [model, cmd.none] 10 | 11 | const withEffect = (model: Model, cmd: cmd.Cmd): State => [model, cmd] 12 | 13 | const doFoo = of(task.of(some({ type: 'FOO' }))) 14 | 15 | const doBaz = of(task.of(some({ type: 'BAZ' }))) 16 | 17 | // --------- 18 | // --- MODEL 19 | // --------- 20 | export interface Model { 21 | x: string 22 | } 23 | 24 | export const init: State = withModel({ x: '' }) 25 | 26 | export const initWithCmd: State = withEffect({ x: '' }, doBaz) 27 | 28 | // ------------ 29 | // --- MESSAGES 30 | // ------------ 31 | export type Msg = 32 | | { type: 'FOO' } 33 | | { type: 'BAR' } 34 | | { type: 'BAZ' } 35 | | { type: 'DO-THE-THING!' } 36 | | { type: 'SUB' } 37 | | { type: 'LISTEN' } 38 | 39 | // ---------- 40 | // --- UPDATE 41 | // ---------- 42 | export function update(msg: Msg, model: Model): State { 43 | switch (msg.type) { 44 | case 'FOO': 45 | case 'BAR': 46 | case 'BAZ': 47 | case 'SUB': 48 | case 'LISTEN': 49 | return withModel({ x: msg.type.toLowerCase() }) 50 | 51 | case 'DO-THE-THING!': 52 | return withEffect(model, doFoo) 53 | } 54 | } 55 | 56 | // -------- 57 | // --- VIEW 58 | // -------- 59 | export interface Dom { 60 | tag: string 61 | text: string 62 | onclick: () => void 63 | } 64 | 65 | export type View = Html 66 | 67 | export function view(model: Model): View { 68 | return button(model.x) 69 | } 70 | 71 | export function button(x: string): View { 72 | return d => ({ tag: 'button', text: x, onclick: () => d({ type: 'FOO' }) }) 73 | } 74 | 75 | export function span(x: string): View { 76 | return () => ({ tag: 'span', text: x, onclick: () => undefined }) 77 | } 78 | 79 | // ----------------- 80 | // --- SUBSCRIPTIONS 81 | // ----------------- 82 | export function subscriptions(m: Model): Observable { 83 | return m.x === 'sub' ? of({ type: 'LISTEN' }) : EMPTY 84 | } 85 | -------------------------------------------------------------------------------- /test/helpers/mock-history.ts: -------------------------------------------------------------------------------- 1 | import { History, LocationDescriptorObject, LocationListener, UnregisterCallback } from 'history' 2 | 3 | /** 4 | * Creates a mocked implementation of the `history.createHashHistory()` function that tracks location changes through the `log` parameter 5 | */ 6 | export function createMockHistory(log: string[]): () => History { 7 | let listener: LocationListener 8 | 9 | return () => ({ 10 | location: { 11 | pathname: log.length > 0 ? log[log.length - 1] : '', 12 | search: '', 13 | state: null, 14 | hash: '' 15 | }, 16 | 17 | push: (path: string | LocationDescriptorObject): void => { 18 | const p = typeof path === 'string' ? path : typeof path.pathname !== 'undefined' ? path.pathname : '' 19 | log.push(p) 20 | 21 | listener( 22 | { 23 | pathname: p, 24 | search: '', 25 | state: null, 26 | hash: '' 27 | }, 28 | 'PUSH' 29 | ) 30 | }, 31 | 32 | listen: (fn: History.LocationListener): UnregisterCallback => { 33 | listener = fn 34 | return () => undefined 35 | }, 36 | 37 | // These are needed by `history` types declaration 38 | length: log.length, 39 | action: 'PUSH', 40 | replace: () => undefined, 41 | go: () => undefined, 42 | goBack: () => undefined, 43 | goForward: () => undefined, 44 | block: () => () => undefined, 45 | createHref: () => '' 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /test/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export const delayedAssert = (f: () => void, delay: number = 50): Promise => 2 | new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | try { 5 | f() 6 | resolve() 7 | } catch (e) { 8 | reject(e) 9 | } 10 | }, delay) 11 | }) 12 | -------------------------------------------------------------------------------- /tsconfig.build-es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./es6", 5 | "target": "es6", 6 | "module": "es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "noEmit": true, 5 | "outDir": "./lib", 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "lib": ["esnext", "dom"], 11 | "sourceMap": false, 12 | "declaration": true, 13 | "jsx": "react", 14 | "alwaysStrict": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true 21 | }, 22 | "include": ["./scripts", "./src", "./test", "./examples"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "space-before-function-paren": false, 5 | "no-use-before-declare": false, 6 | "variable-name": false, 7 | "quotemark": [true, "single", "jsx-double"], 8 | "ordered-imports": [ 9 | true, 10 | { 11 | "import-sources-order": "lowercase-last", 12 | "named-imports-order": "lowercase-last" 13 | } 14 | ] 15 | } 16 | } 17 | --------------------------------------------------------------------------------