Skip to content

Commit a2eee84

Browse files
committed
Graph JSON -> rendered HTML node
1 parent 0c9b29d commit a2eee84

File tree

2 files changed

+189
-34
lines changed

2 files changed

+189
-34
lines changed

static/2024-05-30-interpreted-graph-2/index.html

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,38 @@
44
<title>rxjs</title>
55
</head>
66
<style type="text/css">
7-
.columns {
8-
display: flex;
9-
gap: 16px;
10-
}
7+
.columns {
8+
display: flex;
9+
gap: 16px;
10+
}
1111

12-
.columns > * {
13-
flex: 1;
14-
}
12+
.columns > * {
13+
flex: 1;
14+
}
15+
16+
pre {
17+
font-size: 0.8rem;
18+
}
1519
</style>
1620
<body>
1721
<div id="app">
1822
</div>
1923

20-
<pre id="tree"></pre>
24+
<hr />
25+
<div class="columns">
26+
<div>
27+
<label>UI Tree</label>
28+
<pre id="tree"></pre>
29+
</div>
30+
<div>
31+
<label>Context</label>
32+
<pre id="ctx"></pre>
33+
</div>
34+
<div>
35+
<label>System</label>
36+
<pre id="system"></pre>
37+
</div>
38+
</div>
2139

2240
<script type="module">
2341
import "./index.js";

static/2024-05-30-interpreted-graph-2/index.js

Lines changed: 163 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,95 @@
11
import { BehaviorSubject, combineLatest } from 'https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm';
22

3+
const STREAM = 'https://common.tools/stream-binding.schema.json'
4+
const CELL = 'https://common.tools/cell-binding.schema.json'
5+
6+
function createElement(node, context) {
7+
if (typeof node === 'string') {
8+
const textNode = document.createTextNode(node);
9+
return textNode;
10+
}
11+
12+
if (!node || typeof node !== 'object') return null;
13+
14+
// Handle text nodes
15+
if (!node.tag && node.$id && node.name) {
16+
// Bind the reactive source to update the text node if it changes
17+
if (context[node.name] && context[node.name].subscribe) {
18+
if (node.type == 'slot') {
19+
const uiNode = createElement(context[node.name].getValue(), context)
20+
context[node.name].subscribe(newValue => {
21+
uiNode.innerHTML = '';
22+
uiNode.appendChild(createElement(newValue, context));
23+
});
24+
return uiNode;
25+
} else {
26+
const textNode = document.createTextNode(context[node.name] || '');
27+
context[node.name].subscribe(newValue => {
28+
textNode.textContent = newValue;
29+
});
30+
return textNode;
31+
}
32+
}
33+
}
34+
35+
// Handle element nodes
36+
if (!node.tag && node.type == 'repeat') {
37+
const container = document.createElement('div');
38+
const items = context[node.binding] || [];
39+
items.forEach(item => {
40+
container.appendChild(createElement(node.template, item));
41+
});
42+
return container
43+
}
44+
45+
const element = document.createElement(node.tag);
46+
47+
// Set properties
48+
for (const [key, value] of Object.entries(node.props || {})) {
49+
if (typeof value === 'object' && value.type) {
50+
// Handle specific types and bind reactive sources from context
51+
if (value.type && value["$id"] && value["$id"] === CELL) {
52+
let name = value.name || key;
53+
if (!context[name]) continue;
54+
element[key] = context[name].getValue();
55+
context[name].subscribe(newValue => element[key] = newValue);
56+
} else {
57+
if (value.binding) {
58+
element[key] = context[value.binding];
59+
}
60+
}
61+
} else if (value["$id"] && value["$id"] === STREAM && value.name) {
62+
// Handle event binding to a stream
63+
if (context[value.name]) {
64+
element.addEventListener(key, context[value.name]);
65+
}
66+
} else {
67+
element[key] = value;
68+
}
69+
}
70+
71+
let children = node.children || [];
72+
if (!Array.isArray(children)) {
73+
children = [children];
74+
}
75+
76+
// Recursively create and append child elements
77+
children.forEach(childNode => {
78+
if (childNode.binding && childNode.type == 'literal') {
79+
const node = document.createTextNode(context[childNode.binding])
80+
element.appendChild(node);
81+
return
82+
}
83+
84+
const childElement = createElement(childNode, context);
85+
if (childElement) {
86+
element.appendChild(childElement);
87+
}
88+
});
89+
90+
return element;
91+
}
92+
393
// Example usage with a simplified system.get function
494
const system = {
595
get: (key) => {
@@ -16,28 +106,31 @@ const system = {
16106

17107
// Function to create the RxJS network from the new JSON graph format
18108
function createRxJSNetworkFromJson(graph) {
19-
const context = {};
109+
const context = {
110+
inputs: {},
111+
outputs: {}
112+
};
20113

21114
// Create subjects for each node
22115
graph.nodes.forEach(node => {
23116
const nodeName = node.definition.name;
24-
context[nodeName] = {};
25-
context[nodeName]['out'] = new BehaviorSubject(null);
117+
context.outputs[nodeName] = new BehaviorSubject(null);
26118

27119
// foreach input in the signature, create a subject
28120
if (node.definition.signature) {
29121
const { inputs } = node.definition.signature;
122+
context.inputs[nodeName] = {};
30123
for (const inputName in inputs) {
31-
context[nodeName][inputName] = new BehaviorSubject(null);
124+
context.inputs[nodeName][inputName] = new BehaviorSubject(null);
32125
}
33126
}
34127
});
35128

36129
// Set up reactive bindings based on edges
37130
graph.edges.forEach(edge => {
38131
const [source, target] = Object.entries(edge)[0];
39-
const sourceSubject = context[source]['out'];
40-
const targetSubject = context[target[0]][target[1]];
132+
const sourceSubject = context.outputs[source];
133+
const targetSubject = context.inputs[target[0]][target[1]];
41134

42135
sourceSubject.subscribe(value => {
43136
targetSubject.next(value);
@@ -53,18 +146,18 @@ function createRxJSNetworkFromJson(graph) {
53146
// Evaluate the JavaScript content and bind it to the subject
54147
const func = new Function('system', body);
55148
const result = func(system, {
56-
get: (key) => context[key]['out'].getValue(),
57-
set: (key, value) => context[key]['out'].next(value)
149+
get: (key) => context.outputs[nodeName].getValue(),
150+
set: (key, value) => context.outputs[nodeName].next(value)
58151
});
59-
context[nodeName]['out'].next(result);
152+
context.outputs[nodeName].next(result);
60153
} else if (contentType === 'application/json+vnd.common.ui') {
61154
// Set up template rendering
62155
const { inputs } = signature;
63156
const inputObservables = [];
64157

65158
for (const inputName in inputs) {
66-
if (context[inputName]) {
67-
inputObservables.push(context[inputName].out);
159+
if (context.outputs[inputName]) {
160+
inputObservables.push(context.outputs[inputName]);
68161
}
69162
}
70163

@@ -75,8 +168,8 @@ function createRxJSNetworkFromJson(graph) {
75168
return acc;
76169
}, {});
77170

78-
const renderedTemplate = renderTemplate(node.definition.body, inputValues);
79-
context[nodeName]['out'].next(renderedTemplate);
171+
const renderedTemplate = createElement(node.definition.body, inputValues);
172+
context.outputs[nodeName].next(renderedTemplate);
80173
});
81174
}
82175
});
@@ -162,25 +255,35 @@ const jsonDocument = {
162255
}
163256
},
164257
"body": {
165-
"tag": "todos",
258+
"tag": "ul",
166259
"props": {
167260
"className": "todo"
168261
},
169262
"children": {
170263
"type": "repeat",
171264
"binding": "todos",
172-
"template": [
173-
{
174-
"tag": "li",
175-
"props": {
176-
"todo": {
177-
"$id": "https://common.tools/cell.json",
178-
"type": "todo"
265+
"template": {
266+
"tag": "li",
267+
"props": {},
268+
"children": [
269+
{
270+
"tag": "input",
271+
"props": {
272+
"type": "checkbox",
273+
"checked": { type: 'boolean', binding: 'checked' }
179274
}
180275
},
181-
"children": []
182-
}
183-
]
276+
{
277+
"tag": "span",
278+
"props": {
279+
"className": "todo-label"
280+
},
281+
"children": [
282+
{ type: 'literal', binding: 'label' }
283+
]
284+
}
285+
]
286+
}
184287
}
185288
}
186289
}
@@ -198,8 +301,42 @@ const jsonDocument = {
198301
// Create the RxJS network
199302
const context = createRxJSNetworkFromJson(jsonDocument);
200303

304+
function debug() {
305+
document.querySelector('#tree').innerHTML = JSON.stringify(jsonDocument, null, 2);
306+
document.querySelector('#ctx').innerHTML = JSON.stringify(snapshot(context), null, 2);
307+
document.querySelector('#system').innerHTML = JSON.stringify(system.get('todos'), null, 2);
308+
}
309+
310+
function snapshot(ctx) {
311+
// grab values of behavior subjects
312+
// preserve literals
313+
314+
const snapshot = {
315+
inputs: {},
316+
outputs: {}
317+
}
318+
for (const key in ctx.outputs) {
319+
const value = ctx.outputs[key].getValue()
320+
snapshot.outputs[key] = value
321+
}
322+
323+
for (const key in ctx.inputs) {
324+
snapshot.inputs[key] = {}
325+
for (const inputKey in ctx.inputs[key]) {
326+
const value = ctx.inputs[key][inputKey].getValue()
327+
snapshot.inputs[key][inputKey] = value
328+
}
329+
}
330+
331+
return snapshot
332+
333+
}
334+
201335
// Example output for the UI component
202-
context['ui'].out.subscribe(renderedTemplate => {
336+
context.outputs.ui.subscribe(renderedTemplate => {
203337
console.log(renderedTemplate);
204-
document.getElementById('app').innerHTML = renderedTemplate;
338+
document.getElementById('app').replaceChildren(renderedTemplate)
339+
debug()
205340
});
341+
342+
debug()

0 commit comments

Comments
 (0)