📜 ⬆️ ⬇️

WebComponents as frameworks, component interaction

When it comes to web components, they often say: “What do you want without frameworks? Everything is ready there. ” In fact, there are frameworks created on the basis of implementations of the standards included in the group of web components. There are even relatively good ones like the X-Tag . But today we’ll still understand how simple, elegant and powerful the browser API has become for solving everyday development tasks, including organizing the interaction of components with each other and with other objects from the context of the browser’s execution, and you can always use frameworks with web components, even those that were developed across standards, including through the mechanisms that we will analyze today, at least as stated on the site .

Decisions like a reaction taught us that there should be one big data store and the components signed for its changes, and in web components they are also trying to discern the absence of such a subsystem, actually implying even unconsciously reverse binning and it already exists in web components.

Each element has attributes of the value of which we can change. And if you list the names in the observedAttributes hook, then when they change, attributeChangedCallback () will be automatically called in which we can determine the behavior of the component when the attribute changes. Using getter magic, it is easy to do reverse binding in a similar way.

We have already sketched some project in the first part and today we will continue to cut it further.
')
It’s worth mentioning one limitation right away, at least in the current implementation, that the attribute value can only be a primitive value specified by a literal and reducible to a string, but in general it is possible to transfer “objecs” in this way using single quotes from the outside and double to define values ​​and fields of the project.

<my-component my='{"foo": "bar"}'></my-component> 

To use this value in the code, you can implement an auto-magic getter that will call JSON.parse () on it .

But for now, the numerical value of the counter is enough for us.

We add a new attribute to our element, specify it as observed, click the click handler to increment this counter, and add the update of the displayed value by direct link to the change hook, the logic of which is implemented in a separate reusable updateLabel () method.

 export class MyWebComp extends HTMLElement { constructor() { super(); } connectedCallback() { let html = document.importNode(myWebCompTemplate.content, true); this.attachShadow({mode: 'open'}); this.shadowRoot.appendChild(html); this.updateLabel(); } updateLabel() { this.shadowRoot.querySelector('#helloLabel').textContent = 'Hello ' + this.getAttribute('greet-name') + ' ' + this.getAttribute('count'); } static get observedAttributes() { return ['count']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'count') { this.updateLabel(); } } showMessage(event) { this.setAttribute('count', this.getAttribute('count') + 1); } } 



Each item received an independent, automatically updated counter.

Homework: implement the conversion of the counter to a number and use through an auto-getter;)

Of course, more advanced options are possible. For example, if you do not take into account simple callbacks and events, using the native Proxy and the same getters and setters, you can implement the functionality of reverse binding.

Add my-counter.js file with a class of this kind

 export class MyCounter extends EventTarget { constructor() { super(); this.count = 0; } increment() { this.count++; this.dispatchEvent(new CustomEvent('countChanged', { detail: { count: this.count } })); } } 

We inherited a class from EventTarget so that other classes can subscribe to events thrown by objects of this class and define a count property that will store the counter value.

Now add the instance of this class as a static property for the component.

 <script type="module"> import { MyWebComp } from "./my-webcomp.js"; import { MyCounter } from "./my-counter.js"; let counter = new MyCounter(); Object.defineProperty(MyWebComp.prototype, 'counter', { value: counter }); customElements.define('my-webcomp', MyWebComp); </script> 

And in the component code, we will subscribe to the value change the updateLabel () label update method, into which we will add the value display from the global shared counter. And in the click handler, a direct call to the incremental method.

 export class MyWebComp extends HTMLElement { constructor() { super(); } connectedCallback() { let html = document.importNode(myWebCompTemplate.content, true); this.attachShadow({mode: 'open'}); this.shadowRoot.appendChild(html); this.updateLabel(); this.counter.addEventListener('countChanged', this.updateLabel.bind(this)); } updateLabel() { this.shadowRoot.querySelector('#helloLabel').textContent = 'Hello ' + this.getAttribute('greet-name') + ' ' + this.getAttribute('count') + ' ' + this.counter.count; } static get observedAttributes() { return ['count']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'count') { if (this.shadowRoot) { this.updateLabel(); } } } showMessage(event) { this.setAttribute('count', parseInt(this.getAttribute('count')) + 1); this.counter.increment(); } } 

Despite the fact that each element received two counters, each of its values ​​will be incremented independently, but the value of the shared counter will be the same for both elements, and the individual values ​​will be determined by the number of clicks only on them.



Thus, we got direct binding in direct binding and due to the use of events, this value is weak in updating. Of course, nothing prevents the increment from being implemented through events, by binding the increment () method to the event’s lyser:

 export class MyCounter extends EventTarget { constructor() { super(); this.count = 0; this.addEventListener('increment', this.increment.bind(this)); } increment() { this.count++; this.dispatchEvent(new CustomEvent('countChanged', { detail: { count: this.count } })); } } 

and replacing the method call with an event throw:

 export class MyWebComp extends HTMLElement { ... showMessage(event) { this.setAttribute('count', parseInt(this.getAttribute('count')) + 1); this.counter.dispatchEvent(new CustomEvent('increment')); } } 

What does it change? Now, if during the development the increment () method is removed or changed, the correctness of our code will be violated, but there will be no interpreter errors, i.e. operability will remain. This characteristic is called weak connectivity.

In development, both direct and weak binding are needed, usually it is customary to implement direct binding inside the logic of one component module, and weak binding between different modules and components to provide flexibility and extensibility of the entire system. HTML semantics implies a high level of such flexibility, i.e. preference for weak connectivity, but the application will work more reliably if the connections inside the components are direct.

We can hang up handlers in the old fashion and call events on the global document object.

 export class MyWebComp extends HTMLElement { constructor() { super(); } connectedCallback() { let html = document.importNode(myWebCompTemplate.content, true); this.attachShadow({mode: 'open'}); this.shadowRoot.appendChild(html); this.updateLabel(); this.counter.addEventListener('countChanged', this.updateLabel.bind(this)); document.addEventListener('countChanged', this.updateLabel.bind(this)); } disconnectedCallback() { document.removeEventListener(new CustomEvent('increment')); document.removeEventListener(new CustomEvent('countChanged')); } ... showMessage(event) { this.setAttribute('count', parseInt(this.getAttribute('count')) + 1); this.counter.dispatchEvent(new CustomEvent('increment')); document.dispatchEvent(new CustomEvent('increment')); } } 

 export class MyCounter extends EventTarget { constructor() { super(); this.count = 0; this.addEventListener('increment', this.increment.bind(this)); document.addEventListener('increment', this.increment.bind(this)); } increment() { this.count++; this.dispatchEvent(new CustomEvent('countChanged', { detail: { count: this.count } })); document.dispatchEvent(new CustomEvent('countChanged', { detail: { count: this.count } })); } } 

The behavior is no different, but now we need to think about removing the event handler from the global object if the element itself is removed from the tree. Fortunately, web components provide us not only design hooks, but also destruction, avoiding memory leaks.

There is also one more important point: the events of tree elements can interact with each other through a mechanism called “bubbling”.

When the user clicks on our element, the event, having worked on the element itself, starts to be called on the parent like circles on the water, then on the parent of this parent and so on to the root element.

At any point in this call chain, the event can be intercepted and processed.

This, of course, is not exactly the same event, and its derivatives and contexts, although they will contain links to each other as for example in event.path , will not completely coincide.

However, this mechanism allows you to associate child elements with their parents in such a way that this link does not violate engineering patterns, i.e. without direct links, but only due to their own behavior.

In ancient times, this also gave rise to a lot of trouble, when the events of certain elements caused “circles” or echoes in some sections of the document that were undesirable for reactions.

For years, web developers struggled with the “propaganda” (spreading or surfacing) of events and stopped them and turned them as they could, but for their comfortable use, as in the case with IDs, only the isolation of the shadow tree was lacking.

The magic here is that events do not loot outside the Shadow Tree. But nevertheless, you can catch the event thrown by its children in the host component and roll it up the tree in the form of some other event meaning, well, or just process it.

To understand how it all works, we wrap our webcomp elements in a container like this:

 <my-webcont count=0> <my-webcomp id="myWebComp" greet-name="John" count=0 onclick="this.showMessage(event)"></my-webcomp> <my-webcomp id="myWebComp2" greet-name="Josh" count=0 onclick="this.showMessage(event)"></my-webcomp> </my-webcont> 

The container will have this code:

 export class MyWebCont extends HTMLElement { constructor() { super(); } connectedCallback() { this.addEventListener('increment', this.updateCount.bind(this)); } updateCount(event) { this.setAttribute('count', parseInt(this.getAttribute('count')) + 1); } static get observedAttributes() { return ['count']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'count') { this.querySelectorAll('my-webcomp').forEach((el) => { el.setAttribute('count', newValue); }); } } } 

In connectedCallback (), we will hang the handler on the increment event, which will throw the children. The handler will increment the element’s own counter, and a callback to change its value will go through all the child elements and increment their counters, on which the handlers previously developed by us hang.

The code of the child elements will change slightly, in fact, all we need is for the increment event to throw out the element itself and not its aggregates, and do it with the bubbles: true attribute.

 export class MyWebComp extends HTMLElement { ... showMessage(event) { this.setAttribute('count', parseInt(this.getAttribute('count')) + 1); this.counter.dispatchEvent(new CustomEvent('increment')); this.dispatchEvent(new CustomEvent('increment', { bubbles: true })); document.dispatchEvent(new CustomEvent('increment')); } } 



Now, despite some general randomness, the first element counters will always show the value of the parent. All together, what we have done here today is certainly not an example to follow, but only an overview of the possibilities and techniques by which you can organize a truly modular interaction between components, avoiding a minimum of tools.

You can find the finished code for the reference in the same repository, in the events brunch .

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


All Articles