@reactpy/client 0.3.2 → 1.0.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.
Files changed (58) hide show
  1. package/dist/client.d.ts +29 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +60 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/components.d.ts +3 -4
  6. package/dist/components.d.ts.map +1 -1
  7. package/dist/components.js +38 -37
  8. package/dist/components.js.map +1 -1
  9. package/dist/index.d.ts +7 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +7 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/logger.d.ts +1 -0
  14. package/dist/logger.d.ts.map +1 -1
  15. package/dist/logger.js +1 -0
  16. package/dist/logger.js.map +1 -1
  17. package/dist/mount.d.ts +2 -2
  18. package/dist/mount.d.ts.map +1 -1
  19. package/dist/mount.js +29 -4
  20. package/dist/mount.js.map +1 -1
  21. package/dist/types.d.ts +126 -0
  22. package/dist/types.d.ts.map +1 -0
  23. package/dist/types.js +1 -0
  24. package/dist/types.js.map +1 -0
  25. package/dist/vdom.d.ts +8 -0
  26. package/dist/vdom.d.ts.map +1 -0
  27. package/dist/vdom.js +174 -0
  28. package/dist/vdom.js.map +1 -0
  29. package/dist/websocket.d.ts +6 -0
  30. package/dist/websocket.d.ts.map +1 -0
  31. package/dist/websocket.js +57 -0
  32. package/dist/websocket.js.map +1 -0
  33. package/package.json +23 -22
  34. package/src/client.ts +83 -0
  35. package/src/components.tsx +40 -46
  36. package/src/index.ts +7 -3
  37. package/src/logger.ts +1 -0
  38. package/src/mount.tsx +37 -5
  39. package/src/types.ts +152 -0
  40. package/src/vdom.tsx +256 -0
  41. package/src/websocket.ts +75 -0
  42. package/tsconfig.json +3 -3
  43. package/tsconfig.tsbuildinfo +1 -0
  44. package/dist/messages.d.ts +0 -15
  45. package/dist/messages.d.ts.map +0 -1
  46. package/dist/messages.js +0 -2
  47. package/dist/messages.js.map +0 -1
  48. package/dist/reactpy-client.d.ts +0 -94
  49. package/dist/reactpy-client.d.ts.map +0 -1
  50. package/dist/reactpy-client.js +0 -128
  51. package/dist/reactpy-client.js.map +0 -1
  52. package/dist/reactpy-vdom.d.ts +0 -54
  53. package/dist/reactpy-vdom.d.ts.map +0 -1
  54. package/dist/reactpy-vdom.js +0 -141
  55. package/dist/reactpy-vdom.js.map +0 -1
  56. package/src/messages.ts +0 -17
  57. package/src/reactpy-client.ts +0 -264
  58. package/src/reactpy-vdom.tsx +0 -261
package/dist/vdom.js ADDED
@@ -0,0 +1,174 @@
1
+ import eventToObject from "event-to-object";
2
+ import log from "./logger";
3
+ export async function loadImportSource(vdomImportSource, client) {
4
+ let module;
5
+ if (vdomImportSource.sourceType === "URL") {
6
+ module = await import(vdomImportSource.source);
7
+ }
8
+ else {
9
+ module = await client.loadModule(vdomImportSource.source);
10
+ }
11
+ if (typeof module.bind !== "function") {
12
+ throw new Error(`${vdomImportSource.source} did not export a function 'bind'`);
13
+ }
14
+ return (node) => {
15
+ const binding = module.bind(node, {
16
+ sendMessage: client.sendMessage,
17
+ onMessage: client.onMessage,
18
+ });
19
+ if (!(typeof binding.create === "function" &&
20
+ typeof binding.render === "function" &&
21
+ typeof binding.unmount === "function")) {
22
+ log.error(`${vdomImportSource.source} returned an impropper binding`);
23
+ return null;
24
+ }
25
+ return {
26
+ render: (model) => binding.render(createImportSourceElement({
27
+ client,
28
+ module,
29
+ binding,
30
+ model,
31
+ currentImportSource: vdomImportSource,
32
+ })),
33
+ unmount: binding.unmount,
34
+ };
35
+ };
36
+ }
37
+ function createImportSourceElement(props) {
38
+ let type;
39
+ if (props.model.importSource) {
40
+ if (!isImportSourceEqual(props.currentImportSource, props.model.importSource)) {
41
+ log.error("Parent element import source " +
42
+ stringifyImportSource(props.currentImportSource) +
43
+ " does not match child's import source " +
44
+ stringifyImportSource(props.model.importSource));
45
+ return null;
46
+ }
47
+ else {
48
+ type = getComponentFromModule(props.module, props.model.tagName, props.model.importSource);
49
+ if (!type) {
50
+ // Error message logged within getComponentFromModule
51
+ return null;
52
+ }
53
+ }
54
+ }
55
+ else {
56
+ type = props.model.tagName;
57
+ }
58
+ return props.binding.create(type, createAttributes(props.model, props.client), createChildren(props.model, (child) => createImportSourceElement({
59
+ ...props,
60
+ model: child,
61
+ })));
62
+ }
63
+ function getComponentFromModule(module, componentName, importSource) {
64
+ /* Gets the component with the provided name from the provided module.
65
+
66
+ Built specifically to work on inifinitely deep nested components.
67
+ For example, component "My.Nested.Component" is accessed from
68
+ ModuleA like so: ModuleA["My"]["Nested"]["Component"].
69
+ */
70
+ const componentParts = componentName.split(".");
71
+ let Component = null;
72
+ for (let i = 0; i < componentParts.length; i++) {
73
+ const iterAttr = componentParts[i];
74
+ Component = i == 0 ? module[iterAttr] : Component[iterAttr];
75
+ if (!Component) {
76
+ if (i == 0) {
77
+ log.error("Module from source " +
78
+ stringifyImportSource(importSource) +
79
+ ` does not export ${iterAttr}`);
80
+ }
81
+ else {
82
+ console.error(`Component ${componentParts.slice(0, i).join(".")} from source ` +
83
+ stringifyImportSource(importSource) +
84
+ ` does not have subcomponent ${iterAttr}`);
85
+ }
86
+ break;
87
+ }
88
+ }
89
+ return Component;
90
+ }
91
+ function isImportSourceEqual(source1, source2) {
92
+ return (source1.source === source2.source &&
93
+ source1.sourceType === source2.sourceType);
94
+ }
95
+ function stringifyImportSource(importSource) {
96
+ return JSON.stringify({
97
+ source: importSource.source,
98
+ sourceType: importSource.sourceType,
99
+ });
100
+ }
101
+ export function createChildren(model, createChild) {
102
+ if (!model.children) {
103
+ return [];
104
+ }
105
+ else {
106
+ return model.children.map((child) => {
107
+ switch (typeof child) {
108
+ case "object":
109
+ return createChild(child);
110
+ case "string":
111
+ return child;
112
+ }
113
+ });
114
+ }
115
+ }
116
+ export function createAttributes(model, client) {
117
+ return Object.fromEntries(Object.entries({
118
+ // Normal HTML attributes
119
+ ...model.attributes,
120
+ // Construct event handlers
121
+ ...Object.fromEntries(Object.entries(model.eventHandlers || {}).map(([name, handler]) => createEventHandler(client, name, handler))),
122
+ ...Object.fromEntries(Object.entries(model.inlineJavaScript || {}).map(([name, inlineJavaScript]) => createInlineJavaScript(name, inlineJavaScript))),
123
+ }));
124
+ }
125
+ function createEventHandler(client, name, { target, preventDefault, stopPropagation }) {
126
+ const eventHandler = function (...args) {
127
+ const data = Array.from(args).map((value) => {
128
+ const event = value;
129
+ if (preventDefault) {
130
+ event.preventDefault();
131
+ }
132
+ if (stopPropagation) {
133
+ event.stopPropagation();
134
+ }
135
+ // Convert JavaScript objects to plain JSON, if needed
136
+ if (typeof event === "object") {
137
+ return eventToObject(event);
138
+ }
139
+ else {
140
+ return event;
141
+ }
142
+ });
143
+ client.sendMessage({ type: "layout-event", data, target });
144
+ };
145
+ eventHandler.isHandler = true;
146
+ return [name, eventHandler];
147
+ }
148
+ function createInlineJavaScript(name, inlineJavaScript) {
149
+ /* Function that will execute the string-like InlineJavaScript
150
+ via eval in the most appropriate way */
151
+ const wrappedExecutable = function (...args) {
152
+ function handleExecution(...args) {
153
+ const evalResult = eval(inlineJavaScript);
154
+ if (typeof evalResult == "function") {
155
+ return evalResult(...args);
156
+ }
157
+ }
158
+ if (args.length > 0 && args[0] instanceof Event) {
159
+ /* If being triggered by an event, set the event's current
160
+ target to "this". This ensures that inline
161
+ javascript statements such as the following work:
162
+ html.button({"onclick": 'this.value = "Clicked!"'}, "Click Me")*/
163
+ return handleExecution.call(args[0].currentTarget, ...args);
164
+ }
165
+ else {
166
+ /* If not being triggered by an event, do not set "this" and
167
+ just call normally */
168
+ return handleExecution(...args);
169
+ }
170
+ };
171
+ wrappedExecutable.isHandler = false;
172
+ return [name, wrappedExecutable];
173
+ }
174
+ //# sourceMappingURL=vdom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vdom.js","sourceRoot":"","sources":["../src/vdom.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { CreateReconnectingWebSocketProps } from "./types";
2
+ export declare function createReconnectingWebSocket(props: CreateReconnectingWebSocketProps): {
3
+ current?: WebSocket;
4
+ };
5
+ export declare function nextInterval(currentInterval: number, backoffMultiplier: number, maxInterval: number): number;
6
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,SAAS,CAAC;AAGhE,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gCAAgC;cAOb,SAAS;EAkDpC;AAED,wBAAgB,YAAY,CAC1B,eAAe,EAAE,MAAM,EACvB,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAClB,MAAM,CAOR"}
@@ -0,0 +1,57 @@
1
+ import log from "./logger";
2
+ export function createReconnectingWebSocket(props) {
3
+ const { interval, maxInterval, maxRetries, backoffMultiplier } = props;
4
+ let retries = 0;
5
+ let currentInterval = interval;
6
+ let everConnected = false;
7
+ const closed = false;
8
+ const socket = {};
9
+ const connect = () => {
10
+ if (closed) {
11
+ return;
12
+ }
13
+ socket.current = new WebSocket(props.url);
14
+ socket.current.onopen = () => {
15
+ everConnected = true;
16
+ log.info("Connected!");
17
+ currentInterval = interval;
18
+ retries = 0;
19
+ if (props.onOpen) {
20
+ props.onOpen();
21
+ }
22
+ };
23
+ socket.current.onmessage = (event) => {
24
+ if (props.onMessage) {
25
+ props.onMessage(event);
26
+ }
27
+ };
28
+ socket.current.onclose = () => {
29
+ if (props.onClose) {
30
+ props.onClose();
31
+ }
32
+ if (!everConnected) {
33
+ log.info("Failed to connect!");
34
+ return;
35
+ }
36
+ log.info("Disconnected!");
37
+ if (retries >= maxRetries) {
38
+ log.info("Connection max retries exhausted!");
39
+ return;
40
+ }
41
+ log.info(`Reconnecting in ${(currentInterval / 1000).toPrecision(4)} seconds...`);
42
+ setTimeout(connect, currentInterval);
43
+ currentInterval = nextInterval(currentInterval, backoffMultiplier, maxInterval);
44
+ retries++;
45
+ };
46
+ };
47
+ props.readyPromise.then(() => log.info("Starting client...")).then(connect);
48
+ return socket;
49
+ }
50
+ export function nextInterval(currentInterval, backoffMultiplier, maxInterval) {
51
+ return Math.min(
52
+ // increase interval by backoff multiplier
53
+ currentInterval * backoffMultiplier,
54
+ // don't exceed max interval
55
+ maxInterval);
56
+ }
57
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AACA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,MAAM,UAAU,2BAA2B,CACzC,KAAuC;IAEvC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IACvE,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,eAAe,GAAG,QAAQ,CAAC;IAC/B,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC;IACrB,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE;YAC3B,aAAa,GAAG,IAAI,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvB,eAAe,GAAG,QAAQ,CAAC;YAC3B,OAAO,GAAG,CAAC,CAAC;YACZ,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,KAAK,CAAC,MAAM,EAAE,CAAC;YACjB,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;YACnC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;gBACpB,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE;YAC5B,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBAClB,KAAK,CAAC,OAAO,EAAE,CAAC;YAClB,CAAC;YACD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;gBAC/B,OAAO;YACT,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC1B,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;gBAC9C,OAAO;YACT,CAAC;YACD,GAAG,CAAC,IAAI,CACN,mBAAmB,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CACxE,CAAC;YACF,UAAU,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACrC,eAAe,GAAG,YAAY,CAC5B,eAAe,EACf,iBAAiB,EACjB,WAAW,CACZ,CAAC;YACF,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE5E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,eAAuB,EACvB,iBAAyB,EACzB,WAAmB;IAEnB,OAAO,IAAI,CAAC,GAAG;IACb,0CAA0C;IAC1C,eAAe,GAAG,iBAAiB;IACnC,4BAA4B;IAC5B,WAAW,CACZ,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,34 +1,35 @@
1
1
  {
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.3.2",
2
+ "author": "Mark Bakhit",
3
+ "contributors": [
4
+ "Ryan Morshead"
5
+ ],
10
6
  "dependencies": {
11
- "event-to-object": "file:../event-to-object",
12
- "json-pointer": "^0.6.2"
7
+ "json-pointer": "^0.6.2",
8
+ "preact": "^10.27.2",
9
+ "event-to-object": "file:./packages/event-to-object"
13
10
  },
11
+ "description": "A client for ReactPy implemented in React",
14
12
  "devDependencies": {
15
- "@types/json-pointer": "^1.0.31",
16
- "@types/react": "^17.0",
17
- "@types/react-dom": "^17.0",
18
- "typescript": "^4.9.5"
19
- },
20
- "peerDependencies": {
21
- "react": ">=16 <18",
22
- "react-dom": ">=16 <18"
13
+ "@types/json-pointer": "^1.0.34",
14
+ "typescript": "^5.9.3"
23
15
  },
16
+ "keywords": [
17
+ "react",
18
+ "reactive",
19
+ "python",
20
+ "reactpy"
21
+ ],
22
+ "license": "MIT",
23
+ "main": "dist/index.js",
24
+ "name": "@reactpy/client",
24
25
  "repository": {
25
26
  "type": "git",
26
27
  "url": "https://github.com/reactive-python/reactpy"
27
28
  },
28
29
  "scripts": {
29
30
  "build": "tsc -b",
30
- "test": "npm run check:tests",
31
- "check:tests": "echo 'no tests'",
32
- "check:types": "tsc --noEmit"
33
- }
31
+ "checkTypes": "tsc --noEmit"
32
+ },
33
+ "type": "module",
34
+ "version": "1.0.1"
34
35
  }
package/src/client.ts ADDED
@@ -0,0 +1,83 @@
1
+ import logger from "./logger";
2
+ import type {
3
+ ReactPyClientInterface,
4
+ ReactPyModule,
5
+ GenericReactPyClientProps,
6
+ ReactPyUrls,
7
+ } from "./types";
8
+ import { createReconnectingWebSocket } from "./websocket";
9
+
10
+ export abstract class BaseReactPyClient implements ReactPyClientInterface {
11
+ private readonly handlers: { [key: string]: ((message: any) => void)[] } = {};
12
+ protected readonly ready: Promise<void>;
13
+ private resolveReady: (value: undefined) => void;
14
+
15
+ constructor() {
16
+ this.resolveReady = () => {};
17
+ this.ready = new Promise((resolve) => (this.resolveReady = resolve));
18
+ }
19
+
20
+ onMessage(type: string, handler: (message: any) => void): () => void {
21
+ (this.handlers[type] || (this.handlers[type] = [])).push(handler);
22
+ this.resolveReady(undefined);
23
+ return () => {
24
+ this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
25
+ };
26
+ }
27
+
28
+ abstract sendMessage(message: any): void;
29
+ abstract loadModule(moduleName: string): Promise<ReactPyModule>;
30
+
31
+ /**
32
+ * Handle an incoming message.
33
+ *
34
+ * This should be called by subclasses when a message is received.
35
+ *
36
+ * @param message The message to handle. The message must have a `type` property.
37
+ */
38
+ protected handleIncoming(message: any): void {
39
+ if (!message.type) {
40
+ logger.warn("Received message without type", message);
41
+ return;
42
+ }
43
+
44
+ const messageHandlers: ((m: any) => void)[] | undefined =
45
+ this.handlers[message.type];
46
+ if (!messageHandlers) {
47
+ logger.warn("Received message without handler", message);
48
+ return;
49
+ }
50
+
51
+ messageHandlers.forEach((h) => h(message));
52
+ }
53
+ }
54
+
55
+ export class ReactPyClient
56
+ extends BaseReactPyClient
57
+ implements ReactPyClientInterface
58
+ {
59
+ urls: ReactPyUrls;
60
+ socket: { current?: WebSocket };
61
+ mountElement: HTMLElement;
62
+
63
+ constructor(props: GenericReactPyClientProps) {
64
+ super();
65
+
66
+ this.urls = props.urls;
67
+ this.mountElement = props.mountElement;
68
+ this.socket = createReconnectingWebSocket({
69
+ url: this.urls.componentUrl,
70
+ readyPromise: this.ready,
71
+ ...props.reconnectOptions,
72
+ onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
73
+ });
74
+ }
75
+
76
+ sendMessage(message: any): void {
77
+ this.socket.current?.send(JSON.stringify(message));
78
+ }
79
+
80
+ loadModule(moduleName: string): Promise<ReactPyModule> {
81
+ return import(`${this.urls.jsModulesPath}${moduleName}`);
82
+ }
83
+ }
@@ -1,29 +1,18 @@
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
1
  import { set as setJsonPointer } from "json-pointer";
14
- import {
15
- ReactPyVdom,
16
- ReactPyComponent,
17
- createChildren,
18
- createAttributes,
19
- loadImportSource,
2
+ import type { ChangeEvent, MutableRefObject } from "preact/compat";
3
+ import { createContext, createElement, Fragment, type JSX } from "preact";
4
+ import { useContext, useEffect, useRef, useState } from "preact/hooks";
5
+ import type {
20
6
  ImportSourceBinding,
21
- } from "./reactpy-vdom";
22
- import { ReactPyClient } from "./reactpy-client";
7
+ ReactPyComponent,
8
+ ReactPyVdom,
9
+ ReactPyClientInterface,
10
+ } from "./types";
11
+ import { createAttributes, createChildren, loadImportSource } from "./vdom";
23
12
 
24
- const ClientContext = createContext<ReactPyClient>(null as any);
13
+ const ClientContext = createContext<ReactPyClientInterface>(null as any);
25
14
 
26
- export function Layout(props: { client: ReactPyClient }): JSX.Element {
15
+ export function Layout(props: { client: ReactPyClientInterface }): JSX.Element {
27
16
  const currentModel: ReactPyVdom = useState({ tagName: "" })[0];
28
17
  const forceUpdate = useForceUpdate();
29
18
 
@@ -70,7 +59,7 @@ export function Element({ model }: { model: ReactPyVdom }): JSX.Element | null {
70
59
  }
71
60
 
72
61
  function StandardElement({ model }: { model: ReactPyVdom }) {
73
- const client = React.useContext(ClientContext);
62
+ const client = useContext(ClientContext);
74
63
  // Use createElement here to avoid warning about variable numbers of children not
75
64
  // having keys. Warning about this must now be the responsibility of the client
76
65
  // providing the models instead of the client rendering them.
@@ -86,16 +75,18 @@ function StandardElement({ model }: { model: ReactPyVdom }) {
86
75
  function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element {
87
76
  const client = useContext(ClientContext);
88
77
  const props = createAttributes(model, client);
89
- const [value, setValue] = React.useState(props.value);
78
+ const [value, setValue] = useState(props.value);
90
79
 
91
80
  // honor changes to value from the client via props
92
- React.useEffect(() => setValue(props.value), [props.value]);
81
+ useEffect(() => setValue(props.value), [props.value]);
93
82
 
94
83
  const givenOnChange = props.onChange;
95
84
  if (typeof givenOnChange === "function") {
96
85
  props.onChange = (event: ChangeEvent<any>) => {
97
86
  // immediately update the value to give the user feedback
98
- setValue(event.target.value);
87
+ if (event.target) {
88
+ setValue((event.target as HTMLInputElement).value);
89
+ }
99
90
  // allow the client to respond (and possibly change the value)
100
91
  givenOnChange(event);
101
92
  };
@@ -117,31 +108,34 @@ function UserInputElement({ model }: { model: ReactPyVdom }): JSX.Element {
117
108
  function ScriptElement({ model }: { model: ReactPyVdom }) {
118
109
  const ref = useRef<HTMLDivElement | null>(null);
119
110
 
120
- React.useEffect(() => {
111
+ useEffect(() => {
112
+ // Don't run if the parent element is missing
121
113
  if (!ref.current) {
122
114
  return;
123
115
  }
116
+
117
+ // Create the script element
118
+ const scriptElement: HTMLScriptElement = document.createElement("script");
119
+ for (const [k, v] of Object.entries(model.attributes || {})) {
120
+ scriptElement.setAttribute(k, v);
121
+ }
122
+
123
+ // Add the script content as text
124
124
  const scriptContent = model?.children?.filter(
125
125
  (value): value is string => typeof value == "string",
126
126
  )[0];
127
-
128
- let scriptElement: HTMLScriptElement;
129
- if (model.attributes) {
130
- scriptElement = document.createElement("script");
131
- for (const [k, v] of Object.entries(model.attributes)) {
132
- scriptElement.setAttribute(k, v);
133
- }
134
- if (scriptContent) {
135
- scriptElement.appendChild(document.createTextNode(scriptContent));
136
- }
137
- ref.current.appendChild(scriptElement);
138
- } else if (scriptContent) {
139
- const scriptResult = eval(scriptContent);
140
- if (typeof scriptResult == "function") {
141
- return scriptResult();
142
- }
127
+ if (scriptContent) {
128
+ scriptElement.appendChild(document.createTextNode(scriptContent));
143
129
  }
144
- }, [model.key, ref.current]);
130
+
131
+ // Append the script element to the parent element
132
+ ref.current.appendChild(scriptElement);
133
+
134
+ // Remove the script element when the component is unmounted
135
+ return () => {
136
+ ref.current?.removeChild(scriptElement);
137
+ };
138
+ }, [model.key]);
145
139
 
146
140
  return <div ref={ref} />;
147
141
  }
@@ -179,10 +173,10 @@ function useImportSource(model: ReactPyVdom): MutableRefObject<any> {
179
173
  const vdomImportSource = model.importSource;
180
174
  const vdomImportSourceJsonString = JSON.stringify(vdomImportSource);
181
175
  const mountPoint = useRef<HTMLElement>(null);
182
- const client = React.useContext(ClientContext);
176
+ const client = useContext(ClientContext);
183
177
  const [binding, setBinding] = useState<ImportSourceBinding | null>(null);
184
178
 
185
- React.useEffect(() => {
179
+ useEffect(() => {
186
180
  let unmounted = false;
187
181
 
188
182
  if (vdomImportSource) {
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
+ export * from "./client";
1
2
  export * from "./components";
2
- export * from "./messages";
3
3
  export * from "./mount";
4
- export * from "./reactpy-client";
5
- export * from "./reactpy-vdom";
4
+ export * from "./types";
5
+ export * from "./vdom";
6
+ export * from "./websocket";
7
+ export { default as React } from "preact/compat";
8
+ export { default as ReactDOM } from "preact/compat";
9
+ export * as preact from "preact";
package/src/logger.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  log: (...args: any[]): void => console.log("[ReactPy]", ...args),
3
+ info: (...args: any[]): void => console.info("[ReactPy]", ...args),
3
4
  warn: (...args: any[]): void => console.warn("[ReactPy]", ...args),
4
5
  error: (...args: any[]): void => console.error("[ReactPy]", ...args),
5
6
  };
package/src/mount.tsx CHANGED
@@ -1,8 +1,40 @@
1
- import React from "react";
2
- import { render } from "react-dom";
1
+ import { render } from "preact";
2
+ import { ReactPyClient } from "./client";
3
3
  import { Layout } from "./components";
4
- import { ReactPyClient } from "./reactpy-client";
4
+ import type { MountProps } from "./types";
5
5
 
6
- export function mount(element: HTMLElement, client: ReactPyClient): void {
7
- render(<Layout client={client} />, element);
6
+ export function mountReactPy(props: MountProps) {
7
+ // WebSocket route for component rendering
8
+ const wsProtocol = `ws${window.location.protocol === "https:" ? "s" : ""}:`;
9
+ const wsOrigin = `${wsProtocol}//${window.location.host}`;
10
+ const componentUrl = new URL(
11
+ `${wsOrigin}${props.pathPrefix}${props.componentPath || ""}`,
12
+ );
13
+
14
+ // Embed the initial HTTP path into the WebSocket URL
15
+ componentUrl.searchParams.append("http_pathname", window.location.pathname);
16
+ if (window.location.search) {
17
+ componentUrl.searchParams.append(
18
+ "http_query_string",
19
+ window.location.search,
20
+ );
21
+ }
22
+
23
+ // Configure a new ReactPy client
24
+ const client = new ReactPyClient({
25
+ urls: {
26
+ componentUrl: componentUrl,
27
+ jsModulesPath: `${window.location.origin}${props.pathPrefix}modules/`,
28
+ },
29
+ reconnectOptions: {
30
+ interval: props.reconnectInterval || 750,
31
+ maxInterval: props.reconnectMaxInterval || 60000,
32
+ maxRetries: props.reconnectMaxRetries || 150,
33
+ backoffMultiplier: props.reconnectBackoffMultiplier || 1.25,
34
+ },
35
+ mountElement: props.mountElement,
36
+ });
37
+
38
+ // Start rendering the component
39
+ render(<Layout client={client} />, props.mountElement);
8
40
  }