Web components without Shadow DOM

Hello, Habr!

Recently, I noticed a number of articles criticizing all kinds of web components. Sometimes, this criticism is very harsh and even smacks of hatred. In my opinion, the main problem here is the lack of established practice of working with this group of standards among the developer community. Many familiar models do not always fit organically into projects involving Custom Elements and Shadow DOM , many things have to be viewed from a new angle and not everyone likes it. I have been successfully working with web components for several years and even develop my own librarybased on them, so I think this situation is not very fair. I’ll try, at least partially, to fix it, to the extent of my modest strength. I decided to make a series of compact publications, in each of which I plan to touch on one of the frequent aspects of criticism, as well as to demonstrate a number of technical techniques that may be interesting to those who have not yet decided which side of the barricade he should be on. Today I would like to talk about how to create components without a Shadow DOM.

What for?


The main idea that I want to convey this time is that web components and the Shadow DOM are not the same thing. When using the Shadow DOM, you get two main benefits:

  • An isolated section of the document in which your styles feel safe from external influences and “leaks”
  • A compositional mechanism that allows you to divide the document into what is the structure of the component itself and its contents (descendants of the DOM element in the tree)

However, this mechanics also carries some costs for creating and styling an isolated space, which is quite natural. In some cases (large lists, table cells with data, etc.), I want to avoid these costs for reasons of performance optimization. Now we will fix it:

const MY_CSS = {
  title: 'color: #00f; font-size: 2em',
  item: 'color: #f00; font-size: 1.2em',
};

const DATA = [
  {text: 'Text 1'},
  {text: 'Text 2'},
  {text: 'Text 3'},
];

let template = document.createElement('template');
template.innerHTML = /*html*/ `
<div style="${MY_CSS_.title}">List items:</div>
<div class="my-list">
  ${DATA.map(item => /*html*/ `<div style="${MY_CSS.item}">${item.text}</div>`).join('')}
</div>
`;

class ShadowlessComponent extends HTMLElement {
  constructor() {
    super();
    this._contents = new DocumentFragment();
    this._contents.appendChild(template.content.cloneNode(true));
  }
  connectedCallback() {
    this.appendChild(this._contents);
  }
}

window.customElements.define('shadowless-component', ShadowlessComponent);

If you are already familiar with the Custom Elements standard, you will immediately notice what is happening: instead of calling the method attachShadowin the component constructor, we created a DocumentFragment into which we cloned a pre-prepared template. At this stage, the component is not rendered by the browser and it can be relatively safely modified, for example, bind / insert data.

The next important step is related to the Custom Elements life cycle. Components are added to the general document only after the constructor has fully worked and until that moment, that part of the DOM API that is responsible for working with the parents or descendants of the element, as well as with attributes, will not be available. Therefore, to directly add content to our component, we use connectedCallback.

When creating the template, we used the method for simplicity innerHTML. This operation is performed only once, when creating the “template” element, it is not repeated every time an instance of our component is created. However, this point can also be further optimized by creating patterns imperatively.

In total, using a custom tag in our markup shadowless-component, we get the following result in the browser:

<shadowless-component>
  <div id="caption" style="color: #00f; font-size: 2em">List items:</div>
    <div class="my-list">
      <div style="color: #f00; font-size: 1.2em">Text 1</div>
      <div style="color: #f00; font-size: 1.2em">Text 2</div>
      <div style="color: #f00; font-size: 1.2em">Text 3</div>
    </div>
</shadowless-component>

Since, having got rid of ShadowRoot, we lost the isolation of styles, we added styles to our template using the attribute. In this case, they have priority, this partially solves the problem and can be used in important places. For all other cases, classic styling is available through a common style sheet, and custom tags act as convenient selectors.

display: contents


Web components are the full nodes of your DOM. This means, in addition to the fact that all standard methods of DOM elements are available to you, and that your component is always a kind of container. That is, if you want to add an arbitrary structure of elements to the DOM using the web component, all of them will be descendants of your component, which is not always convenient. In such cases, you can use the new CSS rule - display: contents . Browser support: caniuse.com/#feat=css-display-contents

By default, all components have the display: inline property .

A bit of a mischief


But what if we don't want any extra containers and custom tags at all? Give pure HTML!

OK:

  constructor() {
    super();
    this._contents = new DocumentFragment();
    this._contents.appendChild(template.content.cloneNode(true));
    this._titleEl = this._contents.querySelector('#caption');
    window.setInterval(() => {
      this._titleEl.textContent = Date.now();
    }, 1000);
  }
  connectedCallback() {
    this.parentNode.prepend(this._contents, this);
    this.remove();
  }

As a result, we get this:

<div id="caption" style="color: #00f; font-size: 2em">1581075598392</div>
<div class="my-list">
  <div style="color: #f00; font-size: 1.2em">Text 1</div>
  <div style="color: #f00; font-size: 1.2em">Text 2</div>
  <div style="color: #f00; font-size: 1.2em">Text 3</div>
</div>

All events and bindings continue to work and are controlled by our component, which now exists only in memory. In this case, you will have to take additional care of unsubscribes and other cleaning from garbage at the moment when you want to remove the component completely.

CSS Components


According to the standard, custom tags must be named with the obligatory addition of a "-" character. If you use your tag in the markup, but at the same time do not create any component in JS and add its constructor to the component registry, the browser considers your tag to be an “unknown element” ( HTMLUnknownElement ). By default, these elements are similar in behavior to the span tag. This can be used if you need to create a simple dumb component with a simple structure for which CSS rules :: before , :: after and attr () expressions are enough . Example:

  my-container {
    display: block;
    padding: 10px;
    border: 1px solid currentColor;
  }
  my-container::before {
    content: attr(caption);
    margin-bottom: .6em;
  }

Use in markup:

<my-container caption=""></my-container>

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


All Articles