TailwindCSS animation doesn't work with v-for - javascript

I'm trying to create a list of pop messages in the right top part of the screen. On click this message goes away. Each popup message is a part of slide-fade Transition.
When I have only one popup component, the animation works, however with more than 1 the animation doesn't work. I would really appreciate if anyone can help me out with this problem. Thanks in advance.
Here's the code:
popup.vue
<template>
<Transition name="slide-fade" #click="close">
<div class="bg-green-100 opacity-95 rounded-lg py-5 px-6 mb-3 text-green-700 inline-flex items-center text-sm" role="alert">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="w-8 h-8 mr-2 fill-current" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path>
</svg>
{{ placeholder }}
</div>
</Transition>
</template>
<script>
export default {
name: 'popupComponent',
methods: {
close () {
this.$emit('close', this.arrIndex)
}
},
props: {
placeholder: String,
arrIndex: Number
}
}
</script>
<style scoped>
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(.25,.59,.63,.92);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
</style>
App.vue:
<template>
<div class="absolute z-10 right-0 p-2 max-w-md">
<div class="flex flex-col gap-y-2 max-h-96 overflow-y-hidden">
<popup v-for="(content, index) in popup_content.slice().reverse()" :key="index" #close="closePopup" :arrIndex="(popup_status.length-1) - index" :placeholder="content"/>
</div>
</div>
</template>
<script>
import popupComponent from './components/popup.vue'
export default {
data() {
return {
popup_content: ['Awesome!', 'Nice!'],
}
},
methods: {
closePopup(index) {
this.popup_content.splice(index, 1);
}
},
components: {
'popup': popupComponent
},
name: 'App'
}
</script>

This might be because you use an index as a key.
You should not use index as a key of the v-for in your case, because you are altering the array. Use a unique, non-changing identifier, like an id or in your code example the content:
<popup v-for="(content, index) in popup_content.(...)" :key="content" ...>
Vue relies on the key to track changes in the DOM. With your close function, you delete an item from the array, e.g. if you close the first message, this causes the second message getting index 0, instead of 1. This can cause the strange behaviour you are experiencing. See docs.
In your case it might be better to generate a unique id for each popup message when it is created. Your popup content then would be something like:
popup_content: [
{
id: 'e5803eab...',
text: 'Awesome!'
},
{
id: '8396...',
text: 'Nice!'
}
],
In your for loop you can use the id as a key:
<popup v-for="(popup, index) in popup_content.(...)" :key="popup.id" :placeholder="popup.text" ...>
And you can still use the index for other purposes.

Related

VueJS - Click event is not triggered as parent event prevaults

I am building a search input that fetchs data from my API and lists it in a dropdown list.
Here is the behavior I want my component to have:
If I start typing and my API founds data, it opens the dropdown menu and lists it.
If I click on one of the elements from the list, it is set as 'activeItem' and the dropdown list closes
Else, I can click out of the component (input and dropdown list) and the dropdown list closes
Else, no dropdown list appears and my input works like a regular text input
My issue has to do with Event Bubbling.
My list items (from API) have a #click input that set the clicked element as the 'activeItem'.
My input has both #focusin and #focusout events, that allow me to display or hide the dropdown list.
I can't click the elements in the dropdown list as the #focusout event from the input is being triggered first and closes the list.
import ...
export default {
components: {
...
},
props: {
...
},
data() {
return {
results: [],
activeItem: null,
isFocus: false,
}
},
watch: {
modelValue: _.debounce(function (newSearchText) {
... API Call
}, 350)
},
computed: {
computedLabel() {
return this.required ? this.label + '<span class="text-primary-600 font-bold ml-1">*</span>' : this.label;
},
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
},
methods: {
setActiveItem(item) {
this.activeItem = item;
this.$emit('selectItem', this.activeItem);
},
resetActiveItem() {
this.activeItem = null;
this.isFocus = false;
this.results = [];
this.$emit('selectItem', null);
},
},
emits: [
'selectItem',
'update:modelValue',
],
}
</script>
<template>
<div class="relative">
<label
v-if="label.length"
class="block text-tiny font-bold tracking-wide font-medium text-black/75 mb-1 uppercase"
v-html="computedLabel"
></label>
<div :class="widthCssClass">
<div class="relative" v-if="!activeItem">
<div class="flex items-center text-secondary-800">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5 ml-4 absolute"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<!-- The input that triggers the API call -->
<input
class="text-black py-2.5 pr-3.5 pl-10 text-black focus:ring-primary-800 focus:border-primary-800 block w-full rounded sm:text-sm border-gray-300"
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="isFocus = false"
>
</div>
<!-- The Dropdown list -->
<Card
class="rounded-t-none shadow-2xl absolute w-full z-10 mt-1 overflow-y-auto max-h-48 px-0 py-0"
v-if="isFocus && results.length"
>
<div class="flow-root">
<ul role="list" class="divide-y divide-gray-200">
<!-- API results are displayed here -->
<li
v-for="(result, index) in results"
:key="index"
#click="setActiveItem(result)" <!-- The event I can't trigger -->
>
<div class="flex items-center space-x-4 cursor-pointer px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="result.image ?? this.$page.props.page.defaultImage.url"
:alt="result.title"
/>
</div>
<div class="min-w-0 flex-1">
<p
class="truncate text-sm font-medium text-black"
:class="{
'text-primary-900 font-bold': result.id === activeItem?.id
}"
>
{{ result.title }}
</p>
<p class="truncate text-sm text-black/75">
{{ result.description }}
</p>
</div>
<div v-if="result.action">
<Link
:href="result.action?.url"
class="inline-flex items-center rounded-full border border-gray-300 bg-white px-2.5 py-0.5 text-sm font-medium leading-5 text-black/75 shadow-sm hover:bg-primary-50"
target="_blank"
>
{{ result.action?.text }}
</Link>
</div>
</div>
</li>
</ul>
</div>
</Card>
</div>
<!-- Display the active element, can be ignored for this example -->
<div v-else>
<article class="bg-primary-50 border-2 border-primary-800 rounded-md">
<div class="flex items-center space-x-4 px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="activeItem.image ?? this.$page.props.page.defaultImage.url"
:alt="activeItem.title"
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-black font-bold">
{{ activeItem.title }}
</p>
<p class="truncate text-sm text-black/75 whitespace-pre-wrap">
{{ activeItem.description }}
</p>
</div>
<div class="flex">
<AppButton #click.stop="resetActiveItem();" #focusout.stop>
<svg
class="w-5 h-5 text-primary-800"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</AppButton>
</div>
</div>
</article>
</div>
</div>
</div>
</template>
Here is a look at the input:
With API results (can't click the elements):
When no data is found:
I tried:
handleFocusOut(e) {
console.log(e.relatedTarget, e.target, e.currentTarget)
// No matter where I click:
// e.relatedTarget = null
// e.target = <input id="search" class="...
// e.currentTarget = <input id="search" class="...
}
...
<input
id="search"
class="..."
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="handleFocusOut($event)"
>
The solution:
relatedTarget will be null if the element you click on is not
focusable. by adding the tabindex attribute it should make the element
focusable and allow it to be set as relatedTarget. if you actually
happen to be clicking on some container or overlay element make sure
the element being clicked on has that tabindex="0" added to it so you
can maintain isFocus = true
Thanks to #yoduh for the solution
The root issue looks to be how the dropdown list is being removed from the DOM as soon as the input loses focus because of the v-if on it.
<Card
v-if="isFocus && results.length"
>
This is ok to have, but you'll need to work around it by coming up with a solution that keeps isFocus true whether the focus is on the input or the dropdown. I would suggest your input's #focusout to execute a method that only sets isFocus = false if the focus event's relatedTarget is not any of the dropdown items (can be determined via classname or other attribute). One roadblock to implementing this is that some elements aren't natively focusable, like <li> items, so they won't be set as the relatedTarget, but you can make them focusable by adding the tabindex attribute. Putting it all together should look something like this:
<input
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="loseFocus($event)"
/>
...
<li
v-for="(result, index) in results"
:key="index"
class="listResult"
tabindex="0"
#click="setActiveItem(result)"
>
loseFocus(event) {
if (event.relatedTarget?.className !== 'listResult') {
this.isFocus = false;
}
}
setActiveItem(item) {
this.activeItem = item;
this.isFocus = false;
this.$emit('selectItem', this.activeItem);
}

Div appear and disappear onclick in React hooks

I want an effect like this (before onclick)(after oncilck).Just click on which div to make it disappear, and then let the div that disappeared before show again.
Here is the data code(just an array). I want the "show" variable in the data to control whether to display.But I don't know how to implement the destructuring assignment to the array in the click function.
Thank you for your answer!
const leftBarData=
[{
name:"关于音乐",
color:"bg-music-color",
icon:<div className="iconMusicSize">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
blahblah
</svg>
</div>,
link:"/music",
number:0,
show:true,
},
{
name:"关于编程",
color:"bg-code-color",
icon:<div className="iconSize">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
blah
</svg>
</div>,
link:"/code",
number:1,
show:true,
},
{
name:"关于设计",
color:"bg-design-color",
icon:<div className="iconSize">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
blah
</svg>
</div>,
link:"/design",
number:2,
show:false
},
{
name:"关于本人",
color:"bg-about-color",
icon:<div className="iconSize">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
blah
</svg>
</div>,
link:"/design",
number:3,
show:true
}
]
Here is the map.
const [statusData,setStatusData]=useState(leftBarData)
const changeBar=(pars)=>
{
how to achieve it
}
<div className="flex fixed">
{statusData?.map((index,i)=>(
<Link to={index.link}>
<div className={` ${index.color} ${index.show?"":"hidden"} w-99 h-screen pt-9 `} onClick={()=>changeBar(index)}>
<div className="flex flex-col h-full items-center justify-between">
{index.icon}
<div className="-rotate-90 -translate-y-full mb-10">
<div className="h3 text-white whitespace-nowrap">{index.name}</div>
</div>
</div>
</div>
</Link>
))}
</div>
You can map() your state data to toggle show property of clicked element like this:
const changeBar = pars => {
setStatusData(data => data.map(item => (
item === pars
? {
...item,
show: !item.show
}
: item
)));
};

Create click event on newly appended buttons with JQuery

I want to create alerts that can be hidden, but when I hide one, they are all hidden at the same time.
This is the code I have, where I need to create the "click" event to hide only the newly created button but hide all of them at the same time:
const newAlert = $("#mainBodyContainer").append(`
<div class="alert alert-info shadow-lg p-2 mt-2">
<p>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="stroke-current flex-shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>a</span>
</p>
<strong class="text-xl align-center cursor-pointer alert-del">×</strong>
</div>
`);
const deleteAlertButton = newAlert.find(".alert-del")
deleteAlertButton.on("click", () => {
deleteAlertButton.parent().addClass("hidden");
});
Docs jQuery find: "Get the descendants of each element in the current set of matched elements"
Keyword: elementS.
You don't have a unique selector. Every time you create a new button, your event handler returns ALL .alert-del classes it finds inside #mainBodyContainer. Including the ones already in there.
To solve the issue, make sure each button can be uniquely selected:
let alerts = 1;
function createAlert()
{
const newAlert = $("#mainBodyContainer").append(`
<div class="alert alert-info shadow-lg p-2 mt-2">
<p>
<span>Button ${alerts}</span>
</p>
<strong id="alert_${alerts}" class="text-xl align-center cursor-pointer alert-del">×</strong>
</div>
`);
const deleteAlertButton = $("#alert_"+alerts);
deleteAlertButton.on("click", () => {
deleteAlertButton.parent().addClass("hidden");
});
alerts++;
}
createAlert();
createAlert();
createAlert();
.hidden {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="mainBodyContainer"></div>

vuei18n => translate array in setup() is making the <template/> translations to need an extra step/load to render, how can i solve it?

sorry for the "extra step/load" part, I know It's pretty vague and definitely a poor word choice. I will show you with what I mean with the code and 2 images.
1st:
code:
<template>
<div class="bg-white">
<h1
class="text-4xl font-semibold my-10 text-center capitalize text-black dark:text-white"
>
{{ t('FAQ.title') }}
</h1>
<div class="space-y-4 mx-auto my-4 max-w-screen-lg">
<details class="group" v-for="faq in faqs" :key="faq.question">
<summary
class="flex items-center list-none text-left justify-between p-4 rounded-lg cursor-pointer"
>
<h5 class="font-medium text-black">{{ faq.question }}</h5>
<svg
class="flex-shrink-0 ml-1.5 w-5 h-5 transition duration-500 group-open:-rotate-180"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</summary>
<ul v-if="faq.list">
<li
class="px-4 my-2 ml-4 leading-relaxed text-secondary"
v-for="answer in faq.answer"
:key="answer"
>
{{ answer }}
</li>
</ul>
<p class="px-4 my-2 ml-4 leading-relaxed text-secondary" v-else>
{{ faq.answer }}
</p>
</details>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'FAQ',
setup() {
const { t } = useI18n()
const faqs = [
{
question: t('FAQ.questions[0]'),
answer: t('FAQ.answers.answer1')
},
{
question: t('FAQ.questions[1]'),
answer: [
t('FAQ.answers.answer2[0]'),
t('FAQ.answers.answer2[1]'),
t('FAQ.answers.answer2[2]'),
t('FAQ.answers.answer2[3]'),
t('FAQ.answers.answer2[4]')
],
list: true
},
{
question: t('FAQ.questions[2]'),
answer: t('FAQ.answers.answer3')
},
{
question: t('FAQ.questions[3]'),
answer: t('FAQ.answers.answer4')
},
{
question: t('FAQ.questions[4]'),
answer: t('FAQ.answers.answer5')
},
{
question: t('FAQ.questions[5]'),
answer: t('FAQ.answers.answer6')
},
{
question: t('FAQ.questions[6]'),
answer: t('FAQ.answers.answer7')
},
{
question: t('FAQ.questions[7]'),
answer: t('FAQ.answers.answer8')
},
{
question: t('FAQ.questions[8]'),
answer: t('FAQ.answers.answer9')
},
{
question: t('FAQ.questions[9]'),
answer: t('FAQ.answers.answer10')
},
{
question: t('FAQ.questions[10]'),
answer: t('FAQ.answers.answer11')
},
{
question: t('FAQ.questions[11]'),
answer: t('FAQ.answers.answer12')
}
]
return { faqs, t }
}
})
</script>
As you can see, the is the only one with t() within the template.
The problem is that to render all the rest of the elements, I have to change the flag and go to another section of the page, and then come back, that's the extra step that i mean.
There's anyway I can solve this?
To make the rest of the questions and answers change along with the title, there are two ways you could fix it.
The first way would be to translate the questions and answers in the template like this:
{{ t(faq.question) }}
The second way would be to make faqs a computed property so that it would update whenever t changes.

How do I dynamically show a mobile menu with Tailwind and Vue.js?

I'm trying to build a responsive menu with Tailwind CSS and Vue.js. Currently I have this template:
<template>
<nav class="flex items-center justify-between flex-wrap bg-pink-100 p-6">
<div class="flex items-center flex-shrink-0 mr-6">
<span class="font-semibold text-xl tracking-tight">Pixie</span>
</div>
<div class="block md:hidden" >
<button #click='clickMenu' class="flex items-center px-3 py-2 border rounded" >
<svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
</button>
</div>
<div class="w-full flex-grow md:flex md:items-center md:w-auto" v-if="menuVisible">
<div class="text-sm md:flex-grow">
<a href="#responsive-header" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4">
Features
</a>
<a href="#responsive-header" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4">
Pricing
</a>
<a href="#responsive-header" class="block mt-4 md:inline-block md:mt-0 hover:text-white">
Blog
</a>
</div>
<div>
Sign Up
Log In
</div>
</div>
</nav>
</template>
With this Javascript:
<script>
export default {
data: function() {
return {
menuVisible: true
}
},
methods: {
clickMenu: function() {
this.menuVisible = !this.menuVisible
}
}
}
</script>
All I want to do is initially hide the mobile menu when the breakpoint reaches 'sm' on Tailwind. This would mean the user would have to click the menu button to see the menu, which I think is the expected behavior on mobile devices.
I don't want to build 2 separate menus which get shown on different breakpoints as I want to avoid duplicating code. Is there a way to access the current breakpoint for Tailwind in Vue.js? This would mean I could set the menuVisible to a computed property which only allows it to be visible if the breakpoint is desktop or tablet, or if the user has clicked the menu.
Or is there another better way to do this?
Thanks for any help!
you can write a plugin for it in you app and import it, im using nuxt and this worked for me
export default (context, inject) => {
const burger = () => {
const menu = document.querySelector("#menu");
if (menu.classList.contains("hidden")) {
menu.classList.remove("hidden");
} else {
menu.classList.add("hidden");
}
};
inject("burger", burger);
context.$burger = burger;
};
One way of achieving this could be to configure the TailwindCSS-breakpoints in your tailwind.config.js and to then reuse that file to import the breakpoint-values into your Menu-component.
Here we are setting TailwindCSS breakpoints according to the TailwindCSS documentation. We are actually just setting the default TailwindCSS breakpoint values, but setting them makes them accessible via the file.
//tailwind.config.js
module.exports = {
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px'
}
}
}
Now, in your Menu.vue, you can import the breakpoint from your TailwindCSS-config and write the necessary function, to check if the current window-size is smaller than the md-breakpoint. If it's not, you can simply return true. If it is, you can check, if the menu was toggled open.
// Menu.vue
<script>
const tailwindConfig = require('tailwind.config.js')
export default {
data() {
return {
windowWidth: 0,
menuOpen: false,
mdBreakpoint: Number(tailwindConfig.theme.screens.md.replace('px', ''))
}
},
computed: {
menuVisible() {
return this.windowWidth > mdBreakpoint ? true : this.menuOpen
}
},
methods: {
updateWindowSize() {
this.windowWidth = window.innerWidth
},
clickMenu() {
this.menuOpen = !this.menuOpen
}
},
mounted() {
this.updateWindowSize()
window.addEventListener('resize', this.updateWindowSize)
},
beforeDestroyed() {
window.removeEventListener('resize', this.updateWindowSize)
}
}
</script>

Categories

Resources