В этом посте я покажу вам, как реализовать двустороннюю привязку данных с помощью 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 или на мой веб-сайт, чтобы узнать больше.