Using iframes in an Aria Templates application

Introduction

Aria Templates is based on client-side templating: as templates are stored client side, if a piece of data changes, there is no need to manipulate DOM elements to update the UI, it is only needed to refresh a template or a part of it.

When a template is refreshed, the Aria Templates framework erases the DOM structure and creates a new one with the updated data. This works fine in most cases. However, it is not always desirable to get rid of the previous DOM structure, especially for components, such as iframes, which contain some state to preserve, or which are heavy to reload.

This blog post explains how to integrate iframes seamlessly in an Aria Templates application, while preserving the refresh feature, thanks to the @embed:Element widget. The same technique can also be applied for similar components.

1 – The problem

Consider the following template:

{Template {
    $classpath: "iframes.step1.Sample"
}}
    {macro main()}
        {@aria:Button {
            label: "Refresh template",
            onclick: {
                fn: this.$refresh,
                scope: this,
                resIndex: -1
            }
        }/} Last refresh: ${new Date()+""}
        <br /><br />

        <iframe src="http://ariatemplates.com/"
                style="width:990px;height:500px;border-width:0px;display:block;">
        </iframe>
        <br />
    {/macro}
{/Template}

This template properly displays the iframe, and it is naturally possible to navigate inside the iframe. However, as soon as the template is refreshed, the current state of the iframe is lost and the iframe displays the original http://ariatemplates.com page.

Of course, we could say: let’s avoid refreshing this whole template, and only refresh parts of it. That strategy would probably work for very small applications. But if the iframe is supposed to be inside a tab panel or a dialog and the state of the iframe is supposed to be kept when hiding and opening again the panel or the dialog, or if the iframe is contained in a template used as a sub-template of another template in a big application, it will become harder and harder to fight against the concept of refreshes, on which Aria Templates is built.

Fortunately, the @embed:Element widget was designed to solve this kind of issues, as explained in the next section.

2 – A first (nearly working) solution

The @embed:Element widget is expecting in its configuration a reference to a controller. The controller is called just after the widget is inserted in the DOM (onEmbeddedElementCreate), and just before it is removed from it (onEmbeddedElementDispose). As a consequence, it seems easy to create a controller which manages the iframe DOM element, and makes sure it is reused across refreshes. Let’s consider the following code:

Aria.classDefinition({
    $classpath : 'iframes.step2.EmbedController',
    $constructor : function (url, style) {
        var document = Aria.$window.document;
        var domElt = document.createElement('iframe');
        domElt.setAttribute('src', url);
        domElt.style.cssText = style;
        this._domElt = domElt;
        this._domContainer = null;
    },
    $destructor : function () {
        if (this._domContainer) {
            this._domContainer.removeChild(this._domElt);
            this._domContainer = null;
        }
        this._domElt = null;
    },
    $prototype : {
        onEmbeddedElementCreate : function (domContainer) {
            if (this._domContainer) {
                this.$logError("The iframe controller is used in multiple places!");
                return;
            }
            this._domContainer = domContainer;
            this._domContainer.appendChild(this._domElt);
        },

        onEmbeddedElementDispose : function (domContainer) {
            if (this._domContainer == domContainer) {
                domContainer.removeChild(this._domElt);
                this._domContainer = null;
            }
        }
    }
});

For simplicity, each instance of this controller manages a single iframe DOM element. The DOM element is built in the $constructor. When the @embed:Element widget is inserted in the DOM, we simply append the iframe to the domContainer element, which is the DOM element of the widget. When the widget is about to be removed from the DOM (for example: on refresh), we simply do the reverse (removing the iframe from the domContainer).

Now, here is the template which uses the @embed:Element widget and our new controller. Note that the embed widget library needs to be included in the template header:

{Template {
    $classpath : "iframes.step2.Sample",
    $hasScript : true,
    $wlibs: {
        "embed": "aria.embed.EmbedLib"
    }
}}

    {macro main()}
        {@aria:Button {
            label: "Refresh template",
            onclick: {
                fn: this.$refresh,
                scope: this,
                resIndex: -1
            }
        }/} Last refresh: ${new Date()+""}
        &nbsp; {@aria:Button {
            label: "Reset iframe",
            onclick: resetEmbedController
        }/}
        <br /><br />

        {@embed:Element {
            controller: getEmbedController()
        }/}
        <br />
    {/macro}

{/Template}

And here is the associated script:

Aria.tplScriptDefinition({
    $classpath : 'iframes.step2.SampleScript',
    $dependencies : ['iframes.step2.EmbedController'],
    $prototype : {

        resetEmbedController : function () {
            if (this.data.embedController) {
                this.data.embedController.$dispose();
                this.data.embedController = null;
                this.$refresh();
            }
        },

        getEmbedController : function () {
            if (!this.data.embedController) {
                this.data.embedController = new iframes.step2.EmbedController('http://ariatemplates.com/',
                    'width:990px;height:500px;border-width:0px;display:block;');
            }
            return this.data.embedController;
        }

    }
});

An instance of the controller is created the first time it is needed and then it is stored in the data model.

Unfortunately, among the browsers I tested, this solution only works as expected in Internet Explorer, surprisingly. Other browsers (Firefox, Chrome and Safari) reload an iframe when it is moved in the DOM with removeChild and appendChild. This behavior is reported as a bug in Mozilla and in Webkit, but it will probably not change soon, as bug reports were opened in 2004 and 2007.

3 – A second (working) solution

So, how to improve our previous solution to make it work in all browsers? Simply by making sure the iframe always stays at the same place in the DOM structure. We can append it directly to the <body>, and then visually position it absolutely with the appropriate CSS style. When the widget is displayed we compute the position and size of domContainer and apply it to the iframe, and when the widget is disposed, we use the “display:none” style to hide the iframe. Let’s have a look to the updated controller:

Aria.classDefinition({
    $classpath : 'iframes.step3.EmbedController',
    $dependencies : ['aria.utils.Dom'],
    $constructor : function (url) {
        var document = Aria.$window.document;
        var domElt = document.createElement('iframe');
        domElt.setAttribute('src', url);
        domElt.style.cssText = this._currentStyle = this._getStyle();
        document.body.appendChild(domElt);
        this._domElt = domElt;
        this._domContainer = null;
    },
    $destructor : function () {
        this._domContainer = null;
        if (this._domElt) {
            this._domElt.parentElement.removeChild(this._domElt);
        }
        this._domElt = null;
    },
    $prototype : {

        _getStyle : function () {
            if (this._domContainer) {
                var geometry = aria.utils.Dom.getGeometry(this._domContainer);
                if (geometry) {
                    var scroll = aria.utils.Dom._getDocumentScroll();
                    geometry.x += scroll.scrollLeft;
                    geometry.y += scroll.scrollTop;
                    var res = ["border-width:0px;position:absolute;left:", geometry.x, "px;top:",
                            geometry.y, "px;width:", geometry.width, "px;height:", geometry.height,
                            "px;display:block;"];
                    return res.join("");
                }
            }
            return "display:none;";
        },

        updateStyle : function () {
            var newStyle = this._getStyle();
            if (this._domElt && newStyle != this._currentStyle) {
                this._domElt.style.cssText = newStyle;
                this._currentStyle = newStyle;
            }
        },

        onEmbeddedElementCreate : function (domContainer) {
            if (this._domContainer) {
                this.$logError("The iframe controller is used in multiple places!");
                return;
            }
            this._domContainer = domContainer;
            this.updateStyle();
        },

        onEmbeddedElementDispose : function (domContainer) {
            if (this._domContainer == domContainer) {
                this._domContainer = null;
                this.updateStyle();
            }
        }
    }
});

This controller uses the getGeometry method to get the position and size of the domContainer in the viewport, and it adds the current scroll position to get an absolute position (no longer relative to the viewport).

Note that in order to use the new controller, it’s now necessary to specify a size in the attributes of the @embed:Element widget directly:

{@embed:Element {
    controller: getEmbedController(),
    attributes: {
        style: "width:990px;height:500px;"
    }
}/}

With this controller it is now possible to include persistent iframes in templates.

4 – Improving the solution for dialogs

Now, when trying to include an iframe with the controller of the previous section in a dialog, it does not display anything. Why? Here are the reasons and how to solve the issue:

  • Before displaying a dialog, Aria Templates first checks its dimensions, and this is done out of the viewport (with negative coordinates). So, at the time onEmbeddedElementCreate is called, the domContainer is not yet correctly positioned. To fix this, it is necessary to delay a bit the call to updateStyle with setTimeout:
    onEmbeddedElementCreate : function (domContainer) {
        if (this._domContainer) {
            this.$logError("The iframe controller is used in multiple places!");
            return;
        }
        this._domContainer = domContainer;
        setTimeout(aria.utils.Function.bind(this.updateStyle, this), 1);
    }
  • Another reason is that the dialog is displayed with a high z-index value, so it appears above the iframe, hiding it completely. To fix this, it is necessary to apply a z-index value to the iframe. Even if the following method does not take into account the special behaviors of z-Index (see this interesting article), it allows to compute a z-Index value that will probably work for common use cases:
    _computeZIndex : function (element) {
        var zIndex = 1;
        var body = Aria.$window.document.body;
        while (element && element != body) {
            var elementZIndex = parseInt(element.style.zIndex, 10);
            if (!isNaN(elementZIndex)) {
                zIndex = elementZIndex + 1;
            }
            element = element.parentNode;
        }
        return zIndex;
    }
  • In case the dialog is movable, it is important to update the position of the iframe when the dialog is moved. However, as the iframe is not included in the dialog DOM structure, and because of the way the dialog is currently implemented, it is probably best for the user experience to simply hide the iframe when the user starts dragging the dialog and make it visible again when he drops it. This can be implemented simply with a setVisible method on the controller (see the next section for the code to download).

Source code

Here is a zip file containing the source code of the 4 previous sections. To try those samples, you can simply put the iframes folder at the root of your web server. Then you will probably have to update the /iframes/index.html file so that it contains the correct reference to the Aria Templates framework files installed on your web server.

Conclusion

This blog post explained how to use the @embed:Element widget to display persistent iframes with Aria Templates. The same technique can be applied for other components as well. Note that the first solution is working well for maps, and it is used internally by the @embed:Map widget.

If you found this blog post useful, or if you have any interesting remark, please do not hesitate to leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>