Stopping an animation with an click event in Vuejs? - vue.js

I have a MainMenuItem that has a bunch of static titles. There is one variable item that is for news and events and that has a horizontal ticker, scroll animation on it. This animation is happening inside of the MainMenuItem component, but the click event will be happening from the parent. The click event currently needs to stop the animation and change the font weight (can be seen in the readLink class).
Wth Vue I cannot mutate the prop type itself with something like #click="itemType = ''".
So how can I turn this ticker animation off on click?
Basically, all I would need to do is make the MainMenuItem's itemType = '' and make it the same as every other item. Do I need to make a data type? Or a click event that $emit's an event to the child/base component MainMenuItem?
If there is anymore code that is necessary please let me know!
Cheers!
MainMenuItem.vue (base component with the ticker animation )
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon :name="menuIcon" />
<div v-if="itemType === 'news'" class="_ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._ticker
font-weight: bold
margin-left: 2em
width: 83%
white-space: nowrap
overflow: hidden
position: absolute
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenu.vue (just the location I am using the base component)
<template>
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
itemType="news"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
></MainMenuItem>
</template>
export default {
name: 'MainMenu',
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
}
},
computed: {
readNewsList() {
return this.authUserInfo?.seenNewsPosts || {}
},
}
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>

Since it's a prop the simplest way is to emit an event that executes what you need in the parent and also bind a variable not static prop to be able to change it. example in the child :
<button #click="onClickButton">stop animation </button>
export default {
methods: {
onClickButton (event) {
this.$emit('stopAnimation')
}
}
}
And in the parent you listen to it as so:
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
v-on:stopAnimation="itemType = ''"
></MainMenuItem>
export default {
data() {
return {
itemType: "news",
}
}
}
}

Related

nuxtjs add and remove class on click on elements

I am new in vue and nuxt and here is my code I need to update
<template>
<div class="dashContent">
<div class="dashContent_item dashContent_item--active">
<p class="dashContent_text">123</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">456</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">789</p>
</div>
</div>
</template>
<style lang="scss">
.dashContent {
&_item {
display: flex;
align-items: center;
}
&_text {
color: #8e8f93;
font-size: 14px;
}
}
.dashContent_item--active {
.dashContent_text{
color:#fff;
font-size: 14px;
}
}
</style>
I tried something like this:
<div #click="onClick">
methods: {
onClick () {
document.body.classList.toggle('dashContent_item--active');
},
},
but it changed all elements and I need style change only on element I clicked and remove when click on another
also this code add active class to body not to element I clicked
This is how to get a togglable list of fruits, with a specific class tied to each one of them.
<template>
<section>
<div v-for="(fruit, index) in fruits" :key="fruit.id" #click="toggleEat(index)">
<span :class="{ 'was-eaten': fruit.eaten }">{{ fruit.name }}</span>
</div>
</section>
</template>
<script>
export default {
name: 'ToggleFruits',
data() {
return {
fruits: [
{ id: 1, name: 'banana', eaten: false },
{ id: 2, name: 'apple', eaten: true },
{ id: 3, name: 'watermelon', eaten: false },
],
}
},
methods: {
toggleEat(clickedFruitIndex) {
this.fruits = this.fruits.map((fruit) => ({
...fruit,
eaten: false,
}))
return this.$set(this.fruits, clickedFruitIndex, {
...this.fruits[clickedFruitIndex],
eaten: true,
})
},
},
}
</script>
<style scoped>
.was-eaten {
color: hsl(24, 81.7%, 49.2%);
}
</style>
In Vue2, we need to use this.$set otherwise, the changed element in a specific position of the array will not be detected. More info available in the official documentation.

How to give a dynamically rendered element it's own data value & target it in it's parent component?

I have a BaseMenuItem component that has normal button elements as well as special news elements.
I have added a ticker effect to the news type els and want to stop the ticker on that element when it's clicked. Currently the click event stops the ticker effect on the whole group.
How can I target a single element from that group?
There are two methods, openNews one showing the specific news article that the element is linked to.
And clearItemType that clears the itemType upon recieving the emitted event from the BaseMenuItem component.
I'm just not sure which element to target to change it's itemType.
Does Vuejs have a way to make an unique data value for dynamically generated elements?
If you need anymore information please let me know!
Cheers!
BaseMenuItem
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon v-if="menuIcon" :name="menuIcon" class="text-black mr-md" />
<div #click="$emit('stop-ticker')" v-if="itemType === 'news'" class="ellipsis _ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<slot>
<div class="ml-auto"></div>
<div class="_subtitle mr-md" v-if="subtitle">{{ subtitle }}</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
<ComingSoon v-if="comingSoonShow" />
</slot>
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._subtitle
margin-left: auto
opacity: 0.7
._ticker
position: absolute
font-weight: bold
margin-left: 2em
width: 82%
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenuPage
<template>
<div class="eachMenuGroup" v-if="newsList.length">
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
#stop-ticker="clearItemType"
></MainMenuItem>
</div>
</template>
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>
<script>
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
},
clearItemType() {
this.itemType = ''
return
},
</script>
EDIT: I edited since your latest comment. See below
To target the exact element within your v-for that fired the event, you can use $refs by using an index :
<MainMenuItem v-for="(item, index) in items" :ref="`menuItem--${index}`" #stop-ticker="clearItemType(index)" />
In your method:
clearItemType(index){
console.log(this.$refs[`menuItem--${index}`])
// this is the DOM el that fired the event
}
Edited version after your comments:
If you pass the exact same prop to each el of your v-for you wont be able to modify its value just for one child in the parent context.
Instead, keep an array of distinctive values in your parent. Each child will receive one specific prop that you can change accordingly in the parent either by listening to an child emitted event or by passing index as click event as I've done below for simplicity.
See snippet below
Vue.config.productionTip = false;
const Item = {
name: 'Item',
props: ['name', 'isActive'],
template: `<div>I am clicked {{ isActive }}!</div>`,
};
const App = new Vue({
el: '#root',
components: {
Item
},
data: {
items: ["item1", "item2"],
activeItemTypes: []
},
methods: {
toggle(index) {
this.activeItemTypes = this.activeItemTypes.includes(index) ? this.activeItemTypes.filter(i => i !== index) : [...this.activeItemTypes, index]
}
},
template: `
<div>
<Item v-for="(item, i) in items" :name="item" #click.native="toggle(i)" :isActive="activeItemTypes.includes(i)"/>
</div>
`,
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root"></div>

how to run function when everytime props change in ant-design-vue modal

i have a vue app containing image uploader with crops function. it's succeed to upload and crop the picture and get back the cropped image URL. but the problem is when the user need to re upload their image, the canvas still display the old image. it is also happen when user cancel and reupload the image.
<template>
<div>
<a-row :gutter="16">
<a-col :span="12">
<img ref="image" :src="initialImage">
</a-col>
<a-col :span="12" align="center">
<p><strong>Preview</strong></p>
<img :src="updatedImage" class="preview-image">
</a-col>
</a-row><br />
<a-row :gutter="16">
<a-button type="primary" style="float: right;" #click="crop">Crop</a-button>
<a-button style="float: right; margin-right: 5px;" #click="cancel">Cancel</a-button>
</a-row>
</div>
</template>
<script>
import Cropper from 'cropperjs';
export default {
name: 'PostCropper',
props: {
uploadedImage: String,
},
data() {
return {
cropper: {},
updatedImage: {},
image: {},
initialImage: this.uploadedImage,
};
},
methods: {
crop() {
this.$emit('update-image', this.updatedImage);
},
cancel() {
this.$emit('cancel-upload');
},
},
mounted() {
this.image = this.$refs.image;
this.cropper = new Cropper(this.image, {
zoomable: false,
scalable: false,
aspectRatio: 1,
crop: () => {
const canvas = this.cropper.getCroppedCanvas();
this.updatedImage = canvas.toDataURL('image/png');
},
});
},
};
</script>
<style scoped>
.preview-image {
border-radius: 100px;
width:150px;
height:150px;
}
</style>
i have tried set initialImage and updatedImage with empty string but it's still not work. what should i initialized back after cancel or continue crop the image? or there is another way i could overcome this problem? i've also using vue watcher but it's still not work. hmm.

Repeated data in vue

i`m having a problem with my vue, the problem is im trying to print 2 words, that is 'A.2' and 'B.3', but when im printing it, it just show 'B.3' and 'B.3'. here is my code
this is a simple quiz project, so everytime a user choose option a with true status it should be adding 1 point to the score, i haven`t made that yet.
<template>
<div class="hello">
<h1 v-if="show">hai</h1>
<h1 v-else>hehe</h1>
<p>{{ nama}}</p>
<input type="text" v-model="nama">
<button type="button" v-on:click="hideTitle">Click Me</button>
<h3> 1.Yang Dipakai Di sepatu adalah </h3>
<p>{{ nama}}</p>
<h3 v-for="j in jawaban">
<input type="radio">
{{j}}
</h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data : function() {
return{
nama: 'Luthfi',
show: true
},
{
jawaban: 'A.2',
correct: true
},
{
jawaban: 'B.3',
correct: false
},
{
jawaban: 'C.4',
correct: false
}
},
methods: {
hideTitle() {
this.show = !this.show
}
},
mounted: function () {
this.nama = 'Fitra'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
i expect there is 4 output from option A to D, but it kept showing me same option
In your code, data() returns only one object that contains
{
nama: 'Luthfi',
show: true
}
You must change this like:
data : function() {
return{
nama: 'Luthfi',
show: true,
jawaban: 'A.22',
correct: true,
jawabann: 'B.3'
}
}

How to make the props binding reactive when using 'createElement' function

Vue.config.devtools = false
Vue.config.productionTip = false
let modal = Vue.extend({
template: `
<div class="modal">
<p>Balance: {{ balance }}</p>
<input #input="$emit('input', $event.target.value)" :value="value">
<button #click="$emit('input', balance)">ALL</button>
</div>
`,
props: ['balance', 'value']
})
function makeComponent(data) {
return { render(h) { return h(modal, data) } }
}
Vue.component('app', {
template: `
<div>
<p>Balance: {{ balance }}</p>
<p>To withdraw: {{ withdrawAmount }}</p>
<p>Will remain: {{ balance - withdrawAmount }}</p>
<button #click="onClick">Withdraw</button>
<modal-container ref="container"/>
</div>`,
data () {
return {
withdrawAmount: 0,
balance: 123
}
},
methods: {
onClick () {
this.$refs.container.show(makeComponent({
props: {
balance: String(this.balance),
value: String(this.withdrawAmount)
},
on: {
input: (value) => {
this.withdrawAmount = Number(value)
}
}
}))
}
}
})
Vue.component('modal-container', {
template: `
<div>
<component v-if="canShow" :is="modal"/>
</div>
`,
data () {
return { modal: undefined, canShow: false }
},
methods: {
show (modal) {
this.modal = modal
this.canShow = true
},
hide () {
this.canShow = false
this.modal = undefined
}
}
})
new Vue({
el: '#app'
})
.modal {
background-color: gray;
width: 300px;
height: 100px;
margin: 10px;
padding: 10px;
}
* {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #2c3e50;
line-height: 25px;
font-size: 14px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17-beta.0/vue.js"></script>
<div id="app">
<app></app>
</div>
This is a simplified version of our application using vue-js-modal. The basic Idea is that we pass a component declaration along with required VNodeData to the plugin function and it would append that component to a pre-defined DOM point with proper data bindings.
And our usecase is that:
User clicks 'Withdraw'
Modal pops up with an input field and static field displaying user's balance
User enters the amount he/she wants to withdraw and that amount and resulting balance are displayed in the callee component.
There is a 'ALL' button besides the input field for user to easily enter a number equal to his/her balance
Problem: When user clicks 'ALL', the 'modal' component fires an 'input' event with value of the balance and the callee component receives that event and updates its 'withdrawAmount'. But the 'withdrawAmount' is supposed to be passed back to the 'modal' as 'value' prop and further updates the input field, which doesn't happen.