` will accept an optional `styling` prop object that can be used to pass in styles to the iframe component.
46 |
47 | ### Example
48 |
49 | Running `yarn start` will start an application with a basic Retool app embeded.
50 |
51 | There is a live example here: [https://react-retool.surge.sh](https://react-retool.surge.sh)
52 |
53 | ## Development
54 |
55 | In the project directory, you can run:
56 |
57 | ### `yarn start`
58 |
59 | Runs the app in the development mode.\
60 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
61 |
62 | The page will reload if you make edits.\
63 | You will also see any lint errors in the console.
64 |
65 | ## Publishing
66 |
67 | 1. Bump version with `npm version [major|minor|patch]`
68 | 2. Run `yarn publish:npm`. This will build the project in the `/dst` directory.
69 | 3. Navigate to `/dst` directory.
70 | 4. Publish to npm with `npm publish`
71 |
72 | ## Testing
73 |
74 | Tests exist in the `/src/__tests__` directory. Running `yarn test` will run the test suite.
75 |
76 | ## Support
77 |
78 | Need help? Please report issues or requests to support@retool.com, through in app chat, or on https://community.retool.com/
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-retool",
3 | "version": "1.3.0",
4 | "private": false,
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:tryretool/react-retool.git"
8 | },
9 | "licenses": [
10 | {
11 | "type": "MIT",
12 | "url": "https://github.com/tryretool/react-retool/blob/main/LICENSE"
13 | }
14 | ],
15 | "keywords": [
16 | "react",
17 | "react-component",
18 | "component",
19 | "retool",
20 | "embed-retool"
21 | ],
22 | "babel": {
23 | "presets": [
24 | "@babel/preset-env",
25 | [
26 | "@babel/preset-react",
27 | {
28 | "runtime": "automatic"
29 | }
30 | ]
31 | ],
32 | "plugins": [
33 | [
34 | "@babel/plugin-proposal-class-properties"
35 | ]
36 | ]
37 | },
38 | "peerDependencies": {
39 | "react": ">=17.0.0",
40 | "react-dom": ">=17.0.0"
41 | },
42 | "scripts": {
43 | "start": "react-scripts start",
44 | "build": "react-scripts build",
45 | "test": "react-scripts test",
46 | "eject": "react-scripts eject",
47 | "publish:npm": "rm -rf dst && mkdir dst && babel src/components -d dst --copy-files && mv ./dst/Retool.js ./dst/index.js && cp ./package.json ./dst/package.json && cp ./README.md ./dst/README.md"
48 | },
49 | "eslintConfig": {
50 | "extends": [
51 | "react-app",
52 | "react-app/jest"
53 | ]
54 | },
55 | "browserslist": {
56 | "production": [
57 | ">0.2%",
58 | "not dead",
59 | "not op_mini all"
60 | ],
61 | "development": [
62 | "last 1 chrome version",
63 | "last 1 firefox version",
64 | "last 1 safari version"
65 | ]
66 | },
67 | "devDependencies": {
68 | "@babel/cli": "^7.19.3",
69 | "@babel/plugin-proposal-class-properties": "^7.18.6",
70 | "@babel/preset-react": "^7.18.6",
71 | "@testing-library/react": "^13.4.0",
72 | "jest": "^29.4.1",
73 | "react": "^18.2.0",
74 | "react-dom": "^18.2.0",
75 | "react-scripts": "^5.0.1"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryretool/react-retool/3c73180a18ecdb2c9d6bf21a0de8d8ce55ce8fdc/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryretool/react-retool/3c73180a18ecdb2c9d6bf21a0de8d8ce55ce8fdc/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryretool/react-retool/3c73180a18ecdb2c9d6bf21a0de8d8ce55ce8fdc/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryretool/react-retool/3c73180a18ecdb2c9d6bf21a0de8d8ce55ce8fdc/src/App.css
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import Retool from "./components/Retool";
2 | import { useState } from "react";
3 |
4 | const App = () => {
5 | const iframeStyle = {
6 | border: "2px solid red",
7 | };
8 |
9 | const sample = {
10 | example1: "",
11 | example2: false,
12 | input: "",
13 | };
14 |
15 | const [retoolData, setRetoolData] = useState("");
16 | const [data, setData] = useState(sample);
17 | return (
18 |
19 |
React-Retool
20 |
27 |
28 |
29 |
30 | setData({ ...data, input: e.target.value })}
34 | />
35 |
36 |
37 |
46 | {JSON.stringify(retoolData)}
47 |
48 | );
49 | };
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/__tests__/retool.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, screen } from "@testing-library/react";
3 | import Retool from "../components/Retool";
4 |
5 | describe("react-retool", () => {
6 | it("has the correct src attribute", () => {
7 | render(
8 |
9 | );
10 |
11 | const iframe = screen.getByTitle("retool");
12 | expect(iframe.src).toBe(
13 | "https://support.retool.com/embedded/public/cb9e15f0-5d7c-43a7-a746-cdec870dde9a"
14 | );
15 | });
16 |
17 | it("has the correct height attribute", () => {
18 | render();
19 |
20 | const iframe = screen.getByTitle("retool");
21 | expect(iframe.height).toBe("100%");
22 | });
23 |
24 | it("has the correct width attribute", () => {
25 | render();
26 |
27 | const iframe = screen.getByTitle("retool");
28 | expect(iframe.width).toBe("500px");
29 | });
30 |
31 | it("has the correct sandbox attribute", () => {
32 | render();
33 |
34 | const iframe = screen.getByTitle("retool");
35 | expect(iframe.getAttribute("sandbox")).toBe(
36 | "allow-scripts allow-same-origin allow-popups"
37 | );
38 | });
39 |
40 | it("has the correct allow attribute", () => {
41 | render();
42 |
43 | const iframe = screen.getByTitle("retool");
44 | expect(iframe.getAttribute("allow")).toBe("fullscreen");
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/Retool.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 |
3 | const MINIMUM_SANDBOX_PERMISSIONS = "allow-scripts allow-same-origin";
4 |
5 | const Retool = ({
6 | data,
7 | url,
8 | height,
9 | width,
10 | onData,
11 | sandbox,
12 | allow,
13 | styling,
14 | }) => {
15 | const embeddedIframe = useRef(null);
16 | const [elementWatchers, setElementWatchers] = useState({});
17 |
18 | /* Retool passes up the list of elements to watch on page load */
19 | useEffect(() => {
20 | for (const key in elementWatchers) {
21 | const watcher = elementWatchers[key];
22 | watcher.iframe?.contentWindow.postMessage(
23 | {
24 | type: "PARENT_WINDOW_RESULT",
25 | result: data[watcher.selector],
26 | id: watcher.queryId,
27 | pageName: watcher.pageName,
28 | },
29 | "*"
30 | );
31 | }
32 | }, [data, elementWatchers]);
33 |
34 | /* On page load, add event listener to listen for events from Retool */
35 | useEffect(() => {
36 | // if the URL changes, we want to clear previous set of Element Watchers
37 | setElementWatchers({});
38 |
39 | /* Handle events - if PWQ then create/replace watchers -> return result */
40 | const handler = (event) => {
41 | if (!embeddedIframe?.current?.contentWindow) return;
42 |
43 | /* Handle messages passed up from Retool */
44 | if (
45 | event.origin === new URL(url).origin &&
46 | event.data?.type !== "PARENT_WINDOW_QUERY" &&
47 | event.data?.type !== "intercom-snippet__ready"
48 | ) {
49 | onData?.(event.data);
50 | }
51 |
52 | /* Handle requests from Retool looking for data */
53 | if (event.data.type === "PARENT_WINDOW_QUERY") {
54 | createOrReplaceWatcher(
55 | event.data.selector,
56 | event.data.pageName,
57 | event.data.id
58 | );
59 | postMessageForSelector("PARENT_WINDOW_RESULT", event.data);
60 | }
61 | };
62 |
63 | window.addEventListener("message", handler);
64 |
65 | return () => window.removeEventListener("message", handler);
66 | }, [url]);
67 |
68 | /* Creates or updates the list of values for us to watch for changes */
69 | const createOrReplaceWatcher = (selector, pageName, queryId) => {
70 | const watcherId = pageName + "-" + queryId;
71 | const updatedState = elementWatchers;
72 |
73 | updatedState[watcherId] = {
74 | iframe: embeddedIframe.current,
75 | selector: selector,
76 | pageName: pageName,
77 | queryId: queryId,
78 | };
79 |
80 | setElementWatchers(updatedState);
81 | };
82 |
83 | /* Checks for selectors for data and posts message for Retool to read */
84 | const postMessageForSelector = (messageType, eventData) => {
85 | const maybeData = data[eventData.selector];
86 |
87 | if (maybeData !== undefined) {
88 | embeddedIframe.current.contentWindow.postMessage(
89 | {
90 | type: messageType,
91 | result: maybeData,
92 | id: eventData.id,
93 | pageName: eventData.pageName,
94 | },
95 | "*"
96 | );
97 | } else {
98 | console.log(
99 | `Not sending data back to Retool, nothing found for selector: ${eventData.selector}`
100 | );
101 | }
102 | };
103 |
104 | const sandboxAttrib =
105 | typeof sandbox === "string"
106 | ? `${MINIMUM_SANDBOX_PERMISSIONS} ${sandbox}`
107 | : sandbox === true
108 | ? MINIMUM_SANDBOX_PERMISSIONS
109 | : sandbox;
110 |
111 | return (
112 |
123 | );
124 | };
125 |
126 | export default Retool;
127 |
--------------------------------------------------------------------------------
/src/components/retool.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryretool/react-retool/3c73180a18ecdb2c9d6bf21a0de8d8ce55ce8fdc/src/components/retool.css
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | height:100%
9 | }
10 |
11 | html {
12 | height:100%
13 | }
14 |
15 | #root {
16 | height: 100%;
17 | padding: 40px;
18 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | );
12 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------