Add typescript/monadic
This commit is contained in:
commit
3b1596a730
|
@ -6,3 +6,4 @@
|
||||||
| [option](./option/) | Typescript implementation of the `Maybe` monad |
|
| [option](./option/) | Typescript implementation of the `Maybe` monad |
|
||||||
| [wave38](./wave38/) | Remake of [wave37](https://github.com/Mateiadrielrafael/wave37) I dropped super early into development. |
|
| [wave38](./wave38/) | Remake of [wave37](https://github.com/Mateiadrielrafael/wave37) I dropped super early into development. |
|
||||||
| [pleix-frontend](./pleix-frontend/) | No idea what `pleix` was supposed to be, but this was essentially just a bunch of experiments with [lit-html](https://lit.dev/) |
|
| [pleix-frontend](./pleix-frontend/) | No idea what `pleix` was supposed to be, but this was essentially just a bunch of experiments with [lit-html](https://lit.dev/) |
|
||||||
|
| [monadic](./monadic) | Custom web framework inspired by [halogen](https://github.com/purescript-halogen/purescript-halogen) |
|
||||||
|
|
42
typescript/monadic/.github/workflows/main.yml
vendored
Normal file
42
typescript/monadic/.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
name: CI
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Begin CI...
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node 12
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
|
||||||
|
- name: Use cached node_modules
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: nodeModules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
nodeModules-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: yarn test --ci --coverage --maxWorkers=2
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build
|
||||||
|
env:
|
||||||
|
CI: true
|
5
typescript/monadic/.gitignore
vendored
Normal file
5
typescript/monadic/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.cache
|
5
typescript/monadic/.prettierrc
Normal file
5
typescript/monadic/.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
11
typescript/monadic/.vscode/settings.json
vendored
Normal file
11
typescript/monadic/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"Typesafe",
|
||||||
|
"adriel",
|
||||||
|
"esnext",
|
||||||
|
"matei",
|
||||||
|
"pipeable",
|
||||||
|
"todos",
|
||||||
|
"tslib"
|
||||||
|
]
|
||||||
|
}
|
21
typescript/monadic/LICENSE
Normal file
21
typescript/monadic/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Matei Adriel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
353
typescript/monadic/README.md
Normal file
353
typescript/monadic/README.md
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
# Monadic
|
||||||
|
|
||||||
|
Random functional-ish web framework thing I made for fun.
|
||||||
|
|
||||||
|
# Small tutorial
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
This library lets you render to any kind of template. For this tutorial we'll be using lit-html.
|
||||||
|
|
||||||
|
Components are created using specs. Let's create a base spec which says "render this component to a lit-html template result":
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { withTemplate } from '../../src';
|
||||||
|
import { TemplateResult } from 'lit-html';
|
||||||
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
|
|
||||||
|
// Pipe is a helper from fp-ts which is the equivalent of the pipe operator proposal as a function
|
||||||
|
const baseSpec = pipe(emptySpec, withTemplate<TemplateResult>());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
The most important thing a component has is state. To specify the the type of a component' state use the withState builder:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type State = { count: number };
|
||||||
|
|
||||||
|
const counterComponent = pipe(baseSpec, withState<State>());
|
||||||
|
```
|
||||||
|
|
||||||
|
State is modified using actions. First we need a type for the action. For our counter component our only possible actions are increasing and decreasing the state:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Action = 'increase' | 'decrease';
|
||||||
|
|
||||||
|
const counterComponent = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withState<State>(),
|
||||||
|
withAction<Action>()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building specs
|
||||||
|
|
||||||
|
To build a spec you need to use the `buildSpec` function. BuildSpec takes 3 arguments:
|
||||||
|
|
||||||
|
- The render function: takes some sate and generates a lit-html template
|
||||||
|
- The handleAction function: takes some state and an action and returns some new state
|
||||||
|
- Children: we'll cover this later. For now we'll just pass {} to it
|
||||||
|
|
||||||
|
Let's create a small render function for our counter component:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const counterComponent = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withState<State>(),
|
||||||
|
withAction<Action>(),
|
||||||
|
buildSpec(
|
||||||
|
({ count }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
Count: ${count}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(action, state) => {
|
||||||
|
// just return the state for now
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The render function also takes a second argument exposing some functions to the component. For now let's take a look at dispatch. Dispatch takes an action and calls your handleAction function replacing the component state with the return of that function.
|
||||||
|
|
||||||
|
Let's implement the basic actions for our component
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const counterComponent = pipe(
|
||||||
|
...buildSpec(
|
||||||
|
({ count }, { dispatch }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
Count: ${count}
|
||||||
|
<button @click=${() => dispatch('increase')}>Increase</button>
|
||||||
|
<button @click=${() => dispatch('decrease')}>Decrease</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(action, { count }) => {
|
||||||
|
if (action === 'increase') {
|
||||||
|
return { count: count + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count: count - 1 };
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running a component
|
||||||
|
|
||||||
|
To run a component simply use the runUi function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
runUi({
|
||||||
|
parent: document.getElementById('app') // you can use any dom element here,
|
||||||
|
render, // this is a function which takes the parent and a template and renders it. In this case we use the render function from lit-html
|
||||||
|
initialState: { count: 0 }, // this is the state your component starts out with
|
||||||
|
component: counterComponent, // the component to render
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if you run your app with something like parcel you should see a working counter!
|
||||||
|
|
||||||
|
## Children
|
||||||
|
|
||||||
|
In my library all the state is always in sync. To do that we need to explain monadic how to sync it. This is what the last argument of buildSpec if for. Let's define a simple counters component which keeps a list of counters:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CounterListState = {
|
||||||
|
counters: CounterState[]; // We renamed our previous State type to CounterState to prevent confusion
|
||||||
|
};
|
||||||
|
|
||||||
|
const counterList = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withState<CounterState>(),
|
||||||
|
buildSpec(
|
||||||
|
({ counters }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
Counters go here
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(_, state) => {
|
||||||
|
// We have no actions for now so we can just return the state
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Using components inside another components:
|
||||||
|
|
||||||
|
1. You need to explain typescript what your components looks like. To do this you need to use the withChild state builder which takes 4 arguments:
|
||||||
|
- The name of the component
|
||||||
|
- The input your state syncing function takes. In our case this will be just the index of the counter.
|
||||||
|
- The state your child component has
|
||||||
|
- The output your components raises. We'll talk about this one later
|
||||||
|
|
||||||
|
Let's add our counter component to the spec of our counterList component:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const counterList = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withState<CounterState>(),
|
||||||
|
withChild<"counter", number, CounterState, null>()
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syncing states
|
||||||
|
|
||||||
|
To sync a parent and a child state we need 2 functions:
|
||||||
|
|
||||||
|
- Get: takes the parent state and returns the child state
|
||||||
|
- Set: takes a parent and a child state and merges them
|
||||||
|
|
||||||
|
Let's see this in action by passing an actual child to the 3rd argument of buildSpec:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
buildSpec(
|
||||||
|
...,{
|
||||||
|
// This has to match the name you passed to withChild
|
||||||
|
counter: mkChild(
|
||||||
|
// We told withChild our component takes an input of type number being the index of the counter
|
||||||
|
index => ({
|
||||||
|
// Our
|
||||||
|
get: ({ counters }) => counters[index],
|
||||||
|
set: (state, newCounterState) => {
|
||||||
|
...state,
|
||||||
|
// This function just returns an array with an index modified. Try implementing it yourself :D
|
||||||
|
counters: setIndex(state.counters, index, newCounterState)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
counterComponent // This is the actual component we created earlier
|
||||||
|
// There's also an optional 3rd parameter for outputs but we'll cover that later
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can include the component in our render function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
({ counters }, { child }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
This is a counter list with ${counters.length} counters inside it!
|
||||||
|
<div>
|
||||||
|
${counters.map((_, index) =>
|
||||||
|
child(
|
||||||
|
// This is the name of the counter. It has to match what you passed to withChild
|
||||||
|
'counter',
|
||||||
|
// This is similar to key in react. It's used to keep track of the state of each counter.
|
||||||
|
// Using the index is usually bad practice
|
||||||
|
// You should probably use something like an id for it if you can
|
||||||
|
String(index),
|
||||||
|
// This is the input we pass to our sync functions. It has to match what we gave to withChild
|
||||||
|
index
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Now run your component with some initial state and you should see a counter element for each counter in the state!
|
||||||
|
|
||||||
|
> Exercise: add an input element and a button allowing the creation of new counters
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Outputs are like events. Let's say we want counters to have a "delete" button. To do that we need to allow the counter component to send messages to the counterList component. Let's start by defining the type of our messages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CounterOutput = 'delete';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then let's edit the counter component to emit the output on click. For this the `raise` function is used:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const counterComponent = pipe(
|
||||||
|
...,
|
||||||
|
withOutput<CounterOutput>(),
|
||||||
|
buildSpec(
|
||||||
|
({ count }, { dispatch, raise }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<button @click=${() => raise("delete")}> Delete this counter </button>
|
||||||
|
Count: ${count}
|
||||||
|
<button @click=${() => dispatch('increase')}>Increase</button>
|
||||||
|
<button @click=${() => dispatch('decrease')}>Decrease</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
...
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
And now we need to update the `withChild` builder from the list component to reflect the changes:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
...
|
||||||
|
withChild<"counter", number, CounterState, CounterOutput>()
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we need to actually handle the outputs. You don't always want to do something with an output, so monadic allows you do discard any output you want. To handle the output -> action relation `mkChild` accepts an extra argument which takes an output and returns:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Option<Action>
|
||||||
|
```
|
||||||
|
|
||||||
|
where Option is the Option type from fp-ts.
|
||||||
|
|
||||||
|
Let's create a new action in our list component which deletes a counter based on it's index:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CounterAction = {
|
||||||
|
label: 'delete';
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const counterList = pipe(
|
||||||
|
...,
|
||||||
|
withAction<CounterAction>(),
|
||||||
|
buildSpec(
|
||||||
|
...,
|
||||||
|
(action, state) => {
|
||||||
|
if (action.label === 'delete') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
counters: state.counters.filter((_, index) => index !== action.index),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
And now let's define the function which handles the outputs:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
buildSpec(
|
||||||
|
...,{
|
||||||
|
counter: mkChild(
|
||||||
|
...,
|
||||||
|
(index, output) => output === "delete" ? O.some({ label: "delete", index }) : O.none
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
And voila! Now you should be able to delete any counter!
|
||||||
|
|
||||||
|
## Listening to outputs on root:
|
||||||
|
|
||||||
|
Our counter list doesn't have any outputs currently, but let's assume it had the following set of outputs:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CounterListOutputs =
|
||||||
|
| { label: 'foo'; value: number }
|
||||||
|
| { label: 'bar'; value: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
We can listen to outputs on our root component with a simple for loop:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const main = async () => {
|
||||||
|
// runUi returns an async iterator
|
||||||
|
const output = runUi(...);
|
||||||
|
|
||||||
|
for await (const event of output) {
|
||||||
|
if (event.label === "foo") {
|
||||||
|
console.log(`Received a foo event with a number payload of ${event.value}`)
|
||||||
|
}
|
||||||
|
else if (event.label === "bar") {
|
||||||
|
console.log(`Received a bar event with a string payload of ${event.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
// Handle errors
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Closing notes:
|
||||||
|
|
||||||
|
You access `dispatch` and `raise` inside your action handler as well:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
(action, state, { dispatch, raise }) => {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
12
typescript/monadic/demos/basic/index.html
Normal file
12
typescript/monadic/demos/basic/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Monadic-ui basic</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="./main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
192
typescript/monadic/demos/basic/main.ts
Normal file
192
typescript/monadic/demos/basic/main.ts
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import {
|
||||||
|
runUi,
|
||||||
|
withAction,
|
||||||
|
mkChild,
|
||||||
|
buildSpec,
|
||||||
|
withState,
|
||||||
|
emptySpec,
|
||||||
|
withTemplate,
|
||||||
|
withChild,
|
||||||
|
withOutput,
|
||||||
|
} from '../../src';
|
||||||
|
import { render, html, TemplateResult } from 'lit-html';
|
||||||
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
|
import { constant } from 'fp-ts/es6/function';
|
||||||
|
import * as O from 'fp-ts/es6/Option';
|
||||||
|
|
||||||
|
const baseSpec = pipe(emptySpec, withTemplate<TemplateResult>());
|
||||||
|
|
||||||
|
// Todo component
|
||||||
|
type TodoState = {
|
||||||
|
name: string;
|
||||||
|
done: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TodoAction = 'complete';
|
||||||
|
|
||||||
|
type TodoOutput = 'delete';
|
||||||
|
|
||||||
|
const todo = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withAction<TodoAction>(),
|
||||||
|
withState<TodoState>(),
|
||||||
|
withOutput<TodoOutput>(),
|
||||||
|
buildSpec(
|
||||||
|
({ name, done }, { dispatch, raise }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
Name: ${name} Completed: ${done}
|
||||||
|
<button @click=${() => dispatch('complete')}>Complete todo</button>
|
||||||
|
<button @click=${() => raise('delete')}>Delete todo</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(action, state) => {
|
||||||
|
if (action === 'complete') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Todo list input component
|
||||||
|
type TodoListInputAction =
|
||||||
|
| {
|
||||||
|
label: 'setInput';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| 'create';
|
||||||
|
|
||||||
|
type TodoListInputOutput = 'createTodo';
|
||||||
|
|
||||||
|
const todoListInput = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withAction<TodoListInputAction>(),
|
||||||
|
withState<string>(),
|
||||||
|
withOutput<TodoListInputOutput>(),
|
||||||
|
buildSpec(
|
||||||
|
(value, { dispatch }) => {
|
||||||
|
return html`
|
||||||
|
<input
|
||||||
|
value=${value}
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
dispatch({ label: 'setInput', value: target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button @click=${() => dispatch('create')}>Add todo</button>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(action, state, { raise }) => {
|
||||||
|
if (action === 'create') {
|
||||||
|
raise('createTodo');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return action.value;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Todo list component
|
||||||
|
type TodoListState = {
|
||||||
|
todos: TodoState[];
|
||||||
|
inputValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TodoListAction = 'createTodo' | { label: 'deleteTodo'; index: number };
|
||||||
|
|
||||||
|
const todoList = pipe(
|
||||||
|
baseSpec,
|
||||||
|
withAction<TodoListAction>(),
|
||||||
|
withState<TodoListState>(),
|
||||||
|
withChild<'todo', number, TodoState, null>(),
|
||||||
|
withChild<'input', null, string, TodoListInputOutput>(),
|
||||||
|
buildSpec(
|
||||||
|
(state, { child }) => {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
${child('input', 'input', null)}
|
||||||
|
<div>
|
||||||
|
${state.todos.map((_, index) =>
|
||||||
|
child('todo', String(index), index)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
(action, state) => {
|
||||||
|
if (action === 'createTodo') {
|
||||||
|
return {
|
||||||
|
inputValue: '',
|
||||||
|
todos: [
|
||||||
|
...state.todos,
|
||||||
|
{
|
||||||
|
name: state.inputValue,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (action.label === 'deleteTodo') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
todos: state.todos.filter((_, index) => index !== action.index),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
todo: mkChild(
|
||||||
|
index => ({
|
||||||
|
get: ({ todos }) => todos[index],
|
||||||
|
set: (state, newTodoState) => ({
|
||||||
|
...state,
|
||||||
|
todos: state.todos.map((todoState, currentIndex) =>
|
||||||
|
index === currentIndex ? newTodoState : todoState
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
todo,
|
||||||
|
(index, output) =>
|
||||||
|
output === 'delete'
|
||||||
|
? O.some({ label: 'deleteTodo', index } as TodoListAction)
|
||||||
|
: O.none
|
||||||
|
),
|
||||||
|
input: mkChild(
|
||||||
|
constant({
|
||||||
|
get: ({ inputValue }) => inputValue,
|
||||||
|
set: (state, newInputValue) => ({
|
||||||
|
...state,
|
||||||
|
inputValue: newInputValue,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
todoListInput,
|
||||||
|
constant(O.some('createTodo' as TodoListAction))
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const output = runUi({
|
||||||
|
parent: document.getElementById('app'),
|
||||||
|
render,
|
||||||
|
initialState: { todos: [], inputValue: '' },
|
||||||
|
component: todoList,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const event of output) {
|
||||||
|
console.log('Received an event from the todo list component!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.log('An error ocurred while running the root component');
|
||||||
|
throw err;
|
||||||
|
});
|
14
typescript/monadic/demos/basic/package.json
Normal file
14
typescript/monadic/demos/basic/package.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "basic",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Matei Adriel",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"lit-html": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
10
typescript/monadic/demos/basic/pnpm-lock.yaml
Normal file
10
typescript/monadic/demos/basic/pnpm-lock.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
dependencies:
|
||||||
|
lit-html: 1.2.1
|
||||||
|
lockfileVersion: 5.1
|
||||||
|
packages:
|
||||||
|
/lit-html/1.2.1:
|
||||||
|
dev: false
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-GSJHHXMGLZDzTRq59IUfL9FCdAlGfqNp/dEa7k7aBaaWD+JKaCjsAk9KYm2V12ItonVaYx2dprN66Zdm1AuBTQ==
|
||||||
|
specifiers:
|
||||||
|
lit-html: ^1.2.1
|
46
typescript/monadic/package.json
Normal file
46
typescript/monadic/package.json
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsdx watch",
|
||||||
|
"build": "tsdx build",
|
||||||
|
"test": "tsdx test",
|
||||||
|
"lint": "tsdx lint",
|
||||||
|
"prepare": "tsdx build"
|
||||||
|
},
|
||||||
|
"peerDependencies": {},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "tsdx lint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
},
|
||||||
|
"name": "monadic",
|
||||||
|
"author": "Matei Adriel",
|
||||||
|
"module": "dist/monadic.esm.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"husky": "^4.2.5",
|
||||||
|
"parcel": "^1.12.4",
|
||||||
|
"tsdx": "^0.13.2",
|
||||||
|
"tslib": "^1.11.1",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fp-ts": "^2.6.0",
|
||||||
|
"helpers": "^0.0.6"
|
||||||
|
}
|
||||||
|
}
|
165
typescript/monadic/src/Builder.ts
Normal file
165
typescript/monadic/src/Builder.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import {
|
||||||
|
ComponentConfig,
|
||||||
|
ChildrenConfigs,
|
||||||
|
ChildTemplate,
|
||||||
|
RenderFunction,
|
||||||
|
Child,
|
||||||
|
} from './Component';
|
||||||
|
import { GenericLens } from './helpers';
|
||||||
|
import { constant, identity, flow } from 'fp-ts/es6/function';
|
||||||
|
import * as O from 'fp-ts/es6/Option';
|
||||||
|
|
||||||
|
// This is here since all the builders are pretty much this
|
||||||
|
const alwaysIdentity = constant(identity);
|
||||||
|
|
||||||
|
type ComponentBuilder = <
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
render: ComponentConfig<T, S, A, O, N, C>['render'],
|
||||||
|
handleAction: ComponentConfig<T, S, A, O, N, C>['handleAction'],
|
||||||
|
children: ComponentConfig<T, S, A, O, N, C>['children']
|
||||||
|
) => ComponentConfig<T, S, A, O, N, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a component
|
||||||
|
*
|
||||||
|
* @param render The render function of the component
|
||||||
|
* @param handleAction Function used to handle actions related to the component.
|
||||||
|
* @param children Child slots for the component
|
||||||
|
*/
|
||||||
|
export const makeComponent: ComponentBuilder = (
|
||||||
|
render,
|
||||||
|
handleAction,
|
||||||
|
children
|
||||||
|
) => ({ render, handleAction, children });
|
||||||
|
|
||||||
|
type ChildBuilder = <T, PS, PA, S, I, O>(
|
||||||
|
lens: (input: I) => GenericLens<PS, S>,
|
||||||
|
component: ComponentConfig<T, S, unknown, O, string, {}>,
|
||||||
|
handleOutput?: (input: I, output: O) => O.Option<PA>
|
||||||
|
) => ChildTemplate<T, PS, PA, S, I, O>;
|
||||||
|
|
||||||
|
export const mkChild: ChildBuilder = (
|
||||||
|
lens,
|
||||||
|
component,
|
||||||
|
handleOutput = constant(O.none)
|
||||||
|
) => ({ lens, component, handleOutput });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure which keeps track of the different types needed to build a component.
|
||||||
|
*/
|
||||||
|
export type ComponentSpec<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = [T, S, A, O, N, C];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a child to a spec (type only, the implementation is given when building the spec)
|
||||||
|
*/
|
||||||
|
export const withChild = alwaysIdentity as <
|
||||||
|
N1 extends keyof any,
|
||||||
|
I,
|
||||||
|
S,
|
||||||
|
O
|
||||||
|
>() => <
|
||||||
|
T,
|
||||||
|
PS,
|
||||||
|
A,
|
||||||
|
PO,
|
||||||
|
N0 extends Exclude<string, N1>,
|
||||||
|
C extends ChildrenConfigs<N0>
|
||||||
|
>(
|
||||||
|
spec: ComponentSpec<T, PS, A, PO, N0, C>
|
||||||
|
) => ComponentSpec<T, PS, A, PO, N1 & N0, C & Record<N1, Child<I, S, O>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the action type of a spec
|
||||||
|
*/
|
||||||
|
export const withAction = alwaysIdentity as <A1>() => <
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A0,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
spec: ComponentSpec<T, S, A0, O, N, C>
|
||||||
|
) => ComponentSpec<T, S, A1, O, N, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the output type of a spec
|
||||||
|
*/
|
||||||
|
export const withOutput = alwaysIdentity as <O1>() => <
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O0,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
spec: ComponentSpec<T, S, A, O0, N, C>
|
||||||
|
) => ComponentSpec<T, S, A, O1, N, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the type of state in a spec
|
||||||
|
*/
|
||||||
|
export const withState = alwaysIdentity as <S1>() => <
|
||||||
|
T,
|
||||||
|
S0,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
spec: ComponentSpec<T, S0, A, O, N, C>
|
||||||
|
) => ComponentSpec<T, S1, A, O, N, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify what a component renders to
|
||||||
|
*/
|
||||||
|
export const withTemplate = alwaysIdentity as <T1>() => <
|
||||||
|
T0,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
spec: ComponentSpec<T0, S, A, O, N, C>
|
||||||
|
) => ComponentSpec<T1, S, A, O, N, C>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes implementations of the components watching the spec and creates the component
|
||||||
|
*/
|
||||||
|
export const buildSpec = flow(makeComponent, constant) as <
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
>(
|
||||||
|
render: RenderFunction<T, S, A, O, N, C>,
|
||||||
|
handleAction: ComponentConfig<T, S, A, O, N, C>['handleAction'],
|
||||||
|
children: ComponentConfig<T, S, A, O, N, C>['children']
|
||||||
|
) => (
|
||||||
|
spec: ComponentSpec<T, S, A, O, N, C>
|
||||||
|
) => ComponentConfig<T, S, A, O, N, C>;
|
||||||
|
|
||||||
|
export const emptySpec = (null as any) as ComponentSpec<
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
null,
|
||||||
|
string,
|
||||||
|
{}
|
||||||
|
>;
|
204
typescript/monadic/src/Component.ts
Normal file
204
typescript/monadic/src/Component.ts
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import { mapRecord } from './Record';
|
||||||
|
import { values, GenericLens, areEqual } from './helpers';
|
||||||
|
import * as O from 'fp-ts/es6/Option';
|
||||||
|
import { constant, flow } from 'fp-ts/es6/function';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type for dispatcher functions
|
||||||
|
*/
|
||||||
|
export type Dispatcher<A> = (action: A) => void;
|
||||||
|
|
||||||
|
export type Communicate<A, O> = {
|
||||||
|
dispatch: Dispatcher<A>;
|
||||||
|
raise: (event: O) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanAddChildren<
|
||||||
|
T,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = {
|
||||||
|
child: <K extends N>(key: K, name: string, input: C[K]['input']) => T;
|
||||||
|
};
|
||||||
|
export type RenderFunction<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = (state: S, communicate: Communicate<A, O> & CanAddChildren<T, N, C>) => T;
|
||||||
|
|
||||||
|
export type ComponentConfig<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = {
|
||||||
|
render: RenderFunction<T, S, A, O, N, C>;
|
||||||
|
handleAction: (action: A, state: S, communication: Communicate<A, O>) => S;
|
||||||
|
children: ChildrenTemplates<T, S, A, N, C>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Child<I = unknown, S = unknown, O = unknown> = {
|
||||||
|
input: I;
|
||||||
|
state: S;
|
||||||
|
output: O;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChildrenConfigs<N extends string> = Record<N, Child>;
|
||||||
|
|
||||||
|
export type ChildTemplate<T, PS, PA, S, I, O> = {
|
||||||
|
lens: (input: I) => GenericLens<PS, S>;
|
||||||
|
component: ComponentConfig<T, S, unknown, O, string, {}>;
|
||||||
|
handleOutput: (input: I, output: O) => O.Option<PA>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChildrenTemplates<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> = {
|
||||||
|
[K in N]: ChildTemplate<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
C[K]['state'],
|
||||||
|
C[K]['input'],
|
||||||
|
C[K]['output']
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Children<T, S, N extends string, C extends ChildrenConfigs<N>> = {
|
||||||
|
[K in N]: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
component: Component<
|
||||||
|
T,
|
||||||
|
C[K]['state'],
|
||||||
|
unknown,
|
||||||
|
C[K]['output'],
|
||||||
|
string,
|
||||||
|
{}
|
||||||
|
>;
|
||||||
|
lens: GenericLens<S, C[K]['state']>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Component<
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
A,
|
||||||
|
O,
|
||||||
|
N extends string,
|
||||||
|
C extends ChildrenConfigs<N>
|
||||||
|
> {
|
||||||
|
public childrenMap: Children<T, S, N, C>;
|
||||||
|
public constructor(
|
||||||
|
protected state: S,
|
||||||
|
private config: ComponentConfig<T, S, A, O, N, C>,
|
||||||
|
private raise: Communicate<A, O>['raise'],
|
||||||
|
private pushDownwards: (state: S) => void
|
||||||
|
) {
|
||||||
|
this.childrenMap = mapRecord(this.config.children, constant({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do something only if the state changed.
|
||||||
|
*
|
||||||
|
* @param state The new state.
|
||||||
|
* @param then The thing you want to do with the state
|
||||||
|
*/
|
||||||
|
private setStateAndThen = (then: (state: S) => unknown) => (state: S) => {
|
||||||
|
if (areEqual(state, this.state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
|
||||||
|
then(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagate a new state upstream the tree
|
||||||
|
*
|
||||||
|
* @param state The new state
|
||||||
|
*/
|
||||||
|
protected pushStateUpwards = this.setStateAndThen(state => {
|
||||||
|
for (const { component, lens } of this.children()) {
|
||||||
|
component.pushStateUpwards(lens.get(state));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get a child or create it if it doesn't exist
|
||||||
|
*/
|
||||||
|
private getChild: CanAddChildren<T, N, C> = {
|
||||||
|
child: (key, name, input) => {
|
||||||
|
const hasName = Reflect.has(this.childrenMap[key], name);
|
||||||
|
|
||||||
|
if (!hasName) {
|
||||||
|
const config = this.config.children[key];
|
||||||
|
const lens = config.lens(input);
|
||||||
|
const raise = flow(config.handleOutput, O.map(this.dispatch));
|
||||||
|
const child = this.childrenMap[key] as Record<string, any>;
|
||||||
|
|
||||||
|
child[name] = {
|
||||||
|
lens,
|
||||||
|
component: new Component(
|
||||||
|
lens.get(this.state),
|
||||||
|
this.config.children[key].component,
|
||||||
|
event => raise(input, event),
|
||||||
|
childState => {
|
||||||
|
const newState = lens.set(this.state, childState);
|
||||||
|
|
||||||
|
this.setStateAndThen(this.pushDownwards)(newState);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.childrenMap[key][name].component.getTemplate();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCommunication(): Communicate<A, O> {
|
||||||
|
return {
|
||||||
|
dispatch: value => this.dispatch(value),
|
||||||
|
raise: this.raise,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTemplate(): T {
|
||||||
|
return this.config.render(this.state, {
|
||||||
|
...this.getCommunication(),
|
||||||
|
...this.getChild,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an arbitrary action on the component.
|
||||||
|
*/
|
||||||
|
public dispatch = (action: A) => {
|
||||||
|
const newState = this.config.handleAction(
|
||||||
|
action,
|
||||||
|
this.state,
|
||||||
|
this.getCommunication()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pushStateUpwards(newState);
|
||||||
|
this.pushDownwards(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of all the children of the component
|
||||||
|
*/
|
||||||
|
private children(): this['childrenMap'][N][string][] {
|
||||||
|
return values(this.childrenMap).flatMap(record => values(record)) as any;
|
||||||
|
}
|
||||||
|
}
|
11
typescript/monadic/src/Record.ts
Normal file
11
typescript/monadic/src/Record.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const mapRecord = <K extends string | number, V, W>(
|
||||||
|
record: Record<K, V>,
|
||||||
|
mapper: (v: V, k: K) => W
|
||||||
|
): Record<K, W> => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(record).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
mapper(value as V, key as K),
|
||||||
|
])
|
||||||
|
) as any;
|
||||||
|
};
|
36
typescript/monadic/src/environment.ts
Normal file
36
typescript/monadic/src/environment.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { ComponentConfig, Component } from './Component';
|
||||||
|
import { IterableEmitter } from './iterableEmitter';
|
||||||
|
|
||||||
|
export type EnvConfig<T, S, A, O> = {
|
||||||
|
render: (template: T, parent: HTMLElement) => void;
|
||||||
|
parent: HTMLElement;
|
||||||
|
component: ComponentConfig<T, S, A, O, string, {}>;
|
||||||
|
initialState: S;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not an arrow function cause it's a generator
|
||||||
|
/**
|
||||||
|
* Run a component and render it to the dom
|
||||||
|
*/
|
||||||
|
export async function* runUi<T, S, A, O>(config: EnvConfig<T, S, A, O>) {
|
||||||
|
const reRender = () => config.render(component.getTemplate(), config.parent);
|
||||||
|
|
||||||
|
const outputEmitter = new IterableEmitter<null | O>(null);
|
||||||
|
|
||||||
|
const component = new Component(
|
||||||
|
config.initialState,
|
||||||
|
config.component,
|
||||||
|
outputEmitter.next,
|
||||||
|
_ => {
|
||||||
|
reRender();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
reRender();
|
||||||
|
|
||||||
|
for await (const event of outputEmitter) {
|
||||||
|
if (event) {
|
||||||
|
yield event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
typescript/monadic/src/helpers.ts
Normal file
20
typescript/monadic/src/helpers.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Typesafe version of Object.values
|
||||||
|
*/
|
||||||
|
export const values = Object.values as <T extends keyof object, U>(
|
||||||
|
object: Record<T, U>
|
||||||
|
) => U[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library agnostic way of expressing a lens
|
||||||
|
*/
|
||||||
|
export type GenericLens<T, U> = {
|
||||||
|
get: (v: T) => U;
|
||||||
|
set: (v: T, n: U) => T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some basic equality to not re-render with the same state
|
||||||
|
*/
|
||||||
|
export const areEqual = <T>(a: T, b: T) =>
|
||||||
|
a === b || JSON.stringify(a) === JSON.stringify(b);
|
110
typescript/monadic/src/hkt.ts
Normal file
110
typescript/monadic/src/hkt.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
declare const index: unique symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder representing an indexed type variable.
|
||||||
|
*/
|
||||||
|
export interface _<N extends number = 0> {
|
||||||
|
[index]: N;
|
||||||
|
}
|
||||||
|
export type _0 = _<0>;
|
||||||
|
export type _1 = _<1>;
|
||||||
|
export type _2 = _<2>;
|
||||||
|
export type _3 = _<3>;
|
||||||
|
export type _4 = _<4>;
|
||||||
|
export type _5 = _<5>;
|
||||||
|
export type _6 = _<6>;
|
||||||
|
export type _7 = _<7>;
|
||||||
|
export type _8 = _<8>;
|
||||||
|
export type _9 = _<9>;
|
||||||
|
|
||||||
|
declare const fixed: unique symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a type to be ignored by the application operator `$`. This is used to protect
|
||||||
|
* bound type parameters.
|
||||||
|
*/
|
||||||
|
export interface Fixed<T> {
|
||||||
|
[fixed]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type application (simultaneously substitutes all placeholders within the target type)
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
export type $<T, S extends any[]> = (
|
||||||
|
T extends Fixed<infer U> ? { [indirect]: U } :
|
||||||
|
T extends _<infer N> ? { [indirect]: S[N] } :
|
||||||
|
T extends undefined | null | boolean | string | number ? { [indirect]: T } :
|
||||||
|
T extends (infer A)[] & { length: infer L } ? {
|
||||||
|
[indirect]: L extends keyof TupleTable
|
||||||
|
? TupleTable<T, S>[L]
|
||||||
|
: $<A, S>[]
|
||||||
|
} :
|
||||||
|
T extends (...x: infer I) => infer O ? { [indirect]: (...x: $<I, S>) => $<O, S> } :
|
||||||
|
T extends object ? { [indirect]: { [K in keyof T]: $<T[K], S> } } :
|
||||||
|
{ [indirect]: T }
|
||||||
|
)[typeof indirect];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as a level of indirection to avoid circularity errors.
|
||||||
|
*/
|
||||||
|
declare const indirect: unique symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows looking up the type for a tuple based on its `length`, instead of trying
|
||||||
|
* each possibility one by one in a single long conditional.
|
||||||
|
*/
|
||||||
|
// prettier-ignore
|
||||||
|
type TupleTable<T extends any[] = any, S extends any[] = any> = {
|
||||||
|
0: [];
|
||||||
|
1: T extends [
|
||||||
|
infer A0
|
||||||
|
] ? [
|
||||||
|
$<A0, S>
|
||||||
|
] : never
|
||||||
|
2: T extends [
|
||||||
|
infer A0, infer A1
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>
|
||||||
|
] : never
|
||||||
|
3: T extends [
|
||||||
|
infer A0, infer A1, infer A2
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>
|
||||||
|
] : never
|
||||||
|
4: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>
|
||||||
|
] : never
|
||||||
|
5: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>
|
||||||
|
] : never
|
||||||
|
6: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4, infer A5
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>, $<A5, S>
|
||||||
|
] : never
|
||||||
|
7: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>, $<A5, S>, $<A6, S>
|
||||||
|
] : never
|
||||||
|
8: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>, $<A5, S>, $<A6, S>, $<A7, S>
|
||||||
|
] : never
|
||||||
|
9: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>, $<A5, S>, $<A6, S>, $<A7, S>, $<A8, S>
|
||||||
|
] : never
|
||||||
|
10: T extends [
|
||||||
|
infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, infer A9
|
||||||
|
] ? [
|
||||||
|
$<A0, S>, $<A1, S>, $<A2, S>, $<A3, S>, $<A4, S>, $<A5, S>, $<A6, S>, $<A7, S>, $<A8, S>, $<A9, S>
|
||||||
|
] : never
|
||||||
|
}
|
15
typescript/monadic/src/index.ts
Normal file
15
typescript/monadic/src/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export { runUi, EnvConfig } from './environment';
|
||||||
|
export { ComponentConfig, Dispatcher } from './Component';
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeComponent,
|
||||||
|
mkChild,
|
||||||
|
ComponentSpec,
|
||||||
|
withAction,
|
||||||
|
emptySpec,
|
||||||
|
withState,
|
||||||
|
buildSpec,
|
||||||
|
withTemplate,
|
||||||
|
withChild,
|
||||||
|
withOutput,
|
||||||
|
} from './Builder';
|
38
typescript/monadic/src/iterableEmitter.ts
Normal file
38
typescript/monadic/src/iterableEmitter.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// An event emitter which can be used
|
||||||
|
// with for of loops
|
||||||
|
export class IterableEmitter<T> {
|
||||||
|
// The generation of the emitter
|
||||||
|
// Used to avoid trying to resolve a promise twice
|
||||||
|
private generation = 0;
|
||||||
|
|
||||||
|
// Set the state
|
||||||
|
public next = (_: T) => {};
|
||||||
|
|
||||||
|
public constructor(private state: T) {}
|
||||||
|
|
||||||
|
public alter(mapper: (f: T) => T) {
|
||||||
|
this.next(mapper(this.state));
|
||||||
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
||||||
|
const createPromise = () =>
|
||||||
|
new Promise<T>(resolve => {
|
||||||
|
const generation = this.generation;
|
||||||
|
|
||||||
|
this.next = (value: T) => {
|
||||||
|
if (generation !== this.generation) {
|
||||||
|
throw new Error('Cannot resolve the same generation twice');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generation++;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
yield this.state;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
yield createPromise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
typescript/monadic/test/blah.test.ts
Normal file
7
typescript/monadic/test/blah.test.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { sum } from '../src';
|
||||||
|
|
||||||
|
describe('blah', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(sum(1, 1)).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
23
typescript/monadic/tsconfig.json
Normal file
23
typescript/monadic/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"include": ["src", "types"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"importHelpers": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"*": ["src/*", "node_modules/*"]
|
||||||
|
},
|
||||||
|
"jsx": "react",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
8258
typescript/monadic/yarn.lock
Normal file
8258
typescript/monadic/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue