Custom multi-select and single select issue - javascript

i have created a component which works perfect apart from binding the a string rather than an array. The event's fire correctly.
So on the multi-select version if I select and deselect items the multiSelected variable is updated correctly. However when selecting another value within the single select the singleSelected variable isn't being changed, however the same event is firing.
Dummed down the code here so you can just see the logic and issue:
Vue.component('dropdown', {
template: `<div class="dropdown">
<label v-for="item in items" :key="item.value" :class="{selected:selected.indexOf(item.value) > -1}">
{{ item.label }}
<input type="checkbox" :value="item.value" :checked="selected.indexOf(item.value) > -1" #change="selected = $event" />
</label>
</div>`,
props: [ 'value', 'items', 'multiSelect' ],
computed: {
selected: {
get: function() {
if (this.value === undefined) {
return [];
}
if (!Array.isArray(this.value)) {
return [ this.value ];
}
return this.value;
},
set: function($event) {
let current = this.selected;
if (!this.multiSelect) {
current = $event.target.value;
}
if (this.multiSelect && !$event.target.checked) {
const index = current.indexOf($event.target.value);
if (index > -1) {
current.splice(index, 1)
}
}
if (this.multiSelect && $event.target.checked) {
current.push($event.target.value);
}
console.log(current);
this.$emit('value', current);
}
}
}
});
Vue.component('wrapper', {
template: `
<div>
Single
<dropdown :items="items" v-model="singleSelected" :multi-select="false" name="single" />
<br />
Multi
<dropdown :items="items" v-model="multiSelected" :multi-select="true" name="multi" />
<p>Models</p>
<p>singleSelected: {{ singleSelected }}</p>
<p>multiSelected: {{ multiSelected }}</p>
</div>
`,
data() {
return {
items: [{value:'bmw',label:'BMW',count:1},{value:'audi',label:'Audi',count:1},{value:'kia',label:'KIA',count:1}],
multiSelected: ['kia'],
singleSelected: 'kia',
}
}
});
new Vue().$mount('#app');
.dropdown {
border: 1px solid black;
padding: 10px;
display: block;
}
label {
margin: 5px;
}
.selected {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper>
</wrapper>
</div>

v-model on custom component expects that the component will emit input event, not value
Only reason why your component is (sort of) working in multi-select mode is because you are directly mutating the input array passed to you by a value prop (in other words the emitted event is useless). Don't do that. Create a new array instead before emitting
Also, there is one more issue with single-select mode - after deselecting only checked box, you still emitting the value so the model is set to a value even none of the checkboxes is checked
Personally I find your design strange for several reasons:
Using different v-model "type" (array or string) feels unnatural
In single-select mode it behaves as radio so why not use radio
What will happen if you switch multiSelect prop at runtime?
Vue.component('dropdown', {
template: `<div class="dropdown">
<label v-for="item in items" :key="item.value" :class="{selected:selected.indexOf(item.value) > -1}">
{{ item.label }}
<input type="checkbox" :value="item.value" :checked="selected.indexOf(item.value) > -1" #change="selected = $event" />
</label>
</div>`,
props: [ 'value', 'items', 'multiSelect' ],
computed: {
selected: {
get: function() {
if (this.value === undefined) {
return [];
}
if (!Array.isArray(this.value)) {
return [ this.value ];
}
return this.value;
},
set: function($event) {
if(this.multiSelect) {
if (!$event.target.checked) {
this.$emit('input', this.selected.filter(v => v !== $event.target.value))
} else {
this.$emit('input', [...this.selected, $event.target.value])
}
} else {
this.$emit('input', $event.target.checked ? $event.target.value : "")
}
}
}
}
});
Vue.component('wrapper', {
template: `
<div>
Single
<dropdown :items="items" v-model="singleSelected" :multi-select="false" name="single" />
<br />
Multi
<dropdown :items="items" v-model="multiSelected" :multi-select="true" name="multi" />
<p>Models</p>
<p>singleSelected: {{ singleSelected }}</p>
<p>multiSelected: {{ multiSelected }}</p>
</div>
`,
data() {
return {
items: [{value:'bmw',label:'BMW',count:1},{value:'audi',label:'Audi',count:1},{value:'kia',label:'KIA',count:1}],
multiSelected: ['kia'],
singleSelected: 'kia',
}
}
});
new Vue().$mount('#app');
.dropdown {
border: 1px solid black;
padding: 10px;
display: block;
}
label {
margin: 5px;
}
.selected {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper>
</wrapper>
</div>

Related

In Vue 2 how to have only 1st input type checkbox checked in v-for loop

Thanks for having look I'm kinda stuck.Trying to figure out how to have first checkbox rendered checked by default.
Here is my JS the categories are coming in dynamically
Vue.component('category-filter', {
template: '#category-filter-template',
props: {
appMounted: false,
},
data() {
return {
categories: {},
checkedState: false,
};
},
methods: {
handleCheckboxClicked(e) {
console.log({ e });
},
},
mounted() {
this.appMounted = true;
this.categories =jsContext.categories
},
});
Here is my template I have choose to make styles inline to make component more reusable
<div
class="filter-container--wrapper"
style="
display: -webkit-box;
display: -ms-flexbox;
display: flex;
flex-wrap: wrap;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
margin-bottom: 2rem;
color: #fff;
background-color: #5676a7;
border-radius: 5px;
"
>
<div
id="filter-item"
style="padding: 15px"
v-for="category in categories"
:key="category.id"
>
<input
id="category-name"
type="checkbox"
#click="handleCheckboxClicked($event)"
:value="category.id"
:checked="checkedState"
/>
<label for="category-name">
<span>\{{category.name}}</span>
</label>
</div>
</div>
You can initialy define the value as checked by setting it when you setup the data for the model:
this.categories = Array.from(jsContext.categories || []).map((v,i) => (v.checked = !i, v));
Various issues:
You should use a model instead of :value, then change the model to checked.
Don't mutate props!
If categories is an array then set it as array in data, not object.
Is better to use computed props for the inline style, or if possible always put it in your CSS file or in <style>, you can scope it #category-filter-template .filter-container--wrapper {} if you don't want it conflicting.
<template id="category-filter-template">
<div class="filter-container--wrapper" :style="wrapperStyle">
<div
id="filter-item"
:style="itemStyle"
v-for="category in categories"
:key="category.id"
>
<input
id="category-name"
type="checkbox"
v-model="category.checked"
:checked="category.checked"
/>
<label for="category-name">
<span>\{{category.name}}</span>
</label>
</div>
</div>
</template>
Then your component:
Vue.component('category-filter', {
template: '#category-filter-template',
data() {
return {
categories: []
};
},
computed: {
wrapperStyle () {
return {
'display': 'flex',
'flex-wrap': 'wrap',
'-webkit-box-pack': 'center',
'-ms-flex-pack': 'center',
'justify-content': 'center',
'margin-bottom': ' 2rem',
'color': '#fff',
'background-color': ' #5676a7',
'border-radius': ' 5px'
}
},
itemStyle () {
return {
'padding': '15px'
}
}
},
mounted() {
this.categories = Array.from(jsContext.categories || []).map((v,i) => (v.checked = !i, v))
},
})
See working online: https://playcode.io/847454/
You should set the source of truth in your model, not in your rendering.
You should have something like
mounted() {
this.categories[0].id=true;
}
however, it's not clear what the structure of categories is. Is it an array? If so, you should initialized it as an empty array instead of an object. Also, if you should probably be using v-model instead of :value so that changes in checked status are saved in the model. Finally, I'm not sure if you want the model to be linked to the id attribute. Probably there is another attribute that you want to bind.

How to highlight selected item with multiple lists to choose from in vue js

Im working with Vue js and I have multiple lists rendered but i only want to select(highlight) one element. At the moment multiple items are highlighted with each click. Hope that makes sense. Here is my code snipets below
<template>
<div>
<div class='list-group'>
<a v-for='(product, idx) in adslCapped' class='list-group-item'
v-on:click='toggleActiveIndex(idx)'
:class="{'active': idx == activeIndex}" >
{{product.name}}
</a>
</div>
<div class='list-group'>
<a v-for='(product, idx) in fibre' class='list-group-item'
v-on:click='toggleActiveIndex(idx)'
:class="{'active': idx == activeIndex}" >
{{product.name}}
</a>
</div>
</div>
</template>
data: {
activeIndex: null
},
methods: {
toggleActiveIndex: function(index){
this.activeIndex = index
}
}
So as you can see I have two lists, but when I click on the first item of the first list, then it highlights the first item in both lists. PLease note that these are only code snippets in relation to the issue i'm having.
With your current app structure, if you want only one item to be highlighted among all the lists, you can add another variable which represents the active list.
Then you need to change the condition for the active class and check if the index is the active index AND if the list is the active list.
HTML
<div id="app">
<div class='list-group'>
<a v-for='(product, idx) in adslCapped' class='list-group-item'
v-on:click='toggleActiveIndex(adslCapped, idx)'
:class="{'active': idx == activeIndex && adslCapped == activeList}" >
{{product}}
</a>
</div>
<div class='list-group'>
<a v-for='(product, idx) in fibre' class='list-group-item'
v-on:click='toggleActiveIndex(fibre, idx)'
:class="{'active': idx == activeIndex && fibre == activeList}" >
{{product}}
</a>
</div>
</div>
Script
new Vue({
el: "#app",
data() {
return {
activeIndex: null,
activeList: null,
adslCapped: ['a', 'b', 'c'],
fibre: ['1244', '125215', '02150']
}
},
methods: {
toggleActiveIndex: function(list,index){
this.activeIndex = index;
this.activeList = list;
}
},
})
Demo here
Hope it helps!
I know this is overkill for this simple case but you can abstract list into a component, store selected products separately.
Take a look at the example.
var productList = Vue.component('product-list', {
props: {
value: Object,
products: Array
},
template: `
<div class="product-list"><a class="ist-group-item" :class="{'active': product.id === value?.id}" #click="selectProduct(product)" :key="product.id" v-for="product in products">{{product.name}}</a></div>`,
methods: {
selectProduct(product) {
this.$emit('input', product)
}
}
})
var app = new Vue({
el: "#app",
data() {
return {
selected1: null,
selected2: null,
products: Array(5).fill(0).map((pr, id) => ({
id: id + 1,
name: `Product ${id + 1}`
}))
}
},
mounted() {
//console.log(this.products)
},
methods: {}
});
.product-list {
border: 1px solid black;
padding: 5px;
margin-top: 5px;
}
.ist-group-item {
display: block;
transition: all .3s ease-in;
}
.ist-group-item:hover {
background: lightgray;
}
.ist-group-item.active {
background: black;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<product-list v-model="selected1" :products="products">
</product-list>
<product-list v-model="selected2" :products="products">
</product-list>
</div>
The issue is that activeIndex is only one variable - which can't be reused for setting the index in the both lists.
The issue is that - you should be able to separate which list you want to activate when clicking within the v-for loop.
new Vue({
el: "#app",
data: {
list1: [{
name: "Learn JavaScript",
done: false
},
{
name: "Learn Vue",
done: false
},
{
name: "Play around in JSFiddle",
done: true
},
],
list2: [{
name: "Learn JavaScript",
done: false
},
{
name: "Learn Vue",
done: false
},
{
name: "Play around in JSFiddle",
done: true
},
],
active1Index: null,
active2Index: null
},
methods: {
toggleActive: function(list, index) {
if (list === "list1") {
this.active1Index = index
} else {
this.active2Index = index
}
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
del {
color: rgba(0, 0, 0, 0.3);
}
.active {
background: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class='list-group'>
<a v-for='(product, idx) in list1' class='list-group-item' v-on:click='toggleActive("list1", idx)' :class="{'active': idx == active1Index}">
{{product.name}}
</a>
</div>
<div class='list-group'>
<a v-for='(product, idx) in list2' class='list-group-item' v-on:click='toggleActive("list2", idx)' :class="{'active': idx == active2Index}">
{{product.name}}
</a>
</div>
</div>
Thanks everyone who answered you guys on stackoverflow are the best. Julia's solution is the one that worked for me.
I have to implement it throughout my whole app and lists now.
Thanks Julia

How to conditionally append an element in a scoped Vue Component?

I am trying to create a Component for titles that can be edited when they get double clicked.
The Component takes the h-tag that should be used and the title as props and should produce a normal h-tag, that turns into an input field once double clicked.
This already works if there is only one title on the page, however once there are multiple Components used on one page, it breaks as the Component is not scoped properly. But I can't figure out how.
Here is the code:
<template>
<div class="edit-on-click">
<input
:class="sizeClass"
type="text"
v-if="edit"
v-model="editedTitle"
#blur="finishEdit"
#keyup.enter="finishEdit"
v-focus="true"
/>
<span v-show="!edit" #dblclick.prevent="edit = true"></span>
</div>
</template>
The mounted hook I can't figure out how to scope:
mounted() {
let node = document.createElement(this.size); // Takes h-tag (h1, h2 etc.)
let titleText = document.createTextNode(this.finalTitle); // Takes title
node.appendChild(titleText);
node.classList.add("editable-title");
// This breaks the code once there are multiple components in the document
document.getElementsByTagName("span")[0].appendChild(node);
},
How can I scope this in an efficient way? Thank you very much in advance!
Well, with Vue, you'll probably want to avoid creating DOM elements the "native" way whenever possible, as you might run into race condition where Vue is unaware of the existence of these elements which you probably want be reactive at some point in time (in your case, the <span> double-clicking).
What you could do instead, is perhaps to dynamically "switch between" these different headings with this <component> and the v-bind:is prop. Consider the following example:
Vue.component('EditableHeading', {
template: '#editable-heading',
props: {
size: {
type: String,
default: 'h1'
},
value: {
type: String,
required: true
}
},
data() {
return {
editing: false
}
},
methods: {
confirm(e) {
this.$emit('input', e.target.value);
this.close();
},
start() {
this.editing = true;
this.$nextTick(() => {
this.$el.querySelector('input[type="text"]').select();
});
},
close() {
this.editing = false;
}
}
})
new Vue({
el: '#app',
data: () => ({
titleList: [],
text: 'New Title',
size: 'h3'
}),
methods: {
addNewTitle() {
this.titleList.push({
text: this.text,
size: this.size
});
}
}
})
.edit-on-click {
user-select: none;
}
.heading-size {
margin-top: 1rem;
width: 24px;
}
p.info {
background-color: beige;
border: 1px solid orange;
color: brown;
padding: 4px 5px;
margin-top: 2rem;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<editable-heading
v-for="(title, index) of titleList" :key="index"
v-model="title.text"
:size="title.size">
</editable-heading>
<div>
<label>
Heading size:
<input v-model="size" class="heading-size" />
</label>
</div>
<div>
<label>
Title:
<input v-model="text" />
</label>
</div>
<div>
<button #click="addNewTitle()">Add new title</button>
</div>
<p class="info">
[double-click]: Edit <br />
[enter]: Confirm <br />
[esc/mouseleave]: Cancel
</p>
</div>
<script id="editable-heading" type="text/x-template">
<div class="edit-on-click">
<input
type="text"
v-if="editing"
:value="value"
#blur="close"
#keydown.enter="confirm"
#keydown.esc="close" />
<component :is="size" v-else #dblclick="start">{{value}}</component>
</div>
</script>

How can I handle events in datalist options in vuejs?

I have a requirement where I have to suggest in the data list and when a user selects any of the datalist options, I have to update other input fields accordingly.
Here is my input field and Datalist code.
<input type="text" v-model="party.name" class="form-control form-control-sm shadow-sm" #input="searchPartyByName()" placeholder="Party name" list="queriedParties"/>
<datalist id="queriedParties">
<option v-for="party in queriedParties">{{party.name}}</option>
</datalist>
Now, what I want is, When a user hits enter or click on specific data list option, I want to update my this input field (Which is by default with data list) but I also want to set other form fields.
I have bound other form fields with my party data object. So, Only if I can update my party data object by any event on datalist option, I will be happy! I want something like this.
<option v-for="party in queriedParties" #click="setParty(party)">{{party.name}}</option>
I already tried the above-given example but it's not working. I also tried with #change but it's not working too!
Is there any way to accomplish this? I checked almost all the articles, jsfiddles and codepens available but none of them solves my issue.
datalist doesn't have events but the input does. You should do the following:
<template>
<input type="text" v-model="party.name" .... />
<datalist id="queriedParties">
<option v-for="party in queriedParties">{{party.name}}</option>
</datalist>
</template>
<script>
export default {
watch: {
party: {
deep: true,
handler (old_party, new_party) {
if (old_party.name !== new_party.name) this.searchPartyByName(new_party.name)
}
}
}
</script>
It seems that your queriedParties is an array of objects. Does it work if you have just an array of strings?
For objects use something along these lines:
<template>
<div class="sourceselection">
<div>
<div class="jumbotron">
<h2><span class="glyphicon glyphicon-list-alt"></span> News List</h2>
<h4>Select News Source</h4>
<input v-model="source" list="newssources-list" v-on:input="sourceChanged"
name="source-selection" id="source-selection" class="form-control"
placeholder="Please specify news source ..."/>
<datalist id="newssources-list">
<option v-for="source in sources" v-bind:value="source.name" v-bind:label="source.name"></option>
</datalist>
<div v-if="deepSource">
<h6>{{deepSource.description}}</h6>
<a v-bind:href="deepSource.url" class="btn btn-primary" target="_blank">Go To {{deepSource.name}} Website</a>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'sourceselection',
data () {
return {
sources: [],
source: '',
deepSource: ''
}
},
methods: {
sourceChanged: function(e) {
console.log("source = "+this.source+" new value = "+e.target.value);
var newSource = e.target.value;
// only action if value is different from current deepSource
if (newSource!= this.deepSource.name) {
for (var i=0; i<this.sources.length; i++) {
if (this.sources[i].name == newSource) {
this.deepSource = this.sources[i];
this.source = this.deepSource.name;
}
}
this.$emit('sourceChanged', this.deepSource.id);
}
}
},
created: function () {
var api = "https://newsapi.org/v1/sources?language=en";
this.axios.get(api).then((response) => {
this.sources = response.data.sources;
});
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
There is no event in datalist, so you can't handle, you'd better write your own list. Here is a example open in codepen:
//pug
#app
.form-group.has-feedback
input.input-search.form-control(type='text', v-model='word', placeholder='Search')
ul#list(v-if='Object.keys(filtered_projects).length > 0')
li(v-for='(value, key) in filtered_projects', #click='gotoProjectPage(key)')
span {{value}}
p {{key}}
span.glyphicon.glyphicon-search.form-control-feedback
/*css*/
body {
margin: 10px;
}
#app {
width: 400px;
}
#list {
font-size: 12px;
list-style: none;
margin: 0;
padding: 5px 0;
background-color: white;
border-radius: 0 0 5px 5px;
border: 1px #ccc solid;
}
#list li {
display: block;
padding: 5px 15px;
}
#list li:hover {
background-color: #ccc;
}
#list li span {
font-weight: 550;
}
#list li p {
margin: 5px 0 0;
}
//js
var app = new Vue({
el: '#app',
data: {
word: '',
projects: {"DataCenterMetro":"TEST1","IFF_Handway":"国际香料","SPH_Handway":"上药控股广东有限公司空调系统","QingTang_GZ":"广州地铁_清塘站","BTE_Handway":"白天鹅宾馆","NSSC_SZ":"深圳地铁_南山书城站","TA0301_Handway":"天安云谷二期"}
},
computed: {
filtered_projects: function () {
var vm = this, result = {};
if (vm.word) {
for(var key in vm.projects) {
if(key.toLowerCase().indexOf(vm.word) != -1 || vm.projects[key].toLowerCase().indexOf(vm.word) != -1)
result[key] = vm.projects[key];
}
}
return result;
}
},
created: function () {
var vm = this;
//TODO get projects
},
methods: {
gotoProjectPage: function (key) {
console.log('/map_login?project=' + key);
//TODO handle
}
},
});

Filter core-list elements by string match

Simple filter that will hide any item in core-list that doesn't match the entered string. The filtered elements will have property 'hidden', hence they should not take any space... Obviously there is something wrong and probably core-list has something to do with it. Also when filter is applied and scrolling down then go back up reveals all the elements again :/ Any idea of how can make this filter working with core-list? For this sample i've made the filter to match the name for every list item.
<script src="https://www.polymer-project.org/components/webcomponentsjs/webcomponents.js"></script>
<link rel="import" href="https://www.polymer-project.org/components/core-list/core-list.html">
<my-element></my-element>
<polymer-element name="my-element" attributes="">
<template>
<style>
:host {
display: block;
}
:host core-list {
margin: 8px 0;
height: 400px;
width: 350px;
}
:host core-list div {
border: 1px solid #008000;
}
</style>
<label for="s">Search:</label>
<input id="s" value="{{ filtervalue }}">
<core-list id="list" data="{{ arr }}" height="50">
<template>
<div hidden?="{{ filtervalue | filter(model) }}">
Name: {{ model.name }}, Index: {{ index }}, Selected: {{ selected }}
</div>
</template>
</core-list>
</template>
<script>
(function() {
function genData() {
var arr = [];
for (var i = 0; i < 1000; i++) {
arr.push({
name: "sample" + i,
pos: i
});
}
return arr;
}
Polymer('my-element', {
created: function() {
this.arr = genData();
},
ready: function() {},
filter: function(v, model) {
if (!v) return false;
if (model) {
console.log("v: %o\n%o", v, model.name);
return model.name.indexOf(v) < 0;
}
}
});
})();
</script>
</polymer-element>
Here it goes as an answer:
Here are some filters that can help: polymer-filters/filter-startsWith.js
Filters are referenced from Polymer expressions examples.

Categories

Resources