Today I wanted to share three simple (mostly simple) Vue.js samples that demonstrate some common form UX patterns. In each case, I fully expect that there are probably existing Vue components I could have used instead, but as always, I'm a firm believer in building stuff yourself as a way to practice what you learn. So with that in mind, let's get started!

Duplicating Fields #

For the first demo, I'll show an example of a form that lets you "duplicate" a set of fields to enter additional data. That may not make much sense, so let's start with the demo first so you can see what I mean:

See the Pen Form - Duplicate Row by Raymond Camden (@cfjedimaster) on CodePen.

The form consists of two parts. On top is a set of basic, static fields. On bottom is a place where you can enter information about your friends. Since we don't know how many friends you may have, a field is used to add additional rows. Let's look at the markup for this.

<form id="app">

  <fieldset>
    <legend>Basic Info</legend>
    <p>
      <label for="name">Name</label>
      <input id="name" v-model="name">
    </p>

    <p>
      <label for="age">Age</label>
      <input id="age" v-model="age" type="number">
    </p>
  </fieldset>

  <fieldset>
    <legend>Friends</legend>

    <div v-for="(f,n) in friends">
      <label :for="'friend'+n">Friend {{n+1}}</label>
      <input :id="'friend'+n" v-model="friends[n].name">
      <label :for="'friendage'+n">Friend {{n+1}} Age</label>
      <input :id="'friendage'+n" v-model="friends[n].age" type="number">
    </div>
    
    <p>
      <button @click.prevent="newFriend">Add Friend</button>
    </p>
  </fieldset>
  
  <p>Debug: {{friends}}</p>
</form>

The top portion is vanilla Vue binding. The bottom part is where the interesting bits are. First, I iterate over a list of friends. This is what "grows" when the button is clicked. Note the use of (f,n). This gives me access to each friend as well as a counter. It's a zero based number so when I render it, I add one to it. Also note how I properly use my label with a dynamic ID value: :id="'friend'+n". That was a bit weird to write at first, but it works well.

The JavaScript is pretty simple:

const app = new Vue({
  el:'#app',
  data:{
    name:null,
    age:null,
    friends:[{name:'',age:''}]
  },
  methods:{
    newFriend() {
      //New friends are awesome!
      this.friends.push({name:'', age:''});
    }
  }
})

The only real interesting part here is defaulting friends with the first set of values so I get at least Friend 1 in the UI.

Shipping Same as Billing #

The next UX I wanted to build was something you typically see in order checkouts, "Shipping Same as Billing" (or vice-versa). Basically, letting the user skip entering the same address twice. Here is the finished demo:

See the Pen Shipping same as Billing in Vue.js by Raymond Camden (@cfjedimaster) on CodePen.

I thought this would be simple, and I suppose it was, but I wasn't necessarily sure how it should react once the checkbox was checked. What I mean is, if you say shipping is the same, should we always update? By that I mean, if you change the billing street, do we update the shipping street again? But what if you modified the shipping street? Should we disable shipping if you use the checkbox? But what if you wanted to use this feature to set most of the fields and then tweak one? Yeah, it gets messy. I decided to KISS and just do a copy (if you are checking it) and then don't worry about it. I'm sure there's an argument to be made that I'm totally wrong. Here's the markup:

<form id="app">
  <fieldset>
    <legend>Billing Address</legend>
    
    <p>
      <label for="bstreet">Street</label>
      <input id="bstreet" v-model="billing_address.street">
    </p>
    
    <p>
      <label for="bcity">City</label>
      <input id="bcity" v-model="billing_address.city">
    </p>
    
    <p>
      <label for="bstate">State</label>
      <select id="bstate" v-model="billing_address.state">
        <option value="ca">California</option>
        <option value="la">Louisiana</option>
        <option value="va">Virginia</option>
      </select>
    </p>
 
    <p>
      <label for="bzip">Zip</label>
      <input id="bzip" v-model="billing_address.zip">
    </p>

  </fieldset>

  <fieldset>
    <legend>Shipping Address</legend>
    
    <input type="checkbox" @change="copyBilling" id="sSame" v-model="sSame"> <label for="sSame" class="sSame">Shipping Same as Billing</label><br/>
    
    <p>
      <label for="sstreet">Street</label>
      <input id="sstreet" v-model="shipping_address.street">
    </p>
    
    <p>
      <label for="scity">City</label>
      <input id="scity" v-model="shipping_address.city">
    </p>
    
    <p>
      <label for="sstate">State</label>
      <select id="sstate" v-model="shipping_address.state">
        <option value="ca">California</option>
        <option value="la">Louisiana</option>
        <option value="va">Virginia</option>
      </select>
    </p>
 
    <p>
      <label for="szip">Zip</label>
      <input id="szip" v-model="shipping_address.zip">
    </p>

  </fieldset>

  <!-- debug -->
  <p>
    sSame {{sSame}}<br/>
    Billing {{billing_address}}<br/>
    Shipping {{shipping_address}}
  </p>
  
</form>

And here's the JavaScript:

const app = new Vue({
  el:'#app',
  data:{
    sSame:false,
    billing_address:{
      street:null,
      city:null,
      state:null,
      zip:null
    },
    shipping_address:{
      street:null,
      city:null,
      state:null,
      zip:null
    }
    
  },
  methods:{
    copyBilling() {
      if(this.sSame) {
        for(let key in this.billing_address) {
          this.shipping_address[key] = this.billing_address[key];
        }
      }
    }
  }
})

The interesting bit is in copyBilling. I apologize for the sSame name - it kind of sucks.

Move Left to Right #

For the final demo, I built a "thing" where you have items on the left and items on the right and you click to move them back and forth. There's probably a better name for this and if you have it, leave a comment below. Here is the demo.

See the Pen Move Left to Right by Raymond Camden (@cfjedimaster) on CodePen.

What was tricky about this one is that the select fields used to store data only require you to select items when you want to move them. So I needed to keep track of all the items in each box, as well as when you selected. Here's the markup.

<form id="app">

  <div class="grid">
    <div class="left">
      <select v-model="left" multiple size=10>
        <option v-for="item in leftItems" :key="item.id" 
                :value="item">{{item.name}}</option>
      </select>
    </div>
    
    <div class="middle">
      <button @click.prevent="moveLeft">&lt;-</button>
      <button @click.prevent="moveRight">-&gt;</button>
    </div>
    
    <div class="right">
      <select v-model="right" multiple size=10>
         <option v-for="item in rightItems" :key="item.id" 
                :value="item">{{item.name}}</option>       
      </select>
    </div>
  </div>

  <!-- debug -->
  <p>
    leftItems: {{ leftItems}}<br/>
    left: {{ left}}<br/>
    rightItems: {{ rightItems }}<br/>
    right: {{ right }}
  </p>
  
</form>

And here's the JavaScript. This time it's a bit more complex.

const app = new Vue({
  el:'#app',
  data:{
    left:[],
    right:[],
    leftItems:[],
    rightItems:[],
    items:[
      {id:1,name:"Fred"},
      {id:2,name:"Ginger"},
      {id:3,name:"Zeus"},
      {id:4,name:"Thunder"},
      {id:5,name:"Midnight"}
    ]
    
  },
  created() {
    this.leftItems = this.items;
  },
  methods:{
    moveRight() {
      if(this.left.length === 0) return;
      console.log('move right');
      //copy all of this.left to this.rightItems
      //then set this.left to []
      for(let x=this.leftItems.length-1;x>=0;x--) {
        let exists = this.left.findIndex(ob => {
          return (ob.id === this.leftItems[x].id);
        });
        if(exists >= 0) {
          this.rightItems.push(this.leftItems[x]);
          this.leftItems.splice(x,1);
        }
      }
    },
    moveLeft() {
      if(this.right.length === 0) return;
      console.log('move left');
      for(let x=this.rightItems.length-1;x>=0;x--) {
        let exists = this.right.findIndex(ob => {
          return (ob.id === this.rightItems[x].id);
        });
        if(exists >= 0) {
          this.leftItems.push(this.rightItems[x]);
          this.rightItems.splice(x,1);
        }
      }
    }
    
  }
})

Basically on button click, I look at all the items, and for each, see if it exists in the list of selected items, and if so, it gets shifted eithe rleft or right. I feel like this could be a bit slimmer (I will remind folks once again that I'm a proud failed Google interviewee) but it worked. Remember you can fork my CodePens so I'd love to see a slicker version of this.

So - what do you think? Leave me a comment below with your suggestions, modifications, or bug fixes!

Header photo by rawpixel.com on Unsplash