В этом посте я покажу вам, как реализовать двустороннюю привязку данных с помощью JavaScript.
Что такое привязка данных?
Привязка данных означает наличие где-то модели данных, которая автоматически обновляется из пользовательского интерфейса, например поля ввода.
Как реализовать привязку данных с помощью ванильного JavaScript и HTML
Вот что я придумал:
Это всего лишь эксперимент и ни в коем случае не идеальный. Используйте с осторожностью!
Давайте используем очень простой синтаксис для определения значений, которые должны быть связаны непосредственно в html, например:
<input id="test" type="text" :value="username" :placeholder="usernamePlaceholder" />
Итак, мы хотим привязать входные атрибуты value
и placeholder
к этому образцу объекта данных:
let store = { username: '', usernamePlaceholder: 'Input a username' };
Во-первых, перебираем все элементы, которые имеют наш специальный синтаксис :[attribute]
, и инициализируем его. Для каждого атрибута значения я добавил прослушиватель ввода для обновления хранилища.
[].forEach.call(document.querySelectorAll('*'), e => { e.getAttributeNames().forEach(attr => { if (attr.startsWith(':')) { const propertyName = e.getAttribute(attr); const attributeName = attr.split(':')[1]; if (store[propertyName] !== undefined) e[attributeName] = store[propertyName]; if (attributeName === 'value') e.addEventListener('input', () => { store[propertyName] = e[attributeName]; }); } }); });
Чтобы также обновить другие атрибуты, такие как заполнитель, я использовал файл MutationObserver
. Я добавил объект boundAttributes
, в котором хранятся все связанные атрибуты. Затем я добавил MutationObserver
к каждому элементу, чтобы обновлять хранилище при обновлении связанного атрибута:
[].forEach.call(document.querySelectorAll('*'), e => { const boundAttributes = {}; e.getAttributeNames().forEach(attr => { if (attr.startsWith(':')) { const propertyName = e.getAttribute(attr); const attributeName = attr.split(':')[1]; if (store[propertyName] !== undefined) e[attributeName] = store[propertyName]; if (attributeName === 'value') e.addEventListener('input', () => { store[propertyName] = e[attributeName]; }); boundAttributes[attributeName] = propertyName; } }); const observer = new MutationObserver(muations => muations.forEach(m => { const prop = boundAttributes[m.attributeName]; if ( m.type === 'attribute' && prop && store[prop] !== e[m.attributeName] ) { store[prop] = e[m.attributeName]; } }) ); observer.observe(e, { attributes: true }); });
Теперь, если мы обновим связанный атрибут, например :placeholder="usernamePlaceholder"
, соответствующее свойство в хранилище будет автоматически обновлено:
document.getElementById('test').placeholder = 'test';
console.log(store.usernamePlaceholder); // => test
Теперь он обновляет наш магазин только тогда, когда мы меняем элемент HTML. Чтобы также обновлять наши HTML-элементы при изменении нашего магазина, мы можем обернуть наш магазин в прокси:
store = new Proxy(store, { set: (obj, prop, value) => { // Handle updates her obj[prop] = value; return true; } });
Я сохранил функцию для обновления каждого HTML-элемента в объекте storeListener
, чтобы получить к нему доступ в нашем прокси:
const storeListener = {}; [].forEach.call(document.querySelectorAll('*'), e => { const boundAttributes = {}; e.getAttributeNames().forEach(attr => { if (attr.startsWith(':')) { const propertyName = e.getAttribute(attr); const attributeName = attr.split(':')[1]; if (store[propertyName] !== undefined) e[attributeName] = store[propertyName]; if (attributeName === 'value') e.addEventListener('input', () => { store[propertyName] = e[attributeName]; }); // define a function to be used in our proxy storeListener[propertyName] = value => { e[attributeName] = value; }; boundAttributes[attributeName] = propertyName; } }); const observer = new MutationObserver(muations => muations.forEach(m => { const prop = boundAttributes[m.attributeName]; if ( m.type === 'attribute' && prop && store[prop] !== e[m.attributeName] ) { store[prop] = e[m.attributeName]; } }) ); observer.observe(e, { attributes: true }); }); return new Proxy(store, { set: (obj, prop, value) => { // update our html element: if (obj[prop] !== value && storeListener[prop]) storeListener[prop](value); obj[prop] = value; return true; } });
Наконец, я завернул все в функцию и добавил функцию onUpdate
, чтобы иметь возможность реагировать на обновленные значения.
const bind = (store, onUpdate) => { const storeListener = {}; [].forEach.call(document.querySelectorAll('*'), e => { const boundAttributes = {}; e.getAttributeNames().forEach(attr => { if (attr.startsWith(':')) { const propertyName = e.getAttribute(attr); const attributeName = attr.split(':')[1]; if (store[propertyName] !== undefined) e[attributeName] = store[propertyName]; if (attributeName === 'value') e.addEventListener('input', () => { store[propertyName] = e[attributeName]; onUpdate(propertyName, store[propertyName]); }); storeListener[propertyName] = value => { e[attributeName] = value; onUpdate(propertyName, value); }; boundAttributes[attributeName] = propertyName; } }); const observer = new MutationObserver(muations => muations.forEach(m => { const prop = boundAttributes[m.attributeName]; if ( m.type === 'attribute' && prop && store[prop] !== e[m.attributeName] ) { store[prop] = e[m.attributeName]; onUpdate(store[prop], e[m.attributeName]); } }) ); observer.observe(e, { attributes: true }); }); return new Proxy(store, { set: (obj, prop, value) => { if (obj[prop] !== value && storeListener[prop]) storeListener[prop](value); obj[prop] = value; return true; } }); };
Теперь это можно легко назвать вот так:
let store = { username: '', usernamePlaceholder: 'Input a username' }; store = bind(store, (key, value) => console.log(`${key} set to ${value}`)); store.username = 'Tim';
Демо
Посмотреть в действии можно здесь.
Первоначально опубликовано на https://timobechtel.com 28 марта 2020 г.
Этот пост был частью моего 111-Challenge. Подпишитесь на Twitter или на мой веб-сайт, чтобы узнать больше.