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 theElement
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 returnsnull
.
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>
...
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.
👋 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:
- A way to traverse the DOM upwards.
- To check if element matches the selectorList.
Now, let's talk solutions:
- A way to traverse the DOM upwards *=> use a while loop and the
.parentElement
prop. - 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;
};
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 ourselectorList
.
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
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;
};
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;
};
}
✨ 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;
}
✨ 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
}
}
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]
}
}
🛑 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;
};
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', '');
});
});
};
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 🖖
Top comments (3)
Great post!
Fantastic post, very informative way to unpack one of those "banana" words that can be so vague.
Thanks for the kind words. Stay awesome 😀