Всплывающие окна и меню - это обычные функции веб-приложений. Его часто используют для того, чтобы дать пользователям возможность добавить функциональность в приложение. Частота его использования привела к тому, что разработчики разработали библиотеки, которые мы можем использовать для добавления всплывающих окон и меню в приложения. Библиотеки пользовательского интерфейса, такие как Bootstrap, имеют встроенные меню. Для настраиваемых меню мы можем добавить его, создав div с кнопкой, которая переключает открытие и закрытие меню.

Когда пользователи щелкают за пределами меню и кнопки, меню закрывается. Для приложений Vue.js у нас есть библиотека V-Click-Outside для обработки кликов вне элемента. Мы можем легко использовать его для добавления всплывающих окон и меню в наши приложения.

В этой статье мы напишем приложение для заметок, которое позволит пользователям делать заметки. Будет таблица для отображения заметок и всплывающее меню в каждой строке, которое позволяет пользователям нажимать кнопку для редактирования или удаления заметки. Чтобы начать сборку проекта, мы запускаем Vue CLI, запустив:

npx @vue/cli create bookmark-app

Когда мастер запускается, мы выбираем «Выбрать функции вручную» и выбираем Babel, препроцессор CSS, Vuex и Vue Router.

Далее мы устанавливаем несколько пакетов. Нам нужно, чтобы Axios выполнял HTTP-запросы к нашей серверной части, Bootstrap-Vue для стилизации, Vee-Validate для проверки формы и V-Click-Outside для обработки состояния фокуса входных данных. Для установки пакетов запускаем npm i axios bootstrap-vue vee-validate v-click-outside. После установки пакетов мы можем приступить к созданию нашего приложения для заметок.

Сначала мы создаем нашу форму, позволяющую пользователям добавлять и редактировать свои счета. В папке components создайте файл с именем NoteForm.vue и добавьте:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group>
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <label>Name</label>
          <b-form-input
            type="text"
            v-model="form.name"
            placeholder="Name"
            name="name"
            :state="errors.length == 0"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group>
        <ValidationProvider name="note" rules="required" v-slot="{ errors }">
          <label>Note</label>
          <b-form-textarea
            type="text"
            :state="errors.length == 0"
            v-model="form.note"
            required
            placeholder="Note"
            name="note"
            rows="5"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "NoteForm",
  mixins: [requestsMixin],
  props: {
    note: Object,
    edit: Boolean
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      if (this.edit) {
        await this.editNote(this.form);
      } else {
        await this.addNote(this.form);
      }
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    note: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

Эта форма позволяет пользователям искать блюда по заданному ключевому слову, затем возвращать список ингредиентов для блюд, а затем пользователь может добавить их в список, удалив дубликаты. Мы используем Vee-Validate для проверки наших входных данных. Мы используем компонент ValidationObserver для наблюдения за достоверностью формы внутри компонента и ValidationProvider для проверки правила проверки введенного значения ввода внутри компонента. Внутри ValidationProvider у нас есть ввод BootstrapVue для полей ввода текста. В b-form-input компонентах. Мы также добавляем проверку Vee-Validate, чтобы убедиться, что пользователи заполнили дату перед отправкой. Мы делаем поля name и note , обязательные для пропуска rules, чтобы пользователям приходилось вводить их все, чтобы сохранить заметку.

Мы проверяем значения в функции onSubmit, запустив this.$refs.observer.validate(). Если это разрешается в true, мы запускаем код для сохранения данных, вызывая функции в блоке if, затем мы вызываем getNotes, чтобы получить примечания. Эти функции взяты из requestsMixin, которые мы добавим. Полученные данные сохраняются в нашем магазине Vuex по вызову this.$store.commit.

В этом компоненте у нас также есть блок watch для отслеживания note value, которое получается из хранилища Vuex, которое мы должны построить. Мы получаем последний список ингредиентов по мере обновления note value, так что последний может быть отредактирован пользователем, когда мы копируем значения в this.form.

Затем мы создаем папку mixins и добавляем requestsMixin.js в папку mixins. В файл добавляем:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getNotes() {
      return axios.get(`${APIURL}/notes`);
    },
    addNote(data) {
      return axios.post(`${APIURL}/notes`, data);
    },
    editNote(data) {
      return axios.put(`${APIURL}/notes/${data.id}`, data);
    },
    deleteNote(id) {
      return axios.delete(`${APIURL}/notes/${id}`);
    }
  }
};

Это функции, которые мы используем в наших компонентах для отправки HTTP-запросов к нашей серверной части для сохранения закладок.

Затем в Home.vue замените существующий код на:

<template>
  <div class="page">
    <h1 class="text-center">Note Taking App</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Note</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>Note</b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="n in notes" :key="n.id">
          <b-td>{{n.name}}</b-td>
          <b-td>{{n.note}}</b-td>
          <b-td>
            <b-button @click="toggleMenu(n.id)" class="menu-button">Menu</b-button>
            <div class="dropdown" v-show="openMenu[n.id]" v-click-outside="onClickOutside">
              <b-list-group>
                <b-list-group-item @click="openEditModal(n)">Edit</b-list-group-item>
                <b-list-group-item @click="deleteOneNote(n.id)">Delete</b-list-group-item>
              </b-list-group>
            </div>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
    <b-modal id="add-modal" title="Add Note" hide-footer>
      <NoteForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></NoteForm>
    </b-modal>
    <b-modal id="edit-modal" title="Edit Note" hide-footer>
      <NoteForm @saved="closeModal()" @cancelled="closeModal()" :edit="true" :note="selectedNote"></NoteForm>
    </b-modal>
  </div>
</template>
<script>
// @ is an alias to /src
import NoteForm from "@/components/NoteForm.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    NoteForm
  },
  mixins: [requestsMixin],
  computed: {
    notes() {
      return this.$store.state.notes;
    }
  },
  beforeMount() {
    this.getAllNotes();
  },
  data() {
    return {
      selectedNote: {},
      openMenu: {}
    };
  },
  methods: {
    toggleMenu(id) {
      this.$set(this.openMenu, id, !this.openMenu[id]);
    },
    onClickOutside(event, el) {
      if (!event.target.className.includes("menu-button")) {
        this.openMenu = {};
      }
    },
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(note) {
      this.$bvModal.show("edit-modal");
      this.selectedNote = note;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedNote = {};
      this.getAllNotes();
    },
    async deleteOneNote(id) {
      await this.deleteNote(id);
      this.getAllNotes();
    },
    async getAllNotes() {
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
    }
  }
};
</script>
<style lang="scss" scoped>
.dropdown {
  position: absolute;
  max-width: 100px;
}
.list-group-item {
  cursor: pointer;
}
</style>

Здесь мы отображаем счета в таблице BootstrapVue. Столбцы содержат имя, сумму и срок выполнения, а также кнопку «Изменить», чтобы открыть модальное окно редактирования, и кнопку «Удалить», чтобы удалить запись при ее нажатии. Мы также добавили кнопку «Добавить счет», чтобы открыть модальное окно, позволяющее пользователям добавлять счет. Заметки получаются из серверной части путем запуска функции this.getAllNotes в ловушке beforeMount, которая хранит данные в нашем хранилище Vuex.

В таблице у нас есть заметки, отображаемые в строках таблицы. В крайнем правом столбце у нас есть меню, которое мы создали с нуля. Мы добавили кнопку Меню в каждую строку и div под ней, чтобы служить контейнером для группы списков, которая содержит наши элементы Изменить и Удалить, чтобы пользователи могли щелкнуть по ним, чтобы изменить и удалить заметку соответственно. Мы переключаем меню с помощью функции toggleMenu, когда пользователь нажимает кнопку Меню. Обратите внимание, что нам нужно вызвать функцию this.$set, чтобы принудительно обновить Vue.js, поскольку мы изменяем запись в объекте. Vue не может автоматически обнаруживать изменения в объекте. Подробнее об этой функции см. Https://vuejs.org/v2/api/#Vue-set.

В разделе styles мы стилизуем всплывающее меню, устанавливая класс dropdown с позицией absolute и устанавливая его max-width на 100 пикселей. Положение absolute заставит его складываться поверх нашей таблицы, прямо под кнопкой для каждой строки.

openAddModal, openEditModal, closeModal открывают модальные окна открытия и закрытия и закрывают модальные окна соответственно. Когда вызывается openEditModal, мы устанавливаем переменную this.selectedNote, чтобы мы могли передать ее нашему NoteForm.

Затем в App.vue мы заменяем существующий код на:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Note Taking App</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
button,
.btn.btn-primary {
  margin-right: 10px !important;
}
.button-toolbar {
  margin-bottom: 10px;
}
</style>

чтобы добавить панель навигации Bootstrap в верхнюю часть наших страниц и router-view для отображения определяемых нами маршрутов. Этот style раздел не имеет области действия, поэтому стили будут применяться глобально. В селекторе .page мы добавляем отступы на наши страницы. Мы добавляем отступы к кнопкам в оставшемся style коде.

Затем в main.js замените существующий код на:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import vClickOutside from "v-click-outside";
Vue.use(BootstrapVue);
Vue.use(vClickOutside);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
extend("required", required);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

Мы добавили сюда все необходимые библиотеки, включая BootstrapVue JavaScript и CSS, а также компоненты Vee-Validate вместе с правилом required validation. Мы также включаем сюда нашу библиотеку V-Click-Outside, чтобы мы могли использовать ее в любом компоненте.

В router.js мы заменяем существующий код на:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

чтобы включить домашнюю страницу в наши маршруты, чтобы пользователи могли ее видеть.

И в store.js мы заменяем существующий код на:

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    notes: []
  },
  mutations: {
    setNotes(state, payload) {
      state.notes = payload;
    }
  },
  actions: {}
});

чтобы добавить наше notes состояние в хранилище, чтобы мы могли наблюдать его в блоке computed компонентов NoteForm и HomePage. У нас есть setNotes функция для обновления notes state, и мы используем ее в компонентах, вызывая this.$store.commit(“setNotes”, data);, как мы делали это в NoteForm и HomePage.

Наконец, в 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" />
    <title>Note Taking App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but v-click-outside-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>

чтобы изменить название нашего приложения.

После всей тяжелой работы мы можем запустить наше приложение, запустив npm run serve.

Чтобы запустить серверную часть, мы сначала устанавливаем пакет json-server, запустив npm i json-server. Затем перейдите в папку нашего проекта и запустите:

json-server --watch db.json

В db.json измените текст на:

{
  "notes": []
}

Итак, у нас есть notes endpoints, определенные в requests.js.

После всей кропотливой работы получаем: