Change iframe src without adding history entry

I have an iframe that’s rendering some content. I want to programmatically change the iframe’s src attribute. However I don’t want this change to affect the browser’s history.

My initial attempt is this, but this is causing the browser (Firefox) to add history entries when changing the src:

    get previewUrl(): SafeString {
        return htmlSafe(`...`);
    }
    <iframe src={{this.previewUrl}} />

See also something this question from 2 decades ago: javascript - Reload an IFRAME without adding to the history - Stack Overflow

I’ve never tried this personally but it sounds like the best way is just to replace the iframe element entirely. Also sounds like if the iframe is same-domain you can use iframe.contentWindow.location.replace(...)as in the SO question you posted…

Now my code is rather simple: I change the component’s property, ember changes the DOM node. What is the “ember” way to replace the iframe element?

It’s mildly annoying, but given the specific thing you’re trying to do, I believe this would work:

{{#let this.previewUrl as |url|}}
  <iframe src={{url}} />
{{/let}}

I believe that’ll give you a brand new iframe every time, rather than just updating the src for the current iframe. Assuming it works, I’d also add a comment to that effect to say “this is why we’re doing these weird shenanigans.” :joy:

Interesting hack, however it doesn’t work for me on Ember 3.16.1. The same element remains in the dom and a navigation history entry is added after changing the src-attribute (which is initiated by a <button> triggering an @action).

I’m now forcing ember to remove the node and recreate the node on the next loop of the runloop using next('afterRender') like this:

@action doSomething(): void {
    this.previewUrl = undefined;
    next('afterRender', () => {
        this.previewUrl = htmlSafe("new-url.html");
    });
}
{{#if this.previewUrl }}
    <iframe src={{this.previewUrl}} />
{{/if}}

I’d be interested to hear your opinions on this solution, or should I call it a workaround? And if you have any suggestions for improvements, I’d be interested in hearing them.

two ideas completely off the top of my head…

use {{#with ...}} instead of {{#let ...}} as IIRC with doesn’t render block if the value is falsey so that may mean it would definitely re-render the block if the value changes, just a guess.

make a component for the iframe and see what you have to do to re-render the component, i’m pretty sure it would re-render with something like this, though this is pretty gross:

{{#with (component "iframe-component" src=this.previewUrl) as |IFrame|}}
  <IFrameComponent/>
{{/with}}

As I was thinking about it later I realized that given how the template layer works, it shouldn’t actually rerender there, and I would expect the semantics of with to be the same here. Glimmer will preserve anything it can detect will be stable. Here, it’s clear that the element should be stable regardless of these shenanigans, so Glimmer is doing the right thing.

Cc. @pzuraq and @rwjblue because I can’t actually think of something clean here.

Use two invocations, toggle them with something like toggleProperty. It will force the current one to be torn down and the new one to be rendered fresh.

{{#if this.renderPrimary}}
  <iframe src="{{@iframeURL}}” /> 
{{else}}
  <iframe src="{{@iframeURL}}” />
{{/if}}

Then you make sure your getter for renderPrimary bounces from true to false every time you get a new url to use.

Not super pretty, but should work.

1 Like

Great and useful information, thanks!

I am back with an updated recommendation, because I just hit this in refactoring the LinkedIn ad banner to Octane (!) and thus had cause to think more on how to handle it. You can use a modifier which replaces the iframe location with the location.replace API. That would look like this:

import { modifier } from 'ember-modifier';

export default modifier((iframe, _, { with: targetUrl }) => {
  iframe.contentWindow.location.replace(targetUrl);
});

(You may also want some try/catch handling for cross-origin access attempts.)

Invoking that would look like this:

<iframe
  src="about:blank"
  {{replace-iframe-location with=this.previewUrl}}
></iframe>

That gives you the desired properties and nicely isolates the complexity! The modifier becomes fairly straightforward to test, too, especially compared to trying to test a component class which does this: you just need to mock the url you’re passing in, and then confirm that the iframe renders the corresponding HTML you hand back.

1 Like