In designing this blog, I knew I would need to easily include external experiments. I decided on CodePen, and wanted to be able include them easily.

The blog is hosted on GitHub pages, so I couldn’t add a Liquid plugin, so decided a simple Web component would do the trick best.

I don’t want to include Polymer, so built a native web component implementation with usage based on an existing Polymer version.

Here’s an example:

<codepen-embed id="lvCFr" height="400"></codepen-embed>

It’s simple - it’s essentially a smart wrapper for inserting an iFrame. Code should be easy to follow below.

Indeed, to maintain universal browser support, there is an included fallback which simply inserts an iFrame when web component support is incomplete. The easy implementation allowed me to use the web component across the blog without needing to include Polymer to polyfill.

And the template:

1 <template id="codepenEmbedTemplate" hidden>
2   <!-- we add the hidden attribute for Safari + IE so they don't try to render it -->
3   <style type="text/css">
4     /* custom styles we want to add */
5   </style>
6 
7   <iframe></iframe>
8 
9 </template>

and the Javascript:

  1 <script>
  2 
  3   // Attribute for codepenId
  4   var idAttr = 'id';
  5 
  6   // Our list of supported parameters - must be snake-case
  7   var supportedParams = ['height', 'theme-id', 'slug-hash', 'default-tab'];
  8 
  9   // Create the Codepen URL
 10   // Expects snake-case keys as defined in supportedParams
 11   function createUrl(params) {
 12     var attrs = [];
 13     Object.keys(params).forEach(function(key) {
 14       key != 'id' && attrs.push(key + '=' + params[key]);
 15     });
 16     return '//codepen.io/anon/embed/' + params.id + '?' + attrs.join('&amp;')
 17   }
 18 
 19   // Given a node, determine the attrs defined that we support
 20   function createParams (node) {
 21     // Get the id and parameters
 22     var codepenParams = {
 23       id : node.getAttribute(idAttr)
 24     };
 25     supportedParams.forEach(function (paramName) {
 26       var attr = node.getAttribute(paramName);
 27       if (attr) {
 28         codepenParams[paramName] = attr;
 29       }
 30     });
 31 
 32     //define a default height
 33     codepenParams.height = codepenParams.height || 300;
 34 
 35     return codepenParams;
 36   }
 37 
 38   // Given an iframe, set the src, height, and id based on params, and other attrs
 39   // Must pass params object
 40   function modifyIframe (iframeEl, params) {
 41 
 42     //basics
 43     iframeEl.setAttribute('scrolling', 'no');
 44     iframeEl.setAttribute('framborder', '0');
 45     iframeEl.setAttribute('allowtransparency', 'true');
 46     iframeEl.setAttribute('class', 'cp_embed_iframe');
 47     iframeEl.setAttribute('style', 'width: 100%; overflow: hidden;');
 48 
 49     //dynamic
 50     iframeEl.setAttribute('src', createUrl(params));
 51     iframeEl.setAttribute('height', params.height + 'px');
 52     iframeEl.setAttribute('id', "cp_embed_" + params.id);
 53   }
 54 
 55   if ('registerElement' in document) {
 56     // We support web components!
 57 
 58     // ID of our template
 59     var tmplId = '#codepenEmbedTemplate';
 60 
 61     // Grab our template full of slider markup and styles
 62     var tmpl = document.querySelector(tmplId);
 63 
 64     // Create a prototype for a new element that extends HTMLElement
 65     var CodepenProto = Object.create(HTMLElement.prototype);
 66 
 67     // Setup our Shadow DOM and clone the template
 68     CodepenProto.createdCallback = function() {
 69       var root = this.createShadowRoot();
 70 
 71       var codepenParams = createParams(this);
 72       var iframeEl = tmpl.content.querySelector('iframe');
 73 
 74       modifyIframe(iframeEl, codepenParams);
 75 
 76       root.appendChild(document.importNode(tmpl.content, true));
 77     };
 78 
 79     CodepenProto.attachedCallback = function() {
 80       //onload event
 81     };
 82 
 83     // Register our new element
 84     // Note that unless this is an import, this will be global
 85     var CodepenEmbed = document.registerElement('codepen-embed', {
 86       prototype: CodepenProto
 87     });
 88 
 89   } else {
 90     // We need a fallback!
 91     // Let's put in a iframe basically the same way
 92 
 93     document.addEventListener( "DOMContentLoaded", function () {
 94       // writing our own forEach because avoid [].forEach.call(nodeList) hack
 95       // thoughts articulated at http://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/
 96       var forEach = function (array, callback, scope) {
 97         for (var i = 0; i < array.length; i++) {
 98           callback.call(scope, array[i], i); // passes back stuff we need
 99         }
100       };
101 
102       forEach(document.querySelectorAll('codepen-embed'), function (node, index) {
103 
104         var codepenParams = createParams(node);
105 
106         // no innerHTML()!
107         var iFrameEl = document.createElement('iframe');
108         modifyIframe(iFrameEl, codepenParams);
109 
110         node.appendChild(iFrameEl);
111 
112       });
113     }, false);
114   }
115 
116 </script>

Because of the limited support for HTML imports, I just added a flag to my Jekyll include template which checks for a codepen flag, and if present includes the Codepen-embed template + script.