├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── __tests__ │ └── retool.test.js ├── components │ ├── Retool.js │ └── retool.css ├── index.css ├── index.js └── logo.svg └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | package-lock.json 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dst 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 tryretool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-retool 2 | 3 | A React wrapper for embedding Retool apps. 4 | 5 | ## Install 6 | 7 | ``` 8 | // with npm 9 | $ npm install react-retool --save 10 | 11 | // with yarn 12 | $ yarn add react-retool 13 | ``` 14 | 15 | ## Usage 16 | 17 | ``` 18 | import Retool from 'react-retool'; 19 | 20 | function App() { 21 | const sample = { name: 'Sample data' } 22 | return ( 23 | 27 | ); 28 | } 29 | 30 | export default App; 31 | ``` 32 | 33 | ### Options 34 | 35 | `` expects a `url` prop pointing to an embedded Retool application. You can generate this URL in the editor mode of a Retool app by clicking "Share" then "Public". 36 | 37 | `` will accept an optional `data` object, which is made available to the embedded application. When an embedded Retool application runs a Parent Window Query, `` will check if `data` contains a key matching the Parent Window Query's selector, and if so, return that value to the query. 38 | 39 | `` will accept optional `height` and `width` props which will be used for the dimensions of the embedded window. 40 | 41 | `` will accept an optional `onData` callback that will be called with the data of an event that is sent from the embedded Retool app. These events can be sent from a JavaScript query inside of Retool by using the `parent.postMessage()` syntax. 42 | 43 | `` also accepts optional `allow` and `sandbox` parameters to configure permissions of the iframe used to embed the Retool app. `allow-scripts` and `allow-same-origin` are required in order to run Retool, so if `sandbox` is specified, `allow-scripts` and `allow-same-origin` will always be appended to ensure the Retool app works. 44 | 45 | `` 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 |