I am developing a Vue project with Vuex and Vue 3.
I have a serious problem with my code.
<template>
<div class="px-2 pt-3 text-left">
<div class="flex justify-between items-center">
<h3 class="text-red-700 font-bold text-sm">Texts</h3>
</div>
<div class="mt-4">
<p class="text-sm">Title</p>
<div class="flex items-center border">
<input
v-model.lazy="introSlide.title"
class="
rounded
mb-0
py-2
px-2
focus:outline-none
text-sm
w-full
text-grey-darker
"
placeholder="Enter Title"
/>
</div>
</div>
<div class="mt-4">
<p class="text-sm">Subtitle</p>
<div class="flex items-center border">
<textarea
v-model.lazy="introSlide.subTitle"
class="
rounded
mb-0
py-2
px-2
focus:outline-none
text-sm
w-full
text-grey-darker
min-h-max
h-32
"
placeholder="Presenter Information"
/>
</div>
</div>
<hr />
<div class="flex justify-between items-center mt-6">
<h3 class="text-red-700 font-bold text-sm">Images</h3>
</div>
<div class="mt-4">
<p class="text-sm">Company Logo</p>
<div class="flex items-center border">
<label
class="
flex flex-col
items-center
rounded-lg
tracking-wide
cursor-pointer
border-dashed border-2
w-40
"
:class="introSlide.logo ? '' : 'px-14 py-6'"
>
<input
id="logo_upload"
ref="imgInput"
class="hidden"
type="file"
accept="image/*"
name="logo_upload"
#input="pickFile($event.target.files)"
/>
<img
v-show="introSlide.logo"
:src="`${introSlide.logo}`"
class="rounded-lg w-40 h-24 object-cover"
/>
</label>
<button #click="presentations">testest</button>
</div>
</div>
</div>
</template>
<script>
import introSlideDefault from '#/data/slideContent/introSlide.js'
import { mapMutations } from 'vuex'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { ref, watch, computed } from 'vue'
export default {
setup() {
const store = useStore()
const route = useRoute()
let draftPayload = {
presentationId: route.params.presentationId,
prePopulatedSlide: route.params.slideId,
}
const slideContent = computed(() =>
store.getters['rfps/getslideContentBySlideId'](draftPayload),
)
let introSlide = ref(JSON.parse(JSON.stringify(introSlideDefault)))
const introSlide = computed(() => {
if (slideContent.value == null) {
let introSlideTest = ref({
title: '',
subTitle: '',
logo: '',
})
return introSlideTest
} else {
let draftContent = ref(JSON.parse(slideContent.value.draftSlideContent))
let introSlideTest = {
title: draftContent.value.title,
subTitle: draftContent.value.subTitle,
logo: draftContent.value.logo,
}
return introSlideTest
}
)
watch(
() => introSlide,
(introSlide) => {
store.commit('rfps/setIntroSlide', introSlide.value)
let updateDraftPayload = {
content: JSON.stringify(introSlide.value),
presentationId: route.params.presentationId,
slideId: slideContent.value.slideId,
version: slideContent.value.version,
}
store.dispatch('rfps/updateDraft', updateDraftPayload)
},
{ deep: true },
{ immediate: false },
)
return {
introSlide,
slideContent,
}
},
</script>
If I watch any ref value, For example:
let introSlide = ref({
title: '',
subTitle: '',
logo: '',
})
I can watch for changes with v-model, but if I use a computed value I can't watch it.
I need to get data with getter and to watch it's changes with v-model
you need to be more specify to get the value
const slideContent = computed(() =>
store.getters['rfps/getslideContentBySlideId'](draftPayload).specifyAttribute,
)
Related
I am learning how to test vue components using this combination of technologies, Vue, testing-library and vitest. My component is using vee-validate
TheLogin.vue
<template>
<div class="container mx-auto px-4 h-full">
<div class="flex content-center items-center justify-center h-full">
<div class="w-full lg:w-4/12 px-4">
<VeeForm
v-slot="{ handleSubmit, errors, isSubmitting }"
:validation-schema="schema"
as="div"
>
<form #submit="handleSubmit($event, onSubmit)" method="post">
<div
class="relative flex flex-col min-w-0 break-words w-full mb-6 shadow-lg rounded-lg bg-gray-300 border-0"
>
<div class="rounded-t mb-0 px-6 py-6">
<div class="text-center mb-3">
<h6 class="text-gray-600 text-sm font-bold">Sign in with</h6>
</div>
</div>
<div class="flex-auto px-4 lg:px-10 py-10 pt-0">
<div v-if="loginError" class="text-red-500">{{loginError}}</div>
<div class="text-gray-500 text-center mb-3 font-bold">
<small>Or sign in with credentials</small>
</div>
<div class="relative w-full mb-3">
<label
class="block uppercase text-gray-700 text-xs font-bold mb-2"
for="email"
>Email</label
>
<Field
id="email"
name="email"
placeholder="email"
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
style="transition: all 0.15s ease 0s"
:disabled="isSubmitting"
:class="{ 'border-red-500': errors.email }"
/>
<ErrorMessage class="text-red-500 text-xs" name="email" />
</div>
<div class="relative w-full mb-3">
<label
class="block uppercase text-gray-700 text-xs font-bold mb-2"
for="password"
>Password</label
>
<Field
id="password"
name="password"
type="password"
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
placeholder="Password"
style="transition: all 0.15s ease 0s"
:disabled="isSubmitting"
:class="{ 'border-red-500': errors.password }"
/>
<ErrorMessage
class="text-red-500 text-xs"
name="password"
/>
</div>
<div>
<label class="inline-flex items-center cursor-pointer"
><input
id="customCheckLogin"
type="checkbox"
class="form-checkbox border-0 rounded text-gray-800 ml-1 w-5 h-5"
style="transition: all 0.15s ease 0s"
/><span class="ml-2 text-sm font-semibold text-gray-700"
>Remember me</span
></label
>
</div>
<div class="text-center mt-6">
<button
class="bg-gray-900 text-white active:bg-gray-700 text-sm font-bold uppercase px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 w-full"
type="submit"
style="transition: all 0.15s ease 0s"
:disabled="isSubmitting"
v-text="isSubmitting ? 'Processing' : 'Sign In'"
></button>
</div>
</div>
</div>
</form>
</VeeForm>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import axios from "axios";
import { ErrorMessage, Form as VeeForm, Field } from "vee-validate";
import { ref } from "vue";
import * as yup from "yup"
import { useRouter } from 'vue-router'
const loading = ref(false)
const loginError =ref('')
const router = useRouter()
const schema = yup.object({
email: yup.string().required().min(8).label("Email"),
password: yup.string().required().min(8).label("Password")
});
const onSubmit = async (values, actions) => {
loading.value = true;
const formData = {
email: values.email,
password: values.password
};
axios
.post("backend-url", formData)
.then((response) => {
loading.value = false;
router.push({ name: "products.index" });
})
.catch((error) => {
actions.setErrors(error.response.data.errors);
loginError.value = error.message;
});
};
</script>
And this is my test:
import { describe, it, expect, beforeEach, test } from 'vitest'
import TheLogin from '#/Pages/TheLogin.vue'
import flushPromises from 'flush-promises';
import waitForExpect from 'wait-for-expect';
import {fireEvent, render, screen} from '#testing-library/vue'
describe('TheLogin', () => {
test('renders error message if email is empty', async () => {
const {getAllByRole, getByRole, findByRole } = render(TheLogin);
const button = getByRole('button', { name: /sign in/i })
await fireEvent.click(button)
await flushPromises()
await waitForExpect(() => {
const errorElement = getAllByRole('alert')
//this is better because the error message could change
expect(errorElement[0].textContent).toBeTruthy()
//this also works
//getByText('Email is a required field')
})
})
test('renders error message if password is empty', async () => {
const {getAllByRole, getByPlaceholderText, getByRole } = render(TheLogin)
const button = getByRole('button', { name: /sign in/i })
const emailInput = getByPlaceholderText(/email/i)
await fireEvent.update(emailInput, 'test#gmail.com')
await fireEvent.click(button)
await flushPromises()
await waitForExpect(() => {
const errorElement = getAllByRole('alert')
expect(errorElement[0].textContent).toBeTruthy()
})
})
})
When I delete 1 test, it works fine, but when there are 2 o more tests it fails, the message says this:
TestingLibraryElementError: Found multiple elements with the role
"button" and name /sign in/i
The strange thing is that it looks like it is rendering the component twice, that's why I would like to know how to reset each test, I mean run each test in isolation, thanks.
On the vitest migration guide, it mentions that testing-library won't automatically do DOM cleanup unless you have globals: true in your config. That might be why state is holding over between your tests.
you must put the render in a beforeEach since you are rendering it manually 2 times
beforEach(() => {
render(TheLogin);
})
Using:
test: {
globals: true,
},
in my config was not working because I needed to change this to import defineConfig from vitest/config instead of vite
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config';
import vue from '#vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'#': fileURLToPath(new URL('./src', import.meta.url))
}
},
test: {
globals: true,
},
})
Now it is working
I tried adding a simple rectangle on top of a canvas using Fabric.js in Vue 3
I was able to load rectangle on top of canvas inside a page, where as it doesn't seem to work inside a component.
I tried putting a 2 second timeout as well in component, but it doesn't render anything.
Is there any workaround or am I making any mistake?
<template>
<TransitionRoot as="template" :show="showCanvas">
<Dialog as="div" class="fixed z-10 inset-0" #close="showCanvas = false">
<div class="w-full" style="width: 60%!important; height:auto">
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0"
enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
<DialogOverlay class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</TransitionChild>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"></span>
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div
class="bg-white">
<div>
<div class="mt-3 text-center sm:mt-5">
<!-- This example requires Tailwind CSS v2.0+ -->
<div class="fixed inset-0 overflow-y-auto z-50 flex justify-center items-center">
<div class=" bg-white m-auto rounded-lg shadow-md" role="dialog" aria-modal="true"
aria-labelledby="modal- headline" style="width: 550px !important;height:auto;">
<canvas id="canvas" ></canvas>
</div>
</div>
<div class="flex w-full mx-auto absolute bottom-56 justify-center z-50">
<button type="button"
class="w-50 mr-2 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm"
>
Save
</button>
<button type="button"
class="mt-3 w-50 ml-2 inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm"
#click="closePopup" ref="cancelButtonRef">
Cancel
</button>
</div>
</div>
</div>
</div>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import { fabric } from 'fabric'
import { ref, watch, reactive, onMounted } from 'vue';
import { Dialog, DialogOverlay, DialogTitle, TransitionChild, TransitionRoot } from '#headlessui/vue'
export default {
components:
{
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
},
props: {
showCanvas: {
required: true,
type: Boolean
},
},
setup(props, context) {
reactive(props.showCanvas) // Make the prop reactive so that we can watch for changes
let canvas = undefined
watch(() => props.showCanvas, (selection, prevSelection) => {
if (selection == true) {
bindImage()
}
})
function bindImage()
{
canvas = new fabric.Canvas('canvas');
setTimeout(function(){
var rectangle = new fabric.Rect({
width: 300,
height: 200,
fill: '',
stroke: 'green',
strokeWidth: 5
});
console.log("Rectangle>>>>", rectangle);
// Render the Rect in canvas
canvas.add(rectangle);
}, 2000);
}
function closePopup()
{
context.emit("closeCanvas")
}
return {
closePopup,
}
}
}
</script>
<style>
</style>
You should put them into the onMounted block, for example:
<template>
<canvas width="600" height="600" id="canvas" style="border: 1px solid #ccc;"></canvas>
</template>
<script setup>
import { onMounted } from 'vue'
import { fabric } from 'fabric'
function init() {
let canvas = new fabric.Canvas('canvas') // 实例化fabric,并绑定到canvas元素上
// 圆
let circle = new fabric.Circle({
left: 100,
top: 100,
radius: 50,
})
canvas.add(circle)
}
onMounted(() => {
init()
})
</script>
I have a simple Vue3 application that is making heavy use of TailwindUI components. I'm trying to place a TinyMce editor inside of a slide over component and that works fine. The issue is the entry animation.
On first entry, it slides in like it's supposed to. However, if it is closed and reopened the entry animation is gone. The whole time the exit animation continues to work without issue. Is there a way I can do this and keep the animation intact?
Here is a CodeSandBox with the issue reproduced in it's simplest form.
Here is the relevant code:
App.vue
<template>
<button #click="open = true">Open Menu</button>
<SlideOver :open="open" #close="open = false" />
</template>
<script>
import { ref } from "vue";
import SlideOver from "./components/slide-over.vue";
export default {
name: "App",
components: {
SlideOver,
},
setup() {
const open = ref(false);
return { open };
},
};
</script>
slide-over.vue
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<TransitionRoot as="template" :show="open">
<Dialog
as="div"
static
class="fixed inset-0 overflow-hidden"
#close="$emit('close')"
:open="open"
>
<div class="absolute inset-0 overflow-hidden">
<DialogOverlay class="absolute inset-0" />
<div class="fixed inset-y-0 right-0 pl-10 max-w-full flex">
<TransitionChild
as="template"
enter="transform transition ease-in-out duration-500 sm:duration-700"
enter-from="translate-x-full"
enter-to="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leave-from="translate-x-0"
leave-to="translate-x-full"
>
<div class="w-screen max-w-md">
<div
class="h-full flex flex-col py-6 bg-white shadow-xl overflow-y-scroll"
>
<div class="px-4 sm:px-6">
<div class="flex items-start justify-between">
<DialogTitle class="text-lg font-medium text-gray-900">
Panel title
</DialogTitle>
<div class="ml-3 h-7 flex items-center">
<button
class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
#click="$emit('close')"
>
<span class="sr-only">Close panel</span>
<XIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div class="mt-6 relative flex-1 px-4 sm:px-6">
<TinyMceEditor api-key="no-api-key" />
</div>
</div>
</div>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from "#headlessui/vue";
import { XIcon } from "#heroicons/vue/outline";
import TinyMceEditor from "#tinymce/tinymce-vue";
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
XIcon,
TinyMceEditor,
},
props: {
open: {
type: Boolean,
default: false,
},
},
setup() {},
};
</script>
In my opinion, this is a problem with loading the TinyMce Editor (I don't know exactly what the problem is). I added a delay in loading the editor after opening the modal using watchEffect based on the props open with setTimeout in it and v-if on the TinyMceEditor tag. It may not be a perfect and aesthetic solution, but the animation works smoothly.
Here is a code in codesandbox.io.
And code here: slide-over.vue (App.vue stays the same)
<template>
<TransitionRoot as="template" :show="open">
<Dialog
as="div"
static
class="fixed inset-0 overflow-hidden"
#close="$emit('close')"
:open="open"
>
<div class="absolute inset-0 overflow-hidden">
<DialogOverlay class="absolute inset-0" />
<div class="fixed inset-y-0 right-0 pl-10 max-w-full flex">
<TransitionChild
as="template"
enter="transform transition ease-in-out duration-500 sm:duration-700"
enter-from="translate-x-full"
enter-to="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leave-from="translate-x-0"
leave-to="translate-x-full"
>
<div class="w-screen max-w-md">
<div
class="h-full flex flex-col py-6 bg-white shadow-xl overflow-y-scroll"
>
<div class="px-4 sm:px-6">
<div class="flex items-start justify-between">
<DialogTitle class="text-lg font-medium text-gray-900">
Panel title
</DialogTitle>
<div class="ml-3 h-7 flex items-center">
<button
class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
#click="$emit('close')"
>
<span class="sr-only">Close panel</span>
<XIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div class="mt-6 relative flex-1 px-4 sm:px-6">
<TinyMceEditor v-if="loadEditor" api-key="no-api-key" />
</div>
</div>
</div>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import { ref, watchEffect } from "vue";
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from "#headlessui/vue";
import { XIcon } from "#heroicons/vue/outline";
import TinyMceEditor from "#tinymce/tinymce-vue";
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
XIcon,
TinyMceEditor,
},
props: {
open: {
type: Boolean,
default: false,
},
},
setup(props) {
const loadEditor = ref(false);
watchEffect(() => {
if (props.open) {
setTimeout(function () {
loadEditor.value = true;
}, 10);
} else {
loadEditor.value = false;
}
});
return { loadEditor };
}
};
</script>
I want to access my user details stored in the state when visiting a dynamic (_id.vue) page. I have set the id of the page like this after using the link to the page:
data() {
return {
selectedTutor: null,
id: this.$route.params.id,
}
},
But now I need to define the selectedTutor by searching the id within the data in the vuex state. I was trying to do something like this:
created() {
this.selectedTutor = this.$store.state.tutors.find((tutor) => tutor.id === this.id)
},
But everything stays undefined. So when id equals to the id within the object, that object needs to be set as selectedTutor so I can access all the necessary data to be displayed on the page.
Here you can see the Vuex state
EDIT
_id.vue page
<template>
<div>
<section>
<div>
<h3>
{{ id }}
</h3>
</div>
</section>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'TutorPage',
/* eslint-disable vue/require-prop-types */
layout: 'app',
props: ['id'],
middleware: 'auth',
data() {
return {
selectedTutor: null,
}
},
computed: {
...mapState(['tutors']),
},
created() {
this.selectedTutor = this.$store.state.tutors.find(
(tutor) => tutor.id === this.id
)
},
}
</script>
MainResult Page
<base-grid>
<ul id="tutors" class="grid grid-cols-2 gap-6">
<tutor-item
v-for="tutor in tutors"
:id="tutor.id"
:key="tutor.id"
:name="tutor.attributes.name"
:rate="student.hourlyRate"
:subject="student.subject"
:description="student.biography"
:profile-image="student.imageUrl"
:image-alt="student.imageAlt"
:age="student.age"
:rating="student.rating"
:total-reviews="student.reviewCount"
class="overflow-hidden bg-white border rounded-lg shadow-md"
>
</tutor-item>
</ul>
</base-grid>
Tutor Item (the resultcard)
<template>
<div>
<li>
<div class="flex">
<div class="w-2/3">
<img
class="flex-shrink-0 object-cover w-full h-64 mx-auto bg-gray-200"
:src="profileImage"
:alt="imageAlt"
/>
</div>
<div class="p-6">
<div
class="text-xs font-semibold leading-snug tracking-wide text-gray-500 uppercase"
>
{{ subject }} • {{ age }} jaar
</div>
<NuxtLink :to="'/tutors/' + id">
<h4 class="text-lg font-semibold leading-5 tracking-wide">
{{ name }}
</h4>
</NuxtLink>
<div class="mt-2">
{{ rate }}€
<span class="text-sm text-gray-600">per uur</span>
</div>
<div class="mt-2">
<span class="font-semibold text-light-blue-800"
>{{ rating }}/5 sterren</span
>
<span class="text-sm text-gray-600 truncate">
(na {{ totalReviews }} reviews)
</span>
</div>
<div class="mt-2">{{ description }}</div>
<div>
<div class="mt-4 text-sm font-semibold text-gray-600">
<span>MA</span>
<span
class="inline-block leading-7 text-center text-gray-100 bg-yellow-400 bg-opacity-50 w-7 h-7 rounded-xl"
>DI</span
>
<span>WO</span>
<span
class="inline-block leading-7 text-center text-gray-100 bg-yellow-400 bg-opacity-50 w-7 h-7 rounded-xl"
>DO</span
>
<span
class="inline-block leading-7 text-center text-gray-100 bg-yellow-400 bg-opacity-50 w-7 h-7 rounded-xl"
>VR</span
>
<span>ZA</span>
<span>ZO</span>
</div>
</div>
</div>
</div>
</li>
</div>
</template>
<script>
export default {
/* eslint-disable vue/require-prop-types */
name: 'TutorItem',
props: [
'id',
'firstName',
'lastName',
'name',
'rate',
'subject',
'age',
'rating',
'totalReviews',
'description',
'profileImage',
'imageAlt',
],
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
},
tutorsDetailsLink() {
return this.$route.path + '/' + this.id
},
},
}
</script>
<style></style>
EDIT
Whoops, what a mistake. It was returning the id as a string but I needed a number. This is why it returned undefined. It is solved now! Thanks
I have a vuejs component that displays a modal dialog with a small form inside. When the form is submitted I would like to hide the Modal but cannot figure out how to do it. When submitted the form calls a method in the parent.
Here is the component code
<template>
<div>
<div id="todoModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ title }}</h4>
<button type="button" class="close" data-dismiss="modal">
×
</button>
</div>
<div class="modal-body">
<form id="todoForm" #submit.prevent="$emit('save')">
<div class="form-group">
<label for="name">Todo name</label>
<input
id="name"
v-model="todo.name"
type="text"
class="form-control"
aria-describedby="nameHelp"
placeholder="Enter Todo Name"
/>
<small id="nameHelp" class="form-text text-muted"
>Enter your todo's name</small
>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary mr-4" type="submit" form="todoForm">
<span v-if="mode == 'create'">Create</span>
<span v-if="mode == 'update'">Update</span>
</button>
<button
type="button"
class="btn btn-danger mr-auto"
data-dismiss="modal"
#click="
$parent.showModal = false;
$parent.getTodos();
"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CreateTodo",
props: ["mode", "title", "todo", "showModal"],
ref: "createTodoModal",
mounted() {
if (this.mode == "create") {
this.title = "Create Todo";
}
},
methods: {}
};
</script>
<style scoped></style>
And here is my APP.js file
<template>
<div id="app" class="container mt-5">
<router-view
ref="createtodo"
class="CreateTodo"
name="a"
:todo="todo"
:title="title"
:mode="mode"
:show-modal="showModal"
#save="saveTodo"
></router-view>
</div>
</template>
<script>
import { APIService } from "./APIServices";
const apiService = new APIService();
export default {
name: "App",
data: function() {
return {
mode: "create",
title: "Create Todo",
todo: {},
todos: [],
numberOfTodos: 0,
showModal: false
};
},
mounted: function() {
this.getTodos();
},
methods: {
saveTodo: function() {
if (this.mode == "create") {
apiService.createTodo(this.todo).then(
result => {
if (result.status === 200) {
this.todo = result.data;
this.getTodos();
}
},
error => {}
);
this.showModal = false;
// this.$refs.createtodo.$refs.todoModal
} else if (this.mode == "update") {
apiService.updateTodo(this.todo).then(
result => {
this.getTodos();
},
error => {}
);
this.showModal = false;
} else if (this.mode == "update") {
apiService.updateTodo(this.todo).then(
result => {
this.showModal = false;
this.getTodos();
},
error => {}
);
}
},
}
};
</script>
<style lang="scss">
</style>
I tried using the ref to reference the Modal from APP.js but it does not work.
Add an id to the close X button"
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="close">
<span aria-hidden="true">×</span>
</button>
Then create a method to close the modal:
closeModal() {
document.getElementById('close').click();
},
Like #Dan Stoian reply, you can use ref in vue.js :
<button ref="Close" type="button" data-dismiss="modal" ...>
...
</button>
And in your handler
this.$refs.Close.click();
If you are using boostrap, you need to call the hide an show methods from it, because modal api create html elements on the fly (as the dark background)
I recommend to create a save method instead of call the $emit, where you can call the modal hide method before emit the save signal.
<script>
import $ from 'jquery'
export default {
name: "CreateTodo",
props: ["mode", "title", "todo"],
ref: "createTodoModal",
mounted() {
if (this.mode == "create") {
this.title = "Create Todo";
}
},
methods: {
save() {
$('#ModalId').modal('hide')
this.$emit('save')
}
}
};
</script>
showModal is not needed in this case.
You might use v-if to show/hide modal
In your component:
<div v-if="showModal">
<div id="todoModal" class="modal fade" role="dialog">
...
</div>
</div>
and set showModal to true/false to show/hide component respectively.
Child
<template>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby=""
aria-hidden="true" ref="modalEle">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="exampleModalLabel">{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<slot name="body" />
</div>
<div class="modal-footer">
<slot name="footer"></slot>
<button ref="Close" type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, defineProps, defineExpose } from "vue";
import { Modal } from "bootstrap";
defineProps({
title: {
type: String,
default: "<<Title goes here>>",
},
});
let modalEle = ref(null);
let thisModalObj = null;
onMounted(() => {
thisModalObj = new Modal(modalEle.value);
});
function _show() {
thisModalObj.show();
}
function _close(){
thisModalObj.hide()
}
defineExpose({ show: _show, close: _close });
</script>
Parent
<template>
<Modal title="OMG ITS A MODAL" ref="thisModal">
<template #body>
<div class="col-md-12 mx-auto">
yay modal text
</div>
</template>
<template #footer>
<!-- extra footer stuff !-->
</template>
</Modal>
</template>
<script setup>
import { ref } from 'vue';
import Modal from "../components/ModalCard.vue";
let showModal = ()=>{
thisModal.value.show();
}
let closeModal = ()=>{
thisModal.value.close();
}
</script>
Explanation
So basically what we are doing is exposing a child function to the parent via refs. In the _show and _close functions we are triggering the .show() and .hide() they are bootstrap modal functions we have access too via the thisModalObj. I hope this helps!
Now you can progmatically call thisModal.value.show and thisModal.value.close and itll show and close the modal.
you can use this npm package
npm i vue-js-modal
https://www.npmjs.com/package/vue-js-modal
Your code should then be modified in this way:
<template>
<modal :name="'edit-modal'+id" height="auto">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Edit {{ mName }}</h5>
<button type="button" class="close" #click="hideEditModal(id)">
<span aria-hidden="true">×</span>
</button>
</div>
<form action="/user/update" method="patch" #submit.prevent="updateAssistant">
<div class="modal-body">
<div class="position-relative form-group">
<label for="exampleAddress" class="">Full name</label><input name="name" v-model="editName" id="exampleAddress" placeholder="FullName" type="text" class="form-control">
<div v-if="errors.has('editName')" class="alert alert-danger">
{{ errors.first('editName') }}
</div>
</div>
<div class="position-relative form-group">
<label for="exampleEmail11" class="">Email</label><input name="email" v-model="editEmail" id="exampleEmail11" placeholder="email" type="email" class="form-control">
<div v-if="errors.has('editEmail')" class="alert alert-danger">
{{ errors.first('editEmail') }}
</div>
</div>
<div class="position-relative form-group">
<label for="examplePassword11" class="">Change Password</label><input name="password" v-model="editPassword" id="examplePassword11" placeholder="password" type="password" class="form-control">
<div v-if="errors.has('password')" class="alert alert-danger">
{{ errors.first('password') }}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" #click="hideEditModal(id)">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
<span class="loader" v-if="this.loading" style="position: absolute; bottom: 0.515rem; left: 20px;">
<span class="ball-clip-rotate">
<div style=" border:1px solid #000;"></div>
</span>
</span>
</form>
</modal>
import Errors from '../../Errors.js'
export default {
name: 'AssistantEditModalComponent',
props: [
'mEmail',
'mName',
'id'
],
data () {
return {
editPassword: null,
disabled: false,
errors: Errors,
loading: false
}
},
methods: {
hideEditModal(id) {
this.$modal.hide('edit-modal'+id);
},
setData() {
this.editName = this.mName
this.editEmail = this.mEmail
},
updateAssistant() {
this.disabled = true;
this.loading = true;
const form = {
editName: this.mName,
editEmail: this.mEmail,
password: this.editPassword
}
axios.patch('/user/update/'+this.id, form)
.then((response) => {
this.disabled = false
this.loading = false
this.hideEditModal(this.id)
this.alert(response)
})
.catch((err) => {
this.disabled = false
this.loading = false
Errors.fill(err.response.data.errors)
})
},
alert(response) {
swal(response.data.username, response.data.message, 'success')
},
},
computed: {
editName: {
get: function() {
return this.mName
},
set: function(value) {
this.$emit('update:mName', value);
}
},
editEmail: {
get: function() {
return this.mEmail
},
set: function(value) {
this.$emit('update:mEmail', value);
}
}
}}
If you don't want to add any extra Close button rather than default X button of modal header you could do something like this:
<b-modal
id="user-role"
ref="role-modal"
hide-footer
>
...
</b-modal>
then in your method:
hideModal() {
this.$refs["role-modal"].$refs['close-button'].click()
}