Vue.js - отличная платформа для создания интерфейсных веб-приложений. Он использует архитектуру на основе компонентов, которая упрощает организацию кода. Он позволяет вам использовать новейшие функции JavaScript, что означает, что писать код для создания ваших приложений проще, чем когда-либо. Архитектура на основе компонентов означает, что наше внешнее приложение состоит из частей, вложенных друг в друга. Ключевым компонентом компонентной архитектуры является возможность передачи данных между родительскими и дочерними компонентами. Они передаются через реквизиты, если данные передаются между родительским и дочерним и эмиттерами событий, если они передаются от дочернего и родительского. Указание типа данных в реквизитах позволяет нам проверить, что все, что передается, является правильным типом.
В этой истории мы создадим приложение адресной книги с Vue.js, в котором будет некоторое вложение компонентов с передаваемыми между ними реквизитами. Контактная форма позволяет нам добавлять и редактировать наши контакты, а также мы можем получать и удалять контакты.
Чтобы создать наше приложение, сначала нам нужно быстро настроить серверную часть. Для этого мы используем пакет Node.js под названием JSON Server для запуска нашей серверной части. Документация к пакету находится по адресу https://github.com/typicode/json-server. После запуска он предоставляет нам маршруты для сохранения записей контактов из внешнего интерфейса. Чтобы установить пакет, запустите:
npm install -g json-server
Мы запустим это позже, чтобы сохранить наши контакты.
Теперь мы можем приступить к созданию нашего приложения. Для этого установите Vue CLI, запустив:
npm install -g @vue/cli
Затем создайте приложение, запустив:
vue create vee-validate-address-book-app
vee-validate-address-book-app
- это название нашего приложения. При запуске мастера убедитесь, что вы выбрали включение Vuex и Vue Router, поскольку мы будем использовать его позже. Далее нам нужно установить несколько библиотек. Нам нужен HTTP-клиент, библиотека Material Design, чтобы наше приложение выглядело хорошо, и библиотека Vee-Validate. Для этого запустите npm i axios vee-validate vue-material
. Axios - наш HTTP-клиент для связи с серверной частью. Vue Material - это наша библиотека материального дизайна.
Затем мы создаем наши компоненты, которые вкладываем в компоненты нашей страницы. Для этого создайте папку components
в папке нашего проекта и в ней создайте файл с именем ContactForm.vue
. В этом файле мы помещаем:
<template> <div class="contact-form"> <div class="center"> <h1>{{editing ? 'Edit': 'Add'}} Contact</h1> </div> <form novalidate class="md-layout" @submit="save"> <md-field :class="{ 'md-invalid': errors.has('firstName') }"> <label for="firstName">First Name</label> <md-input name="firstName" v-model="contact.firstName" v-validate="'required'" :disabled="sending" /> <span class="md-error" v-if="errors.has('firstName')">First Name is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('lastName') }"> <label for="lastName">Last Name</label> <md-input name="lastName" v-model="contact.lastName" :disabled="sending" v-validate="'required'" /> <span class="md-error" v-if="errors.has('lastName')">Last Name is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('addressLineOne') }"> <label for="addressLineOne">Address Line 1</label> <md-input name="addressLineOne" v-model="contact.addressLineOne" :disabled="sending" v-validate="'required'" /> <span class="md-error" v-if="errors.has('addressLineOne')">Address line 1 is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('addressLineTwo') }"> <label for="addressLineTwo">Address Line 2</label> <md-input name="addressLineTwo" v-model="contact.addressLineTwo" :disabled="sending" /> <span class="md-error" v-if="errors.has('addressLineTwo')">Address line 2 is required</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('city') }"> <label for="city">City</label> <md-input name="city" v-model="contact.city" :disabled="sending" v-validate="'required'" /> <span class="md-error" v-if="errors.has('city')">City is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('country') }"> <label for="country">Country</label> <md-select name="country" v-model="contact.country" md-dense :disabled="sending" v-validate.continues="'required'" > <md-option :value="c" :key="c" v-for="c in countries">{{c}}</md-option> </md-select> <span class="md-error" v-if="errors.firstByRule('country', 'required')">Country is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('postalCode') }"> <label for="postalCode">Postal Code</label> <md-input name="postalCode" v-model="contact.postalCode" :disabled="sending" v-validate="{ required: true, regex: getPostalCodeRegex() }" /> <span class="md-error" v-if="errors.firstByRule('postalCode', 'required')" >Postal Code is required.</span> <span class="md-error" v-if="errors.firstByRule('postalCode', 'regex')" >Postal Code is invalid.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('phone') }"> <label for="phone">Phone</label> <md-input name="phone" v-model="contact.phone" :disabled="sending" v-validate="{ required: true, regex: getPhoneRegex() }" /> <span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span> <span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('gender') }"> <label for="gender">Gender</label> <md-select name="gender" v-model="contact.gender" md-dense :disabled="sending" v-validate.continues="'required'" > <md-option value="male">Male</md-option> <md-option value="female">Female</md-option> </md-select> <span class="md-error" v-if="errors.firstByRule('gender', 'required')">Gender is required.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('age') }"> <label for="age">Age</label> <md-input type="number" id="age" name="age" autocomplete="age" v-model="contact.age" :disabled="sending" v-validate="'required|between:0,200'" /> <span class="md-error" v-if="errors.firstByRule('age', 'required')">Age is required.</span> <span class="md-error" v-if="errors.firstByRule('age', 'between')">Age must be 0 and 200.</span> </md-field> <br /> <md-field :class="{ 'md-invalid': errors.has('email') }"> <label for="email">Email</label> <md-input type="email" name="email" autocomplete="email" v-model="contact.email" :disabled="sending" v-validate="'required|email'" /> <span class="md-error" v-if="errors.firstByRule('email', 'required')">Email is required.</span> <span class="md-error" v-if="errors.firstByRule('email', 'email')">Email is invalid.</span> </md-field> <md-progress-bar md-mode="indeterminate" v-if="sending" /> <md-button type="submit" class="md-raised">{{editing ? 'Edit':'Create'}} Contact</md-button> </form> </div> </template> <script> import { COUNTRIES } from "@/helpers/exports"; import { contactMixin } from "@/mixins/contactMixin"; export default { name: "ContactForm", mixins: [contactMixin], props: { editing: Boolean, contactId: Number }, computed: { isFormDirty() { return Object.keys(this.fields).some(key => this.fields[key].dirty); }, contacts() { return this.$store.state.contacts; } }, data() { return { sending: false, contact: {}, countries: COUNTRIES.map(c => c.name) }; }, beforeMount() { this.contact = this.contacts.find(c => c.id == this.contactId) || {}; }, methods: { async save(evt) { evt.preventDefault(); try { const result = await this.$validator.validateAll(); if (!result) { return; } if (this.editing) { await this.updateContact(this.contact, this.contactId); await this.getAllContacts(); this.$emit("contactSaved"); } else { await this.addContact(this.contact); await this.getAllContacts(); this.$router.push("/"); } } catch (ex) { console.log(ex); } }, async getAllContacts() { try { const response = await this.getContacts(); this.$store.commit("setContacts", response.data); } catch (ex) { console.log(ex); } }, getPostalCodeRegex() { if (this.contact.country == "United States") { return /^[0-9]{5}(?:-[0-9]{4})?$/; } else if (this.contact.country == "Canada") { return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/; } return /./; }, getPhoneRegex() { if (["United States", "Canada"].includes(this.contact.country)) { return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/; } return /./; } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="scss"> .contact-form { margin: 0 auto; width: 90%; } </style>
В приведенном выше файле у нас есть контактная форма для добавления и обновления контактов в нашей адресной книге. Обратите внимание, что мы вызываем this.$emit(“contactSaved”)
, чтобы передать contactSaved
событие компоненту HomePage
, чтобы он получил уведомление о том, что мы сохранили наш контакт. contactSaved
переходит к компоненту HomePage
, а затем он вызывает contactSaved
, который мы написали в компоненте HomePage
.
В HomePage.vue
мы поместим:
<ContactForm :editing="true" :contactId="selectedContactId" @contactSaved="selectedContactId = undefined; showDialog = false" />
Строка @contactSaved=”selectedContactId = undefined; showDialog = false”
- это то место, где мы обрабатываем contactSaved
событие. Знак @
означает, что это обработчик событий, а код между кавычками - это то, что мы называем. В этом случае это закроет наше диалоговое окно.
Именно здесь Vee-Validate используется чаще всего. Обратите внимание, что в большинстве элементов управления вводом внутри тега form
у нас есть свойство v-validate
, здесь мы указываем, какой тип ввода принимает элемент управления. required
означает, что поле формы является обязательным. regex
означает, что мы проверяем соответствие указанному регулярному выражению. Это позволяет настраивать проверку формы там, где нет встроенных правил для Vee Validate, или когда вам нужно проверить поле по-разному в зависимости от значения другого поля. Например, для номера телефона у нас есть такая функция:
getPhoneRegex() { if (["United States", "Canada"].includes(this.contact.country)){ return /^[2-9]\d{2}[2-9]\d{2}\d{4}$/; } return /./; }
Это позволяет нам проверять номер, чтобы увидеть, соответствует ли он североамериканскому телефонному формату, когда мы въезжаем в США или Канаду. В противном случае мы позволяем людям входить во все, что они хотят.
Аналогично для почтового индекса у нас есть:
getPostalCodeRegex() { if (this.contact.country == "United States") { return /^[0-9]{5}(?:-[0-9]{4})?$/; } else if (this.contact.country == "Canada") { return /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/; } return /./; }
Это позволяет нам проверять почтовые индексы США и Канады.
Чтобы отобразить ошибки, мы можем проверить, существуют ли ошибки для поля формы, а затем отобразить ошибки. Например, для имени мы имеем:
<span class="md-error" v-if="errors.has('firstName')">First Name is required.</span>
errors.has(‘firstName’)
проверяет, соответствует ли поле имени указанным нами критериям проверки. Поскольку мы проверяем, заполнено ли оно, возможна только одна ошибка, поэтому мы можем просто отобразить единственную ошибку, когда errors.has(‘firstName’)
вернет true
. Для чего-то более сложного, например телефона, у нас есть:
<span class="md-error" v-if="errors.firstByRule('phone', 'required')">Phone is required.</span> <span class="md-error" v-if="errors.firstByRule('phone', 'regex')">Phone is invalid.</span>
Это позволяет нам проверять каждое правило проверки отдельно. Что касается поля номера телефона, мы должны проверить, заполнено ли оно и имеет ли заполненный формат допустимый формат. Функция errors.firstByRule
позволяет нам это сделать. errors.firstByRule(‘phone’, ‘required’)
возвращает true
, если поле не заполнено, и false
в противном случае. errors.firstByRule(‘phone’, ‘regex’)
возвращает true
, если номер телефона указан в неправильном формате, и false
в противном случае.
Vee-Validate предоставляет вашему компоненту this.field
объект. Таким образом, мы можем проверить, являются ли поля грязными, что означает, были ли они изменены или нет, добавив:
Object.keys(this.fields).some(key => this.fields[key].dirty)
Каждое свойство является полем формы, и каждое свойство объекта this.fields
имеет свойство dirty
, поэтому мы можем проверить, обрабатываются ли эти поля или нет.
В функции save
объекта methods
мы имеем:
async save(evt) { evt.preventDefault(); try { const result = await this.$validator.validateAll(); if (!result) { return; } if (this.editing) { await this.updateContact(this.contact, this.contactId); await this.getAllContacts(); this.$emit("contactSaved"); } else { await this.addContact(this.contact); await this.getAllContacts(); this.$router.push("/"); } } catch (ex) { console.log(ex); } },
Нам нужно evt.preventDefault()
, чтобы форма не отправлялась обычным способом, то есть без вызова приведенного ниже кода Ajax. this.$validator.validateAll()
проверяет форму. this.$validator
- это объект, предоставленный Vee Validate. Он возвращает обещание, поэтому нам нужно, чтобы функция была async
, и нам нужно await
перед вызовом функции. Если result
ложно, проверка формы завершилась неудачно, поэтому мы запускаем return
, чтобы остановить выполнение остальной части функции. Наконец, если все поля формы действительны, мы можем отправить. Поскольку эта форма используется как для добавления, так и для редактирования контактов, мы должны проверить, какое действие мы выполняем. Если мы редактируем, мы вызываем await this.updateContact(this.contact, this.contactId);
, чтобы обновить наш контакт. В противном случае мы добавляем контакт и вызываем await this.addContact(this.contact);
. В любом случае мы вызываем await this.getAllContacts();
, чтобы обновить наши контакты и поместить их в магазин. Если мы добавляем, то в конце мы перенаправим на домашнюю страницу, вызвав this.$router.push(“/”);
. this.updateContact
, this.addContact
и this.getAllContacts
все из нашего contactMixin
, о котором мы скоро напишем.
Затем мы пишем вспомогательный код. Создайте папку с именем helpers
и в ней создайте файл с именем export.js
и вставьте следующее:
export const COUNTRIES = [ { "name": "Afghanistan", "code": "AF" }, { "name": "Aland Islands", "code": "AX" }, { "name": "Albania", "code": "AL" }, { "name": "Algeria", "code": "DZ" }, { "name": "American Samoa", "code": "AS" }, { "name": "AndorrA", "code": "AD" }, { "name": "Angola", "code": "AO" }, { "name": "Anguilla", "code": "AI" }, { "name": "Antarctica", "code": "AQ" }, { "name": "Antigua and Barbuda", "code": "AG" }, { "name": "Argentina", "code": "AR" }, { "name": "Armenia", "code": "AM" }, { "name": "Aruba", "code": "AW" }, { "name": "Australia", "code": "AU" }, { "name": "Austria", "code": "AT" }, { "name": "Azerbaijan", "code": "AZ" }, { "name": "Bahamas", "code": "BS" }, { "name": "Bahrain", "code": "BH" }, { "name": "Bangladesh", "code": "BD" }, { "name": "Barbados", "code": "BB" }, { "name": "Belarus", "code": "BY" }, { "name": "Belgium", "code": "BE" }, { "name": "Belize", "code": "BZ" }, { "name": "Benin", "code": "BJ" }, { "name": "Bermuda", "code": "BM" }, { "name": "Bhutan", "code": "BT" }, { "name": "Bolivia", "code": "BO" }, { "name": "Bosnia and Herzegovina", "code": "BA" }, { "name": "Botswana", "code": "BW" }, { "name": "Bouvet Island", "code": "BV" }, { "name": "Brazil", "code": "BR" }, { "name": "British Indian Ocean Territory", "code": "IO" }, { "name": "Brunei Darussalam", "code": "BN" }, { "name": "Bulgaria", "code": "BG" }, { "name": "Burkina Faso", "code": "BF" }, { "name": "Burundi", "code": "BI" }, { "name": "Cambodia", "code": "KH" }, { "name": "Cameroon", "code": "CM" }, { "name": "Canada", "code": "CA" }, { "name": "Cape Verde", "code": "CV" }, { "name": "Cayman Islands", "code": "KY" }, { "name": "Central African Republic", "code": "CF" }, { "name": "Chad", "code": "TD" }, { "name": "Chile", "code": "CL" }, { "name": "China", "code": "CN" }, { "name": "Christmas Island", "code": "CX" }, { "name": "Cocos (Keeling) Islands", "code": "CC" }, { "name": "Colombia", "code": "CO" }, { "name": "Comoros", "code": "KM" }, { "name": "Congo", "code": "CG" }, { "name": "Congo, The Democratic Republic of the", "code": "CD" }, { "name": "Cook Islands", "code": "CK" }, { "name": "Costa Rica", "code": "CR" }, { "name": "Cote D\"Ivoire", "code": "CI" }, { "name": "Croatia", "code": "HR" }, { "name": "Cuba", "code": "CU" }, { "name": "Cyprus", "code": "CY" }, { "name": "Czech Republic", "code": "CZ" }, { "name": "Denmark", "code": "DK" }, { "name": "Djibouti", "code": "DJ" }, { "name": "Dominica", "code": "DM" }, { "name": "Dominican Republic", "code": "DO" }, { "name": "Ecuador", "code": "EC" }, { "name": "Egypt", "code": "EG" }, { "name": "El Salvador", "code": "SV" }, { "name": "Equatorial Guinea", "code": "GQ" }, { "name": "Eritrea", "code": "ER" }, { "name": "Estonia", "code": "EE" }, { "name": "Ethiopia", "code": "ET" }, { "name": "Falkland Islands (Malvinas)", "code": "FK" }, { "name": "Faroe Islands", "code": "FO" }, { "name": "Fiji", "code": "FJ" }, { "name": "Finland", "code": "FI" }, { "name": "France", "code": "FR" }, { "name": "French Guiana", "code": "GF" }, { "name": "French Polynesia", "code": "PF" }, { "name": "French Southern Territories", "code": "TF" }, { "name": "Gabon", "code": "GA" }, { "name": "Gambia", "code": "GM" }, { "name": "Georgia", "code": "GE" }, { "name": "Germany", "code": "DE" }, { "name": "Ghana", "code": "GH" }, { "name": "Gibraltar", "code": "GI" }, { "name": "Greece", "code": "GR" }, { "name": "Greenland", "code": "GL" }, { "name": "Grenada", "code": "GD" }, { "name": "Guadeloupe", "code": "GP" }, { "name": "Guam", "code": "GU" }, { "name": "Guatemala", "code": "GT" }, { "name": "Guernsey", "code": "GG" }, { "name": "Guinea", "code": "GN" }, { "name": "Guinea-Bissau", "code": "GW" }, { "name": "Guyana", "code": "GY" }, { "name": "Haiti", "code": "HT" }, { "name": "Heard Island and Mcdonald Islands", "code": "HM" }, { "name": "Holy See (Vatican City State)", "code": "VA" }, { "name": "Honduras", "code": "HN" }, { "name": "Hong Kong", "code": "HK" }, { "name": "Hungary", "code": "HU" }, { "name": "Iceland", "code": "IS" }, { "name": "India", "code": "IN" }, { "name": "Indonesia", "code": "ID" }, { "name": "Iran, Islamic Republic Of", "code": "IR" }, { "name": "Iraq", "code": "IQ" }, { "name": "Ireland", "code": "IE" }, { "name": "Isle of Man", "code": "IM" }, { "name": "Israel", "code": "IL" }, { "name": "Italy", "code": "IT" }, { "name": "Jamaica", "code": "JM" }, { "name": "Japan", "code": "JP" }, { "name": "Jersey", "code": "JE" }, { "name": "Jordan", "code": "JO" }, { "name": "Kazakhstan", "code": "KZ" }, { "name": "Kenya", "code": "KE" }, { "name": "Kiribati", "code": "KI" }, { "name": "Korea, Democratic People\"S Republic of", "code": "KP" }, { "name": "Korea, Republic of", "code": "KR" }, { "name": "Kuwait", "code": "KW" }, { "name": "Kyrgyzstan", "code": "KG" }, { "name": "Lao People\"S Democratic Republic", "code": "LA" }, { "name": "Latvia", "code": "LV" }, { "name": "Lebanon", "code": "LB" }, { "name": "Lesotho", "code": "LS" }, { "name": "Liberia", "code": "LR" }, { "name": "Libyan Arab Jamahiriya", "code": "LY" }, { "name": "Liechtenstein", "code": "LI" }, { "name": "Lithuania", "code": "LT" }, { "name": "Luxembourg", "code": "LU" }, { "name": "Macao", "code": "MO" }, { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" }, { "name": "Madagascar", "code": "MG" }, { "name": "Malawi", "code": "MW" }, { "name": "Malaysia", "code": "MY" }, { "name": "Maldives", "code": "MV" }, { "name": "Mali", "code": "ML" }, { "name": "Malta", "code": "MT" }, { "name": "Marshall Islands", "code": "MH" }, { "name": "Martinique", "code": "MQ" }, { "name": "Mauritania", "code": "MR" }, { "name": "Mauritius", "code": "MU" }, { "name": "Mayotte", "code": "YT" }, { "name": "Mexico", "code": "MX" }, { "name": "Micronesia, Federated States of", "code": "FM" }, { "name": "Moldova, Republic of", "code": "MD" }, { "name": "Monaco", "code": "MC" }, { "name": "Mongolia", "code": "MN" }, { "name": "Montenegro", "code": "ME" }, { "name": "Montserrat", "code": "MS" }, { "name": "Morocco", "code": "MA" }, { "name": "Mozambique", "code": "MZ" }, { "name": "Myanmar", "code": "MM" }, { "name": "Namibia", "code": "NA" }, { "name": "Nauru", "code": "NR" }, { "name": "Nepal", "code": "NP" }, { "name": "Netherlands", "code": "NL" }, { "name": "Netherlands Antilles", "code": "AN" }, { "name": "New Caledonia", "code": "NC" }, { "name": "New Zealand", "code": "NZ" }, { "name": "Nicaragua", "code": "NI" }, { "name": "Niger", "code": "NE" }, { "name": "Nigeria", "code": "NG" }, { "name": "Niue", "code": "NU" }, { "name": "Norfolk Island", "code": "NF" }, { "name": "Northern Mariana Islands", "code": "MP" }, { "name": "Norway", "code": "NO" }, { "name": "Oman", "code": "OM" }, { "name": "Pakistan", "code": "PK" }, { "name": "Palau", "code": "PW" }, { "name": "Palestinian Territory, Occupied", "code": "PS" }, { "name": "Panama", "code": "PA" }, { "name": "Papua New Guinea", "code": "PG" }, { "name": "Paraguay", "code": "PY" }, { "name": "Peru", "code": "PE" }, { "name": "Philippines", "code": "PH" }, { "name": "Pitcairn", "code": "PN" }, { "name": "Poland", "code": "PL" }, { "name": "Portugal", "code": "PT" }, { "name": "Puerto Rico", "code": "PR" }, { "name": "Qatar", "code": "QA" }, { "name": "Reunion", "code": "RE" }, { "name": "Romania", "code": "RO" }, { "name": "Russian Federation", "code": "RU" }, { "name": "RWANDA", "code": "RW" }, { "name": "Saint Helena", "code": "SH" }, { "name": "Saint Kitts and Nevis", "code": "KN" }, { "name": "Saint Lucia", "code": "LC" }, { "name": "Saint Pierre and Miquelon", "code": "PM" }, { "name": "Saint Vincent and the Grenadines", "code": "VC" }, { "name": "Samoa", "code": "WS" }, { "name": "San Marino", "code": "SM" }, { "name": "Sao Tome and Principe", "code": "ST" }, { "name": "Saudi Arabia", "code": "SA" }, { "name": "Senegal", "code": "SN" }, { "name": "Serbia", "code": "RS" }, { "name": "Seychelles", "code": "SC" }, { "name": "Sierra Leone", "code": "SL" }, { "name": "Singapore", "code": "SG" }, { "name": "Slovakia", "code": "SK" }, { "name": "Slovenia", "code": "SI" }, { "name": "Solomon Islands", "code": "SB" }, { "name": "Somalia", "code": "SO" }, { "name": "South Africa", "code": "ZA" }, { "name": "South Georgia and the South Sandwich Islands", "code": "GS" }, { "name": "Spain", "code": "ES" }, { "name": "Sri Lanka", "code": "LK" }, { "name": "Sudan", "code": "SD" }, { "name": "Suriname", "code": "SR" }, { "name": "Svalbard and Jan Mayen", "code": "SJ" }, { "name": "Swaziland", "code": "SZ" }, { "name": "Sweden", "code": "SE" }, { "name": "Switzerland", "code": "CH" }, { "name": "Syrian Arab Republic", "code": "SY" }, { "name": "Taiwan, Province of China", "code": "TW" }, { "name": "Tajikistan", "code": "TJ" }, { "name": "Tanzania, United Republic of", "code": "TZ" }, { "name": "Thailand", "code": "TH" }, { "name": "Timor-Leste", "code": "TL" }, { "name": "Togo", "code": "TG" }, { "name": "Tokelau", "code": "TK" }, { "name": "Tonga", "code": "TO" }, { "name": "Trinidad and Tobago", "code": "TT" }, { "name": "Tunisia", "code": "TN" }, { "name": "Turkey", "code": "TR" }, { "name": "Turkmenistan", "code": "TM" }, { "name": "Turks and Caicos Islands", "code": "TC" }, { "name": "Tuvalu", "code": "TV" }, { "name": "Uganda", "code": "UG" }, { "name": "Ukraine", "code": "UA" }, { "name": "United Arab Emirates", "code": "AE" }, { "name": "United Kingdom", "code": "GB" }, { "name": "United States", "code": "US" }, { "name": "United States Minor Outlying Islands", "code": "UM" }, { "name": "Uruguay", "code": "UY" }, { "name": "Uzbekistan", "code": "UZ" }, { "name": "Vanuatu", "code": "VU" }, { "name": "Venezuela", "code": "VE" }, { "name": "Viet Nam", "code": "VN" }, { "name": "Virgin Islands, British", "code": "VG" }, { "name": "Virgin Islands, U.S.", "code": "VI" }, { "name": "Wallis and Futuna", "code": "WF" }, { "name": "Western Sahara", "code": "EH" }, { "name": "Yemen", "code": "YE" }, { "name": "Zambia", "code": "ZM" }, { "name": "Zimbabwe", "code": "ZW" } ]
Это предоставляет страны, на которые мы ссылаемся в ContactForm.vue
.
Затем мы добавляем наш миксин, чтобы управлять нашими контактами, взаимодействуя с нашей серверной частью. Мы вызываем папку mixins
и создаем в ней файл с именем contactMixin.js
. В файл помещаем:
const axios = require('axios'); const apiUrl = 'http://localhost:3000'; export const contactMixin = { methods: { getContacts() { return axios.get(`${apiUrl}/contacts`); }, addContact(data) { return axios.post(`${apiUrl}/contacts`, data); }, updateContact(data, id) { return axios.put(`${apiUrl}/contacts/${id}`, data); }, deleteContact(id) { return axios.delete(`${apiUrl}/contacts/${id}`); } } }
Это позволит нам включить наши функции в объект methods
объекта компонента, который мы включаем или смешиваем, помещая его в массив mixins
нашего объекта компонента.
Далее мы добавляем наши страницы. Для этого создайте views
папку, если она еще не существует, и добавьте ContactFormPage.vue
. Там положите:
<template> <div class="about"> <ContactForm :edit="false" /> </div> </template> <script> // @ is an alias to /src import ContactForm from "@/components/ContactForm.vue"; export default { name: "ContactFormPage", components: { ContactForm } }; </script>
Это просто отображает ContactForm
компонент, который мы создали. Мы устанавливаем свойство :edit
на false
, чтобы он добавлял наш контакт вместо редактирования.
Мы вложили компонент ContactForm
, включив следующие 2 блока:
import ContactForm from "@/components/ContactForm.vue";
а также
components: { ContactForm }
Здесь мы включаем компонент ContactForm
в наш компонент ContactFormPage
.
Затем мы добавляем нашу домашнюю страницу для отображения списка контактов. В папку views
мы добавляем файл с именем Home.vue
, если он еще не существует. Затем туда мы помещаем:
<template> <div class="home"> <div class="center"> <h1>Address Book Home</h1> </div> <md-table> <md-table-row> <md-table-head md-numeric>ID</md-table-head> <md-table-head>First Name</md-table-head> <md-table-head>Last Name</md-table-head> <md-table-head>Address Line 1</md-table-head> <md-table-head>Address Line 2</md-table-head> <md-table-head>City</md-table-head> <md-table-head>Country</md-table-head> <md-table-head>Postal Code</md-table-head> <md-table-head>Gender</md-table-head> <md-table-head>Age</md-table-head> <md-table-head>Email</md-table-head> <md-table-head></md-table-head> <md-table-head></md-table-head> </md-table-row> <md-table-row v-for="c in contacts" :key="c.id"> <md-table-cell md-numeric>{{c.id}}</md-table-cell> <md-table-cell>{{c.firstName}}</md-table-cell> <md-table-cell>{{c.lastName}}</md-table-cell> <md-table-cell>{{c.addressLineOne}}</md-table-cell> <md-table-cell>{{c.addressLineTwo}}</md-table-cell> <md-table-cell>{{c.city}}</md-table-cell> <md-table-cell>{{c.country}}</md-table-cell> <md-table-cell>{{c.postalCode}}</md-table-cell> <md-table-cell>{{c.gender}}</md-table-cell> <md-table-cell md-numeric>{{c.age}}</md-table-cell> <md-table-cell>{{c.email}}</md-table-cell> <md-table-cell> <md-button class="md-primary" @click="selectedContactId = c.id; showDialog = true">Edit</md-button> </md-table-cell> <md-table-cell> <md-button class="md-accent" @click="removeContact(c.id)">Delete</md-button> </md-table-cell> </md-table-row> </md-table> <md-dialog :md-active.sync="showDialog"> <md-dialog-content> <ContactForm :editing="true" :contactId="selectedContactId" @contactSaved="selectedContactId = undefined; showDialog = false" /> </md-dialog-content> </md-dialog> </div> </template> <script> import { contactMixin } from "@/mixins/contactMixin"; import ContactForm from "@/components/ContactForm.vue"; export default { name: "HomePage", mixins: [contactMixin], components: { ContactForm }, props: { editing: Boolean, id: Number }, computed: { contacts() { return this.$store.state.contacts; } }, data() { return { showDialog: false, selectedContactId: undefined }; }, beforeMount() { this.getAllContacts(); }, methods: { async getAllContacts() { try { const response = await this.getContacts(); this.$store.commit("setContacts", response.data); } catch (ex) { console.log(ex); } }, async removeContact(id) { try { await this.deleteContact(id); await this.getAllContacts(); } catch (ex) { console.log(ex); } } } }; </script> <style scoped> .md-dialog-container { padding: 20px; } .md-content.md-table.md-theme-default { width: 95%; margin: 0 auto; } </style>
Мы получаем наши контакты во время загрузки страницы, вызывая функцию this.getAllContacts
в функции beforeMount
. Обратите внимание, что у нас есть this.getContacts
функция из нашего миксина. Mixins позволяет нам повторно использовать код. Код в нашем mixinx не может иметь то же имя, что и функции в наших methods
объектах в наших компонентах, потому что функции mixin подключаются прямо к нашему methods
, поскольку мы экспортировали объект с полем methods
в нашем коде Mixin.
Обратите внимание, что в приведенном выше коде есть следующее:
props: { editing: Boolean, id: Number }
Здесь мы проверяем, имеет ли переданный реквизит правильный тип. Мы принимаем только логические значения для свойства editing
и числа для id
. Если мы передадим что-нибудь еще, мы получим ошибку.
В App.vue
мы добавляем наше меню и верхнюю панель, помещая следующее:
<template> <div id="app"> <md-toolbar class="md-accent"> <md-button class="md-icon-button" @click="showNavigation = true"> <md-icon>menu</md-icon> </md-button> <h3 class="md-title">Vee Validate Address Book App</h3> </md-toolbar> <md-drawer :md-active.sync="showNavigation" md-swipeable> <md-toolbar class="md-transparent" md-elevation="0"> <span class="md-title">Vee Validate Address Book App</span> </md-toolbar> <md-list> <md-list-item> <router-link to="/"> <span class="md-list-item-text">Home</span> </router-link> </md-list-item> <md-list-item> <router-link to="/contact"> <span class="md-list-item-text">Add Contact</span> </router-link> </md-list-item> </md-list> </md-drawer> <router-view /> </div> </template> <script> export default { name: "app", data: () => { return { showNavigation: false }; } }; </script> <style lang="scss"> .center { text-align: center; } </style>
В main.js
мы добавляем наш шаблонный код, чтобы включить Vue Material и Vee Validate в наше приложение:
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import VueMaterial from 'vue-material' import 'vue-material/dist/vue-material.min.css' import VeeValidate from 'vee-validate'; Vue.use(VeeValidate); Vue.use(VueMaterial); Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')
В router.js
мы добавляем наши маршруты, чтобы мы могли видеть наши страницы:
import Vue from 'vue' import Router from 'vue-router' import HomePage from './views/HomePage.vue' import ContactFormPage from './views/ContactFormPage.vue' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: HomePage }, { path: '/contact', name: 'contact', component: ContactFormPage } ] })
Наконец, в store.js
мы помещаем:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { contacts: [] }, mutations: { setContacts(state, payload) { state.contacts = payload; } }, actions: { } })
для хранения нашего контакта в месте, где все компоненты могут получить доступ. Магазин использует библиотеку Vuex, поэтому у нас есть объект this.$store
для вызова нашей мутации с функцией this.$store.commit
и получения последних данных из хранилища через свойство computed
нашего объекта компонента, например:
contacts() { return this.$store.state.contacts; }
Наконец, в index.html
мы помещаем:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons"> <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css"> <link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default.css"> <title>Address Book App</title> </head> <body> <noscript> <strong>We're sorry but vee-validate-tutorial-app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
чтобы добавить шрифт Roboto и значки материалов в наше приложение.
Теперь мы готовы запустить наш JSON-сервер. Переходим в папку нашего проекта и запускаем json-server — watch db.json
, чтобы запустить сервер. Это позволит нам вызывать эти маршруты без какой-либо конфигурации:
GET /contacts
POST /contacts
PUT /contacts/1
DELETE /contacts/1
Это все необходимые нам маршруты. Данные будут сохранены в db.json
папки, в которой мы находимся, а также в папку проекта нашего приложения.
В итоге получаем следующее:
Проверка типа данных Prop позволяет нам быть уверенными в том, что пропсы, которые мы передали, соответствуют нужному нам типу. Сужает объем нашей отладки при обнаружении ошибок.