Knockout objects


So, at work I came up against an intere^H^H^H^H^H^H infuriating problem with cross-browser embedding of PDF documents, and I want to document it here as there really were no leads at all that I could find with a Google search.

The Situation

A function of the system I work on is document management, and the web project I am working on will give members access to documents pertaining to them, subject to all of the neccesary security restrictions. Many of the documents produced and managed by the system are stored as, or can be converted to, PDF files.

I was working on the frontend for displaying these documents, and ran in to cross-browser compatibility issues (I’m looking at you, as ever, Internet Explorer…) and issues in page rendering caused by timing issues related to Knockout.js, the framework which we’re using to data-bind our views to our view controllers etc.

When I started, the document viewer’s code to include a PDF in line was as follows:

<embed type="appliction/pdf" data-doctype="pdf" data-bind="attr: { src: EmbedUrl }"></embed>

Picking the right element

That’s pretty readable, but I don’t think that <embed> is the correct element to use for this task. It’s an HTML5 element, but its use by IE for this task far predates the advent of HTML5. In the HTML5 spec, it is introduced as:

[…] an integration point for an external (typically non-HTML) application or interactive content.

Which doesn’t suggest document to me. I could almost understand if you said the integration is to a PDF reader plugin, but that isn’t my intent, just a way of achieving the goal of displaying a PDF inline on the page. Fortunately, HTML5 offers a more appropriate element, <object>, which:

[…] can represent an external resource, which, depending on the type of the resource, will either be treated as an image, as a nested browsing context, or as an external resource to be processed by a plugin.

Now that’s exactly what we’re looking for. Brilliant, let’s get on with it! I merrily change things up to the following:

<object type="appliction/pdf" data-doctype="pdf" data-bind="attr: { data: EmbedUrl } />

and that’s where the fun really starts. Load this up in IE - nothing, just a big old empty grey box where the plugin should be. Load it up in Firefox - the Adobe plugin fires up, displays the document, and also an informational warning at the top of the screen to say that “This PDF document might not be displayed correctly.” Huh? It’s right there, and my data-bind is resolving just fine too… In Chrome it’s even worse - it gets a big grey box with “Couldn’t load plugin” and a jigsaw puzzle peice emblazoned in the middle, and a yellow error at the top of the screen to tell the user that it “Could not load Chrome PDF viewer”.

data-bind it all…

After much headscratching, and some great help from the all knowing Stack Overflow, I got the issue that was causing the problems in Chrome and Firefox nailed down - the browser parses the object tag before data-binding is resolved, and fires up the plugin then. At that point, the object tag looks like this:

<object type="application/pdf" data-doctype="pdf" />

which means there’s no URL for the plugin to load, which really confuses the browser and the plugin, and leads to the unpredictable behaviour shown in Chrome and Firefox (but not IE, which just plain doesn’t know what to do with an object tag, at least prior to v10). The solution would be as follows:

<object data-bind="attr: { data: EmbedURL, type: 'application/pdf', 'data-doctype': 'pdf' }" />

which does fix the problems in both Firefox and Chrome, but unfortunately it’s invalid, as the spec for <object> requires at least a type or data attribute be present. We want it to be valid at load time, and for users who do not have javascript enabled (not that it will do a lot, but you know, principles etc.) To make it valid before bindings resolve, we can just take advantage of the fact that data bindings in Knockout overwrite the existing values of attributes:

<object type="application/pdf" data="#" data-doctype="pdf" data-bind="attr: {data: EmbedURL, type: 'application/pdf', 'data-doctype': 'pdf'} />

Circling back to IE

The type and data-doctype within the binding may be superfluous now, but I think they help to communicate intent and that there’s an issue with doing this “the obvious way”. Unfortunately, if we load this page up in IE, we now don’t even get the big grey box… fortunately <object> has us covered, as we can provide a fallback element inside the <object>.

<object data="#" type="application/pdf" data-docType="pdf" data-bind="attr: { data: DownloadPDFLink, type:'application/pdf' }">
    <embed data-bind="attr: { src: DownloadPDFLink, type: 'application/pdf' }" />
</object>

And there you have it! Strictly, I think that the <embed> within the object is still not valid, but it works well and it’s as close as I could get while meeting my requirements. A better approach given the freedom would be to use the fallback capability to provide a download link, and then retain full compliance to HTML5, but not providing the feature to IE users is not acceptable in the requirements of this project. I hope that’s of help to someone, let me know on Twitter if you have any comments!