DEV Community

Cover image for Polyfills - What are they ?
Issam Mani
Issam Mani

Posted on

Polyfills - What are they ?

The web is full of weird terminology, that can be especially daunting for newcomers. The idea behind this blog post and ( hopefully ๐Ÿคž ) upcoming blog posts is to demistify "fancy" terms. In this article we will discuss polyfills.



In plain english



Polyfills are pieces of code that aim to make new features available on browsers ( or JS environments in general ) that don't or won't support said features. Two things to keep in mind:

  • Not all features are polyfillable ( new syntactic features cannot be polyfilled e.g. spread syntax(...).
  • A polyfill only runs, when the feature is missing. Otherwise it should use the native implementation.

A brief history...



The word polyfill was initially introduced [ coined and popularized] by Remy Sharp in 2009. The word itself originates from the name Polyfilla, a british product used to fill cracks and holes in walls.

Polyfilla is a UK product known as Spackling Paste in the US. With that in mind: think of the browsers as a wall with cracks in it. These polyfills help smooth out the cracks and give us a nice smooth wall of browsers to work with.

-- Remy Sharp

Enough history. I said brief, didn't I !



Polyfills vs Transpilers



Before digging any deeper, let's try to make the difference between the terms Polyfill and Transpiler clear.

Remember how I said there is no way to polyfill new js syntax. Well a transpiler ( transformer + compiler ) does just that.

It transforms new syntax into equivalent old syntax that is supported by old browsers. So unlike a polyfill, the code you write is transpiled into alternate code, that would eventually run in the browser.

Keep in mind that a transpiler like babel will use, depending on your target browsers, underneath the hood polyfills to support new features.

If you are still unsure about the difference, here is an SO response that goes into a bit more detail.

Let's write our own polyfill

All right, let's dig in. As I mentioned before a polyfill is just a piece of code, that aims to make some functionality available across all browsers. Usually a good polyfill will check if the target feature already is supported in the browser. If so do nothing da! Otherwise use the available APIs to mimic the behavior of this new feature.

JS : Element.closest(selectorList)

According to MDN:

The closest() method traverses the Element and its parents (heading toward the document root) until it finds a node that matches the provided selector string. Will return itself or the matching ancestor. If no such element exists, it returns null.

So basically given an HTML Element the closest() method returns the closest element in the elements tree that matches at least one of the selectors in the selectorList.

Assume we have the following HTML DOM:

...
<div class="level-1">
    <div class="level-2">
        <p class="level-3"> Polyfills are awesome ๐ŸŽ‰ </p>
    </div>
</div>
...
Enter fullscreen mode Exit fullscreen mode
const paragraph = document.querySelector('p');
paragraph.closest('.level-1'); // Returns <div class="level-1">...</div>
paragraph.closest('.level-1, .level-2'); // Returns <div class="level-2">...</div>
paragraph.closest('.level-3'); // Returns paragrah itself
paragraph.closest('.level-bla'); // Returns null
Enter fullscreen mode Exit fullscreen mode

All right ! Now that we know how this function works , we can start implementing.


๐Ÿ‘‹ Beginner's Tip: This is a good time to open codepen and start experimenting.


Let's think about the problem for a second (or more). We need:

  1. A way to traverse the DOM upwards.
  2. To check if element matches the selectorList.

Now, let's talk solutions:

  1. A way to traverse the DOM upwards *=> use a while loop and the .parentElement prop.
  2. To check if element matches the selectorList => use the .matches() method.
const closest = (element, selectorList) => {
    while(element && !element.matches(selectorList)) {
        element = element.parentElement;
    }
    return element;
};
Enter fullscreen mode Exit fullscreen mode

So in the snippet above, we are defining a function that takes two arguments: element and selectorList. Then we are looping until one of two things happen:

  • element is null, and therefore we have reached the root element.
  • element.matches(selectorList) returns true, and therefore we found the closest element that matches our selectorList.

We can check that this beahves the same way on our previous test set.

...
const paragraph = document.querySelector('p');
closest(paragraph, '.level-1'); // Returns <div class="level-1">...</div>
closest(paragraph,'.level-1, .level-2'); // Returns <div class="level-2">...</div>
closest(paragraph,'.level-3'); // Returns paragrah itself
closest(paragraph,'.level-bla'); // Returns null
Enter fullscreen mode Exit fullscreen mode

The last step is to add the function to the Element's prototype, so that it's available to all instances of the Element object:

Element.prototype.closest = (selectorList) => {
    let element = this;
    while(element && !element.matches(selectorList)) {
        element = element.parentElement;
    }
    return element;
};
Enter fullscreen mode Exit fullscreen mode

One last detail, is that we would rather prefer if our polyfill somehow adds this function to the prototype only if the browser doesn't support it. In other words, we would rather use the browser's native implementation if it's available. A simple if will do !

if(!Element.prototype.closest) {
    Element.prototype.closest = (selectorList) => {
        let element = this;
        while(element && !element.matches(selectorList)) {
            element = element.parentElement;
        }
        return element;
    };
}
Enter fullscreen mode Exit fullscreen mode



โœจ NOTE: This is by no means a production-ready polyfill. For simplicity I assumed a lot of things. A production-ready polyfill would also account for the fact that .matches() may not exist and also check different browser vendor prefixes. A more complete version can be found here

CSS: :blank

As of the time of writing the :blank pseudo-class has very low support . In this section we will try to write a rough polyfill (not complete by any means) just to demonstrate the idea of polyfilling CSS functionality.

Again I will quote the MDN definition ( as one normally does ! ) :

The :blank CSS pseudo-class selects empty user input elements (e.g. <input> or <textarea>).

So using the :blank pseudo-class will look something like this

input:blank{
  background: red;
}
textarea:blank{
  color: green;
}

Enter fullscreen mode Exit fullscreen mode



โœจ NOTE1: Since this is a syntactic proprety of CSS using textarea:blank will be ruled out as an invalid selector by the CSS Parser. So instead we will use textarea[blank]. Just keep in mind that in the rest of this post I will use :blank and [blank] interchangeably.


โœจ NOTE2: This is actually what PostCSS does underneath the hood. It replaces all occurences of :blank with [blank].



Let's think about how we can achieve this. We need :
1. Some way to access our stylesheet(s).
2. Detect selectors of the form selector[blank].
3. Bind our the blank pseudo-class to the selected elements.
4. Update styles when value of input is changed.



๐Ÿ‘‹ Beginner's Tip: This is a good time to open codepen and start experimenting.

These are our requirements. Let's talk about how we can tackle each and everyone:

1. Some way to access our stylesheet => CCSOM
2. Detect :blank function => use a regex
3. Bind our the blank pseudo-class to the selected elements => Add an attribute to the selected inputs
4. Update the value of the state of the input when the the value is changed via JS => listen for the input event

1. Access our stylesheets

First we need to access our CSS Stylesheets. We do so by using CCSOM, specifically by accessing the styleSheets prop on the document.

for(let  styleSheet  of  document.styleSheets) {
  for(let  cssRule  of  styleSheet.cssRules) {
    console.log(cssRule.cssText); // Prints each css rule in our stylesheets
  }
}
Enter fullscreen mode Exit fullscreen mode

More on CSSRule

2. Locate selectors with :blank pseudo-class

Now that we have access to all the CSS rules we can check if any of them have the :blank pseudo-class.

const blankRegex = /(.*)\[blank\]/;
for(let  styleSheet  of  document.styleSheets) {
  for(let  cssRule  of  styleSheet.cssRules) {
    const match = cssRule.selectorText.match(blankRegex);
    if(match) {console.log(match[1]);} // Prints matched selector name i.e input, textarea without the [blank]
  }
}
Enter fullscreen mode Exit fullscreen mode



๐Ÿ›‘ Refactoring detour
Let's try to refactor our code so that it doesn't get messy.
Let's start by defining a function that's responsible for returning an array of selectors.

const extractBlankSelectors = () => {
      const blankRegex = /(.*)\[blank\]/;
      // Returns an array of of all CSSRules
      const cssRules = 
        [...document.styleSheets]
          .map(styleSheet => [...styleSheet.cssRules])
          .flat();

      // Returns an array with our desired selectors
      const blankSelectors = 
        cssRules.map(cssRule => cssRule.selectorText)
                .reduce((acc, curr) => acc.concat(curr.split(",")), [])
                .map(selectorText => selectorText.match(blankRegex) ? selectorText.match(blankRegex)[1] : null)
                .filter(selector => !!selector);

      return blankSelectors;
    };
Enter fullscreen mode Exit fullscreen mode

Here I used a functional approach instead of using for loops as before, but you can achieve the same with the nested for loops. If this looks weird or confusing to you here a nice article talking about this. Also feel free to ask in the comments section.

๐Ÿ›‘Refactoring detour end

3,4. Bind our the blank pseudo-class to the selected elements and watch for change !

Now that we have access to the the desired selectors via extractBlankSelectors we can easily select and add attributes to our target elements.

....
    const bindBlankElements = () => {
      // Select all elements from DOM that match our SelectorList
      const elements = document.querySelectorAll(extractBlankSelectors().join(','));
      elements.forEach(element => {
        // Add blank attribute if value is empty
        if(element.value === '') {
          element.setAttribute('blank', '');
        }
        // Attach an input event listener
        element.addEventListener('input', (ev) => {
          element.value === '' ? element.setAttribute('blank', '') : element.removeAttribute('blank', '');
        });
      });
    };
Enter fullscreen mode Exit fullscreen mode

Initially we select all the elements returned from extractBlankSelectors. Then for each one of them:

  • Add blank attribute if the value is empty.
  • Attach an input event listener. On eah input the callback checks wether the input value is empty or not and applies the blank attribute accordingly.

Here is a codepen to try it out live:

Et voilร  ! We are all done !

Conclusion

As you can see polyfills are a super important concept, that helped the web move forward and developers use new features without the fear of breaking compatibility. If you enjoyed reading this like, share and/or leave a comment. Feedback is always welcome !

Live long and prosper ๐Ÿ––

Useful links

Top comments (3)

Collapse
 
devtony101 profile image
Miguel Manjarres

Great post!

Collapse
 
ash_bergs profile image
Ash

Fantastic post, very informative way to unpack one of those "banana" words that can be so vague.

Collapse
 
issammani profile image
Issam Mani

Thanks for the kind words. Stay awesome ๐Ÿ˜€