Custom Component using Mapbox + React

Hi Retool,

Hoping y'all could help steer us in the right direction.

Our goal is to manually draw polygons on a map and send their JSON representations via API.

To this end, we would like to create a custom component with a Mapbox object (with polygon-drawing capabilities) and extract the generated JSON.

We can successfully implement a Mapbox component that draws polygons (see: Draw a polygon and calculate its area | Mapbox GL JS | Mapbox). However, it's created with plain JS. Our understanding is that one needs a custom component that uses react to enable data sharing across the Retool dashboard.

When creating a custom component with React, we are having trouble creating the Mapbox component. We suspect an issue with imports or with the way the component is declared. The generated error messages don't provide sufficient information on possible error sources. Would you, by chance, have suggestions on how to proceed?

Thank you.

<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@3.9.3/umd/material-ui.production.min.js"></script>

<script src="https://api.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.js"></script>

<script type="text/babel">
	const mapboxgl = window['mapbox-gl'];

  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => (
    <p>Hello, Retool!</p>
  );


	const map = new mapboxgl.Map({
        container: 'map', // container id
        style: 'mapbox://styles/mapbox/dark-v10', //hosted style id
        center: [-99.874, 19.76], // starting position
        zoom: 8 // starting zoom
    });

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

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

Hi @mindatasleep! Welcome to the community! :sunglasses:

It actually does look like we have a demo of this, which can be found here!

And here’s a sample of the custom component code to get you started:

<html>
<head>
<meta charset='utf-8' />
<title>Show drawn polygon area</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.css' rel='stylesheet' />
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
 
<style>
.calculation-box {
height: 75px;
width: 150px;
position: absolute;
bottom: 40px;
left: 10px;
background-color: rgba(255, 255, 255, .9);
padding: 15px;
text-align: center;
}
 
p {
font-family: 'Open Sans';
margin: 0;
font-size: 13px;
}
</style>
 
<script src='https://api.tiles.mapbox.com/mapbox.js/plugins/turf/v3.0.11/turf.min.js'></script>
<script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.0.9/mapbox-gl-draw.js'></script>
<link rel='stylesheet' href='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.0.9/mapbox-gl-draw.css' type='text/css'/>
<div id='map'></div>
<div class='calculation-box'>
<p>Draw a polygon using the draw tools.</p>
<div id='calculated-area'></div>
</div>
 
<script>
mapboxgl.accessToken = 'xxxxxxx';
/* eslint-disable */
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/satellite-v9', //hosted style id
center: [-91.874, 42.760], // starting position
zoom: 12 // starting zoom
});
 
var draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
}
});
map.addControl(draw);
 
map.on('draw.create', updateArea);
map.on('draw.delete', updateArea);
map.on('draw.update', updateArea);
 
function updateArea(e) {
var data = draw.getAll();
window.Retool.modelUpdate({ data })


var answer = document.getElementById('calculated-area');
if (data.features.length > 0) {
var area = turf.area(data);
// restrict to area to 2 decimal points
var rounded_area = Math.round(area*100)/100;
answer.innerHTML = '<p><strong>' + rounded_area + '</strong></p><p>square meters</p>';
} else {
answer.innerHTML = '';
if (e.type !== 'draw.delete') alert("Use the draw tools to draw a polygon!");
}
}
 
</script>
 
</body>
</html>
2 Likes

Hi @ben,

Thanks for sharing this sample!

The example shows how to retrieve data from a custom MapBox component, but how do I pass data to my MapBox component?

I assumed I could just read window.Retool.model after passing in data through the Model component field, but the below code does not work.

Model

{
    earthquakeUrl: "https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson",
}

iFrame Code

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
    <title>View deliveries</title>

      <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.1/mapbox-gl.css' rel='stylesheet' />
    <style>
      body { margin:0; padding:0; }
      #map { position:absolute; top:0; bottom:0; width:100%; }
    </style>
  </head>

<body>
 
<div id='map'></div>
 
<script>
mapboxgl.accessToken = 'omitted';
var map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/light-v10',
  center: [60.0, 50.9],
  zoom: 9.5
});
 
map.on('load', () => {
  map.addSource("deliveries", {
    type: "geojson",
    data: window.Retool.model.earthquakeUrl,
  });
 
  map.addLayer({
    id: "unclustered-point",
    type: "circle",
    source: "deliveries",
    paint: {
      "circle-color": "#11b4da",
      "circle-radius": 4,
      "circle-stroke-width": 1,
      "circle-stroke-color": "#fff"
    }
  });
</script>
 
</body>
</html>

Hardcoding the URL in the iFrame code does work, however, so the problem lies in how I’m passing data to the component.

EDIT

Upon further investigation, the window.Retool object has triggerQuery, modelUpdate, and updateModel properties, but no model property. Does that mean we have to use a React component? If so, would be great to see an example!

1 Like

Would love to see an example aswell :slight_smile:

Hey there!

In order to access the model property, we do need to subscribe to the model and any changes incoming to the model using the following:

window.Retool.subscribe(function(model) {
  if(!model){ return }
  let points = model.points
  <REST OF CODE HERE :)>
}

The subscription would need to wrap any code that relies on model changes, so keep that in mind as well!

EDIT:

The documentation for custom components + vanilla js is located at the bottom of this docs page
https://docs.retool.com/docs/custom-react-components

3 Likes

Ah I see, I just needed to scroll down a little more :no_mouth:

Thanks!

Thank you for the guidance. Here’s a minimalist implementation of how we successfully extracted the Geojson from the map using vanilla JS.

Add ‘data’ component to Custom Component model. This will hold the generated polygons:

{  data: {} }

This function will update model each time the user draws a polygon (full code below):

function updateData(e) {
  var data = draw.getAll();
  window.Retool.modelUpdate({ data })
}

Extract the ‘data’ object from the component’s model. Perhaps onto a jsonexplorer.

{{customcomponent1.model.data}}

Custom Component full code:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Extract drawn polygon area</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>


<script src="https://api.tiles.mapbox.com/mapbox.js/plugins/turf/v3.0.11/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.css" type="text/css">
<div id="map"></div>
<div class="calculation-box">
    <p>Draw a polygon using the draw tools.</p>
    <div id="calculated-area"></div>
</div>

<script>
	mapboxgl.accessToken = 'pk.eyJ1IjoiZXN0b3JudWRhbWUiLCJhIjoiY2s4YXd3aXNkMDJvNDNkb3diMmJqb3FkYiJ9.bDVBManMcJ7p1zPFQFEw9w';
var map = new mapboxgl.Map({
  container: 'map', // container id
  style: 'mapbox://styles/mapbox/dark-v10', //hosted style id
  center: [-99.125519, 19.451054], // starting position
  zoom: 8 // starting zoom
});

    var draw = new MapboxDraw({
        displayControlsDefault: false,
        controls: {
            polygon: true,
            trash: true
        }
    });
    map.addControl(draw);

    map.on('draw.create', updateData);
    map.on('draw.delete', updateData);
    map.on('draw.update', updateData);

    function updateData(e) {
      var data = draw.getAll();
      window.Retool.modelUpdate({ data })
    }


</script>

</body>
</html>
1 Like

This post was super helpful!

Is it possible to check if a point (or many points) are located within a polygon?

Hi @finedesignz !

If you're using the Custom Component described above, we should be able to use a similar calculation from this Stackoverflow post to check and see if our point is inside the Polygon!

If you can share a bit more about your implementation I can take a look!

1 Like