Rendering HTML received from API request

Hi!
Loving Retool.
One thing I couldn't figure out: I have an API resource that returns errors in the form of an HTML page (e.g <html><body>Bad Request - ..details...</body></html>).
I want to display this error in a user friendly format. Specifically, I want to render the HTML.
What's the way to do this?

I tried creating a custom component but couldn't figure out the right way to pass the HTML-as-a-string parameter to the pure JS code such that it will render, and I couldn't find a way to see the generated IFrame code or any console errors.

Thanks

Hi @ypeled, if it's very simple you might actually be able to pass it directly to a text component.

How complex is the HTML you're trying to render?

1 Like

Thanks, that did the trick!
What confused me here - "Markdown" to me is the format used to write .md files. I'm surprised it can be used for HTML.

Thanks!

This did not work in my case. My HTML is, let's say, quite complex.

I get an API response from the Sendgrid and the response is in HTML format. The HTML is the content (body) of the email. I want to render it in Retool so the user does not have to log in to Sendgrid to see what the body looks like.

I've tried the deprecated text component, current component, iframe and custom component, but failed. I've also tried inserting dangerouslySetInnerHTML method in the custom component, but it did not work maybe because my code was wrong. The code for the custom component looks like below.

Model:

{
"content": "{{input_body.value}}"
}

Iframe Code:

<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 }) => (
    <div dangerouslySetInnerHTML={{ __html: `{model.content}`}}
  />;
  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  ReactDOM.render(<ConnectedComponent />, document.getElementById('react'));
</script>

The HTML code works fine in a webpage like Best HTML Viewer, HTML Beautifier, HTML Formatter and to Test HTML Output

Can anyone help me solve this?

Hey @seokjungms, Can you share the HTML (in other words the value of input_body.value) you're hoping to display here?

Wow. Thanks for your quick response.

The value I'm trying to insert into the iframe part of the custom component looks just like a full HTML document. Something like below.

<!DOCTYPE html>
<html>
<head>
<script>
</script>
</head>
<body>
<div></div>
</body>
</html>

When I just paste it into the iframe box, it works perfectly fine. As I'm trying to change its HTML code dynamically, I treid passing the HTML content to the iframe through the model. The method I tried but failed was inserting the following codes into the iframe box:
model.content
(model.content)
{model.content} (<- I thought this would work)
{{model.content}}

It seems the model.content that I defined in the model box seems to work only in the context of a certain code.

Admittedly, I am a newbie in the software engineering, but learning to insert the model's value directly or plainly into the iframe box in the custom component could be helpful for other users, too, I guess. Thanks in advance.:relaxed:

Hey seokjungms, Can you share screenshots of a couple of things

  • What data you're passing
  • How you're passing this data to the Custom Component model
  • How you're referencing this data in your Custom Component

This would be really useful see in order to offer you troubleshooting assistance :slightly_smiling_face:

In case this is helpful, here are our docs on how to pass data to your Custom Component with the Model. They include an example of how you can do so!

I have the same doubt and have made a query in community. Please help.

Hi, sorry for my delayed comment. I've solved this issue by using the window.Retool.suscribe method. My HTML was a HTML document starting from "<!DOCTYPE html.. " which was taken from Sendgrid email template. I wanted to show the email template in the Retool app.

Below is the Model and IFrame Code that I used. In fact, this is almost the same as the code in the doc: Coding custom components

Model:

{
  "body": {{textArea_emailBody.value}},
}

IFrame Code

<html>
  <body>
    <script>    window.Retool.subscribe(function(model){ document.getElementById("body").innerHTML = model.body;})
    </script>
    <div id="body"></div>
  </body>
</html>
1 Like

Is this still a workable solution? I am running into the same issue. I receive an html string from an api that I am trying to display. Works fine when I paste the html directly into the iframe code of custom component but I can't get the html from the query string itself. I tried the above solutions but cannot seem to get it to work. Any suggestions? The query returns a string like this

"<html>
<head><meta charset="utf-8" /></head>
<body>
    <div>                        <script type="text/javascript">window.PlotlyConfig = {MathJaxConfig: 'local'};</script>
        <script type="text/javascript">/**
.....
</script>
</div>
</body>
</html>"

Hey @Joshua_Peterson!

It looks like you need some extra finagling to get scripts to execute when inserting them using innerHTML. This StackOverflow post has one suggestion of how you might do that which seems to work with some testing.

Code
<script>
function nodeScriptReplace(node) {
  if (nodeScriptIs(node) === true) {
    node.parentNode.replaceChild(nodeScriptClone(node), node);
  } else {
    var i = -1,
      children = node.childNodes;
    while (++i < children.length) {
      nodeScriptReplace(children[i]);
    }
  }

  return node;
}
function nodeScriptClone(node) {
  var script = document.createElement("script");
  script.text = node.innerHTML;

  var i = -1,
    attrs = node.attributes,
    attr;
  while (++i < attrs.length) {
    script.setAttribute((attr = attrs[i]).name, attr.value);
  }
  return script;
}

function nodeScriptIs(node) {
  return node.tagName === "SCRIPT";
}
</script>

You might try copying the provided script above as a top-level script tag in your custom component and then doing:

window.Retool.subscribe(function(model){ 
  const body = document.getElementById("body");
  body.innerHTML = model.body;
  nodeScriptReplace(body);
})
1 Like

That worked perfectly! Thank you! It would be a nice feature for an easier interface to insert html from external sources into the html component or custom component.

Ah spoke too soon. It displays the html when the code is first put in the iframe but doesn't update and goes blank soon after. I'm pretty new to webdev so not really sure where to go but I will read up and update if I can figure something out.

UPDATE
The solution displays the figure when first put in the custom component but goes blank when entering preview or running a new query. I found a cleaner implementation on the same post that works in the same manner.

function executeScriptElements(containerElement) {
  const scriptElements = containerElement.querySelectorAll("script");

  Array.from(scriptElements).forEach((scriptElement) => {
    const clonedElement = document.createElement("script");

    Array.from(scriptElement.attributes).forEach((attribute) => {
      clonedElement.setAttribute(attribute.name, attribute.value);
    });
    
    clonedElement.text = scriptElement.text;

    scriptElement.parentNode.replaceChild(clonedElement, scriptElement);
  });
}

Thank you again for the help

1 Like

This seems to work. The componenet was only rendering when I made changes in the script editor for some reason. Adding the subscribe portion to the top level script seemed to fix though I am not sure why.

<script>
function executeScriptElements(containerElement) {
  const scriptElements = containerElement.querySelectorAll("script");

  Array.from(scriptElements).forEach((scriptElement) => {
    const clonedElement = document.createElement("script");

    Array.from(scriptElement.attributes).forEach((attribute) => {
      clonedElement.setAttribute(attribute.name, attribute.value);
    });
    
    clonedElement.text = scriptElement.text;

    scriptElement.parentNode.replaceChild(clonedElement, scriptElement);
  });
}
    window.Retool.subscribe(function(model){ 
      const body = document.getElementById("body");
     body.innerHTML = model.body;
      executeScriptElements(body);
      })
</script>
1 Like