Recently, we needed to embed a number of personal finance calculators in our customer-facing portal. Instead of building very elaborate models for the calculators and spending a lot of time on implementing each one of them from scratch, we wanted to see which calculators users would find most interesting. Iteration speed and testing users' interactions with many calculators and underlying models was more important than building a perfect calculator or having the outputs persisted in the backend.
Based on these requirements, we decided to look for tools that would help the buisness team create calculators easily and quickly and keep developers focused on more pressing development tasks. We found a website that converts spreadsheets with formulas into responsive and dynamic forms that can also display results. Lastly, the site also allows us to download the forms as html files. And all of it for free (which is always good for a startup)!
Instead of just rendering these files individually, we wanted to make them feel like they were native to our app and surround them with the proper context and branding. They couldn't just look like iframes clumsily thrown onto the page. That meant no double scrolling in this case (where you have a scrollbar on the browser window and another one on a particular iframe or div in the DOM). That's not so easy when the forms themselves would get longer or shorter in the iframe based on the user toggling a switch or changing an answer. Also, different calculators have different lengths in their iframes. Thus, no hardcoding allowed because we wanted to reuse the pages and components wrapping those calculators.
You may use the following techniques if
One last word of caution. Be careful which html files or urls you include, otherwise you may expose your users to XSS.
Ok, so far so motivated...
If you want to dive right into the code and see the working example without much further ado, then click here.
If you've read past this point, then I'll provide some context to the code now.
The difficulty is that React's virtual DOM needs to work with the actual
DOM. Some elements like the iframe
's content may load after the virtual
DOM has already mounted the components. Or a user may drag the side of the browser and that
could change the height of the iframe
's content. What we need is for the
component to be told what the height of the iframe's content wants to be and
when that changes. The component could then pass down that height to the
components that wrap the iframe
.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { iframeHeight: undefined };
}
...
}
You can see that my App
component has only one variable in its state,
iframeHeight
. We use will use it to set the height of the elements that contain
the iframe
.
componentDidMount() {
this.checkIfIframeLoaded();
}
checkIfIframeLoaded = () => {
const iframe = document.getElementById('responsive-iframe');
const iframeDoc = iframe.contentDocument ||
iframe.contentWindow.document;
if (iframeDoc.readyState === 'complete' &&
iframeDoc.getElementsByTagName('body').length) {
this.onElementHeightChange(iframe, this.updateIframeHeight);
return;
}
window.setTimeout(this.checkIfIframeLoaded, 100);
}
As soon as the component is done mounting, we check if our particular iframe
exists in the DOM yet. We use the DOM API to see if we can find an iframe
with
a particular id
, in this case 'responsive-iframe'
, and whether the
document inside of it is done loading and
contains a body
tag. If it does, we can get the height of the body and update
iframeHeight
. If not, we wait for 0.1 seconds and try again.
When iframeHeight
changes, we re-render the component.
{% raw %}
render() {
const { iframeHeight } = this.state;
return (
<div className="container" >
<div>
Let's embed this iframe below like
it's part of our site! No double-scrolling
</div>
<div
className="iframe-container"
style={{
...(iframeHeight
? { height: iframeHeight }
: {}
)
}}
>
<iframe
id="responsive-iframe"
style={{
...(iframeHeight
? { height: iframeHeight }
: {}
)
}}
srcdoc={iframeContent}
>
</iframe>
</div>
<div>
Let's embed this iframe above like
it's part of our site! No double-scrolling
</div>
</div>
)
}
{% endraw %}
I'm conditionally applying styles depending on whether we have a height to work with or not. Note that inline styles like this give us a small performance hit.
Okay, if you were reading closely, you noticed that I glossed over
onElementHeightChange
and updateIframeHeight
. What's happening in those?
onElementHeightChange = (el, callback) => {
let lastHeight = el
.contentWindow
.document
.getElementsByTagName('body')[0]
.scrollHeight;
let newHeight;
(function run() {
let bodies = el
.contentWindow
.document
.getElementsByTagName('body')
if (bodies.length) {
newHeight = bodies[0].scrollHeight;
if (lastHeight !== newHeight) {
callback();
}
}
lastHeight = newHeight;
el.onElementHeightChangeTimer = setTimeout(run, 750);
})();
}
We compare the current height of the iframe, lastHeight
, with its newHeight
.
What's with the run
function in parentheses, you ask? It's a recursive function that we
call right away (IIFE).
Basically, we call the run
function, it gets the new height of the iframe
and if that height is different from the previous one, we call the callback
function, which in our case is updateIframeHeight
and we'll discuss it right
after this one. On the last line, we do a setTimeout which will call the run
function again after a certain amount of milliseconds have passed. In practice,
this means that we created a loop that checks every so many milliseconds whether
the height of the iframe has changed, and if so, tells the component via
updateIframeHeight
.
updateIframeHeight = (newHeight) => {
this.setState({ iframeHeight: newHeight });
}
Nothing crazy happening here, just updating the height in the component state... and we're done!
We found a way to continuously have the body inside of an iframe
report its
height to a React component that contains the iframe
. This enabled us to use the
set the height of the containers around the iframe
and make it appear as if it
was just a normal div
that flows and resizes with the rest of the page.
If you have suggestions for better ways to include responsive iframes
, I'd love
to hear about them. Feel free to contact me!
Using the srcdoc
attribute on an iframe
is a fun way to render a full html
string converted to actual markup in a document. This is useful
if you're trying to display the html-version of an email or a scraped website
that you've saved to a text field in your database.
When you access the html or body inside of an iframe with the DOM API you may
notice that the scrollHeight
of the body
and that of the html
element are not the same.
This happens when tags such as body
or p
don't have their margins reset.
That's why you can see me adding style="margin:0;"
to most of the elements in
iframeContent
.
Some people think that you need to clearTimeout
after setTimeout
. However,
that's only the case if you want to stop it before it runs.