📜 ⬆️ ⬇️

Web components: review and use in production

Hello! I use web components in frontend development. In this article I will describe all the features of the web components, as well as how they can be used today, taking into account not yet high support.


Briefly about web components: this is a set of technologies that allow you to use the component approach with encapsulating styles and scripts on the web natively, without connecting any libraries or fireworks. If you're wondering what the standards offer instead of the usual already React or Angular, and how to use it when developing for old browsers, I ask for cat.



The list of materials for detailed study is at the end of the article.


Content:



Introduction


I work as a front-end developer of services in one of the major international campaigns and currently rewriting the project frontend a second time.


In the first version, written according to the canons of 1C-Bitrix, meat from scripts and styles in a variety of templates awaited me. The scripts, of course, were on jQuery, and the styles, of course, were absolutely chaotic, without any structure or order. In connection with the move to a new platform, there was a chance to completely rewrite the project. To restore order, we developed our own component system using the BEM methodology. For each instance of the BEM block in the markup, after loading the page, one object of the corresponding class is created, which begins to control the logic. Thus, everything is rather strictly systematized - the blocks (logic and styles) are reusable and isolated from each other.


After a year of support and refinement of the project revealed a number of shortcomings of such a system. The markup, on the basis of which my pseudo-components work, is kept on attentiveness and the developer's “honest word”: JS hopes that the layout designer has correctly placed all the necessary elements and prescribed classes for them. If a component modifies its DOM, which contains other BEM blocks, or the markup is loaded via ajax, the components of this markup must be initialized manually. It all seemed simple with daily work until a second person appeared on the project. The documentation, unfortunately, although it was quite voluminous, but covered only the basic principles (the volume in this case was a minus). In life, the step to the left or the step to the right broke the established "principles", and reading the finished components only made confusion, since they were written in different ways and at different times.


All this, as well as a potential increase in the number of problems in the development of the planned transition to SPA / PWA, pushed for the next processing of the front. Bicycles, which, in particular, is my component system, are very useful during learning something (in my case - JS), but in a quality project with several developers something more reliable and structured is needed. Currently (however, long ago) we have a lot of frameworks on the web, among which there is something to choose: Preact offers a small size and maximum similarity in development with React I already know, Angular beckons with built-in lovely TypeScript, Vue looks because of the angle and boasts of its simplicity, and much more. Stand alone stands out: it turns out, you can write reusable web components, with wired logic and styles that will not have to be artificially (due to BEM and additional JS-a) to be associated with the already written markup. And all this should work out of the box. Miracle, is not it?


Because of my wild love for standards and faith in a bright future, as well as the similarity of the existing system of components with web components (one JS class per one “component” with isolated logic), it was decided to try to use web components. Most strained support for this business in browsers: web components are too young to cover a wide range of browsers, and even more so to cover browsers that need to be supported by a commercial product (for example, Android 4.4 stock browser and Internet Explorer 11). Mentally, some level of pain and limitations that I could expect and a framework in which I agree to fit into the development was adopted, and I immersed myself in learning theories and practical experiments: how to write the front on web components and roll it out into production so that it worked.


The necessary theoretical minimum for reading the article: pure JavaScript at the level of basic manipulations with the DOM tree, understanding the syntax of classes in ES2015, plus familiarity with any of the frameworks from the category of React.js / Angular / Vue.js.


Theory


Overview


Web components are a collection of standards that allow you to make declaratively described, reusable “widgets” with isolated styles and scripts in the form of your own tags. Standards evolve independently, and are connected into web components quite arbitrarily - in principle, you can use each of the technologies used separately. But together they are as effective as possible.


Usually, all four standards — Custom Elements, Shadow DOM, Templates (HTML Templates), and HTML Imports — are treated separately, and then merged together. Since individually they are weakly useful, we will consider all the possibilities of standards cumulatively, adding them to the already studied earlier.


It is worth recalling that web components are a rather young technology, and standards have undergone many changes. For us, this is mainly expressed in several versions of the Custom Elements and Shadow DOM standards - v0 and v1 . v0 is not currently relevant. We will only consider v1 . Be careful when searching for additional materials! Versions v1 formed only in 2016, which means that all articles and videos until 2016 are guaranteed to speak about the old version of the specification.


Custom Elements


Custom elements are the ability to create new HTML tags with arbitrary names and behavior, for example, <youtube-player src=""></youtube-player> or <yandex-map lat="34.86974" lon="-111.76099" zoom="7"></yandex-map> .


Register a custom item


Of course, we can “create” our own tags (for example, browsers will correctly handle the <noname></noname> ), but in the DOM tree, the element is registered as an object of the HTMLUnknownElement class and has no default behavior. "Revive" each such element will have to manually.


The specification of custom elements allows you to register new tags and set their behavior in accordance with the life cycle - creating, inserting into the DOM, changing attributes, deleting from the DOM. In order to prevent possible conflict of new HTML standard tags and custom tags, the names of the latter must contain at least one hyphen - for example, <custom-tag></custom-tag> or <my-awesome-tag></my-awesome-tag> . Also, custom tags currently cannot be self-closing, even tags without content must be paired.


Since the best way of training is practice, we will write an element that is similar in functionality to the <summary> element. Let's call it <x-spoiler> .


When you add such an element to a DOM tree, it will also become an object of the HTMLUnknownElement class. In order to register an item as custom, and add its customElements behavior to it, you need to use the define method of the global customElements object. The first argument is the name of the tag, the second is the class that describes the behavior. The class should extend the class HTMLElement so that our element has all the qualities and capabilities of other HTML elements. Total:


 class XSpoiler extends HTMLElement {} customElements.define("x-spoiler", XSpoiler); 

After that, the browser will recreate all the x-spoiler tags in the markup as objects of the XSpoiler class, and not HTMLUnknownElement . All new x-spoiler tags that are added to the document via innerHTML , insertAdjacentHTML , append or other methods for working with HTML are immediately created based on the XSpoiler class. You can also create such DOM elements through document.createElement .


If you try to register an item with an already registered name or based on an already registered class, we will get an exception.


The tag name and class name do not have to be the same. Therefore, we can, if necessary, register two custom elements with the same class name under different tags.


Now the user element is, of course, registered, but it does nothing useful. To really liven up our custom item, consider its life cycle.


The life cycle of a custom item


We can add callback methods to create an element, add it to the DOM, change attributes, delete an element from the DOM, and change the parent document. We will use this to implement the spoiler logic: the component will contain a button with the text “Collapse” / “Expand” and a section with the original contents of the tag. Section visibility will be controlled by clicking on the button or attribute value. Button text can also be customized via attributes.


The callback for creating an element is the class constructor. To make it work correctly, you must first call the parent constructor via super . In the constructor, you can set the markup, hang event handlers, do some other preparatory work. In the constructor, as in other methods, this will refer to the DOM element itself, and due to the fact that our custom element extends the HTMLElement , this has methods such as querySelector and properties such as classList .


Add values ​​for the button texts in the constructor, markup of the component and attach the handler to the click on the button, which will change the presence of the opened attribute.


 class XSpoiler extends HTMLElement { constructor() { super(); this.text = { "when-close": "", "when-open": "", } this.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;">${this.innerHTML}</section> `; this.querySelector("button").addEventListener("click", () => { const opened = (this.getAttribute("opened") !== null); if (opened) { this.removeAttribute("opened"); } else { this.setAttribute("opened", ""); } }); } } 

Let us analyze in detail each part of the constructor.


super() calls the constructor of the class HTMLElement . This is in this case a mandatory action if we need an element constructor.


this.text - since this is an object, we can add our own properties. In this case, I will store in the text object auxiliary texts that are displayed on the button.


this.innerHTML set the markup of our DOM element. In this case, we use the text specified slightly above.


this.querySelector("button").addEventListener will add a click event handler that sets or removes the opened attribute. We will work with it as a logical value - the spoiler is either open or closed, therefore, the attribute is either there or not. In the handler, we will check the presence of the attribute through comparison with null , and then either set or delete the attribute.


Now when you click on the created button, the opened attribute will change. So far, changing the attribute does nothing. Before turning to this topic, we modify the code a bit.


Remember that the <button> has a disabled attribute that disables the button? If we write it in the markup, the button will cease to be active; if we delete it, it will again become clickable. We can work with an attribute from JavaScript code using getAttribute , setAttribute and removeAttribute methods. But this is not very convenient, we need three methods for working with attributes, they are long, and, moreover, they work only with strings (the attribute value is always a string). Therefore, in the DOM-elements is often used "reflection" of attributes in the same properties. So, the button.disabled property returns the presence or absence of an attribute. Now compare the two approaches, through direct work with attributes and through properties:


 //   : // : const isDisabled = button.getAttribute("disabled") !== null; // : const isDisabled = button.disabled; //  : // : button.setAttribute("disabled", ""); // : button.disabled = true; //  : // : button.removeAttribute("disabled"); // : button.disabled = false; //    : // : if (button.getAttribute("disabled") !== null) { button.removeAttribute("disabled"); } else { button.setAttribute("disabled", ""); } // : button.disabled = !button.disabled; 

Agree, work through the properties is much more convenient? We implement the same mechanism with our attribute opened , so that its value can be easily obtained and installed. To do this, we use the possibility of getters and setters of properties in classes:


 class XSpoiler extends HTMLElement { constructor() { super(); this.text = { "when-close": "", "when-open": "", } this.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;">${this.innerHTML}</section> `; this.querySelector("button").addEventListener("click", () => { this.opened = !this.opened; }); } get opened() { return (this.getAttribute("opened") !== null); } set opened(state) { if (!!state) { this.setAttribute("opened", ""); } else { this.removeAttribute("opened"); } } } 

For string properties (as for embedded id on any elements and href on links), the getter and setter will look a bit simpler, but the idea is preserved.


You can also add that such a "reflection" may not always be useful in terms of performance. For example, the value attribute works differently for form elements.


Now we have an attribute, it can change its value when a button is clicked, but no more useful action occurs. We could add useful code to hide and display the element directly in the click handler, but then it would be very problematic to change the element's visibility, for example, with another, external JS code.


Instead, you can attach a handler to change the value of the attributes using the attributeChangedCallback method. It will be called each time the attribute is changed, so it will be possible to control the component through the attributes both from the inside and the outside.


The method applies three parameters: attribute name, old value, new value. Since calling this method to change absolutely ALL attributes would be irrational from the point of view of performance, it only works when the properties that are listed in the observedAttributes static class of the current class are changed.


Our component should respond to the change of three attributes - opened , text-when-open and text-when-close . The first will affect the display of the spoiler, and the other two will control the button text. First of all, add the names of these attributes to our class in the observedAttributes static array:


 static get observedAttributes() { return [ "opened", "text-when-open", "text-when-close", ] } 

Now we add the attributeChangedCallback method itself, which, depending on the attribute that was changed, will either change the visibility of the content and display the button text, or change the button text and display it if necessary. To do this, use the switch on the first argument of the method.


 attributeChangedCallback(attrName, oldVal, newVal) { switch (attrName) { case "opened": const opened = newVal !== null; const button = this.querySelector("button"); const content = this.querySelector("section"); const display = opened ? "block" : "none"; const text = this.text[opened ? "when-open" : "when-close"]; content.style.display = display; button.textContent = text; break; case "text-when-open": this.text["when-open"] = newVal; if (this.opened) { this.querySelector("button").textContent = newVal; } break; case "text-when-close": this.text["when-close"] = newVal; if (!this.opened) { this.querySelector("button").textContent = newVal; } break; } } 

Notice that the attributeChangedCallback method works even when the required attributes are initially present on the element. That is, if our component is inserted into the markup immediately with the opened attribute, the spoiler will indeed be opened, since attributeChangedCallback will work immediately after the constructor . Therefore, no additional work on processing the initial value of the attributes in the constructor is necessary (unless, of course, the attribute is traceable).


Now our component really works! When a button is clicked, the value of the opened attribute changes, then the attributeChangedCallback callback is activated, which in turn controls the visibility of the content. State management is through attributes and attributeChangedCallback allows you to control the initial state (we can add opened to the markup immediately if we want to show an open spoiler) or control the state from the outside (any other JS code can set or remove an attribute from our element and it will be processed correctly) . As a bonus, we can customize the text of the control button. View demo result in fresh Chrome !


The main functionality is ready; these are the most frequently used features of user elements. Now consider those callbacks that are used less frequently.


When the element is inserted into the DOM tree, the connectedCallback method is triggered. If the element was already in the markup at the time of registration, or it is created by inserting an HTML line, the constructor will work sequentially, if necessary, attributeChangedCallback , and then connectedCallback . This callback can be used if, for example, you need to know the information about the parent in the DOM tree, or we want to optimize our component and put off some kind of heavy code just before the element is used. However, there are two things to remember: first, if the constructor triggered once for one element, then connectedCallback triggered every time an element is inserted into the DOM, and second, attributeChangedCallback can trigger before connectedCallback , so if you postpone the creation of markup from constructor to connectedCallback , this can lead to an error. The method can be used to assign event handlers or for other heavy operations, such as connecting to a server.


In the same way that you can track the insertion of an element in the DOM, you can track and delete. The disconnectedCallback method is responsible for this. It works, for example, when an element is removed from the DOM using the remove() method. Note: if an element is removed from the DOM tree, but you have a link to the element, it can again be inserted into the DOM, and the connectedCallback will re-activate. When deleting, you can, for example, stop updating the data in the component, delete the timers, delete event handlers assigned to connectedCallback , or close the connection to the server. Please note that disconnectedCallback does not guarantee the execution of its code - for example, when a user closes a page, the method will not be called.


The most rarely used callback is the adoptedCallback method. It is triggered when the element changes the property ownerDocument . This happens, for example, if you create a new window and move an element into it.


User Element Interaction


The component, as we already understood, is controlled by the value of the attributes directly or through properties. But in order to transfer data from a component to the outside, you can use CustomEvents . In the case of our component, it will be rational to add a state change event so that it can be listened to and responded. To do this, add the events property with two CustomEvent objects in the CustomEvent :


 this.events = { "close": new CustomEvent("x-spoiler.changed", { bubbles: true, detail: {opened: false}, }), "open": new CustomEvent("x-spoiler.changed", { bubbles: true, detail: {opened: true}, }), }; 

We also edit attributeChangedCallback so that when a opened sent an event:


 this.dispatchEvent(this.events[opened ? "open" : "close"]); 

Now we can listen to events of interest to us in order to learn about the changing state of the spoiler.


New demo .


More about customElements


When registering an item, we used the define method of the global customElements object. In addition, he has two more useful methods.


customElements.get(name) will return the constructor of the custom element registered under the name name , if there is one, or undefined .


customElements.whenDefined(name) will return a promise that will be executed successfully when an element with the name name is registered, or immediately if the element is already registered. This is especially convenient with the use of await , but that's another topic.


Extend custom items


We can inherit from the user element class to create new custom elements based on existing ones. For example, we can expand our spoiler and add some functionality to it if necessary. It is important to remember to call the same method of the parent class via super.methodName() , if necessary (in the case of the constructor and super() this is mandatory).


Extension of standard elements


The specification also allows extensions to standard HTML tags. For example, you need your own implementation of the button, but at the same time you want to keep the existing functionality of browser buttons, for example, to work correctly with the attributes disabled , tabindex , type and others. However, there are a number of features.


First, when declaring a class, you need to extend the class of the tag you need. In the case of a button, this will be the HTMLButtonElement class. A complete list of classes can be found in the specification .


Secondly, when registering an element with a third parameter, you must pass an object of options, which indicates which particular tag you want to expand (the same class can correspond to several tags).


Thirdly, such a custom element is created as a regular tag that needs to be expanded, but with an is attribute equal to the name of the custom element. If the element is created through document.createElement , is passed as a property of the second argument.


It will look like this:


 class FancyButton extends HTMLButtonElement { } customElements.define("fancy-button", FancyButton, {extends: "button"}); 

 //    document.createElement let button = document.createElement("button", {is: "fancy-button"}); 

 <!--    HTML --> <button is="fancy-button" disabled>Fancy button!</button> 

Styling elements before registration


In the meantime, how the user gets the HTML markup, and how the JavaScript code will be downloaded and executed, it will take some time. To somehow stylize custom elements that are drawn in the DOM, but have not yet been registered and do not work properly, you can use the pseudo-class :defined . The simplest use case is to hide all unregistered user elements:


 *:not(:defined) { display: none; } 

Total


We made a reusable web component based on custom element technology. However, he has many drawbacks. So, for example, we have no isolation of styles: the rule section {display: block !important} will easily break the logic of our component. In general, hanging styles directly in JS is a bad tone. It is also difficult to change the contents of the spoiler: our button, section and click handler will disappear when installing new content via innerHTML . In order to really change the content, you will need to know and take into account the structure of the component. And also the markup is stored right here in the constructor. All this is clearly not what we want from a simple reusable component. In order to fix all these disadvantages, we will use other specifications.


Shadow DOM (Shadow DOM)


The shadow DOM specification will solve the problem of isolating styles and layouts from the environment and internal content.


DOM Encapsulation


The standard DOM model we are used to assumes that all descendants of an element are accessible through childNodes , they can be found via querySelector() , and so on. DOM - pass through, wherever there is a paragraph of text, it will always be found through document.querySelectorAll("p") . However, it is possible to display not what is in the DOM tree, but any other markup, and so that it is ignored by the usual childNode and querySelector . The simplest example of this behavior would be a <video> with several <source> inside. We add only <source> to the DOM, and we see a full-fledged video player, with its own isolated markup (blocks, buttons, and so on). Everything we see on the screen is located in the shadow DOM. How it works?


DOM attachShadow() . DOM- : Shadow DOM, Light DOM Flattened DOM. Consider them separately.


Light DOM — , DOM- : , innerHTML , childNodes querySelectorAll .


Shadow DOM — DOM-, shadowRoot . attachShadow shadowRoot , - innerHTML , appendChild DOM, this.shadowRoot .


Flattened DOM — Shadow DOM Light DOM. , . , Shadow DOM. Light DOM element.innerHTML , Shadow DOM element.shadowRoot.innerHTML , Flattened DOM . Flattened DOM Shadow DOM . , Light DOM . For example:


 <x-demo>!</x-demo> 

 class Demo extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = "  ..."; } } customElements.define("x-demo", Demo); 

DOM, .. «!». , , « ...».


: Shadow DOM , Shadow DOM . open . Shadow DOM shadowRoot .


. <slot> name Shadow DOM slot Light DOM. , Flattened DOM Lignt DOM. : ( 0, 1 ) slot - Light DOM <slot> name Shadow DOM. - <slot> , Shadow DOM. <slot> name , Light DOM slot .


, Shadow DOM Light DOM Shadow DOM <slot> , Light DOM <slot> .


: «» ( ) Shadow DOM, Light DOM <slot> . :


 this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <button type="button">${this.text["when-close"]}</button> <section style="display: none;"><slot></slot></section> `; 

Shadow DOM, this.querySelector("button") this.shadowRoot.querySelector("button") . section .


, , textContent . . , , . .


: , «» DOM- Light DOM. , , : .


, Light DOM slot , <slot> name ( ). Light DOM slot , Shadow DOM <slot> , name ( ). slot - , <slot> ( ).



DOM Light DOM / Shadow DOM , .


. Shadow DOM <style> , Shadow DOM. ,


 section { height: 50%; width: 50%; } 

section , Shadow DOM. : section Shadow DOM . . , .


: :host . . , . , , .


, , , . Shadow DOM attributeChangedCallback . .


- . : :host-context(.red) , .red . , , .


Light DOM, . ::slotted , , - Light DOM. , . Example:


 <name-badge> <h2>Eric Bidelman</h2> <span class="title"> Digital Jedi, <span class="company">Google</span> </span> </name-badge> 

 <style> ::slotted(h2) { margin: 0; font-weight: 300; color: red; } ::slotted(.title) { color: orange; } /*    (      ). ::slotted(.company), ::slotted(.title .company) { text-transform: uppercase; } */ </style> <slot></slot> 

, - , :host :host-context .


:host . , :host . .


Shadow DOM CSS , , , , .


DOM


DOM DOM , . , DOM, , <video> .


DOM JavaScript- , DOM {mode: "open"} .


?


«» Shadow DOM, mode closed . Shadow DOM shadowRoot , null . shadowRoot attachShadow() , , shadowRoot - . Shadow DOM, .


<slot> slotchange , Light DOM, . , .


<slot> assignedNodes , DOM- Light DOM, . DOM- {flatten: true} , ( ).


Light DOM assignedSlot , , .


(HTML Templates)


, HTML.


Shadow DOM . - , innerHTML , - .


<template> , HTML Templates. , , . , DOM-, , <script> , <link> , , querySelector .


DOM- <template> content , DocumentFragment , . , appendChild .


, Shadow DOM: .


, «» . : , . , DOM-, . .


HTML- (HTML Imports)


. , , , , . HTML- : HTML-, (, , ) <link rel="import" href="x-spoiler.html"> .


, html- <template> <script> . , , <script> . html- , , : , , DOM , , document.getElementById . DOM- html- import link . <link> id, , import template , . <script> const ownerDocument = document.currentScript.ownerDocument , , , <template> ownerDocument , document . . !


, . , ( ), <link rel="import"> . . , , , .


Practice



. HTML- :


  1. « ». — . app.html , , ? , 20 . , , , , . , . , , .
  2. , . 100 , , n- . , , , .
  3. , HTTP/2. HTTP/2 , .
  4. document.currentScript . , IE11 . , , id id, <template> , , , .
  5. Firefox HTML- , . , - , Firefox, , .

, , . , . , HTML-. .


How to use


, IE , Edge 12 Safari 7- . .


v1 ( Chrome 54+ , Safari 10.1+ ) DOM v1 ( Chrome 53+ , Safari 10+ ). .


WebComponents/webcomponentsjs . , , webcomponents-sd-ce.js , DOM, HTML-.


, , : , — ShadyDOMShadyCSS DOM. . : <template> .


Promise , CustomEvent , Object.assign() , Array.from() . - , webcomponents-platform es6-promise , polyfill.io .


Element.prototype.insertAdjacentHTML , . , - , issue . UPD: .


, , . ( ) .


WebReflection , ( Firefox). , , , .


, Babel , .


, :



,



. . :



:



, , . , .



— . Babel extends , Chrome , , ES5-. , custom-elements-es5-adapter.js , , . , . ES5-, - ( ), ( ).


DOM , :


 <div id="custom-elements-adapter-test"> <script> (function() { if (isNative(window.customElements.define) === false) { // ,  Element.prototype.remove()   , //        , //  .parentNode.removeChild() document.getElementById("custom-elements-adapter-test").remove(); } function isNative(fn) { return (/\{\s*\[native code\]\s*\}/).test('' + fn); } })(); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/custom-elements-es5-adapter.js"></script> </div> 

, — . — , . — , .


babel-plugin-transform-custom-element-classes . es5-adapter , IE11 , Reflect.construct . , , , . , , — . : , babel-polyfill . . , .


babel-plugin-transform-builtin-classes WebReflection . , , IE11 - , . , WebComponents . — WebReflection, WebComponents . .


, , — Babel 7, . . , Chrome ( ), IE11, Reflect.construct .


: DOM ES5-, , . , — .


constructor : IE Safari HTMLUnknownElementConstructor , . .


.


DOM


, . -, ShadyCSS.prepareTemplate() , , — , . <template id="x-spoiler"> <x-spoiler> , ShadyCSS.prepareTemplate(document.getElementById("x-spoiler"), "x-spoiler") .


: ShadyCSS.styleElement(this) ;


:host() : :host(.zot) :host(.zot:not(.bar)) , :host(.zot:not(.bar:nth-child(2))) — .


::slotted : . , ::slotted(span) , .header ::slotted(span) .


.



<template> . , DOMContentLoaded , , customElements.define() , DOM-, content template . : customElements.define() DOMContentLoaded , customElements.define() . . :


 try { HTMLTemplateElement.bootstrap(document); } catch (e) { } 

<template> DOM , : <template> DOM- . .


<script> : , <template> - : IE . . , - .


gulp


, , : html . <script> , . ( ) <template> , DOM HTML. <template> id , . DOM, <style> , CSS. , HTML , , <style> , css.


, , . For this we need:



gulp , , gulpfile , . github . :


src . html - (, ), scaffolding.js js- components -.


index.html @@templates , -, -, , es5- , app.js -, .


scaffolding.js , DOM ( ).


. : .


scripts <script> app.js :


 gulp.task("scripts", () => //    gulp.src("src/components/*.html") //       .pipe(concat("app.js")) //  .     , //  jsdom,    api //    <script>     .pipe(insert.transform(content => { const document = (new JSDOM(content)).window.document; const scriptsTags = document.querySelectorAll("script"); const scriptsContents = Array.prototype.map.call(scriptsTags, tag => tag.textContent); return scriptsContents.join(""); })) //   scaffolding.js .pipe(gap.prependFile("src/scaffolding.js")) //    Babel .pipe(babel({ presets: ["env"] })) //   .pipe(gulp.dest("dist")) ) 

templates , <template> :


 gulp.task("templates", () => //      gulp.src("src/components/*.html") //      jsdom,    <template> //     data-, ,    //  .     .pipe(insert.transform((content, file) => { const componentName = path.basename(file.path, ".html"); const document = (new JSDOM(content)).window.document; const templatesTags = document.querySelectorAll("template"); templatesTags.forEach(template => template.setAttribute("data-component", componentName)); const templatesHTML = Array.prototype.map.call(templatesTags, tag => tag.outerHTML); return templatesHTML.join(""); })) //        .pipe(concat("templates.html")) //   gulp-html-postcss,     html  <style> .pipe(postcss([ autoprefixer() ])) //   —  html  css .pipe(htmlmin({ collapseWhitespace: true, conservativeCollapse: true, minifyCSS: true, })) //   .pipe(gulp.dest("dist")) ); 

html- @@templates templates.html , templates . gulp, . , php include . gulp .


, , data -? Gulp . , , ShadyCSS.prepareTemplate() . , scaffolding.js :


 document.querySelectorAll("template[data-component]").forEach(template => { ShadyCSS.prepareTemplate(template, template.dataset["component"]); }); 

That's all! -, gulp . .



, . , , HTMLElement . x-component , , . ( , ):


$ $$ . querySelector querySelectorAll Light DOM. Chrome DevTools, jQuery .


fireEvent , . CustomEvent , , dispatchEvent . , c .


 //  this.dispatchEvent(new CustomEvent(`x-timer.ended`, { bubbles: true })); //  this.fireEvent(`x-timer.ended`); 

is localName , — . , , ( ).


getTemplateCopy - id .


makeShadowRoot DOM. , id ( this.is ) getTemplateCopy . DOM ( ShadyCSS.styleElement(this); ), . $ $$ shadowRoot .


 //  ShadyCSS.styleElement(this); this.attachShadow({ mode: "open" }); const template = document.getElementById("x-spoiler"); const templateClone = template.content.cloneNode(true); templateClone.querySelector("button").textContent = this.text["when-close"]; this.shadowRoot.appendChild(templateClone); //  this.makeShadowRoot(); this.shadowRoot.$("button").textContent = this.text["when-close"]; 

. , properties , , / , ( ), . , :


 // ,  get opened() { return this.getAttribute("opened") !== null; } set opened(value) { if (!!value) { this.setAttribute("opened", ""); } else { this.removeAttribute("opened"); } } // ,  properties = { opened: { type: Boolean, }, } 

, - .


- , .


, — , , ( ).


,


, , , - IE11, , ? Chrome, , es5- es5- -. Not order.


, JS , - ( — ES), . , — . ? , . , , , 2018 75%. .


-, , ES5 ES6 . , , Babel scaffolding.js . :


 gulp.task("scripts-es5", buildJS.bind(null, "es5")); gulp.task("scripts-es6", buildJS.bind(null, "es6")); function buildJS(mode) { return gulp.src("src/components/*.html") .pipe(concat(`app-${mode}.js`)) .pipe(insert.transform(content => { const document = (new JSDOM(content)).window.document; const scriptsTags = document.querySelectorAll("script"); const scriptsContents = Array.prototype.map.call(scriptsTags, tag => tag.textContent); return scriptsContents.join(""); })) .pipe(gulpif(mode === 'es5', gap.prependFile("src/scaffolding.js"))) .pipe(gulpif(mode === 'es5', babel({presets: ["env"]}))) .pipe(uglify()) .pipe(gulp.dest("dist")) } 

, app-es5.js app-es6.js . , . , ́ , .


. , 150 ! . , , . , . . html:


 (function () { var wcReady = ("attachShadow" in document.documentElement) && ('customElements' in window); var scripts; if (wcReady) { scripts = [ "./app-es6.js" ]; } else { scripts = [ "https://cdn.polyfill.io/v2/polyfill.js?features=default", "https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/webcomponents-sd-ce.js", "https://cdn.jsdelivr.net/npm/template-mb@2.0.6/template.js", "./app-es5.js" ]; } scripts.forEach(function (script) { insertScript(script); }); function insertScript(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); } })(); 

. : , - bower es-5 . , , .


.


Polymer


-, Polymer . , - - Polymer.Element . , -, .


, -, Polymer. - webcomponents.org Polymer, . , Polymer.


Polymer , , Polymer, , -. Polymer -, , , jQuery DOM JS.


Polymer HTML-.


Polymer, -, , .


Polymer API . , , , HTML- ( ES6 ), HTML- ( ). , , . , .


Polymer, . , , , Polymer.



, — , , . . . , ( , ) . , , PostCSS , . — IDE PostCSS- <style> . .


, node-sass . , <style></style> , .


findings


— , , , -. , - , - .


, (, ::part ::theme DOM ). - — , . - Polymer — , - Polymer, .


Resources



')

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


All Articles