Polyfills - What are they ?
Tags: webdev, javascript, css, beginners
Published: 2021-02-12 13:35:10

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.
<br/>
<br/>
# In plain english
<br/>
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(...)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax).
- A polyfill only runs, when the feature is missing. Otherwise it should use the native implementation.

<br/>
<br/>
# A brief history...
<br/>
The word polyfill was initially introduced [ coined and popularized] by [Remy Sharp](https://remysharp.com/2010/10/08/what-is-a-polyfill/) 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 !
<br/>
<br/>
# Polyfills vs Transpilers
<br/>
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](https://stackoverflow.com/a/48149493) 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:
```html
...
<div class="level-1">
	<div class="level-2">
		<p class="level-3"> Polyfills are awesome 🎉 </p>
	</div>
</div>
...
```
```js
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
```
All right ! Now that we know how this function works , we can start implementing.
<br/>
:wave: **Beginner's Tip**: This is a good time to open [codepen](https://codepen.io/pen/) and start experimenting.
<br/>
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*.

```js
const closest = (element, selectorList) => {
	while(element && !element.matches(selectorList)) {
		element = element.parentElement;
	}
	return element;
};
```
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. 
```js
...
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
```
The last step is to add the function to the Element's [`prototype`](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes), so that it's available to all instances of the `Element` object: 
```js
Element.prototype.closest = (selectorList) => {
	let element = this;
	while(element && !element.matches(selectorList)) {
		element = element.parentElement;
	}
	return element;
};
```
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 !

```js
if(!Element.prototype.closest) {
	Element.prototype.closest = (selectorList) => {
		let element = this;
		while(element && !element.matches(selectorList)) {
			element = element.parentElement;
		}
		return element;
	};
}
```
<br/>
:sparkles: **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](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill)
<br/>

## CSS: `:blank`
As of the time of writing the `:blank` pseudo-class has very [low](https://developer.mozilla.org/en-US/docs/Web/CSS/:blank) 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 
```css
input:blank{
  background: red;
}
textarea:blank{
  color: green;
}

```
<br/>
:sparkles: **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.
<br/>
:sparkles: **NOTE2:** This is actually what [PostCSS](https://github.com/csstools/css-blank-pseudo/blob/HEAD/README-POSTCSS.md) does underneath the hood. It replaces all occurences of `:blank` with `[blank]`.

<br/>
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.

<br>
:wave: **Beginner's Tip**: This is a good time to open [codepen](https://codepen.io/pen/) and start experimenting. 
</br>

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

**1.** Some way to access our stylesheet **=> [`CCSOM`](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model)**
**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`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/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.

```js
for(let  styleSheet  of  document.styleSheets) {
  for(let  cssRule  of  styleSheet.cssRules) {
    console.log(cssRule.cssText); // Prints each css rule in our stylesheets
  }
}
```
More on [`CSSRule`](https://developer.mozilla.org/en-US/docs/Web/API/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.

```js
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]
  }
}
```
<br/>
🛑 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.

```js
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;
    };
```

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](https://opensource.com/article/17/6/functional-javascript) talking about this. Also feel free to ask in the comments section.

🛑Refactoring detour end
<br/>
### 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.
```js
....
    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', '');
        });
      });
    };
```
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: 

{% codepen https://codepen.io/issammani/pen/yLVVWvK %}

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
- [A List of cross browser polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills)
- [Remy Sharp's original post about polyfills](https://remysharp.com/2010/10/08/what-is-a-polyfill/)
- [A third link]()