Skip to content

Commit

Permalink
feat(examples): add rstream-spreadsheet example
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Sep 10, 2019
1 parent c9e5a63 commit 7da74a8
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 0 deletions.
5 changes: 5 additions & 0 deletions examples/spreadsheet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.cache
out
node_modules
yarn.lock
*.js
13 changes: 13 additions & 0 deletions examples/spreadsheet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# rstream-spreadsheet

[Live demo](http://demo.thi.ng/umbrella/rstream-spreadsheet/)

Please refer to the [example build instructions](https://github.com/thi-ng/umbrella/wiki/Example-build-instructions) on the wiki.

## Authors

- Karsten Schmidt

## License

© 2019 Karsten Schmidt // Apache Software License 2.0
23 changes: 23 additions & 0 deletions examples/spreadsheet/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>rstream-spreadsheet</title>
<!-- <link href="https://unpkg.com/tachyons@4/css/tachyons.min.css" rel="stylesheet"> -->
<link href="tachyons.min.css" rel="stylesheet" />
<style></style>
</head>
<body class="sans-serif">
<div id="app"></div>
<div>
<a
class="link"
href="https://github.com/thi-ng/umbrella/tree/master/examples/spreadsheet"
>Source code</a
>
</div>
<script type="text/javascript" src="./src/index.ts"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions examples/spreadsheet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "rstream-spreadsheet",
"version": "0.0.1",
"repository": "https://github.com/thi-ng/umbrella",
"author": "Karsten Schmidt <k+npm@thi.ng>",
"license": "Apache-2.0",
"scripts": {
"clean": "rm -rf .cache build out",
"build": "yarn clean && parcel build index.html -d out --public-url ./ --no-source-maps --no-cache --detailed-report --experimental-scope-hoisting",
"build:webpack": "../../node_modules/.bin/webpack --mode production",
"start": "parcel index.html -p 8080 --open"
},
"devDependencies": {
"parcel-bundler": "^1.12.3",
"terser": "^3.17.0",
"typescript": "^3.4.1"
},
"dependencies": {
"@thi.ng/api": "latest",
"@thi.ng/rstream": "latest",
"@thi.ng/transducers-hdom": "latest"
},
"browserslist": [
"last 3 Chrome versions"
],
"browser": {
"process": false
}
}
251 changes: 251 additions & 0 deletions examples/spreadsheet/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { ConsoleLogger, IObjectOf } from "@thi.ng/api";
import { Atom } from "@thi.ng/atom";
import { isNumber } from "@thi.ng/checks";
import { ILifecycle } from "@thi.ng/hdom";
import { memoize1 } from "@thi.ng/memoize";
import { setIn, setInMany } from "@thi.ng/paths";
import { fromAtom, fromView, setLogger } from "@thi.ng/rstream";
import {
add,
addNode,
div,
mul,
Node,
NodeInputSpec,
removeNode,
sub
} from "@thi.ng/rstream-graph";
import { charRange, float, maybeParseFloat } from "@thi.ng/strings";
import {
assocObj,
comp,
map,
mapIndexed,
partition,
permutations,
push,
range,
transduce
} from "@thi.ng/transducers";
import { updateDOM } from "@thi.ng/transducers-hdom";

const NUM_COLS = 4;
const NUM_ROWS = 10;

const MAX_COL = "A".charCodeAt(0) + NUM_COLS - 1;

const RE_OP2 = /^=\s*([A-Z]\d+)\s*([+*/\-])\s*([A-Z]\d+)$/i;
const RE_SUM = /^=\s*SUM\(\s*([A-Z])(\d+)\s*\:\s*([A-Z])(\d+)\s*\)$/i;

const CELL_STYLE = "div.dib.h2.pa2.ma0.br.bb";

interface Cell {
formula: string;
value: string | number;
backup: string;
focus: boolean;
error: boolean;
}

interface UICell extends ILifecycle {
element?: HTMLDivElement;
focus?: boolean;
}

const DB = new Atom<IObjectOf<Cell>>(
transduce(
map(([col, row]) => [
`${col}${row}`,
{ formula: "", value: "", backup: "", focus: false, error: false }
]),
assocObj(),
permutations(charRange("A", MAX_COL), range(1, NUM_ROWS + 1))
)
);

const graph: IObjectOf<Node> = {};

const removeCell = (id: string) => removeNode(graph, id);

const focusCell = (id: string) => {
DB.swapIn(id, (cell: Cell) =>
setInMany(cell, "focus", true, "backup", cell.formula)
);
};

const blurCell = (id: string) => {
DB.swapIn(id, (cell: Cell) => setIn(cell, "focus", false));
};

const cancelCell = (id: string) => {
DB.swapIn(id, (cell: Cell) =>
setInMany(cell, "focus", false, "formula", cell.backup)
);
};

const updateCell = (id: string, val: string) => {
if (val.startsWith("=")) {
DB.resetIn([id, "formula"], val);
let res = RE_OP2.exec(val);
if (res) {
DB.resetIn([id, "error"], false);
addBinOp(res, id);
} else if ((res = RE_SUM.exec(val))) {
DB.resetIn([id, "error"], false);
addSum(res, id);
} else {
DB.resetIn([id, "error"], true);
}
} else {
removeCell(id);
DB.swapIn(id, (cell) =>
setInMany(cell, "value", val, "formula", "", "error", false)
);
}
};

const cellInput = memoize1(
(id: string): NodeInputSpec => ({
stream: () =>
fromView(DB, [id.toUpperCase(), "value"], (x) =>
maybeParseFloat(x, 0)
)
})
);

const addBinOp = ([_, a, op, b]: RegExpExecArray, id: string) => {
removeCell(id);
addNode(graph, DB, id, {
fn: (<any>{ "+": add, "*": mul, "-": sub, "/": div })[op],
ins: { a: cellInput(a), b: cellInput(b) },
outs: {
"*": [id, "value"]
}
});
};

const addSum = (
[_, acol, arow, bcol, brow]: RegExpExecArray,
cellID: string
) => {
removeCell(cellID);
addNode(graph, DB, cellID, {
fn: add,
ins: transduce(
comp(map(([c, r]) => `${c}${r}`), map((id) => [id, cellInput(id)])),
assocObj<NodeInputSpec>(),
permutations(
charRange(acol.toUpperCase(), bcol.toUpperCase()),
range(parseInt(arow), parseInt(brow) + 1)
)
),
outs: {
"*": [cellID, "value"]
}
});
};

const formatCell = (x: string | number) => (isNumber(x) ? float(2)(x) : x);

const cellBackground = (cell: any) =>
cell.focus
? "bg-yellow"
: cell.formula
? cell.error
? "bg-red"
: "bg-light-green"
: "";

const cell = ([row, col]: [number, string]) =>
<UICell>{
init(el: HTMLDivElement) {
this.element = el;
this.focus = false;
},
render(_: any, cells: any) {
const id = `${col}${row}`;
const cell = cells[id];
return [
`${CELL_STYLE}.w4.overflow-y-hidden.overflow-x-scroll`,
{
class: cellBackground(cell),
contentEditable: true,
onfocus: () => {
this.focus = true;
focusCell(id);
},
onblur: () => {
if (this.focus) {
updateCell(id, this.element!.textContent!.trim());
this.focus = false;
}
blurCell(id);
},
onkeydown: (e: KeyboardEvent) => {
switch (e.key) {
case "Enter":
case "Tab":
updateCell(
id,
this.element!.textContent!.trim()
);
this.element!.blur();
break;
case "Escape":
this.focus = false;
cancelCell(id);
this.element!.blur();
}
}
},
String(
cell.focus && cell.formula
? cell.formula
: formatCell(cell.value)
)
];
}
};

const app = () => {
const CELLS: UICell[][] = transduce(
comp(map(cell), partition(NUM_COLS)),
push(),
permutations(range(1, NUM_ROWS + 1), charRange("A", MAX_COL))
);
return (state: any) => [
"div",
{},
[`${CELL_STYLE}.w2.b.bg-moon-gray`, "\u00a0"],
map(
(col) => [`${CELL_STYLE}.w4.b.bg-moon-gray`, {}, col],
charRange("A", MAX_COL)
),
mapIndexed(
(i, rowid) => [
"div",
{},
[
`${CELL_STYLE}.w2.b.bg-moon-gray.overflow-y-hidden.overflow-x-scroll`,
{},
rowid
],
...CELLS[i].map((cell) => [cell, state])
],
range(1, NUM_ROWS + 1)
)
];
};

// setLogger(new ConsoleLogger("rstream"));

const main = fromAtom(DB);
main.transform(map(app()), updateDOM({ span: false }));

(<any>window)["DB"] = DB;
(<any>window)["graph"] = graph;

if (process.env.NODE_ENV !== "production") {
const hot = (<any>module).hot;
hot && hot.dispose(() => main.done());
}
11 changes: 11 additions & 0 deletions examples/spreadsheet/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": ".",
"target": "es6",
"sourceMap": true
},
"include": [
"./src/**/*.ts"
]
}

0 comments on commit 7da74a8

Please sign in to comment.