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>
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 am using tailwind ui to create a page using Vue.js - v.2
I've looked over this SO thread and I believe I have my transitions in the correct spot.
When I click the menu to show the <MobileSidebar> component, everything works great. When I close the <MobileSidebar> component, the component is just removed from the screen. I do not see the sidebar "slide" off of the browser.
I'm passing the open state as a prop; emitting the status. That seems to be working fine as well.
How can I allow the transition to render before the element is hidden/removed from view?
App.vue
<template>
<MobileSidebar
:open="sidebarOpen"
#toggle-sidebar="toggleSidebar"
/>
</template>
<script>
...
data: () => ({
sidebarOpen: false,
}),
methods: {
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
},
}
</script>
MobileSidebar.vue
<template>
<div
class="fixed inset-0 flex z-40 lg:hidden"
role="dialog"
aria-modal="true"
v-show="open"
>
<transition
enter-active-class="transition-opacity ease-in-out duration-300"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity ease-in-out duration-300"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<div
class="fixed inset-0 bg-gray-600 bg-opacity-75"
aria-hidden="true"
v-show="open"
></div>
</transition>
<transition
enter-active-class="transition ease-in-out duration-300 transform"
enter-class="-translate-x-full"
enter-to-class="translate-x-0"
leave-active-class="transition ease-in-out duration-300 transform"
leave-class="translate-x-0"
leave-to-class="-translate-x-full"
>
<div
class="relative flex-1 flex flex-col max-w-xs w-full bg-white focus:outline-none"
v-show="open"
>
...
</div>
</transition>
</div>
</template>
<script>
...
methods: {
toggleSidebar() {
this.$emit("toggle-sidebar");
},
},
props: {
open: {
type: Boolean,
required: true,
default: false,
},
},
</script>
The issue is still that I didn't have a <transition> wrapping the outter-most div.
This is working properly:
MobileSidebar.vue
<template>
<transition
enter-active-class="transition-opacity ease-linear duration-300"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity ease-linear duration-300"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<div
class="fixed inset-0 flex z-40 lg:hidden"
role="dialog"
aria-modal="true"
v-show="open"
>
...
</div>
</transition>
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,
)
I'm using this code
var ComponentClass = Vue.extend(Notification);
var instance = new ComponentClass({
propsData: {
notification: notification
}
});
instance.$mount();
this.$refs.notificationContainer.appendChild(instance.$el);
to programmatically create a component when a new notification is received through pusher, this is my notification component
<template>
<transition
enter-active-class="transition ease-out duration-100 transform"
enter-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition ease-in duration-75 transform"
leave-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div class="bg-gray-100 px-4 py-2 border-0 rounded relative mb-4 text-gray-700 flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img
class="h-10 w-10 rounded-full"
:src="notification.user.profile_photo_url"
:alt="notification.user.username"
>
</div>
<span class="inline-block align-middle flex-1 ml-2 mr-4">
<strong>{{notification.user.username}}</strong> {{notification.message}}
</span>
<button class="focus:outline-none">
<i
class="fad fa-times text-red-600"
></i>
</button>
</div>
</transition>
</template>
<script>
export default {
props: {
notification: Object
}
}
</script>
but for some reason the transition doesnt run when the component is created
Fixed by adding appear to the transition element