Develop custom components

Learn how to build your own components using React, or HTML and JavaScript.

If you have a use case that isn't handled by Retool's built-in components, you can build your own custom component to solve that use case.

📘

You may not need a custom component

If your use case is something general that seems like it should be supported by a built-in Retool component, consider submitting a feature request before building a custom component. The Retool team would be happy to build your component if it's something that other Retool customers also need.

Prerequisites

This guide assumes that:

  • You've built a Retool app before.
  • You're familiar with React.js. Custom components can be React components or raw HTML/JS; this example uses React components.

Overview

Custom components extend Retool’s functionality beyond what is possible with our component library. A custom component consists of two parts:

  • A place to put the HTML, CSS, and JavaScript which governs the appearance and behavior of the component. Retool puts this code in an iFrame in the outer Retool app.
  • An interface for passing data back and forth between the Retool app and the custom component code.

This guide walks you through how to build and embed custom components in your Retool apps.

Interface

Retool exposes 3 variables for your Custom Component to interact with: triggerQuery, model, and modelUpdate.

  • triggerQuery is a function which accepts a single argument of type string where the argument is the name of an existing query. When you run triggerQuery from your Custom Component, Retool will run the query whose name matches the string provided.
  • model is an object that represents the shared state between Retool and your Custom Component.
  • modelUpdate is a function that accepts a single argument of type object. The argument passed into modelUpdate will be merged with the Custom Component's model. This is a way for the Custom Component to update its own state, and the state that is made available to the rest of the Retool app.

Editor

The Custom Component editor has 3 fields: Model, IFrame Code, and Hide when true.

  • Model is where you define the model variable passed into your Custom Component.
  • Iframe Code is the HTML, CSS, and JavaScript for the Custom Component.
  • Hide when true is a JavaScript expression that produces a boolean value. When the value is true the component is hidden.

Examples

  1. Add a Custom Component to your Canvas. The default Custom Component includes some example code to demonstrate how to interact with the rest of your app from within the Custom Component.

Figure 1. The default Custom Component

  1. Click the Custom Component in order to show the Custom Component Editor.

Figure 2. The Custom Component Editor

The next sections of this guide explain how to use these fields in the context of common tasks, like passing data from your app to your Custom Component.

Implement your Custom Component

The Iframe Code field of the Custom Component Editor is where you put all of the HTML, CSS, and JavaScript code for your Custom Component. As the field name suggests, your Custom Component is embedded in an <iframe> when your app runs.

boilerplate.html below describes the minimum code needed to create a working Custom Component.

<script
  src="https://unpkg.com/[email protected]/umd/react.development.js"
  crossorigin
></script>
<script
  src="https://unpkg.com/[email protected]/umd/react-dom.development.js"
  crossorigin
></script>
<div id="react"></div>
<script type="text/babel">
  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <p>Hello, Retool!</p>
  );
  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  const container = document.getElementById("react");
  const root = ReactDOM.createRoot(container);
  root.render(<ConnectedComponent />);
</script>

Pass data from your app to your Custom Component

Suppose that you want your Custom Component to display the value of a Text Input component. Specifically, if the value in the Text Input component is Alice, you want the Custom Component to display Hello, Alice!.

Figure 3. An app that passes the value of a Text Input component to a Custom Component

To pass the data from your Text Input component to your Custom Component:

  1. Open the Custom Component Editor.
  2. Use the model.json code below for the Model field.
{
  "name": {{textinput1.value ? textinput1.value : 'World'}}
}

As the model.json code above shows, you can hard-code property names or values, or you can use JavaScript to set them dynamically. When passing data dynamically, the Custom Component updates whenever the model updates.

  1. Use the inflow.html code below for the Iframe Code field.
<script
  src="https://unpkg.com/[email protected]/umd/react.development.js"
  crossorigin
></script>
<script
  src="https://unpkg.com/[email protected]/umd/react-dom.development.js"
  crossorigin
></script>
<div id="react"></div>
<script type="text/babel">
  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <p>Hello, {model.name}!</p>
  );
  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  const container = document.getElementById("react");
  const root = ReactDOM.createRoot(container);
  root.render(<ConnectedComponent />);
</script>

Pass data from your Custom Component to your app

Suppose that you want a Text component to display a value that was entered into an input within your Custom Component. Specifically, when the user types Valentine in your Custom Component, you want the Text component to display Hello, Valentine!.

Figure 4. An app that passes data from a Custom Component to a Text component

To pass data from your Custom Component to the rest of your app:

  1. Use the outflow.html code below for the Iframe Code field. The third argument that's passed when the Custom Component is created, modelUpdate, is the key to passing data from your Custom Component to the rest of your app. modelUpdate takes a single argument of type object. This argument represents updates that should be made to the Custom Component's model. Think of modelUpdate() like setState().
<style>
  body {
    border: 5px solid red;
  }
  #react {
    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>
<script
  src="https://unpkg.com/[email protected]/umd/react.development.js"
  crossorigin
></script>
<script
  src="https://unpkg.com/[email protected]/umd/react-dom.development.js"
  crossorigin
></script>
<script src="https://unpkg.com/@material-ui/[email protected]/umd/material-ui.production.min.js"></script>
<div id="react"></div>
<script type="text/babel">
  const { Input } = window["material-ui"];
  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <Input
      color="primary"
      variant="outlined"
      value={model.textInputValue}
      onChange={(e) => modelUpdate({ name: e.target.value })}
    />
  );
  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  const container = document.getElementById("react");
  const root = ReactDOM.createRoot(container);
  root.render(<ConnectedComponent />);
</script>
  1. Set the value of the Text component to Hello, {{ customcomponent1.model.name }}!.

Trigger queries from your Custom Component

Suppose that you want to trigger a query based on text input that a user has typed within your Custom Component. For example, by default your table displays all books in your inventory:

Figure 5. The default view

After the user types something into the input within the Custom Component and then presses Search, the table only shows books with titles that match the user's input:

Figure 6. The updated view based on the user's search

To trigger a query from your Custom Component:

  1. Set any model data that's required for your query. triggerquery.html contains the full Iframe Code that was used for the app in Figures 5 and 6. In this example app the Input component updates the Custom Component's model whenever the value of the Input changes.
<style>
  body {
    border: 5px solid red;
  }
  #react {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
  }
  .button {
    margin-left: 1em;
  }
</style>
<script
  src="https://unpkg.com/[email protected]/umd/react.development.js"
  crossorigin
></script>
<script
  src="https://unpkg.com/[email protected]/umd/react-dom.development.js"
  crossorigin
></script>
<script src="https://unpkg.com/@material-ui/[email protected]/umd/material-ui.production.min.js"></script>
<div id="react"></div>
<script type="text/babel">
  const { Input, Button } = window["material-ui"];
  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <div>
      <Input
        color="primary"
        variant="outlined"
        value={model.textInputValue}
        onChange={(e) =>
          modelUpdate({ name: e.target.value ? e.target.value : "%" })
        }
      />
      <Button
        className="button"
        color="primary"
        variant="outlined"
        onClick={() => triggerQuery("query1")}
      >
        Search
      </Button>
    </div>
  );
  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  const container = document.getElementById("react");
  const root = ReactDOM.createRoot(container);
  root.render(<ConnectedComponent />);
</script>
  1. Use the triggerQuery function to trigger a query from within your Custom Component. In triggerquery.html the trigger is queried when a user clicks the Search button. This is handled by the onClick event listener of the Button component.

  2. Update your query so that it accesses data from your Custom Component's model. query1.sql provides the statement that was used for the app in Figures 5 and 6.

SELECT
  *
FROM
  products
WHERE
  name ILIKE {{ '%' + customcomponent1.model.name + '%' }}

Note that for the app in Figures 5 and 6, the name property of the Custom Component's model is set to % by default, as shown below in model.json. Note also the modelUpdate({ name: e.target.value ? e.target.value : '%' }) call in triggerquery.html. The % value is provided as a fallback for the name property so that the app displays the full inventory by default.

{
  "name": "%"
}

More examples

Custom buttons / controls

Set the model for the Custom Component:

{
  "queryToTrigger": "query1",
  "textInputValue": "Hello world!"
}

Add the following code to the Iframe Code field in order to implement the Custom Component:

<style>
  body {
    margin: 0;
  }
</style>
<script src="https://unpkg.com/[email protected]/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@material-ui/core/umd/material-ui.production.min.js"></script>

<div id="react"></div>

<script type="text/babel">
  const { Button, Card, CardContent, Input } = window.MaterialUI;

  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <Card>
      <CardContent>
        <Button
          color="primary"
          variant="outlined"
          onClick={() => triggerQuery(model.queryToTrigger)}
        >
          Trigger {model.queryToTrigger}
        </Button>
        <br />
        <br />
        <Input
          color="primary"
          variant="outlined"
          value={model.textInputValue}
          onChange={(e) =>
            modelUpdate({ textInputValue: e.target.value })
          }
        />
      </CardContent>
    </Card>
  );

  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  const container = document.getElementById('react')
  const root = ReactDOM.createRoot(container)
  root.render(<ConnectedComponent />)
</script>

Non-React JavaScript

If you're writing pure JavaScript that isn't React code, you can you use window.Retool to update models, trigger queries, or listen to new values injected into your model.

The relevant methods are modelUpdate, triggerQuery, and subscribe:

<html>
  <body>
    <script>
      function updateName(e) {
        window.Retool.modelUpdate({ name: e.target.value });
      }
      function buttonClicked() {
        window.Retool.triggerQuery('query1')
      }
      window.Retool.subscribe(function(model) {
        // subscribes to model updates
        // all model values can be accessed here
        document.getElementById("mirror").innerHTML = model.name || '';
      })
    </script>
    <input onkeyup="updateName(event)" />
    <button onclick="buttonClicked()">Trigger Query</button>
    <div id="mirror"></div>
  </body>
</html>

Fancy Non-React JavaScript Example

This example doesn't use React but loads Chart.js and renders a horizontally stacked bar chart. If you paste the model & IFrame Code into your own Custom Component, it will work out of the box.

{ type: "horizontalBar",
  options: {
  responsive: true,
    stacked: true,
  maintainAspectRatio: false,
  title: {text: "sample horizontal stacked",display: true},
    tooltips: {mode: "index", intersect: true},
  legend: {position: "bottom"},
     scales: {
            xAxes: [{
                stacked: true
            }],
            yAxes: [{
                stacked: true
            }]

        }},
    data: {
    labels: {{['monday', 'tuesday', 'wednesday', 'thursday']}},
      datasets: [
    { label: "type A", data: {{[13,2,3,4]}}, backgroundColor: "rgb(231,95,112)"},
    { label: "type B", data: {{[4,3,23,1]}}, backgroundColor: "rgb(230,171,2)"},
    { label: "type C", data:{{[1,31,12,1]}},        backgroundColor: "rgb(56,108,176)"},
    { label:"type D", data:{{[1,5,3,8]}},        backgroundColor: "rgb(77,77,77)"}
    ] }
}
<html>
<style>
body {
    margin: 0;
}
</style>
<script src="https://www.chartjs.org/dist/2.8.0/Chart.min.js"></script>
<script src="https://www.chartjs.org/samples/latest/utils.js"></script>
<script>
window.Retool.subscribe(function(model) {
    if (!model) { return }
    model.options.tooltips['callbacks'] = {
        footer: function(tooltipItems, data) {
            var sum = 0;
            tooltipItems.forEach(function(tooltipItem) {
                sum += data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
            });
            return sum;
        }
    };
    var chartData = model.data;
    var ctx = document.getElementById('canvas').getContext('2d');
    if (!window.myMixedChart) {
        window.myMixedChart = new Chart(ctx, model);
    } else {
        window.myMixedChart.data = model.data;
        window.myMixedChart.options = model.options;
        window.myMixedChart.update();
    }
})
</script>
<div class="chart-container" style="position: relative;margin: auto; height:100vh; width:100vw;">
    <canvas id="canvas"></canvas>
</div>
</body>
</html>