If you’ve read many of my previous posts, you have probably seen me use polyfills (i.e. CRUD Operations for SharePoint Docs Using Fetch and REST), to patch older browsers with modern functionality like fetch. I generally download the polyfill, upload it to SharePoint, and load it on the page as a user custom action. But there is another way to load polyfills, which is generally called a polyfill service. The idea is that you load the polyfill from some external service, which detects your current browser, and loads just enough polyfill to patch your current browser up to some level of specification compatibility (usually like ES5, but you can also generally ask for specific functionality, like fetch and/or Promise). There are some unique problems with loading this kind of polyfill in SharePoint, mostly due to limitations in user custom actions. In this post I’m going to talk about how to load such a polyfill in SharePoint, but first lets talk a little more about polyfill services in general.
So what is a Polyfill Service, anyway?
It all may sound a bit abstract, but a concrete example will make it pretty clear pretty quickly. I’m going to
use a service called Polyfill.io. To load this service, I can add a
script reference to it, like so:
1 | <script type="text/javascript" src="https://cdn.polyfill.io/v3/polyfill.js"></script> |
Want to see what this does? Just open a new tab in your browser and navigate to the URL https://cdn.polyfill.io/v3/polyfill.js. Now your results are quite possibly
going to be different than mine, but when I do this right now, I get:
1 2 3 4 5 6 7 8 9 | /* Polyfill service v3.31.1 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service. * * Features requested: default * */ /* No polyfills found for current settings */ |
And if I switch the URL to https://cdn.polyfill.io/v3/polyfill.min.js, I get:
1 | /* Disable minification (remove `.min` from URL path) for more info */ |
And if I switch the URL to https://cdn.polyfill.io/v3/polyfill.min.js, I get:
Admittedly, neither of these results are very exciting. That’s because I’m typing this blog post on a reasonably
modern browser, that doesn’t need any patching in order to behave like a reasonably modern browser. On the other
hand, if I open the non-minimized version in Internet Explorer 11, I get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | /* Polyfill service v3.31.1 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service. * * Features requested: default * * - _DOMTokenList, License: ISC (required by "DOMTokenList", "default") * - _ESAbstract.ArrayCreate, License: CC0 (required by "Array.from", "default", "Array.of", "_ESAbstract.ArraySpeciesCreate", "Array.prototype.filter", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.map") * - _ESAbstract.Call, License: CC0 (required by "Array.from", "default", "_ESAbstract.GetIterator", "Map", "Set", "_ESAbstract.IteratorClose", "_ESAbstract.IteratorNext", "_ESAbstract.IteratorStep", "Array.prototype.forEach", "URL", "Symbol", "Symbol.iterator", "_ESAbstract.ToPrimitive", "_ESAbstract.ToString", "Array.of", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "Array.prototype.filter", "Array.prototype.map", "_ESAbstract.OrdinaryToPrimitive") * - _ESAbstract.CreateDataProperty, License: CC0 (required by "_ESAbstract.CreateDataPropertyOrThrow", "Array.from", "default", "Array.of", "_ESAbstract.CreateIterResultObject", "Map", "Set") * - _ESAbstract.CreateDataPropertyOrThrow, License: CC0 (required by "Array.from", "default", "Array.of", "Array.prototype.filter", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.map") * - _ESAbstract.CreateMethodProperty, License: CC0 (required by "Array.from", "default", "Array.of", "Array.prototype.fill", "Map", "Number.isNaN", "Object.assign", "Set", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before", "Array.isArray", "URL", "Object.create", "_ESAbstract.GetIterator", "_ESAbstract.OrdinaryCreateFromConstructor", "_ESAbstract.Construct", "Symbol", "Symbol.iterator", "Object.getOwnPropertyDescriptor", "Object.keys", "Object.defineProperties", "Array.prototype.forEach", "Function.prototype.bind", "Object.getPrototypeOf", "Array.prototype.filter", "Array.prototype.map", "Object.getOwnPropertyNames", "Object.freeze") * - _ESAbstract.Get, License: CC0 (required by "Array.from", "default", "Array.prototype.fill", "Object.assign", "_ESAbstract.IteratorValue", "Map", "Set", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before", "_ESAbstract.IteratorComplete", "_ESAbstract.IteratorStep", "_ESAbstract.IsRegExp", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "Object.defineProperties", "URL", "Object.create", "_ESAbstract.GetIterator", "_ESAbstract.OrdinaryCreateFromConstructor", "_ESAbstract.Construct", "Array.of", "Symbol", "Symbol.iterator", "Array.prototype.forEach", "_ESAbstract.GetPrototypeFromConstructor", "Array.prototype.filter", "Array.prototype.map", "_ESAbstract.OrdinaryToPrimitive", "_ESAbstract.ToPrimitive", "_ESAbstract.ToString", "_ESAbstract.ArraySpeciesCreate") * - _ESAbstract.IsCallable, License: CC0 (required by "Array.from", "default", "Map", "Set", "_ESAbstract.GetMethod", "Array.prototype.forEach", "URL", "Symbol", "Symbol.iterator", "Function.prototype.bind", "_ESAbstract.Construct", "Array.of", "Object.getOwnPropertyDescriptor", "Object.assign", "Array.prototype.filter", "Array.prototype.map", "_ESAbstract.OrdinaryToPrimitive", "_ESAbstract.ToPrimitive", "_ESAbstract.ToString", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.RequireObjectCoercible, License: CC0 (required by "String.prototype.endsWith", "default", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.SameValueNonNumber, License: CC0 (required by "_ESAbstract.SameValueZero", "Map", "default", "Array.from", "Set") * - _ESAbstract.ToBoolean, License: CC0 (required by "_ESAbstract.IteratorComplete", "Map", "default", "Array.from", "Set", "_ESAbstract.IteratorStep", "_ESAbstract.IsRegExp", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "Array.prototype.filter", "Symbol", "Symbol.iterator") * - _ESAbstract.ToInteger, License: CC0 (required by "Array.prototype.fill", "default", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "_ESAbstract.ToLength", "Array.from", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before") * - _ESAbstract.ToLength, License: CC0 (required by "Array.from", "default", "Array.prototype.fill", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before", "Array.prototype.forEach", "URL", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.filter", "Array.prototype.map") * - _ESAbstract.ToObject, License: CC0 (required by "Array.from", "default", "Array.prototype.fill", "Object.assign", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before", "Object.defineProperties", "URL", "Object.create", "Map", "Set", "_ESAbstract.GetIterator", "_ESAbstract.OrdinaryCreateFromConstructor", "_ESAbstract.Construct", "Array.of", "Symbol", "Symbol.iterator", "Array.prototype.forEach", "_ESAbstract.GetV", "_ESAbstract.GetMethod", "Array.prototype.filter", "Array.prototype.map") * - _ESAbstract.GetV, License: CC0 (required by "_ESAbstract.GetMethod", "Array.from", "default", "Map", "Set", "_ESAbstract.GetIterator") * - _ESAbstract.GetMethod, License: CC0 (required by "Array.from", "default", "Map", "Set", "_ESAbstract.IsConstructor", "Array.of", "_ESAbstract.GetIterator", "_ESAbstract.IteratorClose", "_ESAbstract.ToPrimitive", "_ESAbstract.ToString", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.Type, License: CC0 (required by "Map", "default", "Array.from", "Number.isNaN", "_ESAbstract.IsConstructor", "Array.of", "_ESAbstract.GetIterator", "Set", "_ESAbstract.IteratorClose", "_ESAbstract.ToString", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "_ESAbstract.IteratorValue", "_ESAbstract.CreateIterResultObject", "_ESAbstract.IteratorComplete", "_ESAbstract.IteratorStep", "_ESAbstract.IteratorNext", "_ESAbstract.SameValueZero", "Object.create", "_ESAbstract.OrdinaryCreateFromConstructor", "_ESAbstract.Construct", "Symbol", "Symbol.iterator", "_ESAbstract.IsRegExp", "Object.defineProperties", "URL", "_ESAbstract.ToPrimitive", "_ESAbstract.GetPrototypeFromConstructor", "_ESAbstract.OrdinaryToPrimitive", "_ESAbstract.ArraySpeciesCreate", "Array.prototype.filter", "Array.prototype.map") * - _ESAbstract.CreateIterResultObject, License: CC0 (required by "Map", "default", "Array.from", "Set") * - _ESAbstract.GetIterator, License: CC0 (required by "Array.from", "default", "Map", "Set") * - _ESAbstract.GetPrototypeFromConstructor, License: CC0 (required by "_ESAbstract.OrdinaryCreateFromConstructor", "Map", "default", "Array.from", "Set", "_ESAbstract.Construct", "Array.of") * - _ESAbstract.OrdinaryCreateFromConstructor, License: CC0 (required by "Map", "default", "Array.from", "Set", "_ESAbstract.Construct", "Array.of") * - _ESAbstract.IsConstructor, License: CC0 (required by "Array.from", "default", "Array.of", "_ESAbstract.Construct", "_ESAbstract.ArraySpeciesCreate", "Array.prototype.filter", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.map") * - _ESAbstract.Construct, License: CC0 (required by "Array.from", "default", "Array.of", "_ESAbstract.ArraySpeciesCreate", "Array.prototype.filter", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.map") * - _ESAbstract.IsRegExp, License: CC0 (required by "String.prototype.endsWith", "default", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.IteratorClose, License: CC0 (required by "Array.from", "default", "Map", "Set") * - _ESAbstract.IteratorComplete, License: CC0 (required by "Map", "default", "Array.from", "Set", "_ESAbstract.IteratorStep") * - _ESAbstract.IteratorNext, License: CC0 (required by "Map", "default", "Array.from", "Set", "_ESAbstract.IteratorStep") * - _ESAbstract.IteratorStep, License: CC0 (required by "Array.from", "default", "Map", "Set") * - _ESAbstract.IteratorValue, License: CC0 (required by "Array.from", "default", "Map", "Set") * - _ESAbstract.OrdinaryToPrimitive, License: CC0 (required by "_ESAbstract.ToPrimitive", "_ESAbstract.ToString", "Array.from", "default", "Array.of", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.SameValueZero, License: CC0 (required by "Map", "default", "Array.from", "Set") * - _ESAbstract.ToPrimitive, License: CC0 (required by "_ESAbstract.ToString", "Array.from", "default", "Array.of", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith") * - _ESAbstract.ToString, License: CC0 (required by "Array.from", "default", "Array.of", "Array.prototype.fill", "String.prototype.endsWith", "String.prototype.includes", "String.prototype.startsWith", "Array.prototype.indexOf", "Element.prototype.after", "Element.prototype.before", "Array.prototype.forEach", "URL", "Symbol", "Map", "Set", "Symbol.iterator", "Array.prototype.filter", "Array.prototype.map") * - _mutation, License: CC0 (required by "DocumentFragment.prototype.append", "default", "DocumentFragment.prototype.prepend", "Element.prototype.after", "Element.prototype.append", "Element.prototype.before", "Element.prototype.prepend", "Element.prototype.remove", "Element.prototype.replaceWith") * - Array.of, License: CC0 (required by "default") * - Array.prototype.fill, License: CC0 (required by "default") * - DocumentFragment.prototype.append, License: CC0 (required by "default") * - DocumentFragment.prototype.prepend, License: CC0 (required by "default") * - DOMTokenList, License: CC0 (required by "default", "Element.prototype.classList") * - Element.prototype.after, License: CC0 (required by "default") * - Element.prototype.append, License: CC0 (required by "default") * - Element.prototype.before, License: CC0 (required by "default") * - Element.prototype.classList, License: ISC (required by "default") * - Element.prototype.matches, License: CC0 (required by "default", "Element.prototype.closest") * - Element.prototype.closest, License: CC0 (required by "default") * - Element.prototype.prepend, License: CC0 (required by "default") * - Element.prototype.remove, License: CC0 (required by "default") * - Element.prototype.replaceWith, License: CC0 (required by "default") * - Event, License: CC0 (required by "default", "CustomEvent") * - CustomEvent, License: CC0 (required by "default") * - Node.prototype.contains, License: CC0 (required by "default") * - Number.isNaN, License: MIT (required by "default") * - Object.assign, License: CC0 (required by "default") * - Promise, License: MIT (required by "default") * - String.prototype.endsWith, License: CC0 (required by "default") * - String.prototype.includes, License: CC0 (required by "default") * - String.prototype.startsWith, License: CC0 (required by "default") * - Symbol, License: MIT (required by "Map", "default", "Array.from", "Set", "Symbol.iterator", "Symbol.species") * - Symbol.iterator, License: MIT (required by "Array.from", "default", "Map", "Set") * - Symbol.species, License: MIT (required by "Map", "default", "Array.from", "Set") * - Map, License: CC0 (required by "default", "Array.from") * - Set, License: CC0 (required by "default", "Array.from") * - Array.from, License: CC0 (required by "default") * - URL, License: CC0-1.0 (required by "default") */ /* PLUS ABOUT 86 PAGES OF JAVASCRIPT TO IMPLEMENT EVERYTHING DESCRIBED IN THE ABOVE COMMENT */ |
What I’m showing here is actually only the comment at the top of the file. As indicated by the comment I added at
the bottom, I’ve cut out about 86 pages worth of JavaScript from the file, because of course it would be absurd
to show you that in a blog post. But the comment itself is pretty interesting, because it lists all of the
functionality that it’s going to polyfill in order to make IE 11 act like a modern browser with regards to
JavaScript. If I loaded this in IE 9 (or IE 9 compatibility mode), it would have patched even more.
So that’s the basic idea of polyfill services. Detect your browser and patch only what’s needed to level set what
sort of JavaScript will work in your browser…pretty cool.
There is a bit more to it, in that you can also designate specific functionality to patch, instead of patching
everything that’s missing even though you’re probably only going to be using a subset of that. This allows you
to reduce the size of the download, even on older browsers. But the download is actually pretty reasonable and
fast (the above polyfill for IE11 is only 46K minimized).
One other quick point is that you can’t necessarily polyfill everything needed to bring a browser up to some spec
compatibility. For instance:
- If it’s a new object or method, you can probably polyfill it. Examples include CustomEvent and
Array.prototype.filter. - If it’s a new syntax, it can probably be achieved by transpiling, ala Babel or TypeScript. Examples include
Class and fat arrow functions. - If it depends on native implementation in the browser, not available in older browsers, neither polyfills
nor transpiling wil help you. Examples include Service Worker and Bluetooth API. In these cases, you just
need to use a modern browser that supports them or gracefully degrade in an older browser.
Now, What’s the Problem with Polyfill Services and SharePoint?
First, if you just want to use the thing for a single customization on a single page, then you can stick the
script link in an HTML snippet in a Content Editor Web Part, add your own customization below it, and voila…no
problem.
But most of the customization I do for SharePoint, even client side, is more of an enterprise feature and needs
to be loaded on every page. For that, I usually use something like user custom actions, to load my dependencies
and my custom code on each page in the site collection. But what happens when you create a user custom action
with a full URL pointing to a script that is someplace other than the current site collection?
If you said something like “programmer no doughnut,” or it breaks your site pretty badly, you’re absolutely
right. Try to load an external resource in a user custom action, and every page in the site will now load as a
blank, or white, page, until you figure out how to remove that user custom action. That’s the reason why in
previous posts, I didn’t use a service, I downloaded the polyfill locally. The downside is, that this means I’m
either loading the polyfill for every browser, or I have to decide if the current browser needs it because I’m
now not using the service. That was ok for fetch, as I explained in my previous post, because even browsers that
implement it didn’t implement enough of it to work for me, but normally loading a polyfill to override something
that is actually already implemented in the current browser is a no-no.
It is, of course, possible to load an external resource using a user custom action without breaking your site,
but it takes a bit more work. This technique works for loading anything from a Content Delivery Network (CDN) as
a user custom action.
The Fix!
The fix is pretty simple, but subtly tricky. Cutting to the chase, here is the source for the JavaScript file I’m
going to load as a user custom action:
1 2 3 4 5 6 7 8 | var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'https://cdn.polyfill.io/v3/polyfill.min.js'; // very important! script.async = false; document.head.appendChild(script); |
Now any script that I need to load that requires this polyfill needs to be in a separate script file, which needs
to be loaded after the above script (i.e. as a user custom action with a higher sequence number).
Why does this work? Normally, browsers that support the async attribute (all modern browsers) for scripts will
load and execute dynamically added scripts (i.e. added through DOM manipulation or document.write) as quickly as
possible, which means the second script could be executed before the first has been loaded. But, with the async
flag set to false, the dynamic script will pause further rendering until it has executed completely, which means
the second script can count on the first script being ready for use from the start.
And since browsers that don’t support async are synchronous by default, this should work for older browsers too
(with a few sad caveats).
The caveats are that due to bugs or partial implementation of async, the order of execution of these scripts is
not guaranteed in IE 6-9 or Safari 5.0 (see async attribute for
external scripts), but it should work reliably in all reasonably modern browsers. And if I really need
to support IE9, I could use a third party library like RequireJS or HeadJS to load my dependencies reliably regardless of browser
version. Either way, the trick is:
- Load a site collection local file as a user custom action.
- That file dynamically loads the external resource in a synchronous fashion.
- Load other code that depends on the external resource in a separate file, and later than the the first file
in the page life cycle (usually this means as a user custom action with a higher sequence number).
References
- Polyfills:
everything you ever wanted to know, or maybe a bit less – David Gilbertson