About Pym.js

Latest version: 1.3.2 (released Feb. 13, 2018)

Using iframes in a responsive page can be frustrating. It’s easy enough to make an iframe’s width span 100% of its container, but sizing its height is tricky — especially if the content of the iframe changes height depending on page width (for example, because of text wrapping or media queries) or events within the iframe.

Pym.js embeds and resizes an iframe responsively (width and height) within its parent container. It also bypasses the usual cross-domain issues.

Use case: The NPR Visuals team uses Pym.js to embed small custom bits of code (charts, maps, etc.) inside our CMS without CSS or JavaScript conflicts. See an example of this in action.

In version 1.3.0 we have added an optional scroll tracking support. Learn about it here.

Version 1.3.2 is an important security release. We recommend everyone update to 1.3.2 as soon as possible. More information here.

Getting Pym.js

Use the Pym.js CDN

We recommend in most cases that you use the latest stable version of Pym.js hosted on our content delivery network (CDN) at pym.nprapps.org instead of downloading it directly. We will keep the CDN version up-to-date so that we can fix bugs and add new features and you won't have to change anything. Learn more about our versioning strategy.

We recommend using https, but the unsecure http protocol is also supported if you need it.

Download Pym.js

Installing with NPM

If you use NPM to manage your Javascript dependencies then you can install Pym.js by running:

npm install pym.js

Installing with Bower

If you use Bower to manage your Javascript dependencies then you can install Pym.js by running:

bower install pym.js

Having trouble loading Pym.js in your CMS?

Learn about pym-loader.js.

Requirements/Assumptions

  • Pym.js does not require jQuery or any other libraries. Some of the examples use jQuery on the child pages for unrelated features.
  • If you’re pasting the iframe embed code into a CMS or blogging software, make sure that it allows you to embed HTML and JavaScript. Some CMSes (such as WordPress) may strip tags out.
  • The parent and child pages do not need to be on the same domain. If they are not, child URLs should be fully-qualified, e.g., http://blog.apps.npr.org/pym.js/examples/graphic/child.html
  • Pym.js isn't built to handle embedding more than a handful of child frames on the parent. Ten or fewer is a good rule of thumb.

Browser support

Pym.js has been tested in:

  • Internet Explorer 9, 10 (Windows 7), 11 (Windows 8.1)
  • Chrome 32 (Mac 10.9)
  • Chrome 51 (Mac 10.11)
  • Firefox 26 (Mac 10.9)
  • Firefox 47 (Mac 10.11)
  • Safari 7 (Mac 10.9)
  • Safari 9 (Mac 10.11)
  • iOS 7 Safari
  • Android 5.1 Browser

Internet Explorer versions earlier than 9 are not supported. If you want to read more about our tests and testing strategy take a look at the docs in the repo here.

Usage

Resize your browser window to see the responsiveness in action.

On the Parent Page (Where You’re Putting the iFrame)

  • Name your containing div (or any other block-level element) with a unique ID.
  • Do not apply any padding or border to your container div. This will throw off pym's height calculations. If you need padding or border around your container, wrap it in another div and apply that there.
  • Include Pym.js. Only once per page, no matter how many iframes you will have.
  • Create a new pym.Parent(parent_id, child_url); for each responsive iframe. (Pym will generate the actual iframe element.)
  • Optional: To filter incoming message domains, you can pass in a regex. For example: pym.Parent('example', 'child.html', { xdomain: '\\*\.npr\.org' });
<div id="example"></div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>
<script>
    var pymParent = new pym.Parent('example', 'child.html', {});
</script>

Multiple Embeds on the Same Parent Page

You don’t have to do anything special to the child pages, but you do need to keep a few things in mind for the parent page:

  • Each child embed needs a unique div ID (<div id="example-1">, <div id="example-2">, etc.).
  • Include Pym.js only once on the page.
HTML Needed on the Parent Page
<div id="example-1"></div>
<div id="example-2"></div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>
Javascript Needed on the Parent Page
<script>
var pymParent = new pym.Parent('example-1', 'child-1.html', {});
var second = new pym.Parent('example-2', 'child-2.html', {});
</script>

On the Child Page (The Content You Want to Embed)

  • Include Pym.js.
  • Create a new pym.Child();.
  • Optional: If the contents of your iframe are dynamic you will want to pass in a rendering function, like this: pym.Child({ renderCallback: myFunc }); This function will be called once when the page loads and again any time the window is resized.
  • Optional: You can pass in a number of milliseconds to enable automaticaly updating the height at that rate (in addition to onload and onresize events). For example: pym.Child({ polling: 500 });.
  • Optional: If you need finer control over resize events, you can invoke pymChild.sendHeight() at any time to force the iframe to update its size.

Example: Basic Embed

In our simplest example, we have an HTML table that changes format (via CSS media queries). The height of the iframe adjusts to the height of the content onload and onresize.

Javascript Needed on the Child Page
var pymChild = new pym.Child();

Example: Call a Function When the Page Resizes

This might be useful in cases where sections of your child page need to be redrawn based on the new width. (For example, a graphic generated by D3, which would not stretch or reflow on its own.)

Javascript Needed on the Child Page
function drawGraphic(width) {
    ...
}

var pymChild = new pym.Child({ renderCallback: drawGraphic });

Example: Manual resize events

If you have dynamic content and need finer control over resize events, you can invoke pymChild.sendHeight() in the child window at any time to force the iframe to update its size. For example, say you have a quiz, and the content of the page changes when someone selects an answer, affecting the page’s height:

Javascript Needed on the Child Page
function check_answer(e) {
    // highlight the correct answer
    ...

    // send updated height to parent
    pymChild.sendHeight();
}

$('.question').find('li').on('click', check_answer);

var pymChild = new pym.Child();

Example: Resizing after following links in the iframe

If you have links in your iframe that lead to other pages, you can maintain auto-resizing on the subsequent pages by explicitly setting the child ID in each one.

Javascript Needed on Every Child Page
var pymChild = new pym.Child({ id: 'example-follow-links' });
                        

Sending Custom iFrame Events

Although not its original purpose, Pym.js can also be used to send and receive generic event data between the iframe parent and child. A simple, jQuery-like event binding model is used to support this message passing.

In the following example a click on the link in the parent container will navigate the child frame to a new location.

Javascript Needed on the Parent Page
var pymParent = pym.Parent('example', 'child.html', {});

document.getElementById('myLink').addEventListener('click', onLinkClick);

function onLinkClick(e) {
    e.preventDefault();

    pymParent.sendMessage('navigate', e.target.href);
}
Javascript needed on the Child page
var pymChild = new pym.Child();

pymChild.onMessage('navigate', onNavigateMessage);

function onNavigateMessage(url) {
    window.location.href = url;
}

Note that the reverse scenario can be performed as well; children can send messages to the parent with the same sendMessage() function.

Shortcuts for navigating the parent from the child

As scrolling and navigating the parent window is a very common need, there are shortcuts for these actions that do not require you to implement your own events.

Code to scroll and navigate the parent
pymChild.scrollParentTo('about');
pymChild.navigateParentTo('https://github.com/nprapps/pym.js');

Starting in v1.1.0 we have included a new shortcut that allows the child to scroll the parent to a given position within the child document. You can pass the offset desired relative to the child page or use a helper function passing it the id of an element within that child page

Code to scroll the parent to a child position

// download is the id of an existing HTML element on the child page
pymChild.scrollParentToChildEl('download');
pymChild.scrollParentToChildPos(1000);

Here you can see an example of the new scroll shortcut in action.

How It Works

The Pym.js library and a small bit of javascript are injected onto the parent page. This code writes an iframe to the page in a container of your choice. Pym.js will actually replace the contents of the container with the newly created iframe, so it is better to use an empty container from the start to avoid surprises. The request for the iframe’s contents includes querystring parameters for the initialWidth, childId of the child page and parentUrl and parentTitle of the parent page. The initialWidth allows the child to know its size immediately on load, because in iOS, the child frame can not determine its own width accurately. The childId allows multiple children to be embedded on the same page, each with its own communication to the parent.

The child page also includes Pym.js and its own javascript. It initializes cross-iframe communication to the parent, renders any dynamic content and then sends the computed height of the child to the parent via postMessage. Upon receiving this message the parent resizes the containing iframe, thus ensuring the contents of the child are always visible.

The parent page also registers for resize events. Any time one is received, the parent sends the new container width to each child via postMessage. The child re-renders its content and sends back the new height.

Extra parameters can be passed to the parent (title, id, name, ...) through a config object that would then be applied as attributes to the generated iFrame. This is a varying list so it is better to check the API documentation for an up-to-date detailed list

Auto-initializing Pym.js

Certain CMSes prevent custom Javascript from being embedded on the page. In order to allow pym to support those environments, it can be initialized via data- attributes. This limits usage of pym to simple cases, but should still be enough for many users.

(NPR member stations: Use the loader workaround to get this to work in Core Publisher.).

HTML Needed on the Parent Page
<div data-pym-src="child.html">Loading...</div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>

Starting on v1.1.0 you can pass configuration parameters as HTML data attributes by just prefixing the parent configuration parameter with data-pym-. To check all the available parent configuration parameters check the API documentation here

<div data-pym-src="child.html" data-pym-allowfullscreen>Loading</div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>

pym-loader.js: The Pym.js loader

Some content management systems prevent custom Javascript from being embedded on the page, others use pjax to load content, and still others use RequireJS to load libraries. Since Pym.js was designed as a library with support for inclusion using AMD and CommonJS we have encountered certain CMSes scenarios were Pym.js broke in some cases or did not load at all.

That's why we created pym-loader.js, an additional script that acts as a wrapper to deal with all the nitty gritty details to successfully load Pym.js in many common cases. pym-loader.js was developed after much thought and discussion with developers using Pym.js.

Including pym-loader.js on your page automatically and immediately initializes all Pym.js-enabled iframe elements. It works when included via a script tag in environments that use RequireJS and it gracefully handles the complexities of `pjax` navigation.

pym-loader.js works for the reasonable amount of edge use cases that we have encountered so far and it is our hope it will work for others as well. However, we can't cover all cases. In the future, a one-for-all solution may not work and we may need to release specific loaders for specific edge cases. But at least the Pym.js library will remain isolated from these edge cases.

If you have a reasonable amount of control over your CMS's Pym.js implementation, we recommend the raw inclusion of Pym.js as shown in our examples. If you do not have that control over your CMS, are having problems loading Pym.js or just prefer to feel more protected against future changes to your CMS then you can use the loader script.

HTML Needed on the Parent Page
<p data-pym-src="child.html">Loading...</p>
<script type="text/javascript" src="https://pym.nprapps.org/pym-loader.v1.min.js"></script>

Here you can see an example of pym-loader.js in action together with a page that uses require.js to load its javascript assets

Getting the Pym.js loader

Use the Pym.js CDN

We recommend in most cases that you use the latest stable version of Pym.js hosted on our content delivery network (CDN) at pym.nprapps.org instead of downloading it directly. We will keep the CDN version up-to-date so that we can fix bugs and add new features and you won't have to change anything. Learn more about our versioning strategy.

We recommend using https, but the unsecure http protocol is also supported if you need it.

Download pym-loader.js

Pym Status Custom Events

Starting on v1.2.0 we have added Custom Events that can be listened to in order to determine some key events that happen inside Pym.js.

We have added two Custom Events, one inside pym-loader.js and one inside Pym.js.

  • pym-loader:pym-loaded → This event signals when the loader has finished loading Pym.js. You can listen to it following this example.
  • document.addEventListener('pym-loader:pym-loaded', function (e) {
        console.log("pym-loaded event fired");
        console.log(pym);
    }, false);
    
  • pym:pym-initialized → This event signals when Pym.js has finished executing its autoinitialization. You can listen to it following this example.
  • document.addEventListener('pym:pym-initialized', function (e) {
        console.log("pym-initialized event fired);
        console.log(pym.autoInitInstances.length);
    }, false);
    

Note: Pym.js executes autoInit when it gets loaded on the page, since at that time Pym.js itself is still not reachable we do not raise the pym:pym-initialized event then. You can always invoke pym.autoInit() manually from the parent and that will always raise pym:pym-initialized. For example pym-loader.js does exactly that for convenience and compatibility with the multiple CMS configurations where Pym.js is used out there in the wild.

Pym Optional Throttled Scroll Tracking

Starting on v1.3.0 we have added the option in Pym.js for the parent to include scroll tracking and send the viewport and iframe position to the child in a custom iframe event viewport-iframe-position.

In order for Pym.js to add that scroll tracking you'll need to pass a config parameter to the parent.

<div id="example"></div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>
<script>
    var pymParent = new pym.Parent('example', 'child.html', {trackscroll: true});
</script>

The scroll is throttled so that we do not fire an excesive amount of messages to the child. the default throttle time is 100ms, but you can change it if needed, like this:

<div id="example"></div>
<script type="text/javascript" src="https://pym.nprapps.org/pym.v1.min.js"></script>
<script>
    var pymParent = new pym.Parent('example', 'child.html', {trackscroll: true, scrollwait: 500});
</script>

Once the Pym.js parent is configured to track scroll it will send a custom iframe event to the child each time the scroll changes on the parent page, the message viewport-iframe-position contains the following information separated by spaces:

  • Viewport width and height.
  • Iframe top, left, bottom and right positions.

this would be the code needed on the child to listen for that information:

var pymChild = new pym.Child();

pymChild.onMessage('viewport-iframe-position', onScroll);

function onScroll(parentInfo) {
    console.log(parentInfo) // would display for example: 874 776 1091 8 1673 866
}

With that information the child has all the information needed to make the calculations on whether one of its elements is or is not on the current viewport and perform whatever action is required accordingly. (i.e. lazyload visual assets).

Here you can see an example of the new scroll tracking in action. In the example we have added a small tracker library that makes the calculations needed to verify if an element is on the viewport and toggles the background of the element whenever is visible.

Get parent position information

Starting on v1.3.1 we have added a new function to the child in Pym.js in order to be able to ask the parent to send information regarding the current viewport and iframe position to the child.

This addresses the issue of initial load of a child page with lazyloading where we may be tracking the scroll on the parent but since there was no scroll event yet, the child does not have information on positioning of the iFrame on the parent.

var pymChild = new pym.Child();
pymChild.onMessage('viewport-iframe-position', function(parentInfo) {
    console.log(parentInfo);
});
pymChild.getParentPositionInfo();

With this new functionality the child can request the parent position information on load and act accordingly.

Pym.js in the wild

License

MIT