Develop custom components
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
What's really going on here? Basically, we're just giving you a place to write code that gets put into an iframe within your Retool app. This is pretty straight-forward. You can write absolutely any valid HTML/CSS/JS and we'll render it for you. However, this is only useful if whatever you put in the iframe can interact with the rest of your Retool app. This guide focuses on the interface that we provide between your custom iframe code and the rest of your Retool app.
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. See Dynamically Hide Components.
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://cdn.tryretool.com/js/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.tryretool.com/js/react-dom.production.min.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);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</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://cdn.tryretool.com/js/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.tryretool.com/js/react-dom.production.min.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);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</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://cdn.tryretool.com/js/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.tryretool.com/js/react-dom.production.min.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);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</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://cdn.tryretool.com/js/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.tryretool.com/js/react-dom.production.min.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);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</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 Plot.ly charts
You no longer need to code a custom component to create charts with Plotly.
We now have a Plotly Chart component. See Charting data with plotly.js for more information.
Here's how to pass data from a Retool app to a Plot.ly chart.
Set the model for the Plot.ly chart:
{
"plotData": { "x": [1, 2, 3], "y": [1, 2, 3] }
}
Add the following code to the Iframe Code field in order to create the Plot.ly chart:
<style>
body {
margin: 0;
}
</style>
<script
src="https://cdn.tryretool.com/js/react.production.min.js"
crossorigin
></script>
<script
src="https://cdn.tryretool.com/js/react-dom.production.min.js"
crossorigin
></script>
<script src=" https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/create-plotly-component.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
<script src="https://unpkg.com/@material-ui/core/umd/material-ui.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<div id="react" />
<script type="text/babel">
const Plot = createPlotlyComponent.default(Plotly);
console.log(Plot);
const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
<Plot
data={[
{
x: model.plotData.x,
y: model.plotData.y,
type: "bar",
marker: { color: "purple" },
},
]}
onClick={(v) => {
modelUpdate({
selectedPoints: v.points.map((p) => ({ x: p.x, y: p.y })),
});
}}
layout={{ title: "Scatter Plot" }}
style={{ width: "100%", height: "100vh" }}
/>
);
const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</script>
And voila!
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://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
<script src="https://cdn.tryretool.com/js/react-dom.production.min.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["material-ui"];
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);
ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</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 about 1 month ago