Sunday, June 15, 2014

Comparing the Original Owner Document


This is going to be a little esoteric, I think...

I have a means to replicate the old, deprecated applyAuthorStyles in Polymer elements. The idea is that I want certain elements—especially native elements like buttons—to look the same inside of custom elements as they do on the rest of the page. Until recently, this was the purview of the applyAuthorStyles setting in Polymer objects.

The working solution that I have is something of a brute force approach to the problem: I copy all stylesheet <link> and <style> tags into the shadow DOM of my custom element. It turns out that, with a few tweaks, this approach works just fine—even with odd orange borders applied to Bootstrap.com buttons:



A nice benefit of this approach is that there is no secondary request for external <link> stylesheets. The browser makes a request for the resource when it sees the <link> tag in the <head> of the page, but then relies on browser cache when it see a request for the same resource later:



The only drawback to this approach is that it pulls in all of the stylesheet <link> and <style> tags into the shadow DOM—even those that were added dynamically by Polymer. This does not result in any additional HTTP requests nor does it appear to affect styles, but I still object to this.

To get around this, I am going to compare the current CSS-related content in the main page—the “owner document” in Polymer parlance—with the original content of the main page. The only way to accomplish this (that I know of) is to re-fetch the original document via XHR. This seems ugly, but let's get on with it...

I replace yesterday's immediate copying of page styles into my Polymer element:
  attached: function() {
    this.addExternalCss();
    this.updatePizzaState();
  },
With a fetch of the original page via XHR:
Polymer('x-pizza', {
  created: function(){
    this.fetchOriginalOwnerDocument();
  },

Or, since this is Polymer, I use <core-ajax> to fetch the resource:
  fetchOriginalOwnerDocument: function() {
    var doc = this.originalOwnerDocument = document.createElement('core-ajax');
    doc.addEventListener('core-complete', this.addExternalCss.bind(this));
    doc.url = this.ownerDocument.location.href;
    doc.go();
  },
When the original owner document is fetched, then I call yesterday's addExternalCss() method. It still runs through the children of the owner document's <head>. Instead of skipping non-<style> and non-<link> tags. I now need an additional check to skip text that does not match content in the original document. Regular expressions to the rescue:
  addExternalCss: function() {
    var originalHtml = this.originalOwnerDocument.response;

    for (var i=0; i<this.ownerDocument.head.children.length; i++) {
      var el = this.ownerDocument.head.children[i];
      if (el.tagName != 'LINK' && el.tagName != 'STYLE') continue;

      var re = new RegExp(el.outerHTML);
      if (!re.test(originalHtml)) continue;

      var clone = el.cloneNode(true);
      if (clone.href != 'undefined') {
        clone.href = new URL(clone.href, document.baseURI).href;
      }
      this.shadowRoot.appendChild(clone);
    }

    // TODO: is this really the best way to handle this?
    this.element.convertSheetsToStyles(this.shadowRoot);
  },
That actually does the trick. I now have only three new stylesheet related tags appended to my shodow DOM:



I do find that some of the dynamically added CSS does not generate valid regular expressions. Rather than spend time parsing them and dealing with edge cases, I ignore them for now with a lovely try-catch:
  addExternalCss: function() {
    var originalHtml = this.originalOwnerDocument.response;

    for (var i=0; i<this.ownerDocument.head.children.length; i++) {
      var el = this.ownerDocument.head.children[i];
      if (el.tagName != 'LINK' && el.tagName != 'STYLE') continue;

      var re;
        try {
          re = new RegExp(el.outerHTML);
        } catch (x) {
          continue;
        }
      if (!re.test(originalHtml)) continue;

      var clone = el.cloneNode(true);
      if (clone.href != 'undefined') {
        clone.href = new URL(clone.href, document.baseURI).href;
      }
      this.shadowRoot.appendChild(clone);
    }

    // TODO: is this really the best way to handle this?
    this.element.convertSheetsToStyles(this.shadowRoot);
  },
That method is getting out of hand. It was not great before the try-catch, but it just looks way too big now. Still, it works. Hopefully I can distill this down into something easily resusable. Tomorrow.


Day #94

No comments:

Post a Comment