Earlier this week, an old friend of mine and all around good/smart guy Ben Nadel wrote up his experience on building a "dual select" control in AngularJS: "Managing Selections With A Dual-Select Control Experience In Angular 9.1.9". If you aren't aware, a "dual select" control is one where two vertical columns of information are presented and the user can move items from one side to another. Ben had a great animated GIF on his blog entry that he was cool with me sharing:
I've built these types of controls before but had not yet attempted to build it in Vue.js. With that mind, this weekend I worked on an example of it - both in a simple Vue.js application and also as a component version. While I'm sure this could be done differently (and I'd love to see examples in the comments below!), here's how I built it.
Version One
As stated above, I built my first version in a simple application. For thise I made use of CodePen which has recently added Vue SFC (Single File Component) support to their site. While not necessary for my demo I thought I'd give it a try for this first example. I began by building out my HTML. I knew I'd need two select controls with the multiple
attribute and two buttons between them. One to move items to the right and one to move them back to the left.
My initial demo data consisted of an array of users, but to be clear this was arbitrary:
leftUsers: [
"Raymond Camden",
"Lindy Camden",
"Jacob Camden",
"Lynn Camden",
"Jane Camden",
"Noah Camden",
"Maisie Camden",
"Carol Camden",
"Ashton Roberthon",
"Weston Camden"
],
I rendered the left select like so:
<h2>Possible Users</h2>
<select multiple v-model="leftSelectedUsers" @dblclick="moveRight">
<option v-for="user in leftUsers">
{{ user }}
</option>
</select>
Note that my option tags are iterating over my data but my v-model is connected to another value, leftSelectedUsers
. The point of that is to let me have an array of "initial" data and an array representing values selected in the control. That value will be an array whether I pick one or more options.
The right side looks pretty similar:
<h2>Selected Users</h2>
<select multiple v-model="rightSelectedUsers" @dblclick="moveLeft">
<option v-for="user in rightUsers">
{{ user }}
</option>
My two buttons in the middle simply fired off respective calls to move data:
<button @click="moveRight">=></button>
<button @click="moveLeft"><=</button>
You'll notice I also use the "double click" event. This makes it easier to move one item quickly by just quickly clicking on an individual user. Alright, let's check out the JavaScript:
export default {
data() {
return {
leftSelectedUsers:[],
leftUsers: [
"Raymond Camden",
"Lindy Camden",
"Jacob Camden",
"Lynn Camden",
"Jane Camden",
"Noah Camden",
"Maisie Camden",
"Carol Camden",
"Ashton Roberthon",
"Weston Camden"
],
rightSelectedUsers:[],
rightUsers:[]
};
},
methods: {
moveLeft() {
if(!this.rightSelectedUsers.length) return;
console.log('moveLeft',this.rightUsers);
for(let i=this.rightSelectedUsers.length;i>0;i--) {
let idx = this.rightUsers.indexOf(this.rightSelectedUsers[i-1]);
this.rightUsers.splice(idx, 1);
this.leftUsers.push(this.rightSelectedUsers[i-1]);
this.rightSelectedUsers.pop();
}
},
moveRight() {
if(!this.leftSelectedUsers.length) return;
console.log('moveRight', this.leftSelectedUsers);
for(let i=this.leftSelectedUsers.length;i>0;i--) {
let idx = this.leftUsers.indexOf(this.leftSelectedUsers[i-1]);
this.leftUsers.splice(idx, 1);
this.rightUsers.push(this.leftSelectedUsers[i-1]);
this.leftSelectedUsers.pop();
}
}
}
};
In both cases, I check first to see if anything has been selected. If so, I consider it an array and loop from the end of the array to the beginning. I do this because I'm going to be removing items from the array as I process them. The logic basically boils down to - for each of the selected items, I remove them from one array and add them to the other. Honestly that one part was the hardest for me. But that's it, and you can see it working below:
See the Pen Vue Duel Select by Raymond Camden (@cfjedimaster) on CodePen.
Version Two
Alright, so for the second version, I wanted to turn the above into a proper Vue component. I could have gone crazy with the number of options and arguments it took to allow for deep customization, but I decided to keep things simple and limit your options to:
- The name of the left column.
- The data in the left column.
- The name of the right column.
- The data in the right column.
Because CodePen can't (as far as I know) work with multiple SFCs in one pen, I decided to switch to CodeSandbox. On their platform, I created my component and set it up to support the parameters above. Here it is in it's entirety.
<template>
<div id="app" class="container">
<div>
<h2>{{leftLabel}}</h2>
<select multiple v-model="leftSelectedData" @dblclick="moveRight">
<option v-for="item in leftData">{{ item }}</option>
</select>
</div>
<div class="middle">
<button @click="moveRight">=></button>
<button @click="moveLeft"><=</button>
</div>
<div>
<h2>{{rightLabel}}</h2>
<select multiple v-model="rightSelectedData" @dblclick="moveLeft">
<option v-for="item in rightData">{{ item }}</option>
</select>
</div>
</div>
</template>
<script>
export default {
data() {
return {
leftSelectedData: [],
rightSelectedData: []
};
},
props: {
leftLabel: {
type: String,
required: true
},
rightLabel: {
type: String,
required: true
},
leftData: {
type: Array,
required: true
},
rightData: {
type: Array,
required: true
}
},
methods: {
moveLeft() {
if (!this.rightSelectedData.length) return;
for (let i = this.rightSelectedData.length; i > 0; i--) {
let idx = this.rightData.indexOf(this.rightSelectedData[i - 1]);
this.rightData.splice(idx, 1);
this.leftData.push(this.rightSelectedData[i - 1]);
this.rightSelectedData.pop();
}
},
moveRight() {
if (!this.leftSelectedData.length) return;
for (let i = this.leftSelectedData.length; i > 0; i--) {
let idx = this.leftData.indexOf(this.leftSelectedData[i - 1]);
this.leftData.splice(idx, 1);
this.rightData.push(this.leftSelectedData[i - 1]);
this.leftSelectedData.pop();
}
}
}
};
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
color: #2c3e50;
margin-top: 60px;
}
.container {
display: grid;
grid-template-columns: 30% 10% 30%;
align-items: center;
}
.container select {
height: 200px;
width: 100%;
}
.container .middle {
text-align: center;
}
.container button {
width: 80%;
margin-bottom: 5px;
}
</style>
It's roughly the same as what I showed above (although this time you can see my lovely CSS styling), but with variables names that are a bit more abstract. Also note the use of the four props to pass in data. This then allows me to do this in a higher level component:
<DualSelects
leftLabel="Available Users"
rightLabel="Chosen Users"
:leftData="leftUsers"
:rightData="rightUsers"
></DualSelects>
Which frankly I think is freaking cool. By binding the data I can now simply set/get the left and right side at will and let the user customize whats in each list. Here's the CodeSandbox version:
As I said above, I'm sure there is a nicer way to build this and I absolutely wouldn't mind seeing examples below, and finally, thank you again Ben for the inspiration!
Header photo by Levi Stute on Unsplash
Archived Comments
Looks pretty good to me. And, by using the _native_ `select` element, you get so much stuff for free (like being able to click-n-drag across multiple `option` elements). You gotta love two-way data-binding in forms. It makes life so much easier.
I've been using CF since the Allaire days. And got into coldbox about a year ago. Now I'm itching to learn Vue. I'm currently using bootstrap but need to add a dual select - can this component be used in a bootstrap app? And how would I embed it?
It can be used anywhere Vue would be used. As for embedding it, it depends on if you are building a complete SPA or just looking to enhance a page. I guess the main answer is yes, you can embed it, but how depends on what you already have in place.
I am just hoping to enhance the page. It's straight html/bootstrap at the moment.
Then using it would be like any use of Vue. Add the script tags and then the code. If you are not familiar with Vue than I'd recommend looking over the basics first. I'm giving a course next week on the topic (https://cfe.dev/events/vue-....
I registered for the course.
Please make sure to cover hybrid situations where we can use vue components mixed with standard bootstrap/html to ease our way into vue development ;)
I'm not covering that, I can tell you now, but I do talk about Vue in progressive enhancement and Vue in apps. To be clear, using Vue on a page that uses Bootstrap really isn't that different. Outside of the dynamic aspects, Bootstrap is just CSS and markup. Vue won't care about that.