Skip to main content

Components which refuse to live in the Shadows

I still owe you guys two more posts about Letsencrypt and Service Workers, but let me just share a short article about my struggle getting Recaptcha working in the Shadow DOM… and a crazy solution how to get such components working none the less.

  1. You can’t use the standard <div class="g-recaptcha"></div>, because Googles script simply won’t see it.
  2. You can’t use the standard Javascript API rendering mechanism, because first of all most of the documentation uses it with an id reference (which isn’t going to be found inside the Shadow DOM), but even if you use a direct element reference there (which will actually show the Recaptcha box) we reach the most painful issue: Recaptcha stays stuck on the spinner after clicking the checkbox. And yes, I checked, it’s really the Shadow DOM causing the issue, likely due to some crazy callback to some global or something.

So how do we get it to work? By putting it in to the light DOM… which is far easier said than done. My solution boiled down to inserting the <script> tag directly in to the head, a callback function which I registered on the window scope and using a simple promise I could await it’s completion in my element definition. By that point we’ve just reached a solution which would work with most other components, but sadly we can’t evade the never ending spinner of doom yet. But let me outline the above here first before continuing:

(function() {

  var recaptcha = new Promise(function(resolve, reject){
    window.recaptchaElementCallback = function() {
      resolve(grecaptcha);
    };

    var head= document.getElementsByTagName('head')[0];
    var script= document.createElement('script');
    [...]
    head.appendChild(script);
  });

  Polymer({
    [...]
    ready: function(){
      recaptcha.then(function(grecaptcha){
        rc.render(this.$.container, {sitekey: [...]});
      });

So right now the easiest way to fix the before mentioned spinner of doom is by lifting even the component itself out of the Shadow DOM.

this.float = document.createElement('div');
this.float.style = 'position:absolute;';
document.body.appendChild(this.float);

recaptcha.then(function(grecaptcha){
rc.render(this.float, {
  sitekey: '[...]'
});

and next display this float exactly above our own element (which we will conveniently give the same height as our the recaptcha box) by listening for iron-resize events from a Polymer.IronResizableBehavior triggering the following function

setLocationOfBodyFloat: function(){
  var rect = this.getBoundingClientRect();
  this.float.style.left = rect.left + 'px';
  this.float.style.top = rect.top + 'px';
  this.float.style.width = rect.width + 'px';
  this.float.style.height = rect.height + 'px';
}

which will load up neatly on your desktop and actually work (the spinner resolves), but when you scroll the page or any scrolling parent container it will float in the wrong place, so we finish off with the following code in the attached callback:

attached: function(){
  parent = this;
  while(parent = parent.parentNode){
    parent.addEventListener('scroll', this.setLocationOfBodyFloat.bind(this));
 }
},

Either way, I will of course release all this as a custom element at some point as well if I find the time, but I thought it would be valuable to write this up as well, just because it might be useful in more cases.

Comments

  1. Thanks for this. I am using it to make a lit-element equivalent. I will shortly appear as one of the elements in my single page app I am building.

    https://github.com/akc42/football-mobile

    I am currently just coding it and haven't started to test it yet, but I think I will need to add some more code in "setLocationOfBody" to add transform scale to it to resize to fit the element I am trying to load it over. But I need to start to test first to check default dimensions before doing the scaling.

    I did use Polymer when it was first out, but I've been using lit-element for a while now. I have another web component app that I am still developing started in September 2012 on JQuery Mobile, switched to Polymer in Feb 2015 and to Lit-Element around about October 2018.

    ReplyDelete

Post a Comment

Popular posts from this blog

Lazy loading in the HTTP/2 world

Already touched upon this topic in my HTTP/2 write-up (don’t do this without setting up HTTP/2 first), but although I did initial testing back then only got back to it seriously yesterday. So, let met give some very quick instructions on how to get it working in PSK, though likely it will be similar in any other Polymer application. <page-*> elements Start of with making every page an element. In my case these elements are called <page-*> elements, so your index.html looks something like: <iron-pages attr-for-selected="data-route" selected="{{route}}"> <page-welcome data-route="home"></page-welcome> [...] </iron-pages> It’s possible to set this up in different ways as well, but this is the best way I have found so far to organize my application in Polymer. Lazy loading them My approach is to trigger the loading of the elements from the router. This might be somewhat debatable from a logical poi...

Do JS libraries belong in the DOM?

UI-less custom elements? So, when you start using Polymer you will at some point encounter <iron-ajax> and if you’re like me you will be somewhat confused by the fact that it’s an element. After all, it has no UI and and seems to be entirely misplaced in the DOM. Now, after that you can do one of two things: Either accept it as ‘the way it works in Polymer land’ or you can just ignore it and not use elements like that. I - somewhat uncomfortably - did the former 1 and continued making a lot of elements like that. I made complex <auth-api> elements that would consume my API and toolbox elements like <text-utils> that provided a set of functions to work with text. And all seemed good in the land of Polymer. Till on a fateful day I was implementing a complex web worker and I realized I couldn’t use any of those libraries as web workers (and service workers) do not implement the DOM. Which sucked big time as I need functions on my <text-utils> element. Now,...