📜 ⬆️ ⬇️

Condition Management in Polymer 2.0. Outside of parent / child bindings

Let's organize a common state between divided DOM elements without Redux


To demonstrate how easy it is to use the framework and how productively you will work with it, the examples provided are often consciously simplified to fit simple situations. Simplification makes sense when teaching people, but sometimes they can leave a gap in knowledge in those cases when you need to go beyond the trivial and start creating more complex, real-world applications.


When it comes to managing a state, it becomes especially obvious, and Polymer here, of course, is no exception. Typical examples that you will see most often include one parent, one or more children, and some kind of binding between them. But what if things are not so simple? How to transfer state between different parts of the application? Do I need to start adding Redux to do this?

Often you will hear rules like “props down, events up” and “use the Mediator pattern”, and this really makes sense, but here you can miss one thing - they work within a group of elements acting together as one, but these rules are less apply when your elements need to organize the overall state, but the elements are separated in the DOM, maybe even they are in different branches of the DOM tree. You also do not want to create a chain of binders through the elements for which the transmitted data is not important and which do not have to worry about it, just down to get to the element that needs it.
')
Specific solutions to this problem have been developed for some frameworks, sometimes rather heavy:

Angular 2, mm ... 2+, or 4 (the one that is now the last one) offers to store the state in the services available through dependency injection for the components that need these services.

React pushes for the use of a single central repository using Redux and view- components that can subscribe to its updates.

Both of these approaches are essentially subspecies of singleton. Some people are adamant now that “singletons are bad” (meaning that they should be avoided), which is wrong — the whole point of the general condition is that there is only one source of truth about anything. Naturally, any pattern can be used incorrectly, but the real problem with singletons is managing access to the state and synchronizing the state for all concerned, and not that the state becomes general.

The browser already provides us with a collection of singletons, such as window , and we can put our state here. This works great as a namespace until you choose something that should be unique to your application (using “googleMaps” for your own library might not be a good idea). So why not use this method? Well, there is a synchronization problem - if we change the value, then how does someone somewhere else find out that it has changed? We definitely do not want to poll the status source by timer, it would be somewhat cumbersome to issue / subscribe to too many events.

Let's leave the talk about the theory and take a look at a specific example to explore possible options. Suppose we want to have the 'Options' panel in our app-drawer-layout from the Polymer Starter Kit (highlighted in red):

image

Although the view-elements that receive and display the value from 'Options' are so close on the screen (“look, code, it's right HERE!”), They are millions of miles apart from a DOM point of view with various iron-pages, app -drawer, headers and other parts between them.

Here is the code for the 'Options' panel:

 <link rel="import" href="../bower_components/polymer/polymer-element.html"> <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html"> <dom-module id="my-options"> <template> <style> :host { display: block; padding: 16px; } h3, p { margin: 8px 0; } </style> <h3>Options</h3> <p> <paper-checkbox checked="{{ options.subscribe }}">Send Notifications</paper-checkbox> </p> </template> <script> class MyOptions extends Polymer.Element { static get is() { return 'my-options'; } static get properties() { return { options: { type: Object, value: () => ({ subscribe: false }) } } } } window.customElements.define(MyOptions.is, MyOptions); </script> </dom-module> 

We would like to make the options property available from another place so that if someone needs access to its child property subscribe (or any other we add), this someone could get it and it would be updated with any changes but we don’t want everything to be open - we want to control access to it.

Of course, we could start using something like Redux, and although this is indeed an option, such a solution has a significant cost in the form of additional requirements for building and code complexity. This option has undeniable advantages, and if it makes sense to use it in your application for other reasons, it can be a good choice, but it can also be overkill and introduce as many problems as it solves, especially being added to a library that was not originally created under this design.

One of the reasons to fall in love with Polymer is that it is built on the capabilities of the platform and we will see what can be achieved simply by using them + some pure JavaScript.

Now is the right time to understand IIFE, or immediately called functions , which enable us to run the code and declare variables without setting them out. This is the basic structure that wraps the declaration of our class — it declares a function and immediately executes it (exactly as its name implies):

 (function() { // existing code }()); 

This actually does not change anything, except that our MyOptions class is MyOptions from the outside world (this is not important, since the only thing important for us is that it calls window.customElements.define ).

In our application, there will be only one instance of MyOptions , and other elements will need access to it, so we will add a variable that refers to it and set its correct value in the constructor when the element is created:

 let optionsInstance = null; // in class definition: constructor() { super(); if (!optionsInstance) optionsInstance = this; } 

optionsInstance is still hidden inside our IIFE, but now so that we don’t put it in it, it will have access to the initialized MyOptions instance.

We want this instance to be responsible for the values, so we need to make sure that it follows the subscribers interested in the changes. To do this, we add an array property to track the subscribers and instance methods that they can use to register and unregister:

 // in properties: subscribers: { type: Array, value: () => [] } // in class definition: register(subscriber) { this.subscribers.push(subscriber); subscriber.options = this.options; subscriber.notifyPath('options'); } unregister(subscriber) { var i = this.subscribers.indexOf(subscriber); if (i > -1) this.subscribers.splice(i, 1) } 

Note that when the subscriber is registered, we add it to the list, and also initialize a local variable in it that points to the options object. Here we also encounter Polymer change detection — setting the property itself does not notify the subscriber that this has happened, so we need a notifyPath call. We also want to notify all subscribers whenever any properties of the options object change (for example, if 'subscribe' was called, not just when the object reference changes) and we use an observer with an asterisk to say that we are interested. ” all changes ”:

 static get observers() { return [ 'optionsChanged(options.*)' ] } optionsChanged(change) { for(var i = 0; i < this.subscribers.length; i++) { this.subscribers[i].notifyPath(change.path); } } 

The part related to notifications is simple - regardless of the path the observer has changed, this is the same path that we must notify our subscribers of the change, so we simply cycle through them and call notifyPath for each .

Now we have the hooks and notifications that we need for subscribers, and we have two options. Create an accessor element that will be inside the same IIFE (which means it will have access to optionsInstance ):

 class MyOptionsValue extends Polymer.Element { static get is() { return 'my-options-value'; } static get properties() { return { options: { type: Object, notify: true } } } connectedCallback() { super.connectedCallback(); optionsInstance.register(this); } disconnectedCallback() { super.disconnectedCallback(); optionsInstance.unregister(this); } } window.customElements.define(MyOptionsValue.is, MyOptionsValue); 

Sonnected and disconnected callbacks are great for registering and unregistering instances. This means that elements that can be very far from each other in the DOM tree can have direct links to each other and thus avoid the property-binding chain if we are limited to using the DOM structure for communication.

An instance can be used inside an element, being imported into it:

 <link rel="import" href="my-options.html"> 

and establishing a connection with him through the binding:

 <my-options-value options="{{ options }}"></my-options-value> <p>Send notifications option is: <b>[[ options.subscribe ]]</b></p> 

We need to set notify: true in the declaration of the properties of the element due to the bidirectional binding (child-to-parent), indicated by curly braces. The MyOptions instance informs the instance (or instances) of MyOptionsValue about the change, and they, in turn, need to notify the element in which they are located.

It works , and we can turn on or off the checkbox and watch updates, but we have an additional element, an additional binding, and we must add the options property to each view-element if we want to see warnings from the linter about undefined properties:

 class MyView extends Polymer.Element { static get is() { return 'my-view'; } static get properties() { return { options: { type: Object } } } } 

oh, another property of 'options' ...

One way to simplify things a bit is to use mixin. Mixin is similar to class inheritance and makes it possible to combine element definitions, so code can be reused instead of duplication (previously, in Polymer 1.0, mixins were known as behaviors).

Instead of creating an access element in our view-element and binding to the options property that it provides, our view-element itself becomes an access element - it has its own options property and processes the registration and deregistration of itself without additional code, not counting adding myxin to class definition:

 class MyView extends MyOptionsMixin(Polymer.Element) { static get is() { return 'my-view'; } } 

We still need to import my-options.html , but our view-element is simpler and does not require an intermediate access element:

 <p>Send notifications option is: <b>[[ options.subscribe ]]</b></p> 

Now, every time an element needs access to the options property, we simply add a mixin to provide this property, which will be updated automatically. Redux is not required.

This approach actually has a name, it is called a “mono-state” pattern. There are already existing elements, such as iron-meta , that provide a general approach, but in my opinion it is simpler, cleaner and faster to create application-specific implementations — often they are easier to adapt for specific cases and seem more understandable than using intermediate components.

Here is the final, complete code for our classes, which I hope looks simpler. I should also apologize for using “subscribe” as the name, which can be confused with instance subscriptions. Initially, I used the name “notify”, which was even worse (since this is the name of one of the Polymer properties):

 <link rel="import" href="../bower_components/polymer/polymer-element.html"> <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html"> <dom-module id="my-options"> <template> <style> :host { display: block; padding: 16px; } h3, p { margin: 8px 0; } </style> <h3>Options</h3> <p> <paper-checkbox checked="{{ options.subscribe }}">Send Notifications</paper-checkbox> </p> </template> <script> (function() { let optionsInstance = null; class MyOptions extends Polymer.Element { static get is() { return 'my-options'; } static get properties() { return { options: { type: Object, value: () => ({ subscribe: false }) }, subscribers: { type: Array, value: () => [] } } } static get observers() { return [ 'optionsChanged(options.*)' ] } constructor() { super(); if (!optionsInstance) optionsInstance = this; } register(subscriber) { this.subscribers.push(subscriber); subscriber.options = this.options; subscriber.notifyPath('options'); } unregister(subscriber) { var i = this.subscribers.indexOf(subscriber); if (i > -1) this.subscribers.splice(i, 1) } optionsChanged(change) { for(var i = 0; i < this.subscribers.length; i++) { this.subscribers[i].notifyPath(change.path); } } } window.customElements.define(MyOptions.is, MyOptions); MyOptionsMixin = (superClass) => { return class extends superClass { static get properties() { return { options: { type: Object } } } connectedCallback() { super.connectedCallback(); optionsInstance.register(this); } disconnectedCallback() { super.disconnectedCallback(); optionsInstance.unregister(this); } } } }()); </script> </dom-module> 

View element, user:

 <link rel="import" href="../bower_components/polymer/polymer-element.html"> <link rel="import" href="my-options.html"> <link rel="import" href="shared-styles.html"> <dom-module id="my-view2"> <template> <style include="shared-styles"> :host { display: block; padding: 10px; } </style> <div class="card"> <div class="circle">2</div> <h1>View Two</h1> <p>Ea duis bonorum nec, falli paulo aliquid ei eum.</p> <p>Id nam odio natum malorum, tibique copiosae expetenda mel ea.Detracto suavitate repudiandae no eum. Id adhuc minim soluta nam.Id nam odio natum malorum, tibique copiosae expetenda mel ea.</p> <p>Send notifications option is: <b>[[ options.subscribe ]]</b></p> </div> </template> <script> class MyView2 extends MyOptionsMixin(Polymer.Element) { static get is() { return 'my-view2'; } } window.customElements.define(MyView2.is, MyView2); </script> </dom-module> 

Note: the example from this post works because the UI of the 'Options' panel is always the first in the DOM account, so access element subscribers can always find an existing instance. If this is not the case, then it is easy enough to use the function instead, so that the first caller creates a single instance — take a look at the iron-a11y-announcer in which it is implemented.

Also, in case this is not clear enough, although MyOptionsMixin defined inside IIFE, it is actually in the scope of the window , so other elements outside of IIFE can see and use it (if we wrote var MyOptionsMixin… then it would did not work, it would be visible only inside IIFE). I should have used window.MyOptionsMixin to make it clearer, or, as is more common, to use the global namespace (child object in the window ) as well as Polymer does. You may already have it - they are useful for storing configuration properties . The safe way to check and add something to it looks like this:

 MyApp = windows.MyApp || { } MyApp.MyOptionsMixin = ... 

(after which you can always use MyApp.MyOptionsMixin , referring to it).

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


All Articles