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 runtriggerQuery
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 intomodelUpdate
will be merged with the Custom Component'smodel
. 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
- 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.
- Click the Custom Component in order to show 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!
.
To pass the data from your Text Input component to your Custom Component:
- Open the Custom Component Editor.
- 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.
- 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!
.
To pass data from your Custom Component to the rest of your app:
- 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 typeobject
. This argument represents updates that should be made to the Custom Component's model. Think ofmodelUpdate()
likesetState()
.
<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>
- 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:
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:
To trigger a query from your Custom Component:
- 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 theInput
component updates the Custom Component's model whenever the value of theInput
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>
-
Use the
triggerQuery
function to trigger a query from within your Custom Component. Intriggerquery.html
the trigger is queried when a user clicks the Search button. This is handled by theonClick
event listener of theButton
component. -
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>
Updated 18 days ago