How to: Preview a resource in an iframe

Let’s say I have a form to create/edit a resource. On a side of this form I want to show a live preview of my resource. Whenever the form changes I need to update the preview.

At a first glance the solution seems pretty easy, right? Use an IFrame and update the src update on every change.

Let’s start with a naïve implementation of this preview feature:

<!-- app/views/admin/posts.html -->

<form class="form" action="action">
<div class="row">
<div class="col-sm-6">
<div class="form-group"><label for="post_title">Title</label>
<input id="post_title" class="form-control" name="post[title]" type="text" /></div>
<div class="form-group"><label for="post_text">Text</label>
<textarea id="post_text" class="form-control" cols="50" name="post[text]" rows="10"></textarea></div>
<div class="form-group"><label for="post_tags">Tags</label>
<input id="post_tags" class="form-control" name="post[tags]" type="text" /></div>
<div class="col-sm-6"><iframe style="width: 100%; min-height: 450px;" width="300" height="150" data-src=""></iframe></div>
</form><script type="text/javascript">
refreshPreview = function() {
  var $iframe = $('iframe'),
      $form = $('iframe').parents('form');
  $iframe.attr('src', $'src') + "?" + $form.serialize());

$(function() {
  $('form input').change(refreshPreview);

First problem: I was trying to send form data through a GET request, this entails that if the form params become too long I may end up receiving a “Request URI too large” error from the web server. Second problem: it’s not appropriate security-wise to send form data through GET requests, they may contain sensible informations.

So, the real question is: how can I render a POST response in an IFrame?

First things first, let’s do the server part. In my case, with Rails, this means adding a custom route for my resource:

# config/routes.rb

namespace :admin do
resources :posts do
post :preview, on: :collection

And adding a preview action in controller:

# app/controllers/admin/posts_controller.rb

class Admin::PostsController < Admin::BaseController
layout 'admin'

def preview
@post = params.require(:post).permit(:title, :text, :tags)
render 'posts/show', layout: 'application'

The preview action is rendering the frontend view with the application layout. Let’s now deal with the IFrame. It cannot do POST requests of course, but we can do them with the XMLHttpRequest object, and thank to HTML5 there is now the FormData object that encapsulates form fields.

This is the final javascript:

fetchPreview = function(url, formData, callback) {
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState != 4) { return false };
};"POST", url);

previewRenderCompleted = function() {};

refreshPreview = function() {
var $iframe = $('iframe'),
$form = $('iframe').parents('form');

fetchPreview($'src'), new FormData($form[0]), function(content) {
$'load', previewRenderCompleted);
$iframe.contentDocument.write(content != null ? content : '');

$(function() {
$('form input').change(refreshPreview);

In the end what we do is to write the returned content in the iframe using its contentDocument. The good thing about this solution is that by using IFrame’s contentDocument to write the response we can also receive the load event on the iframe itself.

This allowed me to add another piece to my preview: scaling the iframe content to fit the iframe itself. This is pretty easy to do in previewRenderCompleted:

previewRenderCompleted = function() {
  var $iframe     = $('iframe'),
      $iframeDoc  = $($iframe.contentDocument),
      iframeWidth = $iframe.width(),
      ratio       = iframeWidth / $iframeDoc.width(),
      cssScale    = "scale(" + ratio + ")",
      cssOrigin   = "0 0";
  $('body', $iframeDoc).css({
    "-webkit-transform-origin" : cssOrigin,
    "-webkit-transform"        : cssScale,
    "-ms-transform-origin"     : cssOrigin,
    "-ms-transform"            : cssScale,
    "-transform-origin"        : cssOrigin,
    "-transform"               : cssScale

Leave a Reply