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

В Vue.js добавлять всплывающие подсказки легко с помощью директивы V-Tooltip, расположенной по адресу https://github.com/Akryum/v-tooltip. Это директива для настраиваемых всплывающих подсказок. Вы можете изменить цвет, текст, задержку отображения и многие другие параметры, связанные с подсказками.

В этой статье мы создадим приложение с рецептами, в котором есть всплывающие подсказки, которые помогут пользователям добавлять рецепты в форму. Пользователи могут ввести название своего блюда, ингредиенты, шаги и загрузить фотографию. Мы создадим приложение на Vue.js

Мы начинаем создавать приложение с запуска Vue CLI. Запускаем его, введя:

npx @vue/cli create recipe-app

Затем выберите «Выбрать функции вручную». Затем мы выбираем в списке Babel, Vue Router, Vuex и CSS Preprocessor. После этого устанавливаем несколько пакетов. Мы установим Axios для выполнения HTTP-запросов к нашей серверной части. BootstrapVue для стилизации, V-Tooltip для всплывающих подсказок и Vee-Validate для проверки формы. Мы устанавливаем пакеты, запустив npm i axios bootstrap-vue v-tooltip vee-validate.

Теперь переходим к созданию компонентов. Создайте файл с именем RecipeForm.vue в папке components и добавьте:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group
        label="Name"
        v-tooltip="{
          content: 'Enter Your Recipe Name Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></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
        label="Ingredients"
        v-tooltip="{
          content: 'Enter Your Recipe Description Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="ingredients" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.ingredients"
            required
            placeholder="Ingredients"
            name="ingredients"
            rows="8"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">Ingredients is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
<b-form-group
        label="Recipe"
        v-tooltip="{
          content: 'Enter Your Recipe Here',
          classes: ['info'],
          targetClasses: ['it-has-a-tooltip'],
        }"
      >
        <ValidationProvider name="recipe" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.recipe"
            required
            placeholder="Recipe"
            name="recipe"
            rows="15"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">Recipe is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
<b-form-group label="Photo">
        <input type="file" style="display: none" ref="file" @change="onChangeFileUpload($event)" />
        <b-button
          @click="$refs.file.click()"
          v-tooltip="{
            content: 'Upload Photo of Your Dish Here',
            classes: ['info'],
            targetClasses: ['it-has-a-tooltip'],
          }"
        >Upload Photo</b-button>
      </b-form-group>
<img ref="photo" :src="form.photo" class="photo" />
<br />
<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: "RecipeForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    recipe: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid || !this.form.photo) {
        return;
      }
if (this.edit) {
        await this.editRecipe(this.form);
      } else {
        await this.addRecipe(this.form);
      }
      const { data } = await this.getRecipes();
      this.$store.commit("setRecipes", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    },
    onChangeFileUpload($event) {
      const file = $event.target.files[0];
      const reader = new FileReader();
      reader.onload = () => {
        this.$refs.photo.src = reader.result;
        this.form.photo = reader.result;
      };
      reader.readAsDataURL(file);
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    recipe: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>
<style>
.photo {
  width: 100%;
  margin-bottom: 10px;
}
</style>

В этом файле у нас есть форма, позволяющая пользователям вводить свой рецепт. У нас есть текстовые поля и файл для загрузки файлов, чтобы пользователи могли загружать фотографии. Мы используем Vee-Validate для проверки наших входных данных. Мы используем компонент ValidationObserver для наблюдения за действительностью формы внутри компонента и ValidationProvider для проверки правила проверки введенного значения ввода внутри компонента. Внутри ValidationProvider у нас есть ввод BootstrapVue для полей ввода текста.

Каждое поле формы имеет всплывающую подсказку с дополнительными инструкциями. Директива v-tooltip предоставляется библиотекой V-Tooltip. Мы устанавливаем здесь содержимое всплывающей подсказки и классы, и мы можем установить другие параметры, такие как задержка отображения, положение и цвет фона всплывающей подсказки. Полный список опций доступен на https://github.com/Akryum/v-tooltip.

Загрузка фотографий работает, позволяя пользователям открывать диалоговое окно загрузки файла с помощью кнопки «Загрузить фотографию». При нажатии кнопки «Загрузить фото» кнопка щелкает по входу скрытого файла. После того, как пользователь выбирает файл, вызывается функция onChangeFileUpload. В этой функции у нас есть объект FileReader, который устанавливает атрибут src тега img для отображения загруженного изображения, а также поле this.form.photo. readAsDataUrl считывает изображение в строку, чтобы мы могли отправить его без дополнительных усилий.

Эта форма также используется для редактирования рецептов, поэтому у нас есть блок watch для наблюдения за атрибутом recipe, который мы передадим этому компоненту, когда есть что-то для редактирования.

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

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getRecipes() {
      return axios.get(`${APIURL}/recipes`);
    },
addRecipe(data) {
      return axios.post(`${APIURL}/recipes`, data);
    },
editRecipe(data) {
      return axios.put(`${APIURL}/recipes/${data.id}`, data);
    },
deleteRecipe(id) {
      return axios.delete(`${APIURL}/recipes/${id}`);
    }
  }
};

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

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

<template>
  <div class="page">
    <h1 class="text-center">Recipes</h1>
    <b-button-toolbar class="button-toolbar">
      <b-button @click="openAddModal()" variant="primary">Add Recipe</b-button>
    </b-button-toolbar>
<b-card
      v-for="r in recipes"
      :key="r.id"
      :title="r.name"
      :img-src="r.photo"
      img-alt="Image"
      img-top
      tag="article"
      class="recipe-card"
      img-bottom
    >
      <b-card-text>
        <h1>Ingredients</h1>
        <div class="wrap">{{r.ingredients}}</div>
      </b-card-text>
<b-card-text>
        <h1>Recipe</h1>
        <div class="wrap">{{r.recipe}}</div>
      </b-card-text>
<b-button @click="openEditModal(r)" variant="primary">Edit</b-button>
<b-button @click="deleteOneRecipe(r.id)"  variant="danger">Delete</b-button>
    </b-card>
<b-modal id="add-modal" title="Add Recipe" hide-footer>
      <RecipeForm @saved="closeModal()" @cancelled="closeModal()" :edit="false" />
    </b-modal>
<b-modal id="edit-modal" title="Edit Recipe" hide-footer>
      <RecipeForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :recipe="selectedRecipe"
      />
    </b-modal>
  </div>
</template>
<script>
// @ is an alias to /src
import RecipeForm from "@/components/RecipeForm.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    RecipeForm
  },
  mixins: [requestsMixin],
  computed: {
    recipes() {
      return this.$store.state.recipes;
    }
  },
  beforeMount() {
    this.getAllRecipes();
  },
  data() {
    return {
      selectedRecipe: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(recipe) {
      this.$bvModal.show("edit-modal");
      this.selectedRecipe = recipe;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedRecipe = {};
    },
    async deleteOneRecipe(id) {
      await this.deleteRecipe(id);
      this.getAllRecipes();
    },
    async getAllRecipes() {
      const { data } = await this.getRecipes();
      this.$store.commit("setRecipes", data);
    }
  }
};
</script>
<style scoped>
.recipe-card {
  width: 95vw;
  margin: 0 auto;
  max-width: 700px;
}
.wrap {
  white-space: pre-wrap;
}
</style>

В этом файле у нас есть список карточек BootstrapVue для отображения списка записей рецептов и предоставления пользователям возможности открывать и закрывать модальные окна добавления и редактирования. На каждой карточке есть кнопки, позволяющие пользователям редактировать или удалять каждую запись. На каждой карточке внизу есть изображение рецепта, которое было загружено при вводе рецепта.

В разделе scripts у нас есть ловушка beforeMount для получения всех паролей во время загрузки страницы с помощью функции getRecipes, которую мы написали в нашем миксине. При нажатии кнопки «Редактировать» устанавливается переменная selectedRecipe, и мы передаем ее в RecipeForm для редактирования.

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

CSS в классе wrap предназначен для отображения символов разрыва строки как разрыва строки.

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

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Recipes 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;
  margin: 0 auto;
  max-width: 700px;
}
button {
  margin-right: 10px !important;
}
.button-toolbar {
  margin-bottom: 10px;
}
.tooltip {
  display: block !important;
  z-index: 10000;
.tooltip-inner {
    background: black;
    color: white;
    border-radius: 16px;
    padding: 5px 10px 4px;
  }
.tooltip-arrow {
    width: 0;
    height: 0;
    border-style: solid;
    position: absolute;
    margin: 5px;
    border-color: black;
  }
&[x-placement^="top"] {
    margin-bottom: 5px;
.tooltip-arrow {
      border-width: 5px 5px 0 5px;
      border-left-color: transparent !important;
      border-right-color: transparent !important;
      border-bottom-color: transparent !important;
      bottom: -5px;
      left: calc(50% - 5px);
      margin-top: 0;
      margin-bottom: 0;
    }
  }
&[x-placement^="bottom"] {
    margin-top: 5px;
.tooltip-arrow {
      border-width: 0 5px 5px 5px;
      border-left-color: transparent !important;
      border-right-color: transparent !important;
      border-top-color: transparent !important;
      top: -5px;
      left: calc(50% - 5px);
      margin-top: 0;
      margin-bottom: 0;
    }
  }
&[x-placement^="right"] {
    margin-left: 5px;
.tooltip-arrow {
      border-width: 5px 5px 5px 0;
      border-left-color: transparent !important;
      border-top-color: transparent !important;
      border-bottom-color: transparent !important;
      left: -5px;
      top: calc(50% - 5px);
      margin-left: 0;
      margin-right: 0;
    }
  }
&[x-placement^="left"] {
    margin-right: 5px;
.tooltip-arrow {
      border-width: 5px 0 5px 5px;
      border-top-color: transparent !important;
      border-right-color: transparent !important;
      border-bottom-color: transparent !important;
      right: -5px;
      top: calc(50% - 5px);
      margin-left: 0;
      margin-right: 0;
    }
  }
&[aria-hidden="true"] {
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.15s, visibility 0.15s;
  }
&[aria-hidden="false"] {
    visibility: visible;
    opacity: 1;
    transition: opacity 0.15s;
  }
}
</style>

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

Затем в 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 VTooltip from "v-tooltip";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.use(VTooltip);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

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

В 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: {
    recipes: []
  },
  mutations: {
    setRecipes(state, payload) {
      state.recipes = payload;
    }
  },
  actions: {}
});

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

Наконец, в 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>Recipe App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-tooltip-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 измените текст на:

{
  "recipes": [
  ]
}

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

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