Всплывающие подсказки являются общими для подсказок о том, как использовать разные части веб-приложения. Его легко добавить, и это помогает пользователям лучше понять приложение. Они также полезны для отображения длинного текста, который был бы слишком длинным.
В 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
.
После всей кропотливой работы получаем: