Making A Complete Polyfill For The HTML5 Details Element
- By Maksim Chemerisuk
- November 28th, 2014
- 2 Comments
HTML5 introduced a garland of new tags, one of that is details
. This component is a resolution for a common UI component: a collapsible block. Almost each framework, including Bootstrap and jQuery UI, has a possess plugin for a identical solution, nonetheless nothing heed to a HTML5 selection — substantially given many were around prolonged before details
got specified and, therefore, paint opposite approaches. A customary component allows everybody to use a same markup for a sold form of content. That’s given formulating a strong polyfill creates sense1.
Disclaimer: This is utterly a technical article, and while I’ve attempted to minimize a formula snippets, a essay still contains utterly a few of them. So, be prepared!
Existing Solutions Are Incomplete
I’m not2 a initial person3 to try to exercise such a polyfill. Unfortunately, all other solutions vaunt one or another problem:
- No support for destiny content
Support for destiny calm is intensely profitable for single-page applications. Without it, we would have to plead a initialization duty each time we supplement calm to a page. Basically, a developer wants to be means to dumpdetails
into a DOM and be finished with it, and not have to fiddle with JavaScript to get it going. - Not a loyal polyfill for a
open
attribute
According to a specification4,details
comes with aopen
attribute, that is used to toggle a prominence of a essence ofdetails
. - The
toggle
eventuality is missing
This eventuality is a presentation that adetails
component has altered aopen
state. Ideally, it should be a vanilla DOM event.
In this essay we’ll use better-dom5 to make things simpler. The categorical reason is a live extensions6 feature, that solves a problem of invoking a initialization duty for energetic content. (For some-more information, review my minute essay about live extensions7.) Additionally, better-dom outfits live extensions with a set of collection that do not (yet) exist in vanilla DOM nonetheless that come in accessible when implementing a polyfill like this one.
Check out a live demo10.
Let’s take a closer demeanour during all of a hurdles we have to overcome to make details
accessible in browsers that don’t support it.
Future Content Support
To start, we need to announce a live prolongation for a "details"
selector. What if a browser already supports a component natively? Then we’ll need to supplement some underline detection. This is easy with a discretionary second evidence condition
, that prevents a proof from executing if a value is equal to false
:
// Invoke prolongation usually if there is no local support
var open = DOM.create("details").get("open");
DOM.extend("details", typeof open !== "boolean",
constructor: function()
console.log("initialize details…");
);
As we can see, we are perplexing to detect local support by checking for a open
property, that apparently usually exists in browsers that commend details
.
What sets DOM.extend
11 detached from a elementary call like document.querySelectorAll
is that a constructor
duty executes for destiny content, too. And, yes, it works with any library for utilizing a DOM:
// You can use better-dom…
DOM.find("body").append(
"detailssummaryTITLE/summarypTEXT/p/details");
// = logs "initialize details…"
// or any other DOM library, like jQuery…
$("body").append(
"detailssummaryTITLE/summarypTEXT/p/details");
// = logs "initialize details…"
// or even vanilla DOM.
document.body.insertAdjacentElement("beforeend",
"detailssummaryTITLE/summarypTEXT/p/details");
// = logs "initialize details…"
In a following sections, we’ll reinstate a console.log
call with a genuine implementation.
Implementation Of summary
Behavior
The details
component competence take summary
as a child element.
The initial outline component child of details, if one is present, represents an overview of details. If no child outline component is present, afterwards a user representative should yield a possess fable (for example, “Details”).
Let’s supplement rodent support. A click on a summary
component should toggle a open
charge on a primogenitor details
element. This is what it looks like regulating better-dom:
DOM.extend("details", typeof open !== "boolean",
constructor: function()
this
.children("summary:first-child")
.forEach(this.doInitSummary);
,
doInitSummary: function(summary)
summary.on("click", this.doToggleOpen);
,
doToggleOpen: function()
// We’ll cover a open skill value later.
this.set("open", !this.get("open"));
);
The children
process earnings a JavaScript array of elements (not an array-like intent as in vanilla DOM). Therefore, if no summary
is found, afterwards a doInitSummary
duty is not executed. Also, doInitSummary
and doToggleOpen
are private functions12, they always are invoked for a stream element. So, we can pass this.doInitSummary
to Array#forEach
though additional closures, and all will govern rightly there.
Having keyboard support in further to rodent support is good as well. But first, let’s make summary
a focusable element. A standard resolution is to set a tabindex
charge to 0
:
doInitSummary: function(summary)
// Makes outline focusable
summary.set("tabindex", 0);
…
Now, a user attack a space bar or a “Enter” pivotal should toggle a state of details
. In better-dom, there is no proceed entrance to a eventuality object. Instead, we need to announce that properties to squeeze regulating an additional array argument:
doInitSummary: function(summary)
…
summary.on("keydown", ["which"], this.onKeyDown);
Note that we can reuse a existent doToggleOpen
function; for a keydown
event, it usually creates an additional check on a initial argument. For a click eventuality handler, a value is always equal to undefined
, and a outcome will be this:
doInitSummary: function(summary)
summary
.set("tabindex", 0)
.on("click", this.doToggleOpen)
.on("keydown", ["which"], this.doToggleOpen);
,
doToggleOpen: function(key) pivotal === 13
Now we have a mouse- and keyboard-accessible details
element.
summary
Element Edge Cases
The summary
component introduces several corner cases that we should take into consideration:
1. When summary
Is a Child But Not a First Child
Browser vendors have attempted to repair such invalid markup by relocating summary
to a position of a initial child visually, even when a component is not in that position in a upsurge of a DOM. we was confused by such behavior, so we asked a W3C for clarification15. The W3C reliable that summary
contingency be a initial child of details
. If we check a markup in a screenshot above on Nu Markup Checker16, it will destroy with a following blunder message:
Error: Element outline not authorised as child of component sum in this context. […] Contexts in that component outline competence be used: As a initial child of a sum element.
My proceed is to pierce a summary
component to a position of a initial child. In other words, a polyfill fixes a shabby markup for you:
doInitSummary: function(summary)
// Make certain that outline is a initial child
if (this.child(0) !== summary)
this.prepend(summary);
…
2. When a summary
Element Is Not Present
As we can see in a screenshot above, browser vendors insert “Details” as a fable into summary
in this case. The markup stays untouched. Unfortunately, we can’t grasp a same though accessing a shadow DOM19, that unfortunately has diseased support20 during present. Still, we can set adult summary
manually to approve with standards:
constructor: function()
Support For open
Property
If we try a formula next in browsers that support details
natively and in others that don’t, you’ll get opposite results:
details.open = true;
// sum changes state in Chrome and Safari
details.open = false;
// sum state changes behind in Chrome and Safari
In Chrome and Safari, changing a value of open
triggers a further or dismissal of a attribute. Other browsers do not respond to this given they do not support a open
skill on a details
element.
Properties are opposite from elementary values. They have a span of getter and setter functions that are invoked each time we review or allot a new value to a field. And JavaScript has had an API to announce properties given chronicle 1.5.
The good news is that one aged browser we are going to use with a polyfill, Internet Explorer (IE) 8, has partial support for a Object.defineProperty
function. The reduction is that a duty works usually on DOM elements. But that is accurately what we need, right?
There is a problem, though. If we try to set an charge with a same name in a setter duty in IE 8, afterwards a browser will smoke-stack with gigantic recursion and crashes. In aged versions of IE, changing an charge will trigger a change of an suitable skill and clamp versa:
Object.defineProperty(element, "foo",
…
set: function(value)
// The line next triggers gigantic recursion in IE 8.
this.setAttribute("foo", value);
);
So we can’t cgange a skill though changing an charge there. This reduction has prevented developers from regulating a Object.defineProperty
for utterly a prolonged time.
The good news is that I’ve found a solution.
Fix For Infinite Recursion In IE 8
Before describing a solution, I’d like to give some credentials on one underline of a HTML and CSS parser in browsers. In box we weren’t aware, these parsers are case-insensitive. For example, a manners next will furnish a same outcome (i.e. a bottom red for a content on a page):
body color: red;
/* The order next will furnish a same result. */
BODY color: red;
The same goes for attributes:
el.setAttribute("foo", "1");
el.setAttribute("FOO", "2");
el.getAttribute("foo"); // = "2"
el.getAttribute("FOO"); // = "2"
Moreover, we can’t have uppercased and lowercased attributes with a same name. But we can have both on a JavaScript object, given JavaScript is case-sensitive:
var obj = foo: "1", FOO: "2";
obj.foo; // = "1"
obj.FOO; // = "2"
Some time ago, we found that IE 8 supports a deprecated bequest evidence lFlags
21 for charge methods, that allows we to change attributes in a case-sensitive manner:
lFlags
[in, optional]
- Type: Integer
- Integer that specifies either to use a case-sensitive hunt to locate a attribute.
Remember that a gigantic recursion happens in IE 8 given a browser is perplexing to refurbish a charge with a same name and therefore triggers a setter duty over and over again. What if we use a lFlags
evidence to get and set a uppercased charge value:
// Defining a "foo" skill nonetheless regulating a "FOO" attribute
Object.defineProperty(element, "foo",
get: function()
return this.getAttribute("FOO", 1);
,
set: function(value)
// No gigantic recursion!
this.setAttribute("FOO", value, 1);
);
As we competence expect, IE 8 updates a uppercased margin FOO
on a JavaScript object, and a setter duty does not trigger a recursion. Moreover, a uppercased attributes work with CSS too — as we settled in a beginning, that parser is case-insensitive.
Polyfill For The open
Attribute
Now we can conclude an open
skill that works in each browser:
var attrName = document.addEventListener ? "open" : "OPEN";
Object.defineProperty(details, "open",
get: function()
set: function(value)
if (this.open !== value)
console.log("firing toggle event");
if (value)
this.setAttribute(attrName, "", 1);
else
this.removeAttribute(attrName, 1);
);
Check how it works:
details.open = true;
// = logs "firing toggle event"
details.hasAttribute("open"); // = true
details.open = false;
// = logs "firing toggle event"
details.hasAttribute("open"); // = false
Excellent! Now let’s do identical calls, nonetheless this time regulating *Attribute
methods:
details.setAttribute("open", "");
// = silence, nonetheless fires toggle eventuality in Chrome and Safari
details.removeAttribute("open");
// = silence, nonetheless fires toggle eventuality in Chrome and Safari
The reason for such duty is that a relationship between a open
skill and a charge should be bidirectional. Every time a charge is modified, a open
skill should simulate a change, and clamp versa.
The simplest cross-browser resolution I’ve found for this emanate is to overrule a charge methods on a aim component and plead a setters manually. This avoids bugs and a opening chastisement of bequest propertychange
22 and DOMAttrModified
23 events. Modern browsers support MutationObservers
24, nonetheless that doesn’t cover a browser scope.
Final Implementation
Obviously, walking by all of a stairs above when defining a new charge for a DOM component wouldn’t make sense. We need a application duty for that that hides cross-browser quirks and complexity. I’ve combined such a function, named defineAttribute
25, in better-dom.
The initial evidence is a name of a skill or attribute, and a second is a get
and set
object. The getter duty takes a attribute’s value as a initial argument. The setter duty accepts a property’s value, and a returned matter is used to refurbish a attribute. Such a syntax allows us to censor a pretence for IE 8 where an uppercased charge name is used behind a scenes:
constructor: function()
…
this.defineAttribute("open",
get: this.doGetOpen,
set: this.doSetOpen
);
,
doGetOpen: function(attrValue)
attrValue = String(attrValue).toLowerCase();
lapse attrValue === "" ,
doSetOpen: function(propValue)
if (this.get("open") !== propValue)
this.fire("toggle");
// Adding or stealing boolean charge "open"
lapse propValue ? "" : null;
Having a loyal polyfill for a open
charge simplifies a strategy of a details
element’s state. Again, this API is framework-agnostic:
// You can use better-dom…
DOM.find("details").set("open", false);
// or any other DOM library, like jQuery…
$("details").prop("open", true);
// or even vanilla DOM.
document.querySelector("details").open = false;
Notes On Styling
The CSS partial of a polyfill is simpler. It has some elementary character rules:
summary:first-child ~ *
display: none;
details[open] *
display: block;
/* Hide local indicator and use pseudo-element instead */
summary::-webkit-details-marker
display: none;
I didn’t wish to deliver any additional elements in a markup, so apparent choice is to character a ::before
pseudo-element. This pseudo-element is used to prove a stream state of details
(according to either it is open or not). But IE 8 has some quirks, as common — namely, with updating a pseudo-element state. we got it to work scrupulously usually by changing a content
property’s value itself:
details:before
content: '25BA';
…
details[open]:before
content: '25BC';
For other browsers, a zero-border pretence will pull a font-independent CSS triangle. With a double-colon syntax for a ::before
pseudo-element, we can request manners to IE 9 and above:
details::before
content: '';
width: 0;
height: 0;
border: plain transparent;
border-left-color: inherit;
border-width: 0.25em 0.5em;
…
transform: rotate(0deg) scale(1.5);
details[open]::before
content: '';
transform: rotate(90deg) scale(1.5);
The final encouragement is a tiny transition on a triangle. Unfortunately, Safari does not request it for some reason (perhaps a bug), nonetheless it degrades good by ignoring a transition completely:
details::before
…
transition: renovate 0.15s ease-out;
You can find a full source formula on Github27.
Putting It All Together
Some time ago, we started regulating transpilers in my projects, and they are great. Transpilers raise source files. You can even formula in a totally opposite language, like CoffeeScript instead of JavaScript or LESS instead of CSS etc. However, my idea in regulating them is to diminution nonessential sound in a source formula and to learn new facilities in a nearby future. That’s given transpilers do not go opposite any standards in my projects — I’m usually regulating some additional ECMAScript 6 (ES6) things and CSS post-processors (Autoprefixer28 being a categorical one).
Also, to pronounce about bundling, we fast found that distributing *.css
files along with *.js
is somewhat annoying. In acid for a solution, we found HTML Imports29, that aims to solve this kind of problem in a future. At present, a underline has comparatively diseased browser support30. And, frankly, bundling all of that things into a singular HTML record is not ideal.
So, we built my possess proceed for bundling: better-dom has a function, DOM.importStyles
31, that allows we to import CSS manners on a web page. This duty has been in a library given a commencement given DOM.extend
uses it internally. Since we use better-dom and transpilers in my formula anyway, we combined a elementary sup task:
gulp.task("compile", ["lint"], function() "/g, "\$"))
// and modify CSS manners into JavaScript duty calls
.pipe(replace(/([^]+)([^]+)/g,
"DOM.importStyles("$1", "$2");n"))
.pipe(cssFilter.restore())
.pipe(jsFilter)
.pipe(es6transpiler())
.pipe(jsFilter.restore())
.pipe(concat(pkg.name + ".js"))
.pipe(gulp.dest("build/"));
);
To keep it simple, we didn’t put in any discretionary stairs or dependency declarations (see a full source code32). In general, a gathering charge contains a following steps:
- Apply Autoprefixer to a CSS.
- Optimize a CSS, and renovate it into a method of
DOM.importStyles
calls. - Apply ES6 transpilers to JavaScript.
- Concatenate both outputs to a
*.js
file.
And it works! we have transpilers that make my formula clearer, and a usually outlay is a single JavaScript file. Another advantage is that, when JavaScript is disabled, those character manners are totally ignored. For a polyfill like this, such duty is desirable.
Closing Thoughts
As we can see, building a polyfill is not a easiest challenge. On a other hand, a resolution can be used for a comparatively prolonged time: standards do not change mostly and have been discussed during length behind a scenes. Also everybody is regulating a same denunciation and is joining with a same APIs that is a good thing.
With a common proof changed into application functions, a source formula is not unequivocally complex33. This means that, during present, we unequivocally miss modernized collection to make strong polyfills that work tighten to local implementations (or better!). And we don’t see good libraries for this yet, unfortunately.
Libraries such as jQuery, Prototype and MooTools are all about providing additional sugarine for operative with a DOM. While sugarine is great, we also need some-more application functions to build some-more strong and unimportant polyfills. Without them, we competence finish adult with a ton of plugins that are tough to confederate in a projects. May be it’s time to pierce into this direction?
Another technique that has arisen recently is Web Components34. I’m unequivocally vehement by collection like a shade DOM, nonetheless I’m not certain if tradition elements35 are a destiny of web development. Moreover, tradition elements can deliver new problems if everybody starts formulating their possess tradition tags for common uses. My indicate is that we need to learn (and try to improve) a standards initial before introducing a new HTML element. Fortunately, I’m not alone in this; Jeremy Keith, for one, shares a identical view36.
Don’t get me wrong. Custom elements are a good feature, and they really have use cases in some areas. we demeanour brazen to them being implemented in all browsers. I’m usually not certain if they’re a china bullet for all of a problems.
To reiterate, I’d inspire formulating some-more strong and unimportant polyfills. And we need to build some-more modernized collection to make that occur some-more easily. The instance with details
shows that achieving such a idea currently is possible. And we trust this instruction is future-proof and a one we need to pierce in.
(al)
Footnotes
- 1 http://caniuse.com/#feat=details
- 2 https://github.com/mathiasbynens/jquery-details
- 3 https://github.com/manuelbieh/Details-Polyfill
- 4 http://www.w3.org/html/wg/drafts/html/master/interactive-elements.html#the-details-element
- 5 https://github.com/chemerisuk/better-dom
- 6 https://github.com/chemerisuk/better-dom/wiki/Live-extensions
- 7 http://www.smashingmagazine.com/2014/02/05/introducing-live-extensions-better-dom-javascript/
- 8 http://www.smashingmagazine.com/wp-content/uploads/2014/11/1-details-element-in-Safari-8-large-opt.jpg
- 9
- 10 http://chemerisuk.github.io/better-details-polyfill/
- 11 http://chemerisuk.github.io/better-dom/DOM.html#extend
- 12 https://github.com/chemerisuk/better-dom/wiki/Live-extensions#public-members-and-private-functions
- 13 http://www.smashingmagazine.com/wp-content/uploads/2014/11/2-summary-element-is-not-the-first-child-large-opt.jpg
- 14
- 15 http://lists.w3.org/Archives/Public/public-html/2014Nov/0043.html
- 16 http://validator.w3.org/nu/
- 17 http://www.smashingmagazine.com/wp-content/uploads/2014/11/3-summary-element-does-not-exist-large-opt.jpg
- 18
- 19 http://www.w3.org/TR/shadow-dom/
- 20 http://caniuse.com/#feat=shadowdom
- 21 http://msdn.microsoft.com/en-us/library/ie/ms536739(v=vs.85).aspx
- 22 http://msdn.microsoft.com/en-us/library/ie/ms536956(v=vs.85).aspx
- 23 https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events
- 24 https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
- 25 http://chemerisuk.github.io/better-dom/$Element.html#defineAttribute
- 26 http://www.smashingmagazine.com/wp-content/uploads/2014/11/4-details-element-animation.gif
- 27 https://github.com/chemerisuk/better-details-polyfill/blob/master/src/better-details-polyfill.css
- 28 https://github.com/postcss/autoprefixer
- 29 http://www.html5rocks.com/en/tutorials/webcomponents/imports/
- 30 http://caniuse.com/#feat=imports
- 31 http://chemerisuk.github.io/better-dom/DOM.html#importStyles
- 32 https://github.com/chemerisuk/better-dom-boilerplate/blob/master/gulpfile.js#L34
- 33 https://github.com/chemerisuk/better-details-polyfill/blob/master/src/better-details-polyfill.js
- 34 http://webcomponents.org
- 35 http://w3c.github.io/webcomponents/spec/custom/
- 36 https://adactio.com/journal/7431
↑ Back to topShare on Twitter
Making A Complete Polyfill For The HTML5 Details Element
Nenhum comentário:
Postar um comentário