Сегодня, в 11 уроке, который завершает этот учебный курс по основам 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: пользовательские события
> Vue.js для начинающих, урок 10: формы
index.html
:<div id="app">
<div class="cart">
<p>Cart({{ cart.length }})</p>
</div>
<product :premium="premium" @add-to-cart="updateCart"></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, index) in details" :key="index">{{ 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
@click="addToCart"
:disabled="!inStock"
:class="{ disabledButton: !inStock }"
>
Add to cart
</button>
</div>
<div>
<h2><font color="#3AC1EF">Reviews</font></h2>
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul>
<li v-for="review in reviews">
<p>{{ review.name }}</p>
<p>Rating: {{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
<product-review @review-submitted="addReview"></product-review>
</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
}
],
reviews: []
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);
},
updateProduct(index) {
this.selectedVariant = index;
},
addReview(productReview) {
this.reviews.push(productReview)
}
},
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
}
}
}
})
Vue.component('product-review', {
template: `
<form class="review-form" @submit.prevent="onSubmit">
<p v-if="errors.length">
<b>Please correct the following error(s):</b>
<ul>
<li v-for="error in errors">{{ error }}</li>
</ul>
</p>
<p>
<label for="name">Name:</label>
<input id="name" v-model="name">
</p>
<p>
<label for="review">Review:</label>
<textarea id="review" v-model="review"></textarea>
</p>
<p>
<label for="rating">Rating:</label>
<select id="rating" v-model.number="rating">
<option>5</option>
<option>4</option>
<option>3</option>
<option>2</option>
<option>1</option>
</select>
</p>
<p>
<input type="submit" value="Submit">
</p>
</form>
`,
data() {
return {
name: null,
review: null,
rating: null,
errors: []
}
},
methods: {
onSubmit() {
if(this.name && this.review && this.rating) {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.$emit('review-submitted', productReview)
this.name = null
this.review = null
this.rating = null
} else {
if(!this.name) this.errors.push("Name required.")
if(!this.review) this.errors.push("Review required.")
if(!this.rating) this.errors.push("Rating required.")
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: []
},
methods: {
updateCart(id) {
this.cart.push(id);
}
}
})
Reviews
, будет выводить отзывы. Вторая, с заголовком Make a Review
, будет выводить форму для отправки отзывов.product-tabs
. Он будет выводиться в нижней части визуального представления компонента product
. Со временем он заменит собой тот код, который сейчас используется для вывода на странице списка отзывов и формы.Vue.component('product-tabs', {
template: `
<div>
<span class="tab" v-for="(tab, index) in tabs" :key="index">{{ tab }}</span>
</div>
`,
data() {
return {
tabs: ['Reviews', 'Make a Review']
}
}
})
tabs
, содержащий строки, которые мы используем в качестве заголовков вкладок. В шаблоне компонента применяется конструкция v-for
, с помощью которой для каждого элемента массива tabs
создаётся элемент <span>
, содержащий соответствующую строку. То, что формирует этот компонент на данном этапе работы над ним, будет выглядеть так, как показано ниже.selectedTab
. Будем динамически задавать значение этого свойства, пользуясь обработчиком событий, реагирующим на щелчки по заголовкам вкладок:@click="selectedTab = tab"
Reviews
, то в selectedTab
будет записана строка Reviews
. Если будет сделан щелчок по вкладке Make a Review
, то в selectedTab
попадёт строка Make a Review
.Vue.component('product-tabs', {
template: `
<div>
<ul>
<span class="tab"
v-for="(tab, index) in tabs"
@click="selectedTab = tab"
>{{ tab }}</span>
</ul>
</div>
`,
data() {
return {
tabs: ['Reviews', 'Make a Review'],
selectedTab: 'Reviews' // устанавливается с помощью @click
}
}
})
<span>
, использующимся для вывода названий вкладок::class="{ activeTab: selectedTab === tab }"
activeTab
. Вот как выглядит этот стиль:.activeTab {
color: #16C0B0;
text-decoration: underline;
}
tab
:.tab {
margin-left: 20px;
cursor: pointer;
}
activeTab
, в том случае, когда selectedTab
равняется tab
. Так как в selectedTab
записывается название вкладки, по которой только что щёлкнул пользователь, стиль .activeTab
будет применяться именно к активной вкладке.tab
будет находиться Reviews
, то же самое будет записано и в selectedTab
. В результате к первой вкладке будет применён стиль .activeTab
.Reviews
. Это, понятно, отзывы о товаре. Поэтому переместим код вывода отзывов из шаблона компонента product
в шаблон компонента product-tabs
, разместив этот код ниже конструкции, используемой для вывода заголовков вкладок. Вот как теперь будет выглядеть шаблон компонента product-tabs
:template: `
<div>
<ul>
<span class="tab"
:class="{ activeTab: selectedTab === tab }"
v-for="(tab, index) in tabs"
@click="selectedTab = tab"
>{{ tab }}</span>
</ul>
<div>
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul>
<li v-for="review in reviews">
<p>{{ review.name }}</p>
<p>Rating: {{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
</div>
`
<h2><font color="#3AC1EF">
, так как нам больше не нужно выводить заголовок Reviews
над списком отзывов. Вместо этого заголовка будет выводиться заголовок соответствующей вкладки.reviews
, данные которого используются для вывода отзывов, хранится в составе данных компонента product
. Нам нужно передать этот массив в компонент product-tabs
, используя механизм входных параметров компонента. Добавим в объект с опциями, используемый при создании product-tabs
, следующее:props: {
reviews: {
type: Array,
required: false
}
}
reviews
из компонента product
в компонент product-tabs
, воспользовавшись, в шаблоне product
, следующей конструкцией:<product-tabs :reviews="reviews"></product-tabs>
Make a Review
. Это, конечно, форма для отправки отзывов. Для того чтобы подготовить проект к дальнейшей работе над ним — перенесём код подключения компонента product-review
из шаблона компонента product
в шаблон product-tabs
. Разместим следующий код ниже элемента <div>
, используемого для вывода списка отзывов:<div>
<product-review @review-submitted="addReview"></product-review>
</div>
product-tabs
, пришло время создать систему, которая позволит выводить разные элементы страницы основываясь на том, по заголовку какой именно вкладки щёлкнул пользователь.selectedTab
. Мы можем воспользоваться им в директиве v-show
для организации условного рендеринга того, что относится к каждой из вкладок.<div>
, содержащему код формирования списка отзывов, мы можем добавить такую конструкцию:v-show="selectedTab === 'Reviews'"
Reviews
.<div>
, в котором содержится код подключения компонента product-review
, мы добавим следующее:v-show="selectedTab === 'Make a Review'"
Make a Review
.product-tabs
:template: `
<div>
<ul>
<span class="tab"
:class="{ activeTab: selectedTab === tab }"
v-for="(tab, index) in tabs"
@click="selectedTab = tab"
>{{ tab }}</span>
</ul>
<div v-show="selectedTab === 'Reviews'">
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul>
<li v-for="review in reviews">
<p>{{ review.name }}</p>
<p>Rating: {{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
<div v-show="selectedTab === 'Make a Review'">
<product-review @review-submitted="addReview"></product-review>
</div>
</div>
`
addReview
. Что с ним случилось?addReview
— это метод, который объявлен в компоненте product
. Он должен вызываться в том случае, если компонент product-review
(а это — дочерний компонент компонента product
) генерирует событие review-submitted
:<product-review @review-submitted="addReview"></product-review>
product-tabs
. А теперь дочерним компонентом product
является компонент product-tabs
, а product-review
— это теперь не «ребёнок», компонента product
, а его «внук».product-review
с родительским компонентом. Но теперь это — уже не компонент product
. В результате оказывается, что нам, чтобы форма заработала бы правильно, нужно подвергнуть код проекта рефакторингу.var eventBus = new Vue()
main.js
.product-review
к компоненту product
, перевозя сведения о том, что форма был отправлена, и доставляя данные формы из product-review
в product
.product-review
, в методе onSubmit
, есть такая строчка:this.$emit('review-submitted', productReview)
eventBus
вместо this
:eventBus.$emit('review-submitted', productReview)
review-submitted
компонента product-review
. Поэтому изменим код этого компонента в шаблоне компонента product-tabs
на такой:<product-review></product-review>
product
теперь можно удалить метод addReview
. Вместо него мы воспользуемся следующей конструкцией:eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
eventBus
генерирует событие review-submitted
, нужно взять данные, передаваемые в этом событии (то есть — productReview
) и поместить их в массив reviews
компонента product
. Собственно говоря, это очень похоже на то, что до сих пор делалось в методе addReview
, который нам больше не нужен. Обратите внимание на то, что в вышеприведённом фрагменте кода используется стрелочная функция. Этот момент достоин более подробного освещения.this
, оно равнозначно тому ключевому слову this
, которое соответствует сущности, содержащей стрелочную функцию.this
:eventBus.$on('review-submitted', function (productReview) {
this.reviews.push(productReview)
}.bind(this))
review-submitted
. Таким местом в компоненте product
может стать функция mounted
:mounted() {
eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
}
product
будет смонтирован, он будет ожидать появления событий review-submitted
. После того, как такое событие будет сгенерировано, в данные компонента будет добавлено то, что передано в этом событии, то есть — productReview
.Shipping
и Details
, на которых, соответственно, выводится стоимость доставки покупок и сведения о товарах.К сожалению, не доступен сервер mySQL