Vue 2 add class active through exploring list - vue.js

i have problem with input and lists, I would like to go through from input to list element below and add class 'active' to current li element
<div class="control">
<label class="label">Input Test</label>
<input type="text" class="input" #keydown="keyHandler">
<ul>
<li v-for="suggestion in suggestions" v-bind:class="{active}">{{suggestion.message}}</li>
</ul>
</div>
methods : {
keyHandler(e){
if(e.keyCode === 38){
e.preventDefault();
console.log('arrow up')
this.currentKey = e.key
}
else if(e.keyCode === 40){
e.preventDefault();
console.log('arrow down')
this.currentKey = e.key
}
}
}
here is fiddle: https://jsfiddle.net/o8fwf0gh/13/
I would be grateful for help

Hope this helps.
var app = new Vue({
el: '#form',
data: {
currentKey: null,
suggestions: [{
message: 'Foo'
}, {
message: 'Bar'
}, {
message: 'Foobar'
}, {
message: 'pikachu'
}, {
message: 'raichu'
}],
active: false
},
methods: {
keyHandler(e) {
if (e.keyCode === 38) {
e.preventDefault();
console.log('arrow up')
this.setActiveClass(this.currentKey, e.key)
this.currentKey = e.key
} else if (e.keyCode === 40) {
e.preventDefault();
this.setActiveClass(this.currentKey, e.key)
console.log('arrow down')
this.currentKey = e.key
}
},
setActiveClass(previousKey, currentKey) {
if (previousKey) {
var tempIndex = this.suggestions.findIndex(x => x.class == "active");
this.$set(this.suggestions[tempIndex], 'class', 'inactive')
if (currentKey === 'ArrowDown') {
if (tempIndex === this.suggestions.length - 1)
this.$set(this.suggestions[0], 'class', 'active')
else
this.$set(this.suggestions[tempIndex + 1], 'class', 'active')
} else {
if (tempIndex === 0)
this.$set(this.suggestions[this.suggestions.length - 1], 'class', 'active')
else
this.$set(this.suggestions[tempIndex - 1], 'class', 'active')
}
} else {
if(currentKey === 'ArrowUp')
this.$set(this.suggestions[this.suggestions.length - 1], 'class', 'active')
else
this.$set(this.suggestions[0], 'class', 'active')
}
}
}
}
})
and in HTML you can do the following:
<li v-for="suggestion in suggestions" v-bind:class='suggestion.class'>{{suggestion.message}}
Working example here

What you could do is update a data property, let's say selected, with where you should be in the list. By default we'll set it on 0, so that the first element is selected:
data: {
selected: 0,
// other data stuff
}
When pressing the up or down arrow you obviously have to update this.selected as such:
methods : {
keyHandler(e){
if(e.keyCode === 38){
e.preventDefault();
this.selected--;
}
else if(e.keyCode === 40){
e.preventDefault();
this.selected++;
}
}
}
You can then set up your list as such:
<li v-for="(suggestion, index) in suggestions" :class="index === selected ? 'active' : ''">{{suggestion.message}}</li>
What you see inside the class property is know as shorthand syntax. It's basically an if statement that returns 'active' if the index is equal to the list-number that is currently selected. As you can see the index is passed along as a second property inside v-for.
This should do the trick, if I understood correctly what you're trying to achieve. :P

Related

Code works but still getting warning in Vue

i am getting warning "Avoid mutating a prop directly", i know this can be solved through data property or computed usage like mentioned in Vue Official Documentation. But i do not know how to change my code to any of those methods. Please help me with proper code to get rid of this Warning.
My Code looks like this:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= rating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
data() {
return {
hoveredNote: false
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.rating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.rating);
}
}
};
You are modifing rating prop on the rate method, so to avoid the warning you should avoid to modify this prop, one easy solution although not very elegant is to make a local copy of your prop:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= copiedrating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
mounted () {
this.copiedrating = this.rating;
},
data() {
return {
hoveredNote: false,
copiedrating: null,
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.copiedrating = this.copiedrating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.copiedrating);
}
}
Don't modify this.rating directly, just send the event and make the change in parent component....
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0) {
const newRating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, newRating);
}
}
}
...then in parent handle the event (I don't know how parent component or data looks like so this is more pseudo-code...)
<Rating :itemId="track.itemId" :rating="track.rating" #onRate="changeRating" />
methods:
changeRating: function(id, rating) {
let trackIndex = this.tracks.indexOf(track => track.itemId === id)
this.tracks[trackIndex].rating = rating
}

How do I prevent multiple email fields from taking suggestion of just one?

I have a set of email fields. When I start typing in one, it gives me suggestions. Then I select a suggestion, and it gets set for ALL email fields. Is there a way to select email suggestions for just one email field even though there might be several on the page?
It's a vue.js application. Here is the template for the email field:
<v-layout row wrap v-for="(item, index) in prop.roles" :key="index">
<v-flex mb-4>
<div class="ath-container">
<div class="ath-controls-container mb-2">
<v-text-field
:id="'email-' + index"
class="d-inline-block ath-email-field"
outline
label="Email"
v-model="item.email"
placeholder="username#example.com"
:rules="[v => !!v || 'Email is required',
v => /.+#.+/.test(v) || 'Email is invalid' ]"
maxlength="255"
required>
...
</div>
</div>
</v-flex>
</v-layout>
There is an "add" button that allows the user to add more email fields. But I would like them each treated independently when selecting suggested emails from the popup list. Thanks.
EDIT: data and method blocks requested.
data() {
return {
isValid: false,
isCompany: false,
prop: {
companyName: null,
hasAdditionalTitleHolders: null,
roles: [{
email: null,
notifying: false
}],
isMarried: null,
hasSpouse: null
},
dowerRightsFilenameData: null,
rafId: 0, // request animation frame ID (see animateStatusBarIfInView() below)
notifyingAll: false
}
}
methods: {
onDowerRightsFormPicked(e) {
this.dowerRightsFilenameData = e.target.files[0].name;
this.$emit('dower-rights-form-picked', e);
},
deleteDowerRightsForm() {
this.dowerRightsFilenameData = '';
this.$emit('delete-dower-rights-form');
},
animateStatusBarIfInView() {
for (let index = 0; index < this.prop.roles.length; index++) {
if (this.$refs['ath-status-bar-' + index] && this.$refs['ath-status-bar-' + index][0]) {
const athStatusBar = this.$refs['ath-status-bar-' + index][0];
const ath = this.prop.roles[index];
if (this.isInViewport(athStatusBar)) {
let className = 'ath-status-bar-init';
if (ath.acceptedAt) {
className = 'ath-status-bar-accepted';
} else if (ath.verifiedAt) {
className = 'ath-status-bar-verified';
} else if (ath.accountCreatedAt) {
className = 'ath-status-bar-account-created';
} else if (ath.invitedAt) {
className = 'ath-status-bar-invited';
}
athStatusBar.classList.add(className);
}
}
}
// repeat:
if (this.rafId) window.cancelAnimationFrame(this.rafId);
this.rafId = window.requestAnimationFrame(this.animateStatusBarIfInView);
},
isInViewport(element) {
const rec = element.getBoundingClientRect();
return rec.top < (window.innerHeight || document.documentElement.clientHeight) && rec.bottom > 0;
},
notifyAllAths() {
this.notifyingAll = true;
let index = 0;
const allIndexes = this.prop.roles.map(ath => index++);
this.$emit('notify-aths', allIndexes);
}
}

vue select all checkboxes with value generated by computed methods

My page has a Select All checkbox at the top where upon clicking it, it should have checked all the checkboxes. Here's my code:
<div class="columns bottom-border">
<div class="column">Student</div>
<div><a v-on:click="revokePoints()">Revoke</a><br/><input type="checkbox" v-model="selectAll">Select All</div>
</div>
<div class="columns" v-for="(behavior) in sortBehaviors(behaviorList)" :key="behavior._id">
<div class="column">{{ behavior.studentID.firstName }} </div>
<div class="column is-1"><center><input type="checkbox" :value="setCheckedValue(behavior.dataType,behavior._id,behavior.studentID._id,behavior.actionDate)" :id="setCheckedValue(behavior.dataType,behavior._id,behavior.studentID._id,behavior.actionDate)" v-model="checkedIDs"></center></div>
</div>
data() {
return {
positiveName: '',
behaviorList: [],
checkedIDs: [],
selected: []
};
},
computed:{
selectAll: {
get: function () {
return this.behaviorList ? this.selected.length == this.behaviorList.length : false;
},
set: function (value) {
var mySelected = [];
let self = this;
if (value) {
this.behaviorList.forEach(function (behavior) {
var getDataType = behavior.dataType
var getID = behavior._id
var getStudentID = behavior.studentID._id
var getActionDate = behavior.actionDate
var getGeneratedID = self.setCheckedValue(getDataType,getID,getStudentID,getActionDate);
mySelected.push(getGeneratedID);
});
}
self.selected = mySelected;
console.log("self selected")
console.log(self.selected)
}
}
},
methods: {
setCheckedValue(dataType,id,studentID,actionDate){
return "1:" + dataType + "|2:" + id + "|3:" + studentID + "|4:" + actionDate
},
revokePoints(){
var pointsToRevoke = this.checkedIDs;
console.log("pointsToRevoke")
console.log(pointsToRevoke)
}
When I click on the Select All checkbox, console will display that self.selected will have the id of all the checkboxes. But the issue is the checkbox for all the values displayed are not checked...
It is difficult to help because your code is not completed. But I would approach that a bit differently. I hope this codepen can help you.
const list = [
{ id: 1, name: 'New York', checked: true },
{ id: 2, name: 'Sydney', checked: false },
{ id: 3, name: 'London', checked: false },
{ id: 4, name: 'Chicago', checked: true }
]
new Vue({
el: '#app',
data() {
return {
list,
isAllChecked: false
};
},
methods: {
checkAll: function() {
this.list = this.list.map(city => ({ ...city,
checked: !this.isAllChecked
}))
this.isAllChecked = !this.isAllChecked
}
},
computed: {
getAllCheckedIDs: function() {
return this.list.filter(city => city.checked).map(city => city.id)
},
getNotAllCheckedIDs: function() {
return this.list.filter(city => !city.checked).map(city => city.id)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="city in list" :key="city.id">
<label>
{{city.name}}
<input type="checkbox" v-model="city.checked" />
</label>
</li>
</ul>
<button #click="checkAll">Check all</button>
<br/>
<div>Checked IDs: {{getAllCheckedIDs}}</div>
<div>Not Checked IDs: {{getNotAllCheckedIDs}}</div>
</div>

Vue.js - data in component instead of a generated modal

I have a list of terms that have a description. I need these description to be displayed right after the term name instead of in a modal.
this is the vue component:
Vue.component('taxonomy-list', {
template : ''+
'<span><template v-for="(room, index) in event.terms[tax]">' +
'<template v-if="room.url"><a :href="room.url">{{room.name}}</a></template>' +
//HERE
'<template v-else-if="room.desc">{{room.name}}</template>' +
//END
'<template v-else>{{room.name}}</template>' +
'<template v-if="index !== (event.terms[tax].length - 1)">, </template>' +
'</template></span>' ,
props : [ 'tax', 'options', 'event', ],
methods : {
openModal: function( item, options, $event ){
this.$emit( 'open-modal', item, options, $event );
},
}
});
on the second case on click it generate a modal with the description that i need.
This is the generated modal:
Vue.component( 'modal-taxonomy', {
template: '#wcs_templates_modal--taxonomy',
props: [ 'data', 'options', 'content', 'classes' ],
mixins: [wcs_modal_mixins]
});
<script type="text/x-template" id="wcs_templates_modal--taxonomy">
<div class="wcs-modal" :class="classes" v-on:click="closeModal">
<div class="wcs-modal__box">
<div class="wcs-modal__inner">
<div class="wcs-modal__content wcs-modal__content--full">
<h2 v-html="data.name"></h2>
<div v-html="data.content"></div>
</div>
</div>
</div>
</div>
</script>
I just need to print the "data.content" right after the name of the {{room.name}}, but i cant manage to do so...
Thanks guys.
EDIT: thats the modalOpen function:
openModal: function( data, options ){
var $self = this;
this.data = data;
this.options = options;
if( ! this.visible ){
this.visible = true;
}
this.loading = true;
if( typeof data.start !== 'undefined' ){
if( typeof this.events[data.id] === 'undefined' ){
this.getClass( data.id );
} else {
this.data.content = this.events[data.id].content;
this.data.image = this.events[data.id].image;
this.loading = ! this.loading;
}
} else {
if( typeof this.taxonomies[data.id] === 'undefined' ){
this.getTaxonomy( data.id );
} else {
this.data.content = this.taxonomies[data.id];
this.loading = ! this.loading;
}
}
}
The modal component uses room.content to display the description, but that property is initialised only if you call openModel, see this.data.content = this.events[data.id].content; and this.data.content = this.taxonomies[data.id];.
IMO you have two options, either duplicate the openModel logic into the HTML or create a computed property that returns the description (I'd choose the latter).
EDIT
Actually, a method should be better because you extract the description from room. You'll need something like:
Vue.component('taxonomy-list', {
template : ''+
'<span><template v-for="(room, index) in event.terms[tax]">' +
'<template v-if="room.url"><a :href="room.url">{{room.name}}</a></template>' +
//HERE
'<template v-else-if="room.desc">{{room.name}}</template>' +
//END
'<template v-else>{{room.name}}</template>' +
// DESCRIPTION
'{{ description(room) }}' +
// END
'<template v-if="index !== (event.terms[tax].length - 1)">, </template>' +
'</template></span>' ,
props : [ 'tax', 'options', 'event', ],
methods : {
openModal: function( item, options, $event ){
this.$emit( 'open-modal', item, options, $event );
},
description: function( room ) {
// insert the logic used by openModal to set content here and return that string
// this.events[data.id].content or this.taxonomies[data.id]
return 'description...';
},
},
},
});

I can't set "disabled" parameter in Vue to be persistent

I can't set "disabled" parameter to be persistent. If I set disable: true inside data function, it seems that it doesn't do anything.
You can see inside mounted() that it calls checkCanVote() and in there at first the console.log says it is set to false, then I set it to true but on stars hover (star_over()) it is again false?
http://jsfiddle.net/7unqk49k/1/
Template
<div id="star-app" v-cloak class="col-md-4">
<star-rating value="<?php echo $this->rating_rounded; ?>"></star-rating>
</div>
<template id="template-star-rating">
<div class="star-rating">
<label class="star-rating__star" v-for="rating in ratings" :class="{'is-selected': ((value >= rating) && value != null), 'is-disabled': disabled}" #mouseover="star_over(rating)" #mouseout="star_out">
<input class="star-rating star-rating__checkbox" type="radio" :name="name" :disabled="disabled" :id="id" :required="required" v-model="value" #click="set(rating)"> ★
</label>
</div>
</template>
JS
Vue.component('star-rating', {
template: '#template-star-rating',
data: function data() {
return {
value: null,
temp_value: null,
ratings: [1, 2, 3, 4, 5],
disabled: true
};
},
props: {
'name': String,
'value': null,
'id': String,
'disabled': Boolean,
'required': Boolean
},
methods: {
star_over: function star_over(index) {
console.log(this.disabled);
if (this.disabled == true) {
return;
}
this.temp_value = this.value;
this.value = index;
},
star_out: function star_out() {
if (this.disabled == true) {
return;
}
this.value = this.temp_value;
},
set: function set(value) {
if (this.disabled == true) {
return;
}
this.temp_value = value;
this.value = value;
// On click disable - this works
this.disabled = true;
},
checkCanVote: function() {
console.log('Inside checkCanVote');
console.log(this.disabled);
this.disabled = true;
console.log(this.disabled);
}
},
mounted() {
this.checkCanVote();
}
});
new Vue({
el: '#star-app'
});
The actual problem is in naming things.Try to avoid to use same naming for the props that you pass to child component and data properties that you've declared in child component.
To get this working as expected, you should just remove the prop declaration for disabled prop and It should work.
http://jsfiddle.net/9debdkh1/