$emit is not properly triggering root method - vue.js

I'm just learning vue and am having trouble with an $emit. Here's my code, with some extraneous testing clutter.
The console.log from the component is working, and the Vue console tool shows an event is happening. My root element doesn't seem to be catching the emit.
I expect a click on the input radio to call a component method changeCustomer, which then fires this.$emit('change-customer');, which I expect the root element to catch with v-on:change-customer="changeCustomer" or #change-customer="changeCustomer2 (two attempts), and then I have a console.log in each of those methods, but neither are logging.
I'm guessing it's something small, but any help is appreciated!
html:
<div id="customer-lookup"
v-on:change-customer="changeCustomer">
<label for="customer-lookup-input">Customer lookup</label>
<input type="search"
name="customer-lookup-input"
id="customer-lookup-input"
#keyUp="search"
v-model="customerLookupInput"
#change-customer="changeCustomer2"
/>
<customer-result :results="results"></customer-result>
</div>
js:
Vue.component('customer-result', {
props: {
results: {
type: Array,
required: false,
}
},
// language=Vue
template: `
<ul>
<li v-for="result in results" :key="result.id" class="customer-result">
<input type="radio"
:value="result.pk"
name="selected-customer"
v-model="customerRadio"
#change="changeCustomer"
/>
<span class="customer-name">{{ result.name }}</span>
<span class="customer-address_1" v-if="result.address_1">{{ result.address_1}}</span>
<span class="customer-address_2" v-if="result.address_2">{{ result.address_2}}</span>
<span class="customer-address_3" v-if="result.address_3">{{ result.address_3}}</span>
<span class="customer-city" v-if="result.city">{{ result.city}}, </span>
<span class="customer-state" v-if="result.state">{{ result.state}}</span>
<span class="customer-country" v-if="result.country">{{ result.country}}</span>
<span class="customer-email" v-if="result.email">{{ result.email}}</span>
<span class="customer-phone" v-if="result.phone">{{ result.phone}}</span>
</li>
</ul>
`,
data(){
return {
customerRadio: null
}
},
methods: {
changeCustomer(){
console.log('component change customer');
this.$emit('change-customer');
}
}
});
var app = new Vue({
delimiters: ['[[', ']]'], // django
el: '#customer-lookup',
methods: {
search(e){
if( this.customerLookupInput === "" ){
this.results = null;
} else {
searchCustomers(this.customerLookupInput, this); // ajax call that's working
}
},
updateResults(results){
this.results = results; // update from ajax that's working
},
changeCustomer(){
console.log('root change customer')
},
changeCustomer2(){
console.log('root2 change customer');
}
},
data() {
return {
customerLookupInput: null,
results: null,
customerRadio: null
}
}
});

Related

How to toggle between DIVs with bound data in Vue?

So the problem I'm facing is that I have a template that fetches data from a Realtime Firebase Database and at the same time the user can import more data through an input element. I'm using Vue.js and I need the data to be bound to each other.
Here is my template:
<template>
<ul>
<li>
<input type="text" v-model="email" v-on:keyup.enter="addData()"/>
<img #click="addData()" src="#/assets/Plus.png" />
</li>
</ul>
<ul>
<li v-for="(item, key) in emails" :key="key">
<div>
<p>{{ item.data }}</p>
<img #click="deleteDataByKey(key)" src="#/assets/Delete.png" />
</div>
<div class="first">
<input type="text" v-model="comment[key]" v-on:keyup.enter="addComment(key, comment[key])"/>
<img #click="addComment(key, comment[key])" src="#/assets/Plus.png" />
</div>
<div class="second">
<p>{{ comment[key] }}</p>
<img #click="deleteCommentByKey(key)" src="#/assets/Delete.png" />
</div>
</li>
</ul>
</template>
Now what is happening is that I want to show <div class="first"> when there is no comment and when there is one, <div class="second"> should be shown while hiding the first one.
I tried using v-if="comment[key]" but it will toggle the divs straight away.
I also tried to v-model.lazy which seems to be working but then the method to update the db is not called.
I tried using pure JS in the method to change the HTML but it doesn't seem to be working as well.
These are my methods and data:
data() {
return {
emailList: [],
email: "",
comment: []
};
},
addData() {
db.ref("emailItems").push({
data: data
});
this.email = "";
this.fetchData();
},
deleteDataByKey(key) {
db.ref("emailItems"+key).remove();
this.fetchData();
},
addComment(key, comment) {
db.ref(`emailItems/${key}/comment`).set(comment);
},
deleteCommentByKey(key){
db.ref("comment/"+key).remove();
this.fetchData();
},
fetchData() {
db.ref("emailItems")
.once("value")
.then(snapshot => {
this.emailList = snapshot.val().emailItems;
});
}
And the db structure looks like this
Any help would be highly appreciated...
I think you should build more on the (arguably) biggest features of Vue, namely: reactivity & components.
Break down the logic a bit more, until you arrive at elements that do only one thing - those elements can be your components. Then build up the business logic from those atomic components.
Vue.component('NewEmail', {
data() {
return {
email: null,
}
},
methods: {
handleKeyup() {
this.$emit("add-email", {
email: this.email,
comment: null
})
this.email = null
}
},
template: `
<div>
<label>
NEW EMAIL: <input
type="email"
placeholder="Type in an email"
v-model="email"
#keyup.enter="handleKeyup"
/>
</label>
</div>
`
})
Vue.component('SingleEmailRow', {
props: {
email: {
type: Object,
default: null,
}
},
methods: {
handleDeleteClick() {
this.$emit('remove-email', this.email)
},
},
template: `
<li
class="d-flex"
>
<div>
{{ email.email }}
</div>
<div>
<button
#click="handleDeleteClick"
>
X
</button>
</div>
<component
:is="email.comment ? 'HasEmailComment' : 'NoEmailComment'"
:email="email"
v-on="$listeners"
></component>
</li>
`
})
Vue.component('HasEmailComment', {
props: {
email: {
type: Object,
required: true
}
},
methods: {
handleUpdateComment() {
this.$emit('update:comment', { ...this.email,
comment: null
})
}
},
template: `
<div
class="d-flex"
>
<div>
{{ email.comment }}
</div>
<button
title="Remove comment"
#click="handleUpdateComment"
>
-
</button>
</div>
`
})
Vue.component('NoEmailComment', {
props: {
email: {
type: Object,
required: true
}
},
data() {
return {
comment: null,
}
},
methods: {
handleUpdateComment() {
this.$emit('update:comment', { ...this.email,
comment: this.comment
})
}
},
template: `
<div
class="d-flex"
>
<div>
<input
v-model="comment"
type="text"
placeholder="Write a comment"
/>
</div>
<button
title="Add comment"
#click="handleUpdateComment"
>
+
</button>
</div>
`
})
new Vue({
el: "#app",
data() {
return {
emailList: [],
}
},
methods: {
handleAddEmail(newEmail) {
if (!this.emailList.find(({
email
}) => email === newEmail.email)) {
this.emailList.push(newEmail)
}
},
handleRemoveEmail(toRemove) {
this.emailList = this.emailList.filter(({
email
}) => {
return email !== toRemove.email
})
},
handleUpdateComment(newEmail) {
this.emailList = this.emailList.map(email => {
if (email.email === newEmail.email) {
return newEmail
} else {
return email
}
})
}
}
})
.d-flex {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<new-email #add-email="handleAddEmail"></new-email>
<ul v-for="(email, i) in emailList" :key="i">
<single-email-row :email="email" #remove-email="handleRemoveEmail" #update:comment="handleUpdateComment"></single-email-row>
</ul>
</div>
OK, the comment handling in SingleEmailRow could be put to its separate component (based on my thoughts above).
The main points in the snippet above are:
there's only one original data source (emailList in the root component) that is passed down as props where needed, and all the other components & functions just manipulate THAT list via events (reactivity is great!)
because all the components are based on one central data source, they just have to work with the item they get as props. (Ok, they have some internal state, but hey - this is only a snippet! :) )
the two components have only one responsibility:
NewEmail: to add an item to the central emailList
SingleEmailRow: to manage the data in ONE email item
I hope this helps you a bit to reach a solution for your problem.
EDIT: UPDATING SNIPPET
I had to update the snippet, because I wasn't satisfied with it.
Modifications:
adding two new components: HasEmailComment, NoEmailComment
updated SingleEmailRow with the two new components
Now, all the components have only one task to do.

#click bound to each item within v-for loop executed many times when clicking

I have a #click bound to individual items within a v-for loop. In the resulting render, I should have one #click for each item, so clicking one item should trigger the function bound to the item once.
Yet it triggers it as many times as there are items. Why?
<ul>
<li :key="option.value" v-for="option in options">
<QuizCheckbox
#click.native="handleClick(option.value)"
>
{{ option.value }}
</QuizCheckbox>
</li>
</ul>
...
methods: {
handleClick(val) {
console.log(val);
},
EDIT:
If I replaced ... with a simple element, then clicking that doesn't trigger the problem. So it's the <QuizCheckbox> component who's the culprit. However, nothing in it seems to indicate what could cause the problem. Here's the content of QuizCheckbox.vue:
<template>
<div :class="['quiz-checkbox', {'quiz-checkbox--checked': shouldBeChecked}]">
<div :class="['form-checkbox']">
<label class="form-checkbox__label">
<slot/>
<input
:checked="shouldBeChecked"
:value="value"
#change="updateInput"
class="form-checkbox__input"
type="checkbox"
/>
<span class="form-checkbox__checkmark"></span>
</label>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'modelValue',
event: 'change'
},
props: {
value: {
type: String,
},
modelValue: {
type: [Boolean, Array],
default: false
}
},
computed: {
shouldBeChecked() {
if (this.modelValue instanceof Array) {
return this.modelValue.includes(this.value);
}
return this.modelValue;
}
},
created() {
if (!this.$slots.default) {
console.error('QuizCheckbox: requires label to be provided in the slot');
}
},
methods: {
updateInput(event) {
const isChecked = event.target.checked;
if (this.modelValue instanceof Array) {
const newValue = [...this.modelValue];
if (isChecked) {
newValue.push(this.value);
} else {
newValue.splice(newValue.indexOf(this.value), 1);
}
this.$emit('change', newValue);
} else {
this.$emit('change', isChecked);
}
}
}
};
</script>
The code you post seems fine. Although simplified here is the code you post running:
new Vue({
el: 'ul',
data: {
options: [
{
value: 'option1',
},
{
value: 'option2',
}
]
},
methods: {
handleClick(val) {
console.log(val);
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<ul>
<li :key="option.value" v-for="option in options">
<div
#click="handleClick(option.value)"
>
{{ option.value }}
</div>
</li>
</ul>
The problem must be elsewhere.

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.

Vue.JS: turn every matching case to a hyperlink

I'm trying to turn every matching case from an array to a link
// I call the message component using this from another component
<ul>
<li
v-for="message in message"
v-bind:message="message"
is="message"
</ul>
// in the message component I have a mounted that reads the text message
// from the server and filters hashtags
data () {
return {
hashtagsInMessages: ''
};
},
mounted () {
const filterMessageHashtags = _.filter( this.message,text.split( ' ' ), value => hashtags.indexOf(value) != -1 );
this.hashtagsInMessages = filterMessageHashtags;
}
With this, how can I turn a message to a link, for instance:
hey how you doing #cool #fire #water bro?
To this, using Vue.js
hey how you doing #cool #fire #water bro?
This is my solution:
Vue.component("message-el", {
template: "#message",
props: ["message"],
created: function (){
this.messages = this.message.split(" ");
this.hashTags = this.messages.filter(function (message){
return message[0] === "#";
});
},
data: function (){
return {
messages: [],
hashTags: [],
};
},
methods: {
}
});
var app = new Vue({
el: "#app",
data: function (){
return {
}
},
methods: {
}
});
.none {
display: none;
}
<script src="https://unpkg.com/vue#2.5.2/dist/vue.js"></script>
<div id="app">
<message-el message="this is a message #tag1 #tag2">
</message-el>
</div>
<div id="message" class="none">
<div>
<div>
<template v-for="message in messages">
<a v-if="hashTags.indexOf(message) > -1" :href="'#' + message">
{{ message }}
</a>
<span v-else>
{{ message }}
</span>
&nbsp
</template>
</div>
</div>
</div>

How to call $get from Vue.extend?

Examples do not show how to call such function from Vue.extend. Now I am getting error:
ReferenceError: $get is not defined
var guestContent = Vue.extend({
template: `
<p>Guest content</p>
<div v-for="question in questions">
<template v-if="question.isEnabled">
<h3 v-if="question.isEnabled">{{question.question}}</h3>
<!-- First Level -->
<div v-for="firstLevelAnswer in question.answers">
<label v-if="!question.isRadioButton"><span class="firstLevelAnswer"><input type="checkbox" class="big-checkbox" v-on:onclick="setIsSelectedTrue()"/>{{firstLevelAnswer.answer}}</span></label>
<label v-if="question.isRadioButton"><span class="firstLevelAnswer"><input type="radio" class="big-checkbox" name="myvalue"/>{{firstLevelAnswer.answer}}</span></label>
<span v-if="firstLevelAnswer.isTextInput"><input type="text"/></span>
| firstLevelAnswer.isSelected: {{firstLevelAnswer.isSelected}}
</div>
</template>
</div>
</li>
`,
data: function () {
return {
questions: []
}
},
ready()
{
this.getQuestionsContent(),
this.foo()
},
methods:
{
getQuestionsContent()
{
this.$http.get('http://127.0.0.1:8080/js/questions.json').then(function(response)
{
this.questions = response.data;
});
},
foo()
{
console.log($get('questions.question')); //HERE IS PROBLEM
}
}
}
);
Also can I check of value firstLevelAnswer.isSelected from foo? How can I reach it? It's generated in for loop and do not like that it can exists here...
$get is an instance method of Vue components, so you have to call it with this.$get:
foo()
{
console.log(this.$get('questions.question'));
}