Рубрики
Без рубрики

Еще один двусторонний артикул передачи данных

Недавно я прочитал Роберт Ярборо (https://www.codentementor.io/robertyarborough is (как и почему я построил 2-стороннюю библиотеку связывания передачи данных …

Автор оригинала: Joe Tagliaferro.

Я недавно прочитал статью Роберта Ярборо (как и почему я построил двухстороннюю библиотеку связывания данных), и в то время как его библиотека была очень легкой, легкой в реализации, и, безусловно, будет работать для многих простых случаев использования, я думал, что ему не хватало в некоторых областях Отказ Так что с этим в виду, я поставил о создании чего-то более прочного и масштабируемого.

Перейти к моему подходу Просмотр завершенного кодепена

Опасения с предыдущим подходом

EventListener взрывается

В своей статье Роберт берет очень прямой подход к синхронизации входных данных, добавив Onkeyup Функция для каждого элемента, имеющего определенные CSS Класс Это диктует данные по объему, к которому она связывается. Хотя это будет работать для небольших приложений, поскольку количество синхронизации элементов к данным увеличивается, вы увидите более вязкие характеристики. Вот a jsperf Иллюстрирование того, как браузер обрабатывает связывание многих слушателей событий на многие элементы.

Событие на Keyup.

Я думаю, потому что использование Изменить или вход будет создать бесконечную петлю, когда был обновлен входной элемент, использовал Robert keyup спусковать $ Scope удивительные. Хотя это избегало очевидной ловушки разбития всего приложения, ему не хватает некоторых общих функций, которые я считаю, что многие пользователи будут ожидать, что такие как вставка, используя меню правой кнопкой мыши. Потому что я уже был решен не создавать EventListener Для каждого элемента это также была проблема, которая была бы легко обозначена.

Пустая инициализация

Я также обнаружил, что из-за keyup Сеттер события и объекта, являющиеся единственными триггерами для синхронизации данных, если вы приобретете значение входного элемента, он не будет обновляться, пока на этот вход не будет нажимается клавиша. И если вы введете в один из других входов, которые разделяют этот источник данных, или вы меняете $ Scope Значение через JavaScript вы полностью перезаписываете начальное значение.

Предопределенные ключи данных

Несмотря на то, что определяя ключи данных, которые вы планируете синхронизировать, все хорошо, и хорошо, я пришел к выводу, что требуется слишком много бойной для моего вкуса. Знание моих собственных привычек забыть, чтобы они где-то обновили строку и проводить слишком долго, пытаясь выяснить, почему мой код не работает, только чтобы иметь действительно разочаровывающие «ах Хах» в конце концов, я решил, что это был шаблон, который В конечном итоге подключите меня в Insanse и добавляют динамические ключи данных в качестве требования для моей версии.

КСУ Класс на основе связывания

Как уже упоминалось выше CSS Класс На основе селектора использовали в качестве средства идентификации того, какие элементы должны быть синхронизированы на $ Scope Отказ Это маленький, но важный надзор. Использование классов CSS таким образом, уходит вы подвержены воздействию потенциальных проблем наследования стиля. Если вы случайно используете класс для стиля элемента, он может повлиять на все приложение. Или если вы стилируете этот класс намеренно и позже решили применить данные к другому элементу, который вы не хотите наследовать эти стили, вы бы просили серьезные проблемы регрессии и добавления нового класса ко всем вашим существующим данным связанные элементы. Во всяком случае, это было простым, чтобы решить, поэтому я просто сделал другой подход.

Мой подход

Наконец, стейк и картофель! Учитывая проблемы, которые я перечислил выше, я закончил этот список функций в качестве требований:

  • ES2015 + (только потому, что это почти 2018)
  • Нет связывания класса CSS на основе
  • Ограничить количество EventListeners.
  • Обрабатывать все формы изменения данных, а не только от keyup или установка через код
  • Динамические клавиши данных
  • Автоматическая синхронизация с значениями по умолчанию

Нет связывания класса CSS на основе

Поскольку это корень довольно много других вопросов, и в значительной степени определяет, как остальные функции библиотеки, давайте начнем здесь. Вместо того, чтобы выбрать элементы, чтобы связать с их класса CSS, мы собираемся использовать атрибут данных, называемый Область данных Отказ Если вы не знакомы с атрибутами HTML данных, я предлагаю на них чтение, поскольку у них есть целый вариант, который делает их очень легко работать в JavaScript. Хорошо, к нашему index.html Файл собирается получить некоторые входные элементы.

...

  
    
    
    

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

.input-1 {
  background-color: #f00
}
.input-2 {
  color: #fff;
  background-color: #00f
}
.input-3 {
  background-color: #0f0
}
.input-4 {
  background-color: #ff0
}

Теперь у нас должно быть что-то, что выглядит так.

Стиль-вход

JS Отказ от ответственности

Я должен быстро вовести, чтобы упомянуть, что JavaScript в этом проекте будет использоваться относительно новые функции и может не работать в более старых браузерах, но я уверен, что IE11 + – это безопасная ставка из коробки, при условии, что вы транспилируете с Бабел Отказ Если вы не знакомы с Бабел или ES2015 + В общем, к сожалению, это выходит из объема этого поста, но не стесняйтесь оставлять комментарий и, возможно, я буду удвоить этот проект и переписать его в ES5.

Ограничить количество слушателей событий

Теперь, когда у нас есть некоторые входы в Orchestrate, давайте начнем написать некоторый код для обработки привязки данных. Мой план ограничения EventListener Подсчет – отслеживать, какой элемент сосредоточен на странице, и есть только слушатели событий на этом элементе. Затем, когда элемент теряет фокус, каждый EventListener следует удалить. Для этого нам нужно создать окно Слушатель на Фокус Событие, которое будет захватывать все Фокус События на странице.

let target;
let $scope = {};

document.addEventListener('DOMContentLoaded', (event) => {
  window.addEventListener('focus', handleFocus, true);
});

function handleFocus(evt) {
  if (target) {
    if (target === evt.target) {
      // Don't reset the listeners if the event target is the current target
      return;
    }
    target.removeEventListener('input', handleUpdate);
  }
  target = evt.target;
  target.addEventListener('input', handleUpdate)
}

function handleUpdate(evt) {
  // We're going to handle updating $scope here.
}

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

Обрабатывать все формы изменения данных, а не только из нажатия клавиши или настройки через код

Именно в этот момент я решил лучше всего начать идентифицировать различные структуры, которые я буду обращаться в приложении и создать несколько классов, чтобы помочь организовать логику. Я пришел к выводу, что мне потребуют три класса:

  • Область: Основной класс, который содержит все данные и предоставляет прокси для всех экземпляров Scopeitem
  • Скупет: Этот класс будет обрабатывать каждое отдельное значение данных и отслеживать элементы, которые необходимо обновлять, когда он меняется.
  • Цель: Классная обертка для Фокус Руководство мы сделали выше.

Хотя Область Будет главный класс, который мы Ultimataly выставляем на любой приложение, используя эту библиотеку, давайте начнем с Часопетем С Область это глифированный Часопетем фабрика.

Область предмета

export default class ScopeItem {
  constructor(value = null) {
    // Creates an empty array of observers and sets _value
    this.observers = [];
    this._value = value;

    return this;
  }
    
  // Getter to return the private `_value` property.
  get value() {
    return this._value;
  }
    
  // Setter that calls updateObservers
  set value(value) {
    this.updateObservers(value);
    this._value = value;
  }

  // Iterates over all observers and calls syncContent for each
  updateObservers = (value) => {
    this.observers.forEach(this.syncContent(value));
  }

  // Synchronizes value from ScopeItem to node
  syncContent = (value) => {
    return (elem) => {
      if (elem.nodeName === 'INPUT') {
        const type = elem.getAttribute('type');
        if (type === 'checkbox' || type === 'radio') {
          return elem.checked = value;
        }
        return elem.value = value;
      }
      return elem.innerHTML = value;
    	};
  };

  // Inserts a new observer element and syncs it's content
  addObserver = (elem) => {
    this.syncContent(this._value)(elem);
    this.observers.push(elem);
  }

  // Removes an observer element
  removeObserver = (elem) => {
    const index = this.observers.indexOf(elem);
    if (index > -1) {
      this.observers.pop(index);
    }
  }
}

Давайте определим этот метод методом, будем ли мы.

constructor(value = null) {
  // Creates an empty array of observers and sets _value
  this.observers = [];
  this._value = value;
   
  return this;
}

Конструктор для Часопетем начинается с создания пустого массива для Наблюдатели предписание. Это будет использоваться для отслеживания элементов, которые необходимо обновлять, когда значение обновляется. Далее это берет ценность аргумент, какой по умолчанию на null Если не поставлено, и назначьте его частным _Value имущество.

// Getter to return the private `_value` property.
get value() {
  return this._value;
}
   
// Setter that calls updateObservers
set value(value) {
  this.updateObservers(value);
  this._value = value;
}

Добрать для ценность просто возвращает _Value имущество. Сеттер С другой стороны, называет UpdateBservers Способ с новым значением до обновления _Value имущество.

// Iterates over all observers and calls syncContent for each
updateObservers = (value) => {
  this.observers.forEach(this.syncContent(value));
}

// Synchronizes value from ScopeItem to node
syncContent = (value) => {
  return (elem) => {
    if (elem.nodeName === 'INPUT') {
      const type = elem.getAttribute('type');
      if (type === 'checkbox' || type === 'radio') {
        return elem.checked = value;
      }
      return elem.value = value;
    }
    return elem.innerHTML = value;
  };
}

Наконец мы в конечном итоге на UpdateBservers Метод, который итерации на каждого наблюдателя и выполняет Synccontent Метод, который обновляет либо их ценность , проверено или innerhtml Собственность в зависимости от их NodeName Отказ

Так с нашей Часопетем Класс Complete Мы будем обновлять каждый зарегистрированный Осленчик элемент в любое время A Часопетем экземпляр обновляется. Давайте теперь сможем добавить эти наблюдатели в первую очередь.

Динамические клавиши данных

Обработка создания динамических ключей данных на самом деле намного проще, чем звучит. Прокси Объект позволяет создавать кабинет Сеттер и Добрать Методы любого свойства объекта прокси. Это позволяет нам создать новый Часопетем Каждый раз, когда элемент имеет новое значение для Область данных Отказ

Это также может быть достигнуто с использованием Объект.defineproperty каждый раз undefined ключ встречается, но Прокси более церный, поэтому мы будем использовать это.

Сфера

import ScopeItem from "./ScopeItem";

// Wrapper class for creating the Scope Proxy
export default class Scope {
  constructor() {
    // Creates proxy
    this.$scope = {};
    this.proxy = this.createProxy();

    // Get the list of elements that utilize `data-scope`
    const scopedInputs = document.querySelectorAll("[data-scope]");

    // Subscribe all `scopedInputs` to the scope instance
    scopedInputs.forEach(this.subscribeToScope);

    // Return the proxy for easy access to values in code
    return this.proxy;
  }

    // Static function to normalize element values
  static parseElementValue = (elem) => {
    if (elem.nodeName === "INPUT") {
      const type = elem.getAttribute("type");
      if (type === "checkbox" || type === "radio") {
        return elem.checked;
      }
      return elem.value;
    }
    return elem.innerHTML;
  };

  // Creates a Proxy that instantiates a new ScopeItem for every key accessed and only allows the value to be updated
  createProxy = () => {
    return new Proxy(this.$scope, {
      get(target, key) {
        if (!target[key]) {
          target[key] = new ScopeItem();
        }

        return target[key].value;
      },
      set(target, key, value) {
        if (!target[key]) {
          target[key] = new ScopeItem();
        }

        target[key].value = value;

        return true;
      }
    });
  };
    
  // Adds element to scope observer array to be updated on changes
  subscribeToScope = (elem) => {
    const scopeKey = elem.dataset.scope;

    if (!this.$scope[scopeKey]) {
      this.$scope[scopeKey] = new ScopeItem(Scope.parseElementValue(elem));
    }

    this.$scope[scopeKey].addObserver(elem);
  };
}

И снова мы сломаем этот метод вниз по методу.

constructor() {
  // Creates proxy
  this.$scope = {};
  this.proxy = this.createProxy();

  // Get the list of elements that utilize `data-scope`
  const scopedInputs = document.querySelectorAll("[data-scope]");

  // Subscribe all `scopedInputs` to the scope instance
  scopedInputs.forEach(this.subscribeToScope);

  // Return the proxy for easy access to values in code
  return this.proxy;
}

Конструктор Это довольно немного в Область класс. Это начинается с создания пустого объекта как $ Scope недвижимость, а также заполняет прокси Свойство, используя CreateProxy метод.

Далее он хранит все элементы из DOM, который содержит Область данных атрибут в Scopedinputs Постоянный, который затем итасирован, выполняя Абонентскоскоп Способ на каждом элементе.

Наконец, прокси Свойство фактически возвращается, так как это удобнее для внешнего кода для интерфейса с базовым Часопетем экземпляры.

// Static function to normalize element values
static parseElementValue = (elem) => {
  if (elem.nodeName === "INPUT") {
    const type = elem.getAttribute("type");
    if (type === "checkbox" || type === "radio") {
      return elem.checked;
    }
    return elem.value;
  }
  return elem.innerHTML;
};

Следующий метод является Статический Метод называется BarseelementValue Отказ Это необходимо для того, чтобы правильно установить Часопетем Значения, основанные на типе элементов, из значения устанавливаются значение.

// Creates a Proxy that instantiates a new ScopeItem for every key accessed and only allows the value to be updated
createProxy = () => {
  return new Proxy(this.$scope, {
    get(target, key) {
      if (!target[key]) {
        target[key] = new ScopeItem();
      }

      return target[key].value;
    },
    set(target, key, value) {
      if (!target[key]) {
        target[key] = new ScopeItem();
      }

      target[key].value = value;

      return true;
    }
  });
};

CreateProxy Метод, как указано в начале этого раздела. Это создает динамический Геттерс и Соседниты Для всех свойств $ Scope имущество. Это позволяет нам автоматически создавать Часопетем Для любого ключа доступно либо путем чтения или записи.

Далее это позволяет спрятать основной Часопетем Свойства, которые не имеют интереса для кого-то, используя библиотеку. Доступ к любой клавише в Область Экземпляр вернется только ценность Недвижимость из соответствующего Часопетем пример.

// Adds element to scope observer array to be updated on changes
subscribeToScope = (elem) => {
  const scopeKey = elem.dataset.scope;

  if (!this.$scope[scopeKey]) {
    this.$scope[scopeKey] = new ScopeItem(Scope.parseElementValue(elem));
  }

  this.$scope[scopeKey].addObserver(elem);
};

Используется в Конструктор Абонентскоскоп Метод принимает элемент, содержащий A Область данных атрибут и добавляет его к соответствующему Часопетем Наблюдатели Недвижимость через Addobserver метод.

Это также единственный метод, который напрямую обращается к $ Scope Имущество, поскольку ему нужен доступ к базовому Часопетем Addobserver метод.

Внутри этого метода выполнена еще один из наших требований (автоматическая синхронизация со значениями по умолчанию). Прохождение значения элемента для нового Часопетем Если $ Scope Не содержит ожидаемого ключа, в свою очередь, установит это значение для любых следующих элементов, которые добавляются в качестве наблюдателей к этому Часопетем Отказ

Цель (Revizited)

Теперь все пришел полный круг, как мы создаем Цель класс для обработки всех EventListener управление. Потому что Цель Класс потребует возможность обновить Государство , нам нужно пройти ссылку, а также создать экземпляр внутри Государство класс. Итак, давайте изменим наш импорт и Конструктор в Состояние

import ScopeItem from "./ScopeItem";
import Target from "./Target"; // Add this line

// Wrapper class for creating the Scope Proxy
export default class Scope {
  constructor() {
    // Creates proxy and Target reference
    this.$scope = {};
    this.proxy = this.createProxy();
    this.target = new Target(this.proxy); // Add this line
        ...
  }
...
}

А вот полная Цель класс.

import Scope from "./Scope";

// Class for managing target element and minimizing active eventListeners;
export default class Target {
  // Takes a scope instance as an argument to handle updates
  constructor($scope) {
    this.$scope = $scope;

    // Event listener object to remove listeners later in the lifecycle
    this.eventListeners = {
      input: this.handleUpdate,
      change: this.handleUpdate
    };
    // Sets the initial element to the active document element
    this.element = document.activeElement;

    // Listens for all focus events on the window object
    window.addEventListener("focus", this.handleFocus, true);
  }

  // Getter to return the private `_element` property.
  get element() {
    this._element;
  }
  // Setter that performs event listener cleanup and setup whenever element changes
  set element(element) {
    this.removeEventListeners(this._element);
    this.addEventListeners(element);

    return this._element = element;
  }

  // Removes event listeners from element that is losing focus
  removeEventListeners = (element) => {
    if (!element) {
      return;
    }

    for (let key in this.eventListeners) {
      element.removeEventListener(key, this.eventListeners[key]);
    }
  };

  // Adds event listeners from element that is gaining focus
  addEventListeners = (element) => {
    if (!element) {
      return;
    }

    for (let key in this.eventListeners) {
      element.addEventListener(key, this.eventListeners[key]);
    }
  };

  // Updates the value of the ScopeItem that the elements `data-scope` attribute signifies on change
  handleUpdate = (evt) => {
    const scopeKey = evt.target.dataset ? evt.target.dataset.scope : null;
    if (scopeKey) {
      this.$scope[scopeKey] = Scope.parseElementValue(evt.target);
    }
  };

  // Updates the value of the ScopeItem that the elements `data-scope` attribute signifies on change
  handleFocus = (evt) => {
    this.element = evt.target;
  };
}

И окончательный разрыв

constructor($scope) {
  this.$scope = $scope;

  // Event listener object to remove listeners later in the lifecycle
  this.eventListeners = {
    input: this.handleUpdate,
    change: this.handleUpdate
  };
  // Sets the initial element to the active document element
  this.element = document.activeElement;

  // Listens for all focus events on the window object
  window.addEventListener("focus", this.handleFocus, true);
}

Это все должно выглядеть немально знакомым от раньше. Конструктор Сначала присваивает это собственное $ государство Имущество государству прошло как аргумент. Тогда он определяет EventListeners Имущество, которое удобно позже для добавления и удаления EventListeners без труда. элемент Свойство установлено на текущий Активность документа. И, наконец, Фокус EventListener добавляется к окно как раньше.

// Getter to return the private `_element` property.
get element() {
  this._element;
}
// Setter that performs event listener cleanup and setup whenever element changes
set element(element) {
  this.removeEventListeners(this._element);
  this.addEventListeners(element);

  return this._element = element;
}

элемент Добрать еще одна простая обертка для _элемент Собственность, пока это Сеттер Ручки снятия и добавления слушателей событий как элемент Peoperty изменился.

// Removes event listeners from element that is losing focus
removeEventListeners = (element) => {
  if (!element) {
    return;
  }

  for (let key in this.eventListeners) {
    element.removeEventListener(key, this.eventListeners[key]);
  }
};

// Adds event listeners from element that is gaining focus
addEventListeners = (element) => {
  if (!element) {
    return;
  }

  for (let key in this.eventListeners) {
    element.addEventListener(key, this.eventListeners[key]);
  }
};

Оба addeventListeners и RemoveeVentListeners Методы выполняют более или менее ту же операцию. Они петля по ключам в EventListeners Свойство и добавьте или удалить соответствующие EventListener из аргумента элемента.

// Updates the value of the ScopeItem that the elements `data-scope` attribute signifies on change
handleUpdate = (evt) => {
  const scopeKey = evt.target.dataset ? evt.target.dataset.scope : null;
  if (scopeKey) {
    this.$scope[scopeKey] = Scope.parseElementValue(evt.target);
  }
};

ручка Метод выполняется на обоих Изменить и вход События ( Изменить был добавлен для поддержки Checkbox и Radio) и использует Scope.pareElementValue Статический способ обновления соответствующего Часопетем что в настоящее время целевой элемент обязан.

// Updates the value of the ScopeItem that the elements `data-scope` attribute signifies on change
handleFocus = (evt) => {
  this.element = evt.target;
};

И последнее, но не менее важно, у нас есть Handlefocus Метод, который все еще делает то же самое, что он сделал в начале этого поста, он присваивает элемент цели мероприятия для элемент Свойство, вызывающее все EventListener удаление и дополнение.

Бонус раунд !!!

Просто потому, что это действительно было намного больше усилий после всего этого, я добавил возможность добавлять и удалять элементы DOM, которые используют Область данных на лету. Мы просто добавляем МутацияОбсеревер к Область Класс, который наблюдает за изменениями в организме и либо добавляет элемент для Часопетем ‘s Наблюдатели свойство при создании или удаляет один при удалении. Добавьте следующее в Область класс.

export default class Scope {
  constructor() {
    ...
    this.target = new Target(this.proxy);
    this.observeMutations(); // Add this line
        ...
  }
    // Manages scope through any DOM manipulation events
  observeMutations = () => {
    // create an observer instance
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === "childList") {
          mutation.addedNodes.forEach(elem => {
            this.handleNodeInserted(elem);
          });
          mutation.removedNodes.forEach(elem => {
            this.handleNodeRemoved(elem);
          });
        }
      });
    });

    // configuration of the observer:
    const config = { childList: true };

    // pass in the body as the target node, as well as the observer options
    observer.observe(document.querySelector("body"), config);
  };

  // Called from the MutationObserver when an element is inserted into the DOM
  handleNodeInserted = element => {
    const scopeKey = element.dataset ? element.dataset.scope : null;
    if (scopeKey) {
      this.subscribeToScope(element);
    }
  };

  // Called from the MutationObserver when an element is removed from the DOM
  handleNodeRemoved = element => {
    const scopeKey = element.dataset ? element.dataset.scope : null;
    if (scopeKey) {
      this.$scope[scopeKey].removeObserver(element);
    }
  };
    ...
}