diff --git a/core/package.json b/core/package.json index 0fea9f1..15f5f5e 100644 --- a/core/package.json +++ b/core/package.json @@ -15,6 +15,7 @@ "test": "jest", "test-watch": "jest --watch", "prettify": "prettier --write .", + "tsc": "tsc", "compile": "rimraf dist && tsc" } } diff --git a/core/src/TeaCup/ObjectSerializer.ts b/core/src/TeaCup/ObjectSerializer.ts index dcd5dca..dda1973 100644 --- a/core/src/TeaCup/ObjectSerializer.ts +++ b/core/src/TeaCup/ObjectSerializer.ts @@ -1,3 +1,28 @@ +/* + * MIT License + * + * Copyright (c) 2019 Rémi Van Keisbelck + * + * 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. + * + */ + import { Dict } from './Dict'; import { Tuple } from './Tuple'; import { Just, Maybe, Nothing } from './Maybe'; diff --git a/core/src/TeaCup/Port.ts b/core/src/TeaCup/Port.ts new file mode 100644 index 0000000..736e8e3 --- /dev/null +++ b/core/src/TeaCup/Port.ts @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2019 Rémi Van Keisbelck + * + * 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. + * + */ + +import {Sub} from "./Sub"; + +export class Port { + private subs: PortSub[] = []; + + send(t: T): void { + this.subs.forEach((s) => s.notify(t)); + } + + subscribe(f: (t: T) => M): Sub { + return new PortSub( + f, + (p) => this.subs.push(p), + (p) => { + this.subs = this.subs.filter((x) => x !== p); + }, + ); + } +} + +class PortSub extends Sub { + constructor( + private readonly f: (t: T) => M, + private readonly _onInit: (p: PortSub) => void, + private readonly _onRelease: (p: PortSub) => void, + ) { + super(); + } + + protected onInit() { + this._onInit(this); + } + + protected onRelease() { + this._onRelease(this); + } + + notify(t: T): void { + this.dispatch(this.f(t)); + } +} diff --git a/core/src/TeaCup/index.ts b/core/src/TeaCup/index.ts index 923a89c..81b77a4 100644 --- a/core/src/TeaCup/index.ts +++ b/core/src/TeaCup/index.ts @@ -42,3 +42,4 @@ export * from './ListWithSelection'; export * from './ObjectSerializer'; export * from './Try'; export * from './UUID'; +export * from './Port'; diff --git a/samples/src/App.tsx b/samples/src/App.tsx index d3f1a2f..d42dd20 100755 --- a/samples/src/App.tsx +++ b/samples/src/App.tsx @@ -55,6 +55,8 @@ import * as ClassMsgs from './Samples/ClassMsgs'; import * as Sful from './Samples/StatefulInView'; import * as Rest from './Samples/Rest'; import * as TimeSample from './Samples/TimeSample'; +import * as PortsSample from './Samples/PortsSample'; +import {appSamplePorts} from "./Samples/PortsSample"; enum Tab { All, @@ -160,6 +162,7 @@ interface Samples { readonly sful: Sful.Model; readonly rest: Rest.Model; readonly time: TimeSample.Model; + readonly ports: PortsSample.Model; } type Msg = @@ -172,6 +175,7 @@ type Msg = | { type: 'sful'; child: Sful.Msg } | { type: 'rest'; child: Rest.Msg } | { type: 'timeSample'; child: TimeSample.Msg } + | { type: 'portsSample'; child: PortsSample.Msg } | { type: 'urlChange'; location: Location } | { type: 'newUrl'; url: string } | { type: 'noop' } @@ -189,6 +193,7 @@ function initSamples(): [Model, Cmd] { const sful = Sful.init(); const rest = Rest.init(); const time = TimeSample.init(); + const ports = PortsSample.init(); return [ { tag: 'samples', @@ -202,6 +207,7 @@ function initSamples(): [Model, Cmd] { sful: sful[0], rest: rest[0], time: time[0], + ports: ports[0], }, }, Cmd.batch([ @@ -214,6 +220,7 @@ function initSamples(): [Model, Cmd] { sful[1].map(mapSful), rest[1].map(mapRest), time[1].map(mapTimeSample), + ports[1].map(mapPortsSample), ]), ]; } @@ -328,6 +335,13 @@ function mapTimeSample(m: TimeSample.Msg): Msg { }; } +function mapPortsSample(m: PortsSample.Msg): Msg { + return { + type: 'portsSample', + child: m, + } +} + function view(dispatch: Dispatcher, model: Model) { switch (model.tag) { case 'home': @@ -545,6 +559,12 @@ function viewSamples(dispatch: Dispatcher, samples: Samples) { {Rest.view(map(dispatch, mapRest), samples.rest)}

Time

{TimeSample.view(map(dispatch, mapTimeSample), samples.time)} +

Ports

+ {PortsSample.view(map(dispatch, mapPortsSample), samples.ports)} + ); } @@ -603,12 +623,16 @@ function update(msg: Msg, model: Model): [Model, Cmd] { const macRest = Rest.update(msg.child, s.rest); return [{ ...s, rest: macRest[0] }, macRest[1].map(mapRest)]; }); - case 'timeSample': return mapSample((s: Samples) => { const macTime = TimeSample.update(msg.child, s.time); return [{ ...s, time: macTime[0] }, macTime[1].map(mapTimeSample)]; }); + case "portsSample": + return mapSample((s: Samples) => { + const mac = PortsSample.update(msg.child, s.ports); + return [{ ...s, ports: mac[0]}, mac[1].map(mapPortsSample)]; + }); case 'urlChange': return init(msg.location); @@ -631,6 +655,7 @@ function subscriptions(model: Model): Sub { ParentChild.subscriptions(parentChild).map(mapParentChild), Raf.subscriptions(raf).map(mapRaf), TimeSample.subscriptions(time).map(mapTimeSample), + PortsSample.subscriptions().map(mapPortsSample), ]); default: return Sub.none(); diff --git a/samples/src/Samples/PortsSample.tsx b/samples/src/Samples/PortsSample.tsx new file mode 100644 index 0000000..379ea0f --- /dev/null +++ b/samples/src/Samples/PortsSample.tsx @@ -0,0 +1,77 @@ +/* + * MIT License + * + * Copyright (c) 2019 Rémi Van Keisbelck + * + * 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. + * + */ + +import * as React from 'react'; +import { Cmd, Dispatcher, noCmd, Port, Sub } from 'tea-cup-core'; + +export type Model = number; + +export type Msg = { tag: 'inc' } | { tag: 'set-value'; value: number }; + +function onSetValue(value: number): Msg { + return { + tag: 'set-value', + value, + }; +} + +// the ports allow to send Msgs to the update loop from the outside +export const appSamplePorts = { + setCounter: new Port(), +}; + +// export as a global to play in dev tools +// @ts-ignore +window['appSamplePorts'] = appSamplePorts; + +export function init(): [Model, Cmd] { + return noCmd(0); +} + +export function view(dispatch: Dispatcher, model: Model) { + return ( +
+

Counter with ports

+ Value = {model} + +
+ ); +} + +export function update(msg: Msg, model: Model): [Model, Cmd] { + switch (msg.tag) { + case 'inc': { + return noCmd(model + 1); + } + case 'set-value': { + return noCmd(msg.value); + } + } +} + +export function subscriptions(): Sub { + // wire ports to get Msgs into our update func + return appSamplePorts.setCounter.subscribe(onSetValue); +} diff --git a/tea-cup/package.json b/tea-cup/package.json index f9a16de..713bc05 100644 --- a/tea-cup/package.json +++ b/tea-cup/package.json @@ -15,6 +15,7 @@ "test": "jest", "test-watch": "jest --watch", "prettify": "prettier --write .", + "tsc": "tsc", "compile": "rimraf dist && tsc", "samples": "tsc --outFile ./dist/Samples/index.js && cp ./src/Samples/index.html ./dist/Samples" }, diff --git a/tea-cup/src/TeaCup/Program.ts b/tea-cup/src/TeaCup/Program.ts index 946eeb0..da0fd4f 100644 --- a/tea-cup/src/TeaCup/Program.ts +++ b/tea-cup/src/TeaCup/Program.ts @@ -45,44 +45,24 @@ export interface ProgramProps { export class Program extends Component, never> { readonly uuid = nextUuid(); private readonly bd: Dispatcher; - private readonly devTools?: DevTools; private count: number = 0; - private readonly initialCmd: Cmd; - private currentModel: Model; - private currentSub: Sub; + private initialCmd?: Cmd; + private currentModel?: Model; + private currentSub?: Sub; constructor(props: Readonly>) { super(props); - this.devTools = this.props.devTools; - if (this.devTools) { - this.devTools.connected(this); - } - const mac = (this.devTools && this.devTools.initFromSnapshot()) || props.init(); - if (this.devTools) { - this.fireEvent({ - tag: 'init', - time: new Date().getTime(), - model: mac[0], - }); - } - const sub = props.subscriptions(mac[0]); - this.currentModel = mac[0]; - this.currentSub = sub; - // connect to sub - const d = this.dispatch.bind(this); - this.bd = d; - sub.init(d); - this.initialCmd = mac[1]; + this.bd = this.dispatch.bind(this); } private fireEvent(e: DevToolsEvent) { - if (this.devTools) { - this.devTools.onEvent(e); + if (this.props.devTools) { + this.props.devTools.onEvent(e); } } dispatch(msg: Msg) { - if (this.devTools && this.devTools.isPaused()) { + if (this.props.devTools && this.props.devTools.isPaused()) { // do not process messages if we are paused return; } @@ -90,62 +70,93 @@ export class Program extends Component, nev this.count++; const count = this.count; const currentModel = this.currentModel; - const updated = this.props.update(msg, currentModel); - if (this.devTools) { - this.fireEvent({ - tag: 'updated', - msgNum: count, - time: new Date().getTime(), - msg: msg, - modelBefore: currentModel, - modelAfter: updated[0], - cmd: updated[1], - }); - } - const newSub = this.props.subscriptions(updated[0]); - const prevSub = this.currentSub; + if (currentModel) { + const updated = this.props.update(msg, currentModel); + if (this.props.devTools) { + this.fireEvent({ + tag: 'updated', + msgNum: count, + time: new Date().getTime(), + msg: msg, + modelBefore: currentModel, + modelAfter: updated[0], + cmd: updated[1], + }); + } + const newSub = this.props.subscriptions(updated[0]); + const prevSub = this.currentSub; - const d = this.dispatch.bind(this); + const d = this.dispatch.bind(this); - newSub.init(d); - prevSub.release(); + newSub.init(d); + prevSub && prevSub.release(); - // perform commands in a separate timout, to - // make sure that this dispatch is done - setTimeout(() => { - // console.log("dispatch: processing commands"); - // debug("performing command", updated[1]); - updated[1].execute(d); - // debug("<<< done"); - }, 0); + // perform commands in a separate timout, to + // make sure that this dispatch is done + setTimeout(() => { + // console.log("dispatch: processing commands"); + // debug("performing command", updated[1]); + updated[1].execute(d); + // debug("<<< done"); + }, 0); - this.currentModel = updated[0]; - this.currentSub = newSub; - this.forceUpdate(); + this.currentModel = updated[0]; + this.currentSub = newSub; + + // trigger rendering + this.forceUpdate(); + } } componentDidMount() { + const { devTools } = this.props; + if (devTools) { + devTools.connected(this); + } + const mac = (devTools && devTools.initFromSnapshot()) || this.props.init(); + if (devTools) { + this.fireEvent({ + tag: 'init', + time: new Date().getTime(), + model: mac[0], + }); + } + const sub = this.props.subscriptions(mac[0]); + this.currentModel = mac[0]; + this.currentSub = sub; + // connect to sub + sub.init(this.bd); + this.initialCmd = mac[1]; + // trigger initial command setTimeout(() => { - this.initialCmd.execute(this.bd); + this.initialCmd && this.initialCmd.execute(this.bd); }, 0); + + // trigger rendering after mount + this.forceUpdate(); } render(): ReactNode { - return this.props.view(this.bd, this.currentModel); + if (this.currentModel && this.bd) { + return this.props.view(this.bd, this.currentModel); + } + return null; } setModel(model: Model, withSubs: boolean) { - let newSub: Sub; - if (withSubs) { - newSub = this.props.subscriptions(model); - } else { - newSub = Sub.none(); + if (this.bd) { + let newSub: Sub; + if (withSubs) { + newSub = this.props.subscriptions(model); + } else { + newSub = Sub.none(); + } + newSub.init(this.bd); + this.currentSub && this.currentSub.release(); + this.currentModel = model; + this.currentSub = newSub; + this.forceUpdate(); } - newSub.init(this.bd); - this.currentSub.release(); - this.currentModel = model; - this.currentSub = newSub; - this.forceUpdate(); } }