Лучший способ общаться между экземплярами одного и того же веб-компонента с помощью Polymer?

Я пытаюсь синхронизировать некоторые из свойств моего веб-компонента между экземплярами одного и того же элемента, поэтому, если одно из этих свойств изменяется, то одно и то же свойство обновляется во всех экземплярах с соответствующей привязкой и событиями.

Примечание. Я хочу использовать "Полимерные системы данных" для обмена сообщениями между экземплярами.

Пример

мой-element.html

<dom-module id="my-element">
  <script>
    Polymer({
      is: 'my-element',

      properties: {
        myProp: {
          type: String,
          notify: true
      }
    });
  </script>
</dom-module>

мой-другой-element.html

<dom-module id="my-other-element">
  <template>
    <my-element my-prop="{{otherProp}}"></my-element>
  </template>
  <script>
    Polymer({
      is: 'my-other-element',
      properties: {
        otherProp: {
          type: String,
          notify: true,
          readOnly: true
        }
      }
    })
  </script>
</dom-module>

мой-app.html

<dom-module id="my-app">
  <template>
    <my-element id="element"></my-element>
    <my-other-element id="otherElement"
      on-other-prop-changed="onPropChanged"
    ></my-other-element>
  </template>
  <script>
    Polymer({
      is: 'my-app',

      attached: function () {
        // should set 'myProp' to 'test' and trigger
        // the event 'my-prop-changed' in all my-element instances
        this.$.element.myProp = 'test'
      },

      onPropChanged: function (ev, detail) {
        console.log(detail.value); // should print 'test'
        console.log(this.$.element.myProp); // should print 'test'
        console.log(this.$.otherElement.otherProp); // should print 'test'
      }
    });
  </script>
</dom-module>

PD: Было бы хорошо использовать стандартные шаблоны и хорошие практики.

Ответ 1

TL;DR

Я создал настраиваемое поведение, которое синхронизирует свойства всех элементов с notify: true. Рабочий прототип: JSBin.

В настоящее время этот прототип не различает разные типы элементов, а это означает, что он может только синхронизировать экземпляры одного и того же настраиваемого элемента - но это можно изменить без особых усилий.

Вы также можете настроить поведение, чтобы синхронизировать только желаемые свойства, а не только все с notify: true. Однако, если вы берете этот путь, имейте в виду, что все свойства, которые вы хотите синхронизировать, должны иметь notify: true, так как поведение прослушивается событием <property-name>-changed, которое запускается только в том случае, если свойство имеет notify: true.

Подробности

Начните с пользовательского поведения SyncBehavior:

(function() {
    var SyncBehaviorInstances = [];
    var SyncBehaviorLock = false;

    SyncBehavior = {
        attached: function() {
            // Add instance
            SyncBehaviorInstances.push(this);

            // Add listeners
            for(var property in this.properties) {
                if('notify' in this.properties[property] && this.properties[property].notify) {
                    // Watch all properties with notify = true
                    var eventHanler = this._eventHandlerForPropertyType(this.properties[property].type.name);
                    this.listen(this, Polymer.CaseMap.camelToDashCase(property) + '-changed', eventHanler);
                }
            }
        },

        detached: function() {
            // Remove instance
            var index = SyncBehaviorInstances.indexOf(this);
            if(index >= 0) {
                SyncBehaviorInstances.splice(index, 1);
            }

            // Remove listeners
            for(var property in this.properties) {
                if('notify' in this.properties[property] && this.properties[property].notify) {
                    // Watch all properties with notify = true
                    var eventHanler = this._eventHandlerForPropertyType(this.properties[property].type.name);
                    this.unlisten(this, Polymer.CaseMap.camelToDashCase(property) + '-changed', eventHanler);
                }
            }
        },

        _eventHandlerForPropertyType: function(propertyType) {
            switch(propertyType) {
                case 'Array':
                    return '__syncArray';
                case 'Object':
                    return '__syncObject';
                default:
                    return '__syncPrimitive';
            }
        },

        __syncArray: function(event, details) {
            if(SyncBehaviorLock) {
                return; // Prevent cycles
            }

            SyncBehaviorLock = true; // Lock

            var target = event.target;
            var prop = Polymer.CaseMap.dashToCamelCase(event.type.substr(0, event.type.length - 8));

            if(details.path === undefined) {
                // New array -> assign by reference
                SyncBehaviorInstances.forEach(function(instance) {
                    if(instance !== target) {
                        instance.set(prop, details.value);
                    }
                });
            } else if(details.path.endsWith('.splices')) {
                // Array mutation -> apply notifySplices
                var splices = details.value.indexSplices;

                // for all other instances: assign reference if not the same, otherwise call 'notifySplices'
                SyncBehaviorInstances.forEach(function(instance) {
                    if(instance !== target) {
                        var instanceReference = instance.get(prop);
                        var targetReference = target.get(prop);

                        if(instanceReference !== targetReference) {
                            instance.set(prop, targetReference);
                        } else {
                            instance.notifySplices(prop, splices);
                        }
                    }
                });
            }

            SyncBehaviorLock = false; // Unlock
        },

        __syncObject: function(event, details) {
            var target = event.target;
            var prop = Polymer.CaseMap.dashToCamelCase(event.type.substr(0, event.type.length - 8));

            if(details.path === undefined) {
                // New object -> assign by reference
                SyncBehaviorInstances.forEach(function(instance) {
                    if(instance !== target) {
                        instance.set(prop, details.value);
                    }
                });
            } else {
                // Property change -> assign by reference if not the same, otherwise call 'notifyPath'
                SyncBehaviorInstances.forEach(function(instance) {
                    if(instance !== target) {
                        var instanceReference = instance.get(prop);
                        var targetReference = target.get(prop);

                        if(instanceReference !== targetReference) {
                            instance.set(prop, targetReference);
                        } else {
                            instance.notifyPath(details.path, details.value);
                        }
                    }
                });
            }
        },

        __syncPrimitive: function(event, details) {
            var target = event.target;
            var value = details.value;
            var prop = Polymer.CaseMap.dashToCamelCase(event.type.substr(0, event.type.length - 8));

            SyncBehaviorInstances.forEach(function(instance) {
                if(instance !== target) {
                    instance.set(prop, value);
                }
            });
        },
    };
})();

Обратите внимание, что я использовал шаблон IIFE, чтобы скрыть переменную, содержащую все экземпляры настраиваемого элемента my-element. Это важно, поэтому не меняйте его.

Как вы можете видеть, поведение состоит из шести функций, а именно:

  • attached, который добавляет текущий экземпляр в список экземпляров и регистрирует слушатели для всех свойств с помощью notify: true.
  • detached, который удаляет текущий экземпляр из списка экземпляров и удаляет слушатели для всех свойств с помощью notify: true.
  • _eventHandlerForPropertyType, который возвращает имя одной из функций 4-6, в зависимости от типа свойства.
  • __syncArray, который синхронизирует свойства типа массива между экземплярами. Обратите внимание, что я игнорирую текущую цель и реализую простой механизм блокировки, чтобы избежать циклов. Метод обрабатывает два сценария: назначение нового массива и изменение существующего массива.
  • __syncObject, который синхронизирует свойства типа объекта между экземплярами. Обратите внимание, что я игнорирую текущую цель и реализую простой механизм блокировки, чтобы избежать циклов. Метод обрабатывает два сценария: назначение нового объекта и изменение свойства существующего объекта.
  • __syncPrimitive, который синхронизирует примитивные значения свойств между экземплярами. Обратите внимание, что я игнорирую текущую цель, чтобы избежать циклов.

Чтобы протестировать мое новое поведение, я создал образец пользовательского элемента:

<dom-module id="my-element">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>

        <h2>Hello [[id]]</h2>
        <ul>
            <li>propString: [[propString]]</li>
            <li>
                propArray:
                <ol>
                    <template is="dom-repeat" items="[[propArray]]">
                        <li>[[item]]</li>
                    </template>
                </ol>
            </li>
            <li>
                propObject:
                <ul>
                    <li>name: [[propObject.name]]</li>
                    <li>surname: [[propObject.surname]]</li>
                </ul>
            </li>
        </ul>
    </template>

    <script>
        Polymer({
            is: 'my-element',
            behaviors: [
                SyncBehavior,
            ],
            properties: {
                id: {
                    type: String,
                },
                propString: {
                    type: String,
                    notify: true,
                    value: 'default value',
                },
                propArray: {
                    type: Array,
                    notify: true,
                    value: function() {
                        return ['a', 'b', 'c'];
                    },
                },
                propObject: {
                    type: Object,
                    notify: true,
                    value: function() {
                        return {'name': 'John', 'surname': 'Doe'};
                    },
                },
            },
            pushToArray: function(item) {
                this.push('propArray', item);
            },
            pushToNewArray: function(item) {
                this.set('propArray', [item]);
            },
            popFromArray: function() {
                this.pop('propArray');
            },
            setObjectName: function(name) {
                this.set('propObject.name', name);
            },
            setNewObjectName: function(name) {
                this.set('propObject', {'name': name, 'surname': 'unknown'});
            },
        });
    </script>
</dom-module>

Он имеет одно свойство String, одно свойство Array и одно свойство Object; все с notify: true. Пользовательский элемент также реализует поведение SyncBehavior.

Чтобы объединить все вышеперечисленное в рабочем прототипе, вы просто делаете это:

<template is="dom-bind">
    <h4>Primitive type</h4>
    propString: <input type="text" value="{{propString::input}}" />

    <h4>Array type</h4>
    Push to propArray: <input type="text" id="propArrayItem" /> <button onclick="_propArrayItem()">Push</button> <button onclick="_propNewArrayItem()">Push to NEW array</button> <button onclick="_propPopArrayItem()">Delete last element</button>

    <h4>Object type</h4>
    Set 'name' of propObject: <input type="text" id="propObjectName" /> <button onclick="_propObjectName()">Set</button> <button onclick="_propNewObjectName()">Set to NEW object</button> <br />

    <script>
        function _propArrayItem() {
            one.pushToArray(propArrayItem.value);
        }

        function _propNewArrayItem() {
            one.pushToNewArray(propArrayItem.value);
        }

        function _propPopArrayItem() {
            one.popFromArray();
        }

        function _propObjectName() {
            one.setObjectName(propObjectName.value);
        }

        function _propNewObjectName() {
            one.setNewObjectName(propObjectName.value);
        }
    </script>

    <my-element id="one" prop-string="{{propString}}"></my-element>
    <my-element id="two"></my-element>
    <my-element id="three"></my-element>
    <my-element id="four"></my-element>
</template>

В этом прототипе я создал четыре экземпляра my-element. У одного propString связанного с входом, в то время как другие вообще не имеют привязок. Я создал простую форму, которая охватывает каждый сценарий, о котором я мог думать:

  • Изменение примитивного значения.
  • Нажатие элемента в массив.
  • Создание нового массива (с одним элементом).
  • Удаление элемента из массива.
  • Установка свойства объекта.
  • Создание нового объекта.

ИЗМЕНИТЬ

Я обновил свой пост и прототип, чтобы решить следующие проблемы:

  • Синхронизация не-примитивных значений, а именно Array и Object.
  • Правильное преобразование имен свойств из Dash case в случай Camel (и наоборот).

Ответ 2

Мы создали компонент для синхронизации данных между разными экземплярами. Наш компонент:

<dom-module id="sync-data">
  <template>
    <p>Debug info: {scope:[[scope]], key:[[key]], value:[[value]]}</p>
  </template>
  <script>
    (function () {
      var items = []

      var propagateChangeStatus = {}

      var togglePropagationStatus = function (status) {
        propagateChangeStatus[this.scope + '|' + this.key] = status
      }

      var shouldPropagateChange = function () {
        return propagateChangeStatus[this.scope + '|' + this.key] !== false
      }

      var propagateChange = function (key, scope, value) {
        if (shouldPropagateChange.call(this)) {
          togglePropagationStatus.call(this, false)
          var itemsLength = items.length
          for (var idx = 0; idx < itemsLength; idx += 1) {
            if (items[idx] !== this && items[idx].key === key && items[idx].scope === scope) {
              items[idx].set('value', value)
            }
          }
          togglePropagationStatus.call(this, true)
        }
      }

      Polymer({

        is: 'sync-data',

        properties: {
          key: {
            type: String,
            value: ''
          },
          scope: {
            type: String,
            value: ''
          },
          value: {
            type: String,
            notify: true,
            observer: '_handleValueChanged',
            value: ''
          }
        },

        created: function () {
          items.push(this)
        },

        _handleValueChanged: function (newValue, oldValue) {
          this.typeof = typeof newValue
          propagateChange.call(this, this.key, this.scope, newValue)
        }

      })
    })()
  </script>
</dom-module>

И мы используем его в таком компоненте:

<sync-data
  key="email"
  scope="user"
  value="{{email}}"></sync-data>

И в другом компоненте, подобном этому:

<sync-data
  key="email"
  scope="user"
  value="{{userEmail}}"></sync-data>

Таким образом, мы получаем естественное поведение полимера для событий и привязок

Ответ 3

Мое личное мнение о таких проблемах заключается в использовании архитектуры потока.

вы создаете элемент обертки, который передает всю информацию детям. Все изменения происходят через главный компонент.

<app-wrapper>
<component-x attr="[[someParam]]" />
<component-x attr="[[someParam]]" />
<component-x attr="[[someParam]]" />
</app-wrapper> 

component-x запускает событие изменения значения на app-wrapper, а app-wrapper обновляет someValue, обратите внимание на одностороннюю привязку.

В этом есть компонент который реализует архитектуру redux, но также можно закодировать свой собственный. Это более или менее шаблон наблюдателя

Ответ 4

Попробуйте это для my-app.html. Я не вижу причин не использовать двусторонние привязки здесь.

<dom-module id="my-app">
  <template>
    <my-element my-prop="{{myProp}}"></my-element>
    <my-element my-prop="{{myProp}}"></my-element>
  </template>
  <script>
    Polymer({
      is: 'my-app',
      ready: function() {
        this.myProp = 'test';
      }
    });
  </script>
</dom-module>

Хотя, вероятно, лучше использовать myProp значение по умолчанию, используя объект properties, а не обратный вызов ready. Пример:

    Polymer({
      is: 'my-app',
      properties: {
        myProp: {
          type: String,
          value: 'test'
      }
    });