Недавно у меня возникла необходимость обновлять состояние компонента каждый раз, когда его содержимое (слот, дочерние элементы и т. Д.) Менялось. Для контекста это компонент формы, который отслеживает состояние допустимости входных данных.

Я думал, что это будет проще, чем было на самом деле, и не нашел там большого количества контента. Так что, получив решение, которым я доволен, я решил поделиться. Построим вместе :)

Следующие ниже фрагменты кода написаны в формате API опций, но должны работать с Vue.js версии 2 и версии 3, если не указано иное.

Установка

Давайте начнем с формы, которая отслеживает его состояние действительности, изменяет класс на основе состояния и отображает его дочерние элементы как <slot/>.

<template>
  <form :class="{ '--invalid': isInvalid }">
    <slot />
  </form>
</template>
<script>
export default {
  data: () => ({
    isInvalid: false,
  }),
};
</script>

Чтобы обновить свойство isInvalid, нам нужно прикрепить к какому-либо событию обработчик событий. Мы могли бы использовать событие submit, но я предпочитаю событие input.

Форма не запускает событие ввода, но мы можем использовать шаблон, называемый делегирование события. Мы прикрепим слушателя к родительскому элементу (<form>), который запускается каждый раз, когда событие происходит на его дочерних элементах (<input>, <select>, <textarea> и т. Д.).

Каждый раз, когда в <slot> содержимом этого компонента происходит «входное» событие, форма фиксирует это событие.

<template>
  <form :class="{ '--invalid': isInvalid }" @input="validate">
    <slot />
  </form>
</template>
<script>
export default {
  data: () => ({
    isInvalid: false,
  }),
  methods: {
    validate() {
      // validation logic
    }
  }
};
</script>

Логика проверки может быть настолько простой или сложной, насколько вам нравится. В моем случае я хочу снизить шум, поэтому я воспользуюсь собственным form.checkValidity() API, чтобы проверить, действительна ли форма на основе атрибутов проверки HTML.

Для этого мне нужен доступ к элементу <form>. Vue упрощает это с помощью refs или свойства $el. Для простоты я буду использовать $el.

<template>
  <form :class="{ '--invalid': isInvalid }" @input="validate">
    <slot />
  </form>
</template>
<script>
export default {
  data: () => ({
    isInvalid: false,
  }),
  methods: {
    validate() {
      this.isInvalid = !this.$el.checkValidity()
    }
  }
};
</script>

Это работает очень хорошо. Когда компонент монтируется на страницу, Vue присоединяет прослушиватель событий и при любом входном событии обновляет состояние действительности формы. Мы могли бы даже вызвать метод validate() из события жизненного цикла mounted, чтобы увидеть, является ли форма недействительной в момент ее монтирования.

Проблема

У нас тут небольшая проблема. Что произойдет, если содержимое формы изменится? Что произойдет, если <input> будет добавлен в DOM после монтирования формы?

В качестве примера назовем наш компонент формы «MyForm», а внутри другого компонента, называемого «App», мы реализуем «MyForm». «Приложение» может отображать некоторые входные данные внутри содержимого слота «MyForm».

<template>
  <MyForm>
    <input v-model="showInput" id="toggle-name" name="toggle-name" type="checkbox">
    <label for="toggle-name">Include name?</label>
    <template v-if="showInput">
      <label for="name">Name:</label>
      <input id="name" name="name" required>
    </template>
    <button type="submit">Submit</button>
  </MyForm>
</template>
<script>
export default {
  data: () => ({
    showInput: false
  }),
}
</script>

Если «Приложение» реализует условную логику для отображения некоторых входных данных, наша форма должна знать об этом. В этом случае мы, вероятно, захотим отслеживать действительность формы при каждом изменении ее содержимого, а не только при «входных» событиях или mounted перехватчиках жизненного цикла. В противном случае мы можем отобразить неверную информацию.

Решение

После небольшого исследования и тестирования лучшее решение, которое я придумал, - это использовать MutationObserver API. Этот API встроен в браузер и позволяет нам, по сути, отслеживать изменения в содержимом узла DOM. Одно интересное преимущество заключается в том, что он не зависит от фреймворка.

Что нам нужно сделать, так это создать новый MutationObserver экземпляр при монтировании нашего компонента. Конструктору MutationObserver требуется функция обратного вызова для вызова при возникновении изменений, а экземпляру MutationObserver требуется элемент для отслеживания изменений и объект настроек.

<script>
export default {
  // other code
  mounted() {
    const observer = new MutationObserver(this.validate);
    observer.observe(this.$el, {
      childList: true,
      subtree: true 
    });
    this.observer = observer;
  },
  // For Vue.js v2 use beforeDestroy
  beforeUnmount() {
    this.observer.disconnect();
  }
  // other code
};
</script>

Обратите внимание, что мы также задействуем событие жизненного цикла beforeUnmount (для Vue.js v2 используйте beforeDestroy), чтобы отключить нашего наблюдателя, что должно очистить всю выделенную им память.

Большинство деталей на месте, но я хочу добавить еще одну вещь. Давайте передадим состояние isInvalid в слот, к которому будет иметь доступ контент. Это называется слот с ограниченной областью действия, и он невероятно полезен.

После этого наш завершенный компонент мог бы выглядеть так:

<template>
  <form :class="{ '--invalid': isInvalid }">
    <slot v-bind="{ isInvalid }" />
  </form>
</template>
<script>
export default {
  data: () => ({
    isInvalid: false,
  }),
  mounted() {
    this.validate();
    const observer = new MutationObserver(this.validate);
    observer.observe(this.$el, {
      childList: true,
      subtree: true 
    });
    this.observer = observer;
  },
  beforeUnmount() {
    this.observer.disconnect();
  }
  methods: {
    validate() {
      this.isInvalid = !this.$el.checkValidity()  
    },
  },
};
</script>

При такой настройке родительский компонент может добавлять любое количество входов в наш компонент формы и добавлять любую логику условного рендеринга, которая ему нужна. Пока во входных данных используются атрибуты проверки HTML, форма будет отслеживать, находится ли она в допустимом состоянии.

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

Например, если наш компонент назывался <MyForm>, и мы хотели «отключить» кнопку отправки, когда форма недействительна, это могло бы выглядеть так:

<template>
  <MyForm>
    <template slot:default="form">
      <label for="name">Name:</label>
      <input id="name" name="name" required>
      <button
        type="submit"
        :class="{ disabled: form.invalid }"
      >
        Submit
      </button>
    </template>
  </MyForm>
</template>

Обратите внимание, что я не использую атрибут disabled для отключения кнопки, потому что некоторые люди, такие как Крис Фердинанди и Скотт О’Хара, считают, что это анти-шаблон доступности (подробнее об этом здесь).

Для меня это имеет смысл. Делайте то, что для вас имеет смысл.

Резюме

Это была интересная проблема, вдохновленная работой над Vuetensils. Чтобы получить более надежное решение для форм, взгляните на компонент VForm этой библиотеки. Мне это нравится.

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

Если вам понравилась эта статья, поделитесь, пожалуйста. Есть отзывы или рекомендации? Ударь меня в Твиттере. И если вы хотите знать, когда я опубликую еще подобные статьи, подпишитесь на мою рассылку. Ваше здоровье!