sexta-feira, 28 de novembro de 2014

Making A Complete Polyfill For The HTML5 Details Element

Making A Complete Polyfill For The HTML5 Details Element


  • By Maksim Chemerisuk

  • November 28th, 2014

  • CSSHTML5JavaScript

  • 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:


  1. 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 dump details into a DOM and be finished with it, and not have to fiddle with JavaScript to get it going.

  2. Not a loyal polyfill for a open attribute

    According to a specification4, details comes with a open attribute, that is used to toggle a prominence of a essence of details.

  3. The toggle eventuality is missing

    This eventuality is a presentation that a details component has altered a open 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.


1-details-element-in-Safari-8-opt8
The details component in Safari 8 (18149)

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.extend11 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


2-summary-element-is-not-the-first-child-opt13
What a Chrome browser outputs when a summary component is not a initial child. (18149)

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


3-summary-element-does-not-exist-opt17
What a Chrome browser outputs when a summary component is not benefaction (18149)

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 lFlags21 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 propertychange22 and DOMAttrModified23 events. Modern browsers support MutationObservers24, 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 defineAttribute25, 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;


4-details-element-animation26
A representation animation for a toggle triangle

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.importStyles31, 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:


  1. Apply Autoprefixer to a CSS.

  2. Optimize a CSS, and renovate it into a method of DOM.importStyles calls.

  3. Apply ES6 transpilers to JavaScript.

  4. 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. 1 http://caniuse.com/#feat=details

  2. 2 https://github.com/mathiasbynens/jquery-details

  3. 3 https://github.com/manuelbieh/Details-Polyfill

  4. 4 http://www.w3.org/html/wg/drafts/html/master/interactive-elements.html#the-details-element

  5. 5 https://github.com/chemerisuk/better-dom

  6. 6 https://github.com/chemerisuk/better-dom/wiki/Live-extensions

  7. 7 http://www.smashingmagazine.com/2014/02/05/introducing-live-extensions-better-dom-javascript/

  8. 8 http://www.smashingmagazine.com/wp-content/uploads/2014/11/1-details-element-in-Safari-8-large-opt.jpg

  9. 9

  10. 10 http://chemerisuk.github.io/better-details-polyfill/

  11. 11 http://chemerisuk.github.io/better-dom/DOM.html#extend

  12. 12 https://github.com/chemerisuk/better-dom/wiki/Live-extensions#public-members-and-private-functions

  13. 13 http://www.smashingmagazine.com/wp-content/uploads/2014/11/2-summary-element-is-not-the-first-child-large-opt.jpg

  14. 14

  15. 15 http://lists.w3.org/Archives/Public/public-html/2014Nov/0043.html

  16. 16 http://validator.w3.org/nu/

  17. 17 http://www.smashingmagazine.com/wp-content/uploads/2014/11/3-summary-element-does-not-exist-large-opt.jpg

  18. 18

  19. 19 http://www.w3.org/TR/shadow-dom/

  20. 20 http://caniuse.com/#feat=shadowdom

  21. 21 http://msdn.microsoft.com/en-us/library/ie/ms536739(v=vs.85).aspx

  22. 22 http://msdn.microsoft.com/en-us/library/ie/ms536956(v=vs.85).aspx

  23. 23 https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events

  24. 24 https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

  25. 25 http://chemerisuk.github.io/better-dom/$Element.html#defineAttribute

  26. 26 http://www.smashingmagazine.com/wp-content/uploads/2014/11/4-details-element-animation.gif

  27. 27 https://github.com/chemerisuk/better-details-polyfill/blob/master/src/better-details-polyfill.css

  28. 28 https://github.com/postcss/autoprefixer

  29. 29 http://www.html5rocks.com/en/tutorials/webcomponents/imports/

  30. 30 http://caniuse.com/#feat=imports

  31. 31 http://chemerisuk.github.io/better-dom/DOM.html#importStyles

  32. 32 https://github.com/chemerisuk/better-dom-boilerplate/blob/master/gulpfile.js#L34

  33. 33 https://github.com/chemerisuk/better-details-polyfill/blob/master/src/better-details-polyfill.js

  34. 34 http://webcomponents.org

  35. 35 http://w3c.github.io/webcomponents/spec/custom/

  36. 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