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