Автор оригинала: 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); } }; ... }