v-on click, add handler only if condition has been met - javascript

After some research the following suggestion by Mr. Evan You was found:
https://github.com/vuejs/vue/issues/7349#issuecomment-354937350
So without any hesitation I gave it a try:
Component template
<template>
<div v-on='{ click: dataType === `section` ? toggleSectionElements : null }'>
... magic
</div>
<template>
JS Logic
<script>
export default {
name: `product-section`,
props: [`section`, `sectionName`, `depth`],
methods: {
toggleSectionElements() {
... magic
}
},
computed: {
dataType() {
if (this.$props.section.sections || this.$props.depth === 0) {
return `section`
} else {
return `element`
}
}
}
}
</script>
But for described case it results in error during rendering:
[Vue warn]: Invalid handler for event "click": got null
Can someone please suggest what has been done wrong? :thinking:
Update
The way Data Model looks like:
DataModel: {
mainSectionA: {
sections: {
sectionA: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
values: { ... }
}
sectionB: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
},
values: { ... }
},
mainSectionB: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } },
elementC: { values: { ... } },
... elements
},
values: { ... }
}
}

Just change it to the below and it will work
v-on="condition ? { mouseover: handler } : {}"
or, if your handler is called mouseover
v-on="condition ? { mouseover } : {}"

Instead of polluting your template with ternary logic, you should actually perform the check inside the click handler instead. It not only makes your template more readable, but also makes maintaining the code easier since all logic has been abstracted and delegated to the event handler's callback instead.
Quick solution
Therefore the quick solution is to actually ensure that the toggleSectionElements() will only work when a correct dataType is present. This can be achieved by using a guard clause:
toggleSectionElements() {
// Guard clause to prevent further code execution
if (this.dataType() !== 'section')
return;
// Magic here
}
Even better, is that if separate handlers should be assigned to each dataType: you can then create a factory function for that purpose:
methods: {
// This is just a factory function
toggleElements() {
switch (this.dataType()) {
case 'section':
return this.toggleSectionElements;
case 'element':
// Something else...
}
},
toggleSectionElements() {
// Magic for section element
}
}
Suggestion: using atomic components
Since it might be costly to bind click event handlers to elements that end up doing nothing, you can also break down your component to be more atomic. The collection element will be responsible of receiving an array of "section" or "element", and each "section"/"element" will have its own component, something like this:
You have a collection component, say <my-collection>, that holds all "section" and "element" components
"section" component will use the <my-section> component
"element" component will use the <my-element> component
This is when VueJS becomes really powerful: you can use dynamic component inside <my-collection> to determine which component to use depending on the dataType encountered.
This is done by running a v-for through the collection, and then using v-bind:is="..." to determine whether a specific collection item should be using "section" or "element". I understand that this is probably going to go out of scope of your original question, but it's a worthwhile design to consider:
const collectionComponent = Vue.component('my-collection', {
template: '#my-collection-component',
data: function() {
return {
collection: [{
dataType: 'section',
description: 'Hello I am section 1'
}, {
dataType: 'element',
description: 'Hello I am element 1'
}, {
dataType: 'section',
description: 'Hello I am section 2'
}, {
dataType: 'element',
description: 'Hello I am element 2'
}]
}
},
methods: {
componentToUse(dataType) {
return 'my-' + dataType;
}
}
});
const sectionComponent = Vue.component('my-section', {
template: '#my-section-component',
props: ['itemData'],
methods: {
toggle() {
console.log('Doing some magic.');
}
}
});
const elementComponent = Vue.component('my-element', {
template: '#my-element-component',
props: ['itemData']
});
new Vue({
el: '#app'
});
.box {
border: 1px solid #999;
cursor: pointer;
margin: 10px;
padding: 10px;
}
.box:hover {
background-color: #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-collection />
</div>
<script type="text/x-template" id="my-collection-component">
<div>
<component
v-for="(item, i) in collection"
v-bind:key="i"
v-bind:is="componentToUse(item.dataType)"
v-bind:itemData="item" />
</div>
</script>
<script type="text/x-template" id="my-section-component">
<div #click="toggle" class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will invoke a section-specific logic</p>
</div>
</script>
<script type="text/x-template" id="my-element-component">
<div class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will do nothing</p>
</div>
</script>

here:
click: dataType === `section` ? toggleSectionElements : null
in the not-equal case you pass null, but the value on click expects a function. you can try an emptry function:
click: dataType === `section` ? toggleSectionElements : ()=>{}

In Vue 3 you can pass null to the listener. Combining it with optional chaining you can do this:
#click="handler?.() || null"
Same for old browsers:
#click="handler ? handler() : null"

Related

VueJS - Run Code after event is successfully emitted

I'm trying to clear up the form in the child component after the event containing the entered form data has been successfully passed from the child to parent component. However, I notice that the form gets cleared before the data gets propagated via the event to the parent component, such that the event passes empty values to the parent. I tried delaying the clearForm() using a timeout, but it didn't help. Is there a way to modify the behavior such that the clearForm() happens only after the event completes and the data has been saved?
Attached is the code.
Child Component
<template>
<!-- Contains a form -- >
</template>
<script>
export default {
data() {
return {
additionalInfo:
{
id: new Date().toISOString(),
fullName: '',
preAuthorize: '',
serviceAddress: ''
},
validation: {
fullNameIsValid: true,
serviceAddressIsValid: true
},
formIsValid: true,
addServiceButtonText: '+ Add Service Notes (Optional)',
serviceNotes: [],
showServiceNotes: false,
enteredServiceNote: '', //service notes addendum
}
},
computed : {
// something
},
methods: {
setServiceNotes(){
this.showServiceNotes = !this.showServiceNotes;
},
addAnotherParty(){
this.validateForm();
if(!this.formIsValid){
return;
}
this.$emit('add-parties', this.additionalInfo); //event
console.log(this.clearForm);
},
clearForm(){
this.additionalInfo.fullName = '';
this.additionalInfo.serviceAddress = '';
this.additionalInfo.preAuthorize = false;
}
}
}
</script>
Parent Component
<template>
<div>
<base-card
ref="childComponent"
#add-parties="updateAdditionalInfoList">
<!-- Wrapper for the `Parties Being Served` component-->
<template v-slot:title>
<slot></slot>
</template>
</base-card>
</div>
</template>
<script>
export default {
data() {
return {
hasElement: false,
selectedComponent: 'base-card',
additionalInfoList : [],
clearForm: false
}
},
methods: {
updateAdditionalInfoList(additionalInfo){ //save changes passed via event
this.additionalInfoList.push(additionalInfo);
console.log('emitted');
console.log(this.additionalInfoList);
setTimeout(() => {
this.$refs.childComponent.clearForm(); //clear the form in child
}, 2000);
}
}
}
</script>
Try this
addAnotherParty(){
this.validateForm();
if(!this.formIsValid){
return;
}
let emitObj = JSON.parse(JSON.stringify(this.additionalInfo));
this.$emit('add-parties', emitObj); //event
console.log(this.clearForm);
}
If your object is not deep then you can use
let emitObj = Object.assign({}, this.additionalInfo);
instead of stringify and parse

Vue v-bind:class not working immediately when changing the binded object's value

In the Vue, to add the dynamical class to the element, I think that the developers use the v-bind:class.
But in the below example, the v-bind:class doesn't work properly.
//Html
<div id="mainapp">
<span class="star" v-bind:class="{gold:obj.selected}" v-on:click="clickStar()">star</span>
</div>
//Script
var app = new Vue({
el:"#mainapp",
data:{
obj:{}
},
methods:{
clickStar:function(){
if(this.obj.selected == undefined) this.obj.selected =false;
//this.obj.selected=!this.obj.selected;
this.$set(this.obj, 'selected', !this.obj.selected);
console.log(this.obj);
}
}
})
JsFiddle Example
When clicking the element , span tag, the obj.selected value is changed by the clickStar function.
But v-bind:class doesn't work though $set is used when changing the obj.
Reason that Dom is not updated
What am I wrong?
How could I solve this issue?
you should define all of your data properties when it initialsed.
change your data object to.
data : {
object: {
selected:false
}
}
fiddle updated js fiddle
Combine the class attributes:
:class="`star ${obj.selected ? 'gold' : ''}`"
methods:{
clickStar () {
if (this.obj.hasOwnProperty('selected') {
this.obj.selected = !this.obj.selected
} else {
this.$set(this.obj, 'selected', true);
}
}
}
Adding reactive properties
Another approach would be to use a ref:
<span ref="star" class="star" #click="clickStar">star</span>
methods:{
clickStar () {
if (this.obj.hasOwnProperty('selected') {
this.obj.selected = !this.obj.selected
} else {
this.$set(this.obj, 'selected', true);
}
if (this.obj.selected) {
this.$refs.star.$el.classList.add('gold')
} else {
this.$refs.star.$el.classList.remove('gold')
}
}
}

Vue Js: Issue with scoped slots and IE11

My component looks like this:
<template>
<div>
<div v-if="!loaded">
<p><i class="fas fa-spinner fa-spin"></i> Loading feed</p>
</div>
<div v-else>
<div data-slider ref="feedSlider" v-if="length > 0">
<div class="swiper-wrapper">
<div class="slide" v-for="record in records" :key="record.id">
<slot :record="record"></slot>
</div>
</div>
</div>
<div v-else>
<p>There are no records available.</p>
</div>
</div>
</div>
</template>
<script>
import Swiper from 'swiper';
import AjaxCaller from '../../mixins/AjaxCaller';
export default {
mixins: [AjaxCaller],
data() {
return {
loaded: false,
records: [],
length: 0,
}
},
mounted() {
this.makeCall(this.success, this.failure);
},
methods: {
success(response) {
this.loaded = true;
if (!response.data.records) {
return;
}
this.records = response.data.records;
this.length = this.records.length;
if (this.length < 2) {
return;
}
setTimeout(() => {
this.initiateSlider();
}, 1000);
},
initiateSlider() {
(new Swiper(this.$refs.feedSlider, {
effect: 'slide',
slideClass: 'slide',
slideActiveClass: 'slide-active',
slideVisibleClass: 'slide-visible',
slideDuplicateClass: 'slide-duplicate',
slidesPerView: 1,
spaceBetween: 0,
loop: true,
speed: 2000,
autoplay: {
delay: 5000,
},
autoplayDisableOnInteraction: false,
}));
},
failure(error) {
this.stopProcessing();
console.log(error);
}
}
}
</script>
The imported mixin AjaxCaller, which works fine with any other component:
<script>
export default {
props: {
url: {
type: String,
required: true
},
method: {
type: String,
default: 'post'
}
},
data() {
return {
processing: false
}
},
computed: {
getMethodParams() {
if (this.method === 'post') {
return {};
}
return this.requestData();
},
postMethodData() {
if (this.method === 'get') {
return {};
}
return this.requestData();
}
},
methods: {
requestData() {
return {};
},
startProcessing() {
this.processing = true;
this.startProcessingEvent();
},
stopProcessing() {
this.processing = false;
this.stopProcessingEvent();
},
startProcessingEvent() {},
stopProcessingEvent() {},
makeCall(success, failure) {
this.startProcessing();
window.axios.request({
url: this.url,
method: this.method,
params: this.getMethodParams,
data: this.postMethodData
})
.then(success)
.catch(failure);
}
}
}
</script>
And here's how I call it from within the view:
<feed-wrapper url="{{ route('front.news.feed') }}">
<div slot-scope="{ record }">
<p>
<a :href="record.uri" v-text="record.name"></a><br />
<span v-text="record.excerpt"></span>
</p>
</div>
</feed-wrapper>
Everything works fine in any browser other than IE 11 (and lower).
It even works in Edge - no issues what so ever.
In IE I get
[Vue warn]: Failed to generate render function:
Syntax Error: Expected identifier in ...
It doesn't even get to execute method call from within the mounted segment.
I use laravel-mix with Laravel so everything is compiled using webpack with babel so it's not ES6 related issue.
I've already spent whole night trying to un-puzzle this so any help would be much appreciated.
I know you've already said that you don't believe it's an ES6 issue but the evidence suggests it is.
IE11 doesn't support destructuring. If you type something like var {record} = {} into your IE11 console you'll see this same error message, 'Expected identifier'.
Try doing a search through the compiled code in your original error message and look for the word record. I suspect you'll find something like this:
fn:function({ record })
If you see that it means that the destructuring has made it to the browser without being compiled through Babel.
Exactly why this is happening depends on where you're using that scoped slot template. If you're using it inside a single-file component it should be going through Babel but if you aren't then it may be making it to the browser without transpiling. You said that you're calling it 'from within the view' but that doesn't clarify exactly how you're using it. There's a note about this in the docs, for what it's worth:
https://v2.vuejs.org/v2/guide/components-slots.html#Destructuring-slot-scope
Assuming you aren't able to fix the transpiling problem directly (e.g. by moving the template to somewhere it'll go through Babel) you can just remove the ES6 destructuring. So something like:
<div slot-scope="slotProps">
and then using slotProps.record instead of record in the code that follows.

Vuejs 2: debounce not working on a watch option

When I debounce this function in VueJs it works fine if I provide the number of milliseconds as a primitive. However, if I provide it as a reference to a prop, it ignores it.
Here's the abbreviated version of the props:
props : {
debounce : {
type : Number,
default : 500
}
}
Here is the watch option that does NOT work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
Here is a watch option that DOES work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, 500)
}
It suspect that it is a scope issue but I don't know how to fix it. If I replace the watch method as follows...:
watch : {
term : function () {
console.log(this.debounce);
}
}
... I get the correct debounce value (500) appearing in the console.
Another variation to #Bert's answer is to build the watcher's function in created(),
// SO: Vuejs 2: debounce not working on a watch option
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<div>
<input type="text" v-model="term">
</div>
`,
data(){
return {
term: "",
debounceFn: null
}
},
created() {
this.debounceFn = _.debounce( () => {
console.log('Debounced term: ' + this.term);
}, this.debounce)
},
watch : {
term : function () {
this.debounceFn();
}
},
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<div id="app">
<debounce :debounce="2000"></debounce>
</div>
Example on CodePen
The primary issue here is using this.debounce as the interval when defining your debounced function. At the time _.debounce(...) is run (when the component is being compiled) the function is not yet attached to the Vue, so this is not the Vue and this.debounce will be undefined. That being the case, you will need to define the watch after the component instance has been created. Vue gives you the ability to do that using $watch.
I would recommend you add it in the created lifecycle handler.
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
Note above that the code also calls unwatch which before the component is destroyed. This is typically handled for you by Vue, but because the code is adding the watch manually, the code also needs to manage removing the watch. Of course, you will need to add unwatch as a data property.
Here is a working example.
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<input type="text" v-model="term">
`,
data(){
return {
unwatch: null,
term: ""
}
},
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<debounce :debounce="250"></debounce>
</div>
The debounced method needs to be abstracted since we need to call the same function everytime the watch is triggered. If we place the debounced method inside a Vue computed or watch property, it will be renewed everytime.
const debouncedGetData = _.debounce(getData, 1000);
function getData(val){
this.newFoo = val;
}
new Vue({
el: "#app",
template: `
<div>
<input v-model="foo" placeholder="Type something..." />
<pre>{{ newFoo }}</pre>
</div>
`,
data(){
return {
foo: '',
newFoo: ''
}
},
watch:{
foo(val, prevVal){
debouncedGetData.call(this, val);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app"></div>
Good Luck...
new Vue({
el: '#term',
data: function() {
return {
term: 'Term',
debounce: 1000
}
},
watch: {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="term">
<input v-model="term">
</div>

How do I display data through components with Vue.js (using Vueify)?

I'm having trouble getting data to display in my Vue components. I'm using Vueify and I'm trying to load an array of listings from the listings.vue component and I keep getting errors. Also, I don't understand how to pull in the data via the computed method. Any help would be appreciated.
This is the error I'm getting in the console:
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
[Vue warn]: $mount() should be called only once.
Here is my app.vue
// app.vue
<style>
.red {
color: #f00;
}
</style>
<template>
<div class="container">
<div class="listings" v-component="listings" v-repeat="listing"></div>
</div>
</template>
<script>
module.exports = {
replace: true,
el: '#app',
components: {
'listings': require('./components/listings.vue')
}
}
</script>
Here is my listings.vue component
<style>
.red {
color: #f00;
}
</style>
<template>
<div class="listing">{{title}} <br> {{description}}</div>
</template>
<script>
module.exports = {
data: {
listing: [
{
title: 'Listing title number one',
description: 'Description 1'
},
{
title: 'Listing title number two',
description: 'Description 2'
}
]
},
// computed: {
// get: function () {
// var request = require('superagent');
// request
// .get('/post')
// .end(function (res) {
// // Return this to the data object above
// // return res.title + res.description (for each one)
// });
// }
// }
}
</script>
The first warning means when you are defining a component, the data option should look like this:
module.exports = {
data: function () {
return {
listing: [
{
title: 'Listing title number one',
description: 'Description 1'
},
{
title: 'Listing title number two',
description: 'Description 2'
}
]
}
}
}
Also, don't put ajax requests inside computed properties, since the computed getters gets evaluated every time you access that value.

Categories