На предыдущем уроке нашего курса по Vue вы узнали о том, как создавать компоненты, и о том, как передавать данные от родительских сущностей дочерним с использованием механизма входных параметров (props). А что если данные нужно передавать в обратном направлении? Сегодня, в девятом уроке, вы узнаете о том, как наладить двустороннюю связь между компонентами разного уровня.



> Vue.js для начинающих, урок 1: экземпляр Vue
> Vue.js для начинающих, урок 2: привязка атрибутов
> Vue.js для начинающих, урок 3: условный рендеринг
> Vue.js для начинающих, урок 4: рендеринг списков
> Vue.js для начинающих, урок 5: обработка событий
> Vue.js для начинающих, урок 6: привязка классов и стилей
> Vue.js для начинающих, урок 7: вычисляемые свойства
> Vue.js для начинающих, урок 8: компоненты
> Vue.js для начинающих, урок 9: пользовательские события

Цель урока


Нам надо, чтобы компонент product мог бы сообщать родительской сущности, корневому экземпляру Vue, о том, что произошло некое событие. При этом product должен отправлять, вместе с уведомлением о возникновении события, некие данные.

Начальный вариант кода


В файле index.html учебного проекта сейчас находится такой код:

<div id="app">
  <product :premium="premium"></product>
</div>

Вот — содержимое файла main.js:

Vue.component('product', {
  props: {
    premium: {
      type: Boolean,
      required: true
    }
  },
  template: `
  <div class="product">
    <div class="product-image">
      <img :src="image" />
    </div>

    <div class="product-info">
      <h1>{{ title }}</h1>
      <p v-if="inStock">In stock</p>
      <p v-else>Out of Stock</p>
      <p>Shipping: {{ shipping }}</p>

      <ul>
        <li v-for="detail in details">{{ detail }}</li>
      </ul>
      <div
        class="color-box"
        v-for="(variant, index) in variants"
        :key="variant.variantId"
        :style="{ backgroundColor: variant.variantColor }"
        @mouseover="updateProduct(index)"
      ></div>

      <button
        v-on:click="addToCart"
        :disabled="!inStock"
        :class="{ disabledButton: !inStock }"
      >
        Add to cart
      </button>

      <div class="cart">
        <p>Cart({{ cart }})</p>
      </div>
    </div>
  </div>
  `,
  data() {
    return {
      product: 'Socks',
      brand: 'Vue Mastery',
      selectedVariant: 0,
      details: ['80% cotton', '20% polyester', 'Gender-neutral'],
      variants: [
        {
          variantId: 2234,
          variantColor: 'green',
          variantImage: './assets/vmSocks-green.jpg',
          variantQuantity: 10
        },
        {
          variantId: 2235,
          variantColor: 'blue',
          variantImage: './assets/vmSocks-blue.jpg',
          variantQuantity: 0
        }
      ],
      cart: 0,
    }
  },
    methods: {
      addToCart() {
        this.cart += 1;
      },
      updateProduct(index) {
        this.selectedVariant = index;
        console.log(index);
      }
    },
    computed: {
      title() {
        return this.brand + ' ' + this.product;
      },
      image() {
        return this.variants[this.selectedVariant].variantImage;
      },
      inStock() {
        return this.variants[this.selectedVariant].variantQuantity;
      },
      shipping() {
        if (this.premium) {
          return "Free";
        } else {
          return 2.99
        }
      }
    }
})

var app = new Vue({
  el: '#app',
  data: {
    premium: true
  }
})

Задача


Теперь, когда product представлен самостоятельным компонентом, то, что код, имеющий отношение к корзине, находится в product, смысла не имеет. Если у каждого товара будет своя корзина, за состоянием которой нам нужно наблюдать, в нашем приложении заведётся большой беспорядок. Вместо этого мы хотели бы, чтобы корзина существовала бы на уровне корневого экземпляра Vue. Так же нам нужно, чтобы компонент product сообщал бы корневому экземпляру Vue о добавлении товаров в корзину, то есть — о нажатиях на кнопку Add to cart.

Решение


Переместим данные, имеющие отношение к корзине, обратно в корневой экземпляр Vue:

var app = new Vue({
  el: '#app',
  data: {
    premium: true,
    cart: 0
  }
})

Далее — перенесём шаблон корзины обратно в index.html, приведя его код к такому виду:

<div id="app">
  <div class="cart">
    <p>Cart({{ cart }})</p>
  </div>

  <product :premium="premium"></product>
</div>

Теперь, если открыть страницу приложения в браузере и нажать на кнопку Add to cart, ничего, как и ожидается, не произойдёт.


Нажатие на кнопку Add to cart пока ни к чему не приводит

А что должно происходить по нажатию на эту кнопку? Нам нужно, чтобы при нажатии на неё корневой экземпляр Vue получал бы уведомление, которое вызывало бы метод, приводящий корзину в актуальное состояние, то есть, обновляющий значение, которое хранится в cart.

Для того чтобы этого добиться, давайте сначала перепишем код метода addToCart компонента product.

Сейчас он выглядит так:

addToCart() {
  this.cart += 1;
},

Приведём его к такому виду:

addToCart() {
  this.$emit('add-to-cart');
},

Что всё это значит?

А значит это вот что. Когда вызывается метод addToCart, генерируется пользовательское событие с именем add-to-cart. Другими словами — когда нажимают на кнопку Add to cart, вызывается метод, генерирующий событие, сообщающее о том, что только что была нажата кнопка (то есть — что только что произошло событие, вызванное нажатием кнопки).

Но прямо сейчас ничто в приложении не ожидает появления этого события, не прослушивает его. Давайте добавим прослушиватель событий в index.html:

<product :premium="premium" @add-to-cart="updateCart"></product>

Тут мы пользуемся конструкцией вида @add-to-card аналогично тому, как пользуемся конструкцией :premium. Но если :premium — это «трубопровод», по которому можно передавать данные дочернему компоненту от родительского, то @add-to-cart можно сравнить с «радиоприёмником» родительского компонента, который принимает от дочернего компонента сведения о том, что была нажата кнопка Add to cart. Так как «радиоприёмник» находится в теге <product>, вложенном в <div id="app">, это значит, что при поступлении сведений о нажатии на Add to cart будет вызван метод updateCart, находящийся в корневом экземпляре Vue.

Код @add-to-cart=«updateCart» в переводе на обычный язык выглядит так: «Когда услышишь, что произошло событие add-to-cart, вызови метод updateCart».

Этот метод, который теперь будет объявлен в объекте с опциями, используемом при создании экземпляра Vue, вы, точно, уже где-то видели:

methods: {
  updateCart() {
    this.cart += 1;
  }
}

Собственно говоря, это — точно такой же метод, который использовался раньше в product. Но теперь он находится в корневом экземпляре Vue и вызывается по нажатию на кнопку Add to cart.


Кнопка снова работает

При нажатии на кнопку, находящуюся в компоненте product, вызывается метод addToCart, который генерирует событие. Корневой экземпляр Vue, «слушая радио», узнаёт о том, что данное событие произошло и вызывает метод updateCart, который увеличивает число, хранящееся в cart.

Мы добились своей цели, но в реальном приложении знание о том, что произошло некое событие, что некий товар добавлен в корзину, особой пользы не принесёт. В реальности нужно знать хотя бы о том, какой именно товар добавлен в корзину. А это значит, что в событии, которое генерируется в ответ на нажатие на кнопку, нужно передавать ещё и какие-то данные.

Данные, передаваемые в событии, можно описать в виде второго аргумента, передаваемого $emit в коде метода addToCart компонента product:

this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);

Теперь в событии передаётся идентификатор (variantId) товара, который пользователь хочет добавить в корзину. Это значит, что мы, вместо того чтобы просто увеличивать количество товара в корзине, можем пойти дальше и хранить в корзине более подробные сведения о добавленных в неё товарах. Для этого сначала преобразуем корзину в массив, записав пустой массив в cart:

cart: []

Далее — перепишем метод updateCart. Во-первых — он теперь будет принимать id — тот самый идентификатор товара, который передаётся теперь в событии, во-вторых — он теперь будет помещать то, что получил, в массив:

methods: {
  updateCart(id) {
    this.cart.push(id);
  }
}

После однократного нажатия на кнопку в массив попадает идентификатор товара. Массив выводится на странице.


Массив с идентификатором товара выводится на странице

Нам не нужно выводить на странице весь массив. Нас устроит вывод количества товаров, добавленных в корзину, то есть — в массив cart. Поэтому мы можем переписать код тега <p>, в котором выводятся сведения о количестве товаров, добавленных в корзину, так:

<p>Cart({{ cart.length }})</p>


На странице выводятся сведения о количестве товаров, добавленных в корзину

Теперь мы просто выводим на странице длину массива, или, другими словами, количество товаров в корзине. Внешне корзина выглядит так же, как и прежде, но теперь мы, вместо простого увеличения значения числового свойства cart, сохраняем в массиве cart сведения о том, какой именно товар был добавлен в корзину.

Практикум


Добавьте в проект кнопку, которая удаляет из массива cart товар, добавленный туда ранее. По нажатию на эту кнопку должно генерироваться событие, содержащее сведения об идентификаторе товара, который нужно убрать из корзины.

  • Вот заготовка, которую вы можете использовать для решения этой задачи.
  • Вот решение задачи.

Итоги


Вот что вы сегодня узнали:

  • Компонент может сообщать родительской сущности о том, что в нём что-то произошло, пользуясь конструкцией $emit.
  • Родительский компонент может использовать обработчик события, заданный с использованием директивы v-on (или её сокращённой версии @), для организации реакции на события, генерируемые дочерними компонентами. Если событие происходит — в родительском компоненте может быть вызван обработчик события.
  • Родительский компонент может пользоваться данными, переданными в событии, сгенерированном дочерним компонентом.

Если вы изучаете курс и дошли до этого урока — просим рассказать о том, с какой целью вы занимаетесь, чего хотите достичь, освоив Vue.

> Vue.js для начинающих, урок 1: экземпляр Vue
> Vue.js для начинающих, урок 2: привязка атрибутов
> Vue.js для начинающих, урок 3: условный рендеринг
> Vue.js для начинающих, урок 4: рендеринг списков
> Vue.js для начинающих, урок 5: обработка событий
> Vue.js для начинающих, урок 6: привязка классов и стилей
> Vue.js для начинающих, урок 7: вычисляемые свойства
> Vue.js для начинающих, урок 8: компоненты
> Vue.js для начинающих, урок 9: пользовательские события