📜 ⬆️ ⬇️

About creating an improved JavaScript library for working with the DOM

Currently, jQuery is the de facto library for working with the DOM. It can be used with popular MV * frameworks (such as Backbone), has many plug-ins and a very large community. On the other hand, JavaScript is becoming more popular and many developers are beginning to wonder how standard APIs work and when you can simply use them without adding an additional library.

In the late days of working with jQuery, I began to notice various problems with this library. Most of them are fundamental, therefore they cannot be fixed without losing backward compatibility, which, of course, is important. I, like many others, continued to use the library for a while, meeting annoying quirks every day.

Then Daniel Buchner created SelectorListener , and the idea of ​​live extensions was born. I started thinking about creating a set of functions that would allow the creation of unobtrusive and independent DOM components using the best approach. The task was to review the existing solutions and create a more understandable, testable, small, but at the same time self-sufficient library.

Adding useful functions to the library


The idea of live extensions contributed to the development of the better-dom project , although besides it there are other interesting features that make the library unique. Let's do a quick review of them:

Live extensions

In jQuery, there is the concept of live events. Behind the scenes, they use event delegation to handle existing and future elements. However, in many cases, greater flexibility is required. For example, if the widget is supposed to add additional elements to the document tree during initialization that should interact or replace existing ones, live events do not work. To solve the problem I am introducing the live extension.
')
The goal is to declare an extension once, and after that it should work for future content regardless of the complexity of the widget. This is an important feature because it allows you to create web pages declaratively, so it is well suited for AJAX applications.

Consider a simple example. Let's say our task is to implement a fully customizable tooltip. Pseudo-selector :hover does not match, because the position of the tooltip changes depending on the mouse cursor. Event delegation is also not suitable - it is too expensive to listen to mouseover and mouseleave for all elements on the page. This is where live extensions come into play.
 DOM.extend("[title]", { constructor: function() { var tooltip = DOM.create("span.custom-title"); //  textContent     // title       tooltip.set("textContent", this.get("title")).hide(); this //   tooltip .set("title", null) //       .data("tooltip", tooltip) //    .on("mouseenter", this.onMouseEnter, ["clientX", "clientY"]) .on("mouseleave", this.onMouseLeave) //     DOM .append(tooltip); }, onMouseEnter: function(x, y) { this.data("tooltip").style({left: x, top: y}).show(); }, onMouseLeave: function() { this.data("tooltip").hide(); } }); 

Our tooltip can now be styled using the .custom-title selector in CSS:
 .custom-title { position: fixed; border: 1px solid #faebcc; background: #faf8f0; } 

However, the fun begins when new elements with the title attribute are added to the page. They will be picked up by the extension without calling any initializing function .

Live extensions are self-sufficient, so they do not need to jerk a particular function in order to work with future content. This means that they can be combined with any existing library for DOM and simplify the application logic by dividing the UI code into many small independent parts.

In conclusion, a few words about Web components . One of the sections of the specification, called " Decorators ", is designed to solve a similar problem. He currently uses markup and special syntax to put listeners on children. But this is still a very early draft:
Decorators, unlike other sections of Web Components, do not yet have a specification.

Native animations

Thanks to Apple , CSS now has good animation support . In the past, animations were implemented in JavaScript using setInterval and setTimeout . It was a cool thing, but now ... something like a bad practice. Native animations will always be smoother: they are usually faster, require less energy and simply do not appear in browsers that do not support them.

There is no animate method in better-dom: only show , hide and toggle . To capture the state of a hidden item in the CSS library uses the standardized attribute aria-hidden .

To illustrate the approach, let's add a simple animation to the tooltip we wrote earlier:
 .custom-title { position: fixed; border: 1px solid #faebcc; background: #faf8f0; /*  */ opacity: 1; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; } .custom-title[aria-hidden=true] { opacity: 0; } 

Inside the show and hide attribute, aria-hidden changes its value to false or true . This is enough to show the animation means CSS.

More examples of animations using better-dom.

Built-in template engine

HTML lines are bulky. When I started looking for a replacement, I found an excellent Emmet project. Currently, it is quite popular as a plug-in for text editors and has a clean and compact syntax. Compare:
 body.append("<ul><li class='list-item'></li><li class='list-item'></li><li class='list-item'></li></ul>"); 

which is equivalent to
 body.append("ul>li.list-item*3"); 

In better-dom, methods that accept HTML strings as arguments also support emmet abbreviations. The abbreviation parser is fast , so you can not think about losses in performance. There is also a function for precompiling templates , which can be used as needed.

Internationalization support

Developing a UI widget often requires localization, which is not always an easy task. Many have solved this problem in their own way. With better-dom, I hope that changing the language will be as easy as changing the state of the CSS selector .

From an ideological point of view, language switching is like changing the “presentation” of content. There are several pseudo-selectors in CSS2 that help describe this model :lang and :before . Take a look at the code below:
 [data-i18n="hello"]:before { content: "Hello Maksim!"; } [data-i18n="hello"]:lang(ru):before { content: " !"; } 

The trick is that the content property changes according to the current language, which is determined by the value of the lang attribute for the html element. Using the data-i18n attribute, we can use a more general entry:
 [data-i18n]:before { content: attr(data-i18n); } [data-i18n="Hello Maksim!"]:lang(ru):before { content: " !"; } 

Of course, such CSS code does not look attractive, so in better-dom there are two helpers: i18n and DOM.importStrings . The first is used to update the data-i18n attribute with the corresponding value, and the second locates strings for a particular language.
 label.i18n("Hello Maksim!"); // label  "Hello Maksim!" DOM.importStrings("ru", "Hello Maksim!", " !"); //     "ru",  label   " !" label.set("lang", "ru"); //  label  " !"     

Parameterized strings are also supported: just add ${param} variables to the key string:
 label.i18n("Hello ${user}!", {user: "Maksim"}); // label  "Hello Maksim!" 

Improved Native APIs


Usually we want to adhere to standards. But sometimes the standards are not entirely friendly. The DOM is very confusing and to make it enjoyable, you need to wrap it in a convenient API. Despite the many improvements made by different libraries, some things can be done better:

Getter and setter

A native DOM has notions of attributes and properties of an element that can behave differently. Suppose there is a markup on the page:
 <a href="/chemerisuk/better-dom" id="foo" data-test="test">better-dom</a> 

To explain the unfriendliness of the native DOM, let's work with it a bit:
 var link = document.getElementById("foo"); link.href; // => "https://github.com/chemerisuk/better-dom" link.getAttribute("href"); // => "/chemerisuk/better-dom" link["data-test"]; // => undefined link.getAttribute("data-test"); // => "test" link.href = "abc"; link.href; // => "https://github.com/abc" link.getAttribute("href"); // => "abc" 

So, the attribute value is equal to the corresponding string in HTML, while a property of an element with the same name may have special behavior, for example, generating the full URL in the example above. This difference can sometimes confuse.

In practice, it is difficult to imagine when such a division may be useful. Moreover, the developer must always keep track of what value he works with, which adds unnecessary complexity.

In better-dom, things are simpler: each element has only smart getter and setter .
 var link = DOM.find("#foo"); link.get("href"); // => "https://github.com/chemerisuk/better-dom" link.set("href", "abc"); link.get("href"); // => "https://github.com/abc" link.get("data-attr"); // => "test" 

In the first step, the methods do a search for the properties of the element and, if defined, use it for operations. Otherwise, work with the corresponding attribute. For bulenovsky attributes ( checked , selected , etc.) you can simply use true or false . Changing these properties on the element updates the corresponding attribute (native behavior).

Improved event handling

Event handling is a significant part of the coding for the DOM. One fundamental problem that I discovered is that having an event object in the element listeners forces developers who like the code under test to measure the first argument or create an additional function that accepts event properties used in this handler.
 var button = document.getElementById("foo"); button.addEventListener("click", function(e) { handleButtonClick(e.button); }, false); 

It really bothers and adds an extra function call. What if we select the changing part as an argument: this will get rid of the closure:
 var button = DOM.find("#foo"); button.on("click", handleButtonClick, ["button"]); 

By default, the event handler accepts an array of ["target", "defaultPrevented"] , so there is no need to add the last argument to read these properties:
 button.on("click", function(target, canceled) { //   }); 

Later linking is also supported (I recommend reading the article from Peter Michaux on the topic ). This is a more flexible alternative to conventional event handlers, which, by the way, is present in the standard . It can be useful in cases when you need to make frequent calls to the on and off methods.
 button._handleButtonClick = function() { alert("click!"); }; button.on("click", "_handleButtonClick"); button.fire("click"); //   "clicked" button._handleButtonClick = null; button.fire("click"); //    

In conclusion, it is worth mentioning that in better-dom there are no methods like click() , focus() , submit() etc., which are present in the standard and have different behavior in browsers. The only way to call them is to use the fire method, which executes the default behavior when none of the handlers returned false :
 link.fire("click"); //    link.on("click", function() { return false; }); link.fire("click"); //         

Functional support

ES5 has standardized several useful methods for arrays, such as map , filter , some , etc. They allow you to conduct operations on collections in a standardized form. As a result, there are today projects like Underscore or Lo-Dash , which allow you to use these methods in older browsers.

Each element (or collection) in better-dom has the methods below out of the box:

 var urls, activeLi, linkText; urls = menu.findAll("a").map(function(el) { return el.get("href"); }); activeLi = menu.children().filter(function(el) { return el.hasClass("active"); }); linkText = menu.children().reduce(function(memo, el) { return memo || el.hasClass("active") && el.find("a").get() }, false); 

Some jQuery issues


Most of the problems below cannot be fixed in jQuery without losing backward compatibility. Another reason why it was decided to create a new library.

"Magic" function $

Everyone has ever heard that the function $ (dollar) is “magic”. The name consisting of only one character is not very clear; the function looks like an operator built into the language. That is why inexperienced developers simply call it wherever it is needed.

Behind the scenes $ is a rather complicated function . Its frequent execution, especially inside events such as mousemove or scroll , can be the cause of poor UI responsiveness.

Despite numerous articles that promote caching of jQuery objects, developers continue to embed $ . This is because the library syntax contributes to this coding style.

Another problem with this function is that it is responsible for two completely different tasks. People have become accustomed to this syntax, but this is not a good practice of function design in general:
 $("a"); // =>   ,    “a” $("<a>"); // =>   <a>  jQuery  

In better-dom, the responsibility of the $ -functions covers several methods : find[All] and DOM.create . The find[All] methods are used to search for elements by a CSS selector. DOM.create creates new elements in memory. Function names clearly state what these functions do.

The value of the square bracket operator

Another reason for the problem with too frequent calls to the dollar function is the square bracket operator. When a new jQuery object is created, all related elements are stored in numeric properties. It is important to note that the value of this property contains an instance of the native element (not a jQuery wrapper):
 var links = $("a"); links[0].on("click", function() { ... }); // ! $(links[0]).on("click", function() { ... }); //    

Because of this feature, each functional method in jQuery or another library (like Underscore) requires the current element to be wrapped in $() within an iteration function. Therefore, the developer must always remember with what object he works: native or a wrapper, despite the fact that the library is used to work with the DOM.

In better-dom, the square bracket operator returns a library object, so you can forget about native elements . The only legal way to access them is to use a special legacy method.
 var foo = DOM.find("#foo"); foo.legacy(function(node) { //   Hammer   swipe Hammer(node).on("swipe", function(e) { //   swipe }); }); 

But in reality, it is needed in very rare cases, for example, when compatibility with a native function or another DOM library is needed (like Hammer from the example above).

Problems with return false

One thing that really blew my mind is the weird handling of return false in event listeners. In accordance with W3C standards, this value should, in most cases, override the default behavior. In jQuery, return false additionally stops event delegation !

There are several problems here:
  1. The stopPropagation() call itself can create compatibility problems, since he breaks the possibility of other listeners to do their work in the occurrence of such an event
  2. most developers (even experienced) do not expect such behavior

It's not clear why the jQuery community decided to go against standards. And better-dom doesn't intend to repeat this error: return false inside an event handler only causes preventDefault() , as expected.

find and findAll

Search for items is one of the most expensive operations in the browser. Two native functions can be used to implement it: querySelector and querySelectorAll . The difference between them is that the first one stops the search after the first match.

This feature can significantly reduce the number of iterations in appropriate cases. In my tests , the speed gain can be up to 20 times . You can also expect the gap to increase depending on the size of the document tree.

jQuery has a find method that uses querySelectorAll in general. To date, there is no method here that would use querySelector to find only the first matching element.

There are two different methods in better-dom: find and findAll . They allow you to use querySelector optimization above. To estimate the potential gain, I made a sample by the number of entries in the source code of the last commercial project:

Definitely the find method is much more popular. This means that querySelector optimization takes place in most cases, so it can give a tangible gain in code performance on the client.

Conclusion


Developing with live extensions really makes life easier on the front-end. Separating the UI into many small parts helps to create more independent (= reliable) solutions. But, as seen above, better-dom is not only about them (although this was the original main goal).

During development, I realized one important thing: if the current standards are not completely satisfied or there are ideas on how to do it better, just implement and prove that they work . And it is very fun!

More information about the better-dom library can always be found on GitHub .

Source: https://habr.com/ru/post/209140/


All Articles