@reactpy/client 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +10 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +172 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +5 -0
- package/dist/messages.d.ts +24 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +1 -0
- package/dist/mount.d.ts +3 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +6 -0
- package/dist/reactpy-client.d.ts +86 -0
- package/dist/reactpy-client.d.ts.map +1 -0
- package/dist/reactpy-client.js +133 -0
- package/dist/reactpy-vdom.d.ts +54 -0
- package/dist/reactpy-vdom.d.ts.map +1 -0
- package/dist/reactpy-vdom.js +140 -0
- package/package.json +22 -20
- package/src/components.tsx +231 -0
- package/src/index.ts +5 -0
- package/src/logger.ts +5 -0
- package/src/messages.ts +32 -0
- package/src/mount.tsx +8 -0
- package/src/reactpy-client.ts +274 -0
- package/src/reactpy-vdom.tsx +261 -0
- package/tsconfig.json +14 -0
- package/src/components.js +0 -220
- package/src/contexts.js +0 -6
- package/src/element-utils.js +0 -82
- package/src/event-to-object.js +0 -240
- package/src/import-source.js +0 -134
- package/src/index.js +0 -4
- package/src/mount.js +0 -105
- package/src/server.js +0 -46
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import serializeEvent from "event-to-object";
|
|
2
|
+
export async function loadImportSource(vdomImportSource, client) {
|
|
3
|
+
let module;
|
|
4
|
+
if (vdomImportSource.sourceType === "URL") {
|
|
5
|
+
module = await import(vdomImportSource.source);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
module = await client.loadModule(vdomImportSource.source);
|
|
9
|
+
}
|
|
10
|
+
if (typeof module.bind !== "function") {
|
|
11
|
+
throw new Error(`${vdomImportSource.source} did not export a function 'bind'`);
|
|
12
|
+
}
|
|
13
|
+
return (node) => {
|
|
14
|
+
const binding = module.bind(node, {
|
|
15
|
+
sendMessage: client.sendMessage,
|
|
16
|
+
onMessage: client.onMessage,
|
|
17
|
+
});
|
|
18
|
+
if (!(typeof binding.create === "function" &&
|
|
19
|
+
typeof binding.render === "function" &&
|
|
20
|
+
typeof binding.unmount === "function")) {
|
|
21
|
+
console.error(`${vdomImportSource.source} returned an impropper binding`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
render: (model) => binding.render(createImportSourceElement({
|
|
26
|
+
client,
|
|
27
|
+
module,
|
|
28
|
+
binding,
|
|
29
|
+
model,
|
|
30
|
+
currentImportSource: vdomImportSource,
|
|
31
|
+
})),
|
|
32
|
+
unmount: binding.unmount,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createImportSourceElement(props) {
|
|
37
|
+
let type;
|
|
38
|
+
if (props.model.importSource) {
|
|
39
|
+
if (!isImportSourceEqual(props.currentImportSource, props.model.importSource)) {
|
|
40
|
+
console.error("Parent element import source " +
|
|
41
|
+
stringifyImportSource(props.currentImportSource) +
|
|
42
|
+
" does not match child's import source " +
|
|
43
|
+
stringifyImportSource(props.model.importSource));
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
else if (!props.module[props.model.tagName]) {
|
|
47
|
+
console.error("Module from source " +
|
|
48
|
+
stringifyImportSource(props.currentImportSource) +
|
|
49
|
+
` does not export ${props.model.tagName}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
type = props.module[props.model.tagName];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
type = props.model.tagName;
|
|
58
|
+
}
|
|
59
|
+
return props.binding.create(type, createAttributes(props.model, props.client), createChildren(props.model, (child) => createImportSourceElement({
|
|
60
|
+
...props,
|
|
61
|
+
model: child,
|
|
62
|
+
})));
|
|
63
|
+
}
|
|
64
|
+
function isImportSourceEqual(source1, source2) {
|
|
65
|
+
return (source1.source === source2.source &&
|
|
66
|
+
source1.sourceType === source2.sourceType);
|
|
67
|
+
}
|
|
68
|
+
function stringifyImportSource(importSource) {
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
source: importSource.source,
|
|
71
|
+
sourceType: importSource.sourceType,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function createChildren(model, createChild) {
|
|
75
|
+
if (!model.children) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return model.children.map((child) => {
|
|
80
|
+
switch (typeof child) {
|
|
81
|
+
case "object":
|
|
82
|
+
return createChild(child);
|
|
83
|
+
case "string":
|
|
84
|
+
return child;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function createAttributes(model, client) {
|
|
90
|
+
return Object.fromEntries(Object.entries({
|
|
91
|
+
// Normal HTML attributes
|
|
92
|
+
...model.attributes,
|
|
93
|
+
// Construct event handlers
|
|
94
|
+
...Object.fromEntries(Object.entries(model.eventHandlers || {}).map(([name, handler]) => createEventHandler(client, name, handler))),
|
|
95
|
+
// Convert snake_case to camelCase names
|
|
96
|
+
}).map(normalizeAttribute));
|
|
97
|
+
}
|
|
98
|
+
function createEventHandler(client, name, { target, preventDefault, stopPropagation }) {
|
|
99
|
+
return [
|
|
100
|
+
name,
|
|
101
|
+
function () {
|
|
102
|
+
const data = Array.from(arguments).map((value) => {
|
|
103
|
+
if (!(typeof value === "object" && value.nativeEvent)) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
const event = value;
|
|
107
|
+
if (preventDefault) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
}
|
|
110
|
+
if (stopPropagation) {
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
}
|
|
113
|
+
return serializeEvent(event.nativeEvent);
|
|
114
|
+
});
|
|
115
|
+
client.sendMessage({ type: "layout-event", data, target });
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
function normalizeAttribute([key, value]) {
|
|
120
|
+
let normKey = key;
|
|
121
|
+
let normValue = value;
|
|
122
|
+
if (key === "style" && typeof value === "object") {
|
|
123
|
+
normValue = Object.fromEntries(Object.entries(value).map(([k, v]) => [snakeToCamel(k), v]));
|
|
124
|
+
}
|
|
125
|
+
else if (key.startsWith("data_") ||
|
|
126
|
+
key.startsWith("aria_") ||
|
|
127
|
+
DASHED_HTML_ATTRS.includes(key)) {
|
|
128
|
+
normKey = key.split("_").join("-");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
normKey = snakeToCamel(key);
|
|
132
|
+
}
|
|
133
|
+
return [normKey, normValue];
|
|
134
|
+
}
|
|
135
|
+
function snakeToCamel(str) {
|
|
136
|
+
return str.replace(/([_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
|
|
137
|
+
}
|
|
138
|
+
// see list of HTML attributes with dashes in them:
|
|
139
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#attribute_list
|
|
140
|
+
const DASHED_HTML_ATTRS = ["accept_charset", "http_equiv"];
|
package/package.json
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Ryan Morshead",
|
|
3
|
+
"main": "dist/index.js",
|
|
4
|
+
"types": "dist/index.d.ts",
|
|
5
|
+
"description": "A client for ReactPy implemented in React",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"name": "@reactpy/client",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"version": "0.2.1",
|
|
3
10
|
"dependencies": {
|
|
4
|
-
"
|
|
11
|
+
"event-to-object": "^0.1.2",
|
|
5
12
|
"json-pointer": "^0.6.2"
|
|
6
13
|
},
|
|
7
|
-
"description": "A client for ReactPy implemented in React",
|
|
8
14
|
"devDependencies": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
15
|
+
"@types/json-pointer": "^1.0.31",
|
|
16
|
+
"@types/react": "^17.0",
|
|
17
|
+
"@types/react-dom": "^17.0",
|
|
18
|
+
"prettier": "^3.0.0-alpha.6",
|
|
19
|
+
"typescript": "^4.9.5"
|
|
13
20
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
"src/**/*.js"
|
|
16
|
-
],
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"main": "src/index.js",
|
|
19
|
-
"name": "@reactpy/client",
|
|
20
21
|
"peerDependencies": {
|
|
21
|
-
"react": ">=16",
|
|
22
|
-
"react-dom": ">=16"
|
|
22
|
+
"react": ">=16 <18",
|
|
23
|
+
"react-dom": ">=16 <18"
|
|
23
24
|
},
|
|
24
25
|
"repository": {
|
|
25
26
|
"type": "git",
|
|
26
27
|
"url": "https://github.com/reactive-python/reactpy"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
|
-
"
|
|
30
|
-
"format": "prettier --write
|
|
31
|
-
"test": "
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
"build": "tsc -b",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"test": "npm run check:tests",
|
|
33
|
+
"check:format": "prettier --check .",
|
|
34
|
+
"check:tests": "echo 'no tests'",
|
|
35
|
+
"check:types": "tsc --noEmit"
|
|
36
|
+
}
|
|
35
37
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createElement,
|
|
3
|
+
createContext,
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
Fragment,
|
|
9
|
+
MutableRefObject,
|
|
10
|
+
ChangeEvent,
|
|
11
|
+
} from "react";
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
import { set as setJsonPointer } from "json-pointer";
|
|
14
|
+
import { LayoutUpdateMessage } from "./messages";
|
|
15
|
+
import {
|
|
16
|
+
ReactPyVdom,
|
|
17
|
+
ReactPyComponent,
|
|
18
|
+
createChildren,
|
|
19
|
+
createAttributes,
|
|
20
|
+
loadImportSource,
|
|
21
|
+
ImportSourceBinding,
|
|
22
|
+
} from "./reactpy-vdom";
|
|
23
|
+
import { ReactPyClient } from "./reactpy-client";
|
|
24
|
+
|
|
25
|
+
const ClientContext = createContext<ReactPyClient>(null as any);
|
|
26
|
+
|
|
27
|
+
export function Layout(props: { client: ReactPyClient }): JSX.Element {
|
|
28
|
+
const currentModel: ReactPyVdom = useState({ tagName: "" })[0];
|
|
29
|
+
const forceUpdate = useForceUpdate();
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
props.client.onMessage<LayoutUpdateMessage>(
|
|
33
|
+
"layout-update",
|
|
34
|
+
({ path, model }) => {
|
|
35
|
+
if (path === "") {
|
|
36
|
+
Object.assign(currentModel, model);
|
|
37
|
+
} else {
|
|
38
|
+
setJsonPointer(currentModel, path, model);
|
|
39
|
+
}
|
|
40
|
+
forceUpdate();
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
props.client.start();
|
|
44
|
+
return () => props.client.stop();
|
|
45
|
+
}, [currentModel, props.client]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<ClientContext.Provider value={props.client}>
|
|
49
|
+
<Element model={currentModel} />
|
|
50
|
+
</ClientContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Element({ model }: { model: ReactPyVdom }): JSX.Element | null {
|
|
55
|
+
if (model.error !== undefined) {
|
|
56
|
+
if (model.error) {
|
|
57
|
+
return <pre>{model.error}</pre>;
|
|
58
|
+
} else {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let SpecializedElement: ReactPyComponent;
|
|
64
|
+
if (model.tagName in SPECIAL_ELEMENTS) {
|
|
65
|
+
SpecializedElement =
|
|
66
|
+
SPECIAL_ELEMENTS[model.tagName as keyof typeof SPECIAL_ELEMENTS];
|
|
67
|
+
} else if (model.importSource) {
|
|
68
|
+
SpecializedElement = ImportedElement;
|
|
69
|
+
} else {
|
|
70
|
+
SpecializedElement = StandardElement;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return <SpecializedElement model={model} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function StandardElement({ model }: { model: ReactPyVdom }) {
|
|
77
|
+
const client = React.useContext(ClientContext);
|
|
78
|
+
// Use createElement here to avoid warning about variable numbers of children not
|
|
79
|
+
// having keys. Warning about this must now be the responsibility of the client
|
|
80
|
+
// providing the models instead of the client rendering them.
|
|
81
|
+
return createElement(
|
|
82
|
+
model.tagName === "" ? Fragment : model.tagName,
|
|
83
|
+
createAttributes(model, client),
|
|
84
|
+
...createChildren(model, (child) => {
|
|
85
|
+
return <Element model={child} key={child.key} />;
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element {
|
|
91
|
+
const client = useContext(ClientContext);
|
|
92
|
+
const props = createAttributes(model, client);
|
|
93
|
+
const [value, setValue] = React.useState(props.value);
|
|
94
|
+
|
|
95
|
+
// honor changes to value from the client via props
|
|
96
|
+
React.useEffect(() => setValue(props.value), [props.value]);
|
|
97
|
+
|
|
98
|
+
const givenOnChange = props.onChange;
|
|
99
|
+
if (typeof givenOnChange === "function") {
|
|
100
|
+
props.onChange = (event: ChangeEvent<any>) => {
|
|
101
|
+
// immediately update the value to give the user feedback
|
|
102
|
+
setValue(event.target.value);
|
|
103
|
+
// allow the client to respond (and possibly change the value)
|
|
104
|
+
givenOnChange(event);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use createElement here to avoid warning about variable numbers of children not
|
|
109
|
+
// having keys. Warning about this must now be the responsibility of the client
|
|
110
|
+
// providing the models instead of the client rendering them.
|
|
111
|
+
return createElement(
|
|
112
|
+
model.tagName,
|
|
113
|
+
// overwrite
|
|
114
|
+
{ ...props, value },
|
|
115
|
+
...createChildren(model, (child) => (
|
|
116
|
+
<Element model={child} key={child.key} />
|
|
117
|
+
)),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ScriptElement({ model }: { model: ReactPyVdom }) {
|
|
122
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
123
|
+
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (!ref.current) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const scriptContent = model?.children?.filter(
|
|
129
|
+
(value): value is string => typeof value == "string",
|
|
130
|
+
)[0];
|
|
131
|
+
|
|
132
|
+
let scriptElement: HTMLScriptElement;
|
|
133
|
+
if (model.attributes) {
|
|
134
|
+
scriptElement = document.createElement("script");
|
|
135
|
+
for (const [k, v] of Object.entries(model.attributes)) {
|
|
136
|
+
scriptElement.setAttribute(k, v);
|
|
137
|
+
}
|
|
138
|
+
if (scriptContent) {
|
|
139
|
+
scriptElement.appendChild(document.createTextNode(scriptContent));
|
|
140
|
+
}
|
|
141
|
+
ref.current.appendChild(scriptElement);
|
|
142
|
+
} else if (scriptContent) {
|
|
143
|
+
let scriptResult = eval(scriptContent);
|
|
144
|
+
if (typeof scriptResult == "function") {
|
|
145
|
+
return scriptResult();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, [model.key, ref.current]);
|
|
149
|
+
|
|
150
|
+
return <div ref={ref} />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ImportedElement({ model }: { model: ReactPyVdom }) {
|
|
154
|
+
const importSourceVdom = model.importSource;
|
|
155
|
+
const importSourceRef = useImportSource(model);
|
|
156
|
+
|
|
157
|
+
if (!importSourceVdom) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const importSourceFallback = importSourceVdom.fallback;
|
|
162
|
+
|
|
163
|
+
if (!importSourceVdom) {
|
|
164
|
+
// display a fallback if one was given
|
|
165
|
+
if (!importSourceFallback) {
|
|
166
|
+
return null;
|
|
167
|
+
} else if (typeof importSourceFallback === "string") {
|
|
168
|
+
return <div>{importSourceFallback}</div>;
|
|
169
|
+
} else {
|
|
170
|
+
return <StandardElement model={importSourceFallback} />;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
return <div ref={importSourceRef} />;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function useForceUpdate() {
|
|
178
|
+
const [, setState] = useState(false);
|
|
179
|
+
return () => setState((old) => !old);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function useImportSource(model: ReactPyVdom): MutableRefObject<any> {
|
|
183
|
+
const vdomImportSource = model.importSource;
|
|
184
|
+
|
|
185
|
+
const mountPoint = useRef<HTMLElement>(null);
|
|
186
|
+
const client = React.useContext(ClientContext);
|
|
187
|
+
const [binding, setBinding] = useState<ImportSourceBinding | null>(null);
|
|
188
|
+
|
|
189
|
+
React.useEffect(() => {
|
|
190
|
+
let unmounted = false;
|
|
191
|
+
|
|
192
|
+
if (vdomImportSource) {
|
|
193
|
+
loadImportSource(vdomImportSource, client).then((bind) => {
|
|
194
|
+
if (!unmounted && mountPoint.current) {
|
|
195
|
+
setBinding(bind(mountPoint.current));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
unmounted = true;
|
|
202
|
+
if (
|
|
203
|
+
binding &&
|
|
204
|
+
vdomImportSource &&
|
|
205
|
+
!vdomImportSource.unmountBeforeUpdate
|
|
206
|
+
) {
|
|
207
|
+
binding.unmount();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}, [client, vdomImportSource, setBinding, mountPoint.current]);
|
|
211
|
+
|
|
212
|
+
// this effect must run every time in case the model has changed
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!(binding && vdomImportSource)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
binding.render(model);
|
|
218
|
+
if (vdomImportSource.unmountBeforeUpdate) {
|
|
219
|
+
return binding.unmount;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return mountPoint;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const SPECIAL_ELEMENTS = {
|
|
227
|
+
input: UserInputElement,
|
|
228
|
+
script: ScriptElement,
|
|
229
|
+
select: UserInputElement,
|
|
230
|
+
textarea: UserInputElement,
|
|
231
|
+
};
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
package/src/messages.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReactPyVdom } from "./reactpy-vdom";
|
|
2
|
+
|
|
3
|
+
export interface IMessage {
|
|
4
|
+
type: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ConnectionOpenMessage = {
|
|
8
|
+
type: "connection-open";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ConnectionCloseMessage = {
|
|
12
|
+
type: "connection-close";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type LayoutUpdateMessage = {
|
|
16
|
+
type: "layout-update";
|
|
17
|
+
path: string;
|
|
18
|
+
model: ReactPyVdom;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type LayoutEventMessage = {
|
|
22
|
+
type: "layout-event";
|
|
23
|
+
target: string;
|
|
24
|
+
data: any;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type IncomingMessage =
|
|
28
|
+
| LayoutUpdateMessage
|
|
29
|
+
| ConnectionOpenMessage
|
|
30
|
+
| ConnectionCloseMessage;
|
|
31
|
+
export type OutgoingMessage = LayoutEventMessage;
|
|
32
|
+
export type Message = IncomingMessage | OutgoingMessage;
|
package/src/mount.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "react-dom";
|
|
3
|
+
import { Layout } from "./components";
|
|
4
|
+
import { ReactPyClient } from "./reactpy-client";
|
|
5
|
+
|
|
6
|
+
export function mount(element: HTMLElement, client: ReactPyClient): void {
|
|
7
|
+
render(<Layout client={client} />, element);
|
|
8
|
+
}
|