1
Fork 0

Add typescript/monadic

This commit is contained in:
prescientmoon 2024-03-04 16:04:28 +01:00
commit 3b1596a730
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
23 changed files with 9599 additions and 0 deletions

View file

@ -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) |

View 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
View file

@ -0,0 +1,5 @@
*.log
.DS_Store
node_modules
dist
.cache

View file

@ -0,0 +1,5 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": true
}

View file

@ -0,0 +1,11 @@
{
"cSpell.words": [
"Typesafe",
"adriel",
"esnext",
"matei",
"pipeable",
"todos",
"tslib"
]
}

View 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.

View 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 }) => {
...
}
```

View 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>

View 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;
});

View 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"
}
}

View 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

View 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"
}
}

View 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,
{}
>;

View 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;
}
}

View 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;
};

View 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;
}
}
}

View 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);

View 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
}

View 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';

View 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();
}
}
}

View file

@ -0,0 +1,7 @@
import { sum } from '../src';
describe('blah', () => {
it('works', () => {
expect(sum(1, 1)).toEqual(2);
});
});

View 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

File diff suppressed because it is too large Load diff