Programmatically add v-on directives to DOM elements - vue.js

<span #click="showModal = $event.target.innerHtml>Tag 1</span>
<span #click="showModal = $event.target.innerHtml>Tag 2</span>
<span #click="showModal = $event.target.innerHtml>Tag 3</span>
Clicking in any of the 3 spans will make this.showModal to have the value of each of the span content elements. But this code looks repetitive and unnecessary. I know I can create a component with v-for and have the data for the span contents somewhere else, but I want to know how to do this for very specific reasons. I'd like to have this:
<span>Tag 1</span>
<span>Tag 2</span>
<span>Tag 3</span>
And a function, e.g. in the hook mounted() of the component, that adds the v-on directive for click to each one of them.
Can you help me?
Thanks.

You could try something like this:
<template>
<span v-for="tag in tags" #click="showModal(tag)" v-text="tag"></span>
</template>
<script>
export default {
data() {
return {
tags: ['Tag 1', 'Tag 2', 'Tag 3']
}
},
methods: {
showModal(tag) {
console.log("Showing modal for tag:", tag)
}
}
}
</script>
Hope this helps!

You can add a method which is called on clicks that reads the element's HTML content.
The template:
<span #click="doStuff">Tag 1</span>
<span #click="doStuff">Tag 2</span>
<span #click="doStuff">Tag 3</span>
The method:
doStuff(e) {
this.showModal = e.target.innerHTML
}

You could set up a method to call when the tag is clicked and pass the id of the tag that was clicked through to handle appropriately.
Assuming that you have an array of the tag text:
data: function() {
return {
tagTotal: ['Tag 1', 'Tag 2', 'Tag 3'];
}
}
Then in the HTML section:
<span v-for="tag in tagTotal" #click="methodToCall(tag)">
{{ tag }}
</span>
Then in your mounted, methods, or created section you could add:
mounted: {
methodToCall: function(tag) {
showModal = tag;
// or 'this.showModal = tag' if showModal is a part of the componenet.
}
}

I've finally added the listeners manually with vanilla js, in order to save code:
mounted: function() {
let spans = document.querySelectorAll('span');
spans.forEach(el => {
el.addEventListener('click', this.clickTag);
})
}
methods: {
clickTag(event) { this.showModal = event.target.innerHTML }
}
It's important not using an arrow function for mounted because otherwise it won't bind the vue instance for this.
Thanks for your answers.

If direct-process Dom elements, custom directive will be one option.
Vue.config.productionTip = false
let vMyDirective = {}
vMyDirective.install = function install (_Vue) {
_Vue.directive('my-directive', {
inserted: function (el, binding, vnode) {
el.addEventListener('click', () => {
_Vue.set(vnode.context, binding.value.model, el.innerHTML)
}, false)
}
})
}
Vue.use(vMyDirective)
new Vue({
el: '#app',
data() {
return {
testValues: ['label a', 'label b'],
showModal: 'nothing!!!'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>showModal: {{showModal}}</h2>
<div>
<p v-for="(item, index) in testValues" v-my-directive="{'model': 'showModal'}">Test:<span>{{item}}</span></p>
</div>
</div>

Related

How to fire an event in mount in Vuejs

I have a sidebar that you can see below:
<template>
<section>
<div class="sidebar">
<router-link v-for="(element, index) in sidebar" :key="index" :to="{ name: routes[index] }" :class='{active : (index==currentIndex) }'>{{ element }}</router-link>
</div>
<div class="sidebar-content">
<div v-if="currentIndex === 0">
Profile
</div>
<div v-if="currentIndex === 1">
Meine Tickets
</div>
</div>
</section>
</template>
<script>
export default {
mounted() {
EventBus.$on(GENERAL_APP_CONSTANTS.Events.CheckAuthentication, () => {
this.authenticated = authHelper.validAuthentication();
});
console.log()
this.checkRouter();
},
data(){
return {
currentIndex:0,
isActive: false,
sidebar: ["Profile", "Meine Tickets"],
routes: ["profile", "my-tickets"],
authenticated: authHelper.validAuthentication(),
}
},
computed: {
getUser() {
return this.$store.state.user;
},
},
methods: {
changeSidebar(index) {
this.object = this.sidebar[index].products;
this.currentIndex=index;
},
checkRouter() {
let router = this.$router.currentRoute.name;
console.log(router);
if(router == 'profile') {
this.currentIndex = 0;
} else if(router == 'my-tickets') {
this.currentIndex = 1;
}
},
},
}
</script>
So when the link is clicked in the sidebar, the route is being changed to 'http://.../my-account/profile' or 'http://.../my-account/my-tickets'. But the problem is currentIndex doesn't change therefore, the content doesn't change and also I cannot add active class into the links. So how do you think I can change the currentIndex, according to the routes. Should I fire an event, could you help me with this also because I dont know how to do it in Vue. I tried to write a function like checkRouter() but it didn't work out. Why do you think it is happening? All solutions will be appreciated.
So if I understand correctly, you want currentIndex to be a value that's based on the current active route? You could create it as a computed property:
currentIndex: function(){
let route = this.$router.currentRoute.name;
if(router == 'profile') {
return 0;
} else if(router == 'my-tickets') {
return 1;
}
}
I think you could leverage Vue's reactivity a lot more than you are doing now, there's no need for multiple copies of the same element, you can just have the properties be reactive.
<div class="sidebar-content">
{{ sidebar[currentIndex] }}
</div>
Also, you might consider having object be a computed property, something like this:
computed: {
getUser() {
return this.$store.state.user;
},
object() {
return this.sidebar[currentIndex].products;
}
},
Just use this.$route inside of any component template. Docs .You can do it simple without your custom logic checkRouter() currentIndex. See simple example:
<div class="sidebar-content">
<div v-if="$route.name === 'profile'">
Profile
</div>
<div v-if="$route.name === 'my-tickets'">
Meine Tickets
</div>
</div>

Update properties of component in Vue.js

I've been searching for the answer 2nd day. But still couldn't find solution.
I have the modal window template. And the main page template from where I need to update modal window size by clicking on the button (span). Shortly it's like this for HTML:
<template id="modal">
<div>
<div :class="'modal-' + size">
...
</div>
</div>
</template>
<template id="list">
<div>
<span #click="onDetails">
Show Details
</span>
</div>
<modal size="md" #showdetails="showdetails();" ref="modal">
...
</modal>
</template>
And for JS:
Vue.component("modal", {
template: "#modal",
props: {
size: {
type: String,
default: ""
}
},
methods: {
onDetails() {
this.$emit("showdetails")
}
}
})
var List = Vue.extend({
template: "#list",
methods: {
showDetails() {
if(this.$refs.modal.size == "md") {
this.$refs.modal.size = "lg"
}
<additional code here>
}
}
})
When I'm accessing this.$refs.modal.size for read - it's OK. When I'm just changing it from showDetails - OK if only this action in the function. When I'm put something else instead of - size is not updating.
For example:
this.$refs.modal.size = "lg" - will work
this.$refs.modal.theme = "danger"; this.$refs.modal.size = "lg" - neither of them are updating
What am I doing wrong?
You need to assign the attribute value of Size by the javascript method setAttribute . Example : this.$refs.modal.setAttribute('size', 'lg')
There is a working demo below:
new Vue({
el: '#app',
methods: {
showdetails() {
console.log(this.$refs.modal.getAttribute('size'));
this.$refs.modal.setAttribute('size', 'lg')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.15/vue.js"></script>
<div id='app'>
<button size="md" #click="showdetails" ref="modal">Click</button>
</div>

Vue updating components at the same time after push

I am building a form in Vue
I have a component that looks as follow:
<template>
<transition name="preview-pane">
<label>{{ option.group }}</label>
<input type="text" class="form-control"
:name="`group_name[${index}]`"
v-on:input="option.group = $event.target.value"
:value="option.group">
<a ref="#" class="btn btn-primary float-right" #click="$emit('copy')" role="button">{{ __('Copy') }} </a>
</transition>
</template>
<script>
export default {
props: {
option: {
group: ''
},
index: {}
}
}
</script>
My Vue instance is as follow:
var products = new Vue({
el: '#products',
data: {
options: []
},
methods: {
add() {
this.options.push({
group: ''
})
},
copy(index) {
this.options.push(this.options[index])
}
}
})
And last my html looks as follow
<product-option
v-for="(option, index) in options"
:key="index"
:option="option"
:index="index"
#copy="copy(index)">
</product-option>
I have one button that basically takes one of the options and push it once again (copy method on the vue instance). When I run everything seems fine but then when I change the input it update the props of all the components that have been copied.
What can I do to make vue understand that each component should work separately?
Well in case someone have the same issue, I sort it out like this:
copy(index) {
var object = this.options[index]
var newObject = {}
for (const property in object) {
newObject[property] = object[property]
}
this.options.push(newObject)
}

Vue data property not reactive

Considering the code below, I can not make the active data property reactive. In this case I would like to show a div only on mouseover on an image.
<template>
<div>
<img #mouseover="showInfo" class="cursor-pointer w-full" :src="project.images[0].url" width="100%">
<div v-show="active" class="bg-red h-12">
Info about the image
</div>
</div>
</template>
<script>
export default {
props: ['project'],
data: function () {
return {
active: false
}
},
methods: {
showInfo: () => {
this.active = !this.active;
}
}
}
</script>
I have tried using v-if instead and printing active but to no effect. What am I doing wrong?
Don't use arrow functions to define methods, this will not work.
Just replace
showInfo: () => {
this.active = !this.active;
}
With:
showInfo() {
this.active = !this.active;
}
This can be simpler if you just change the value in HTML and you don't require a separate method for that.
#mouseover="active = !active"
It will look like this,
<div>
<img #mouseover="active = !active" class="cursor-pointer w-full" :src="project.images[0].url" width="100%">
<div v-show="active" class="bg-red h-12">
Info about the image
</div>
</div>
data() {
return {
active: false
}
},
Update your data like the one above.
Also I'd rename your function to toggleInfo as it can also hide it on mouse out.
And remove the arrow.
toggleInfo() {
this.active = !this.active;
}

Vuejs v-on click doesn't work inside component

I use VueJs and I create the following component with it.
var ComponentTest = {
props: ['list', 'symbole'],
data: function(){
return {
regexSymbole: new RegExp(this.symbole),
}
},
template: `
<div>
<ul>
<li v-for="item in list"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
`,
methods: {
replaceSymbole: function(name){
return name.replace(this.regexSymbole, '<span v-on:click="test">---</span>');
},
test: function(event){
console.log('Test ...');
console.log(this.$el);
},
}
};
var app = new Vue({
el: '#app',
components: {
'component-test': ComponentTest,
},
data: {
list: [{"id":1,"name":"# name1"},{"id":2,"name":"# name2"},{"id":3,"name":"# name3"}],
symbole: '#'
},
});
and this my html code
<div id="app">
<component-test :list="list" :symbole="symbole"></component-test>
</div>
When I click on the "span" tag inside "li" tag, nothing append.
I don't have any warnings and any errors.
How I can call my component method "test" when I click in the "span" tag.
How implement click event for this case.
You cannot use vue directives in strings that you feed to v-html. They are not interpreted, and instead end up as actual attributes. You have several options:
Prepare your data better, so you can use normal templates. You would, for example, prepare your data as an object: { linkText: '---', position: 'before', name: 'name1' }, then render it based on position. I think this is by far the nicest solution.
<template>
<div>
<ul>
<li v-for="(item, index) in preparedList" :key="index">
<template v-if="item.position === 'before'">
<span v-on:click="test">{{ item.linkText }}</span>
{{ item.name }}
</template>
<template v-else-if="item.position === 'after'">
{{ item.name }}
<span v-on:click="test">{{ item.linkText }}</span>
</template>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(question) {
if (question.indexOf("#") === 0) {
return {
linkText: "---",
position: "before",
name: question.replace("#", "").trim()
};
} else {
return {
linkText: "---",
position: "after",
name: question.replace("#", "").trim()
};
}
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
}
}
};
</script>
You can put the click handler on the surrounding li, and filter the event. The first argument to your click handler is the MouseEvent that was fired.
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id" v-on:click="clickHandler"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
data() {
return {
regexSymbole: new RegExp(this.symbole)
};
},
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(name) {
return name.replace(
this.regexSymbole,
'<span class="clickable-area">---</span>'
);
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
},
clickHandler(event) {
const classes = event.srcElement.className.split(" ");
// Not something you do not want to trigger the event on
if (classes.indexOf("clickable-area") === -1) {
return;
}
// Here we can call test
this.test(event);
}
}
};
</script>
Your last option is to manually add event handlers to your spans. I do not!!! recommend this. You must also remove these event handlers when you destroy the component or when the list changes, or you will create a memory leak.