VueJS default/starting a value ina list of searched terms - vue.js

Trying to make a search plugin using Vue and I'm having a problem with adding a starting/default value to the list of options. If I comment out the pair or template lines involving the start prop the rest of it works fine, but nothing renders if I leave them in.
Component Code:
Vue.component('search', {
props: {
type: String,
hidein: String,
start: {
type: Object,
default: null
}
},
//props: ['type', 'hidein', 'start'],
data: function () {
return {
search: "",
select: "",
results: [],
};
},
template: '<div #load="loaded"><input :id="hidein" type="text" v-model="search" #keyup="updateList">'+
'<input type="hidden" :name="hidein" v-model="select" class="form-control">'+
'<div v-if="start">Current: <span #click="select=start.id" :class="{\'label label-success\':(select==start.id)}>'+
'+ {{start.name}}</span></div>'+
'<div v-if="results.length">Do you mean:<ul>'+
'<li v-for="result in results" :key="result.id"><span #click="select=result.id" :class="{\'label label-success\':(select==result.id)}">'+
'+ {{result.name}}</span></li>'+
'</ul></div></div>',
methods: {
updateList: function(e) {
var response = [];
console.log("search: "+this.search);
$.ajax({
method: "GET",
url: "/api/search/"+this.type,
data: { key: this.search }
}).done(function( msg ) {
this.results = msg;
console.log(this.results);
}.bind(this));
},
loaded: function () {
this.select=!!this.start ? this.start.id : null;
}
},
});
Component Call:
<search type="ships" hidein="ship_id" ></search>
Can anyone tell me what I'm doing wrong? (Besides the hacked together template, that's hopefully a completely separate issue with the pipeline I'm having)

There is a missing " here
:class="{\'label label-success\':(select==start.id)}
But also, please, use a template literal to make your life easier.
`<div #load="loaded"><input :id="hidein" type="text" v-model="search" #keyup="updateList">
<input type="hidden" :name="hidein" v-model="select" class="form-control">
<div v-if="start">
Current:
<span #click="select=start.id" :class="{'label label-success':(select==start.id)}">
{{start.name}}
</span>
</div>
<div v-if="results.length">
Do you mean:
<ul>
<li v-for="result in results" :key="result.id">
<span #click="select=result.id" :class="{'label label-success':(select==result.id)}">
{{result.name}}
</span>
</li>
</ul>
</div>
</div>`

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.

$emit is not properly triggering root method

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
}
}
});

How To Check A Radio Button in Vue.JS 2 WITHOUT using v-model

I am trying to set the checked value of a Radio Button list in Vue.JS 2. I've reviewed existing articles here and tried them and also several different manual approaches and I just cannot get this to work at all.
I am NOT using v-model here as I'm working on a custom radio button list control which is consumed by a forms builder. This is further complicated by the fact that I am building, on top, a nested radio button list to handle nullable booleans. Putting that complexity aside, my radio component looks like this....
<template>
<div class="form__radio-list">
<span class="form__radio-list__intro">{{ introText }}</span>
<ul class="form__radio-list__options">
<li v-for="item in options"
:key="item.key ? item.key : item"
class="form__radio-list__options__item">
<input type="radio"
:id="item.key ? item.key.toKebabCase() : item.toKebabCase()"
:name="def"
:value="item.value != undefined ? item.value : item"
:disabled="disabled"
:checked="isChecked(item)"
#input="onInput"
#change="$emit('change', $event.target.checked)">
<label :for="item.key ? item.key : item">{{ item.text ? item.text : item }}</label>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
def: {
type: String,
required: true
},
introText: {
type: String,
default: ''
},
options: {
type: Array,
required: true
},
initialValue: {
type: [String, Number, Boolean],
default: null
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
value: this.initialValue
}
},
methods: {
_parseValue() {
return this.value ? parseInt(this.value) : null
},
isChecked(item) {
const checked = item.value === this.value || item === this.value || item.value == this._parseValue()
this.$logger.logObject({ item, parentValue: this.value, checked }, 'Checking value for an item')
return checked
},
onInput($event) {
this.value = $event.target.checked
this.$emit('input', $event.target.checked)
}
}
}
</script>
From the logging I can see that the value of 'checked' SHOULD be set correctly (but it isn't).
I also tried splitting the input tag into a 'v-if' statement so I'd have one with a checked parameter set and one without (although this felt horrible) and that worked from an HTML point of view (checked="checked" appeared where I would expect it to) but, on the browser neither of the 2 options were checked.
I am consuming the component through a boolean component renederer that looks like this...
<template>
<hh-radio class="form__radio-list--yes-no"
:def="def"
:intro-text="introText"
:options="options"
:initial-value="initialValue"
:disabled="disabled"
#change="$emit('change', $event)"
#input="$emit('input', $event)" />
</template>
<script>
export default {
props: {
def: {
type: String,
required: true
},
introText: {
type: String,
default: ''
},
initialValue: {
type: [String, Boolean],
default: null
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
options() {
return [{
key: 'yes',
text: 'Yes',
value: true
}, {
key: 'no',
text: 'No',
value: false
}]
}
}
}
</script>
This is then consumed ultimately on a form like this...
<hh-yes-no class="editable-segment-field__bool"
:def="pvc-enabled"
:initial-value="pvc.value"
#input="onInput" />
The value pass throughs seem to work fine - The key issue that I have is that it will NOT specify the currently selected item from any existing data.
I have tried suggestions here - Vue.JS radio input without v-model and here - Vue.JS checkbox without v-model without much success.
Using the example given me below I've tried to strip this back as far as I can, adding in pieces of the dynamic elements from my components as I go to identify the problem root.
I now have a simpler component which looks like this...
<template>
<div :class="`form__radio-list ${(this.def ? `form__radio-list--${this.def} js-${this.def}` : '' )}`">
<span class="form__radio-list__intro">{{ introText }}</span>
<ul class="form__radio-list__options">
<li class="form__radio-list__options__item">
<input id="awesome"
type="radio"
name="isawesome"
:checked="radio === 'Awesome'"
value="Awesome"
#change="radio = $event.target.value">
<label for="awesome">Awesome</label>
</li>
<li class="form__radio-list__options__item">
<input id="super"
type="radio"
name="isawesome"
:checked="radio === 'Super Awesome'"
value="Super Awesome"
#change="radio = $event.target.value">
<label for="super">Super</label>
</li>
</ul>
<span>Selected: {{ value }} | {{ radio }}</span>
</div>
</template>
<script>
export default {
props: {
def: {
type: String,
required: true
},
introText: {
type: String,
default: ''
},
initialValue: {
type: [String, Boolean],
default: null
},
disabled: {
type: Boolean,
default: false
}
},
// delete this
data() {
return {
value: this.initialValue,
radio: 'Awesome'
}
},
computed: {
options() {
return [{
key: 'yes',
text: 'Yes',
value: true
}, {
key: 'no',
text: 'No',
value: false
}]
}
}
}
</script>
I managed to get this to fail as soon as I added name="isawesome" to the radio button items. It seems that when you introduce 'name' something goes awry. Surely I need 'name' to prevent multiple radio button lists interacting with each other or is this something that Vue handles which I've been unaware of.
Here is an working example:
new Vue({
el: "#app",
data: {
radio: 'Awesome'
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<span>Vue is:</span> <br>
<label>Awesome
<input
type="radio"
:checked="radio === 'Awesome'"
value="Awesome"
#change="radio = $event.target.value"
>
</label>
<label>Super Awesome
<input
type="radio"
:checked="radio === 'Super Awesome'"
value="Super Awesome"
#change="radio = $event.target.value"
>
</label>
<hr>
<span>Selected: {{radio}}</span>
</div>
This appears to be a bug / oddity with Vue.js. Using roli roli's example I was able to take both his example and my requirement and keep tweaking until they met in the middle so I could find out the problem via process of elimination.
Here is a copy of the component with both elements together. As #birdspider commented above, I would not expect this to work in HTML. In Vue, however, it DOES....
<template>
<div :class="`form__radio-list ${(this.def ? `form__radio-list--${this.def} js-${this.def}` : '' )}`">
<span class="form__radio-list__intro">{{ introText }}</span>
<ul class="form__radio-list__options">
<li class="form__radio-list__options__item">
<input id="yes"
type="radio"
:checked="value === true"
:value="true"
#change="value = $event.target.value">
<label for="yes">Yes</label>
</li>
<li class="form__radio-list__options__item">
<input id="no"
type="radio"
:checked="value === false"
:value="false"
#change="value = $event.target.value">
<label for="no">No</label>
</li>
<li class="form__radio-list__options__item">
<input id="awesome"
type="radio"
:checked="radio === 'Awesome'"
value="Awesome"
#change="radio = $event.target.value">
<label for="awesome">Awesome</label>
</li>
<li class="form__radio-list__options__item">
<input id="super"
type="radio"
:checked="radio === 'Super Awesome'"
value="Super Awesome"
#change="radio = $event.target.value">
<label for="super">Super</label>
</li>
</ul>
<span>Selected: {{ value }} | {{ radio }}</span>
</div>
</template>
<script>
export default {
props: {
def: {
type: String,
required: true
},
introText: {
type: String,
default: ''
},
initialValue: {
type: [String, Boolean],
default: null
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
value: this.initialValue,
radio: 'Awesome'
}
}
}
</script>
This will work completely fine and renders as 2 separate radio button lists - one for yes / no and one for awesome / super awesome. If you try and add a 'name' tag to the radio inputs then the checked state is no longer set in the radio button group at all.
---- UPDATE ----
This seems like a bug where an attempt to add 'name' should simply be ignored by Vue, but it isn't. However, this isn't the case if you create an ES5 style component in codepen (I am unable to repro this in codepen for this reason.)
Using ES6 style files, this component will not set any checked values (credit to #birdspider for half of this simplified example)...
<template>
<div>
<ul class="form__radio-list__options">
<li class="form__radio-list__options__item">
<input id="awesome" type="radio" name="isawesome"
:checked="radio === 'Awesome'"
value="Awesome"
#change="onChange($event.target.value)">
<label for="awesome">Awesome</label>
</li>
<li class="form__radio-list__options__item">
<input id="super" type="radio" name="isawesome"
:checked="radio === 'Super Awesome'"
value="Super Awesome"
#change="onChange($event.target.value)">
<label for="super">Super</label>
</li>
</ul>
<ul class="form__radio-list__options">
<li class="form__radio-list__options__item">
<input id="awesome" type="radio" name="other"
:checked="radio2 === 'other'"
value="other"
#change="onChange2($event.target.value)">
<label for="awesome">other</label>
</li>
<li class="form__radio-list__options__item">
<input id="super" type="radio" name="other"
:checked="radio2 === 'another'"
value="another"
#change="onChange2($event.target.value)">
<label for="super">another</label>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
radio: 'Awesome',
radio2: 'another'
}
},
methods: {
onChange(e) {
this.radio = e
},
onChange2(e) {
this.radio2 = e
}
}
}
</script>
(if anyone can tell me how to get it running in Codepen or JS Fiddle like this then that would be great!)
If I remove the name attributes then it works.
If you put essentially the same thing in Codepen as a single Vue instance rather than a component file then that works too.

How to toggle individual row at a time in vue for rows generated from an array? [duplicate]

I work with single file components and have a list in one of them. This list should work like a accordion, but as far as I can find in the Vuejs docs, it's not that easy to make each item open separately very easily. The data (questions and answers) is retrieved from an ajax call. I use jQuery for that, but would like to know how I can make the accordion work Vuejs-style. Any help would be appreciated!
Here's the code:
export default {
name: 'faq-component',
props: ['faqid', 'faqserviceurl', 'ctx'],
data: function () {
return {
showFaq: "",
totalFaqs: this.data,
isOpen: true
}
},
watch: {
'showFaq': function(val, faqid, faqserviceurl) {
var self = this;
$.ajax ({
url: this.faqserviceurl,
type: 'GET',
data: {id: this.faqid, q: val, scope:1},
success: function (data) {
self.totalFaqs = data;
},
error: function () {
$("#answer").html('Sorry');
}
});
}
},
methods: {
'toggle': function() {
this.isOpen = !this.isOpen
}
}
}
<template>
<div class="card faq-block">
<div class="card-block">
<form>
<div class="form-group">
<input class="form-control" type="text" placeholder="Your question" id="faq" v-model="showFaq">
</div>
</form>
<div id="answer"></div>
<ul class="faq">
<li v-for="faq in totalFaqs">
<p class="question" v-html="faq.vraag" v-bind:class={open:isOpen} #click="isOpen = !isOpen"></p>
<p class="answer" v-html="faq.antwoord"></p>
</li>
</ul>
</div>
</div>
</template>
Add an isOpen property to each object in totalFaqs and use that instead of your single isOpen property in data.
<p class="question" v-html="faq.vraag" v-bind:class={open: faq.isOpen} #click="faq.isOpen = !faq.isOpen"></p>
If you can't change the model from the server side, then add it client side.
success: function (data) {
data.forEach(d => self.$set(d, 'isOpen', false))
self.totalFaqs = data
}

Vuejs open/toggle single item

I work with single file components and have a list in one of them. This list should work like a accordion, but as far as I can find in the Vuejs docs, it's not that easy to make each item open separately very easily. The data (questions and answers) is retrieved from an ajax call. I use jQuery for that, but would like to know how I can make the accordion work Vuejs-style. Any help would be appreciated!
Here's the code:
export default {
name: 'faq-component',
props: ['faqid', 'faqserviceurl', 'ctx'],
data: function () {
return {
showFaq: "",
totalFaqs: this.data,
isOpen: true
}
},
watch: {
'showFaq': function(val, faqid, faqserviceurl) {
var self = this;
$.ajax ({
url: this.faqserviceurl,
type: 'GET',
data: {id: this.faqid, q: val, scope:1},
success: function (data) {
self.totalFaqs = data;
},
error: function () {
$("#answer").html('Sorry');
}
});
}
},
methods: {
'toggle': function() {
this.isOpen = !this.isOpen
}
}
}
<template>
<div class="card faq-block">
<div class="card-block">
<form>
<div class="form-group">
<input class="form-control" type="text" placeholder="Your question" id="faq" v-model="showFaq">
</div>
</form>
<div id="answer"></div>
<ul class="faq">
<li v-for="faq in totalFaqs">
<p class="question" v-html="faq.vraag" v-bind:class={open:isOpen} #click="isOpen = !isOpen"></p>
<p class="answer" v-html="faq.antwoord"></p>
</li>
</ul>
</div>
</div>
</template>
Add an isOpen property to each object in totalFaqs and use that instead of your single isOpen property in data.
<p class="question" v-html="faq.vraag" v-bind:class={open: faq.isOpen} #click="faq.isOpen = !faq.isOpen"></p>
If you can't change the model from the server side, then add it client side.
success: function (data) {
data.forEach(d => self.$set(d, 'isOpen', false))
self.totalFaqs = data
}