DEV Community

Raymond Camden
Raymond Camden

Posted on • Originally published at raymondcamden.com on

Building Three Common Form Interfaces in Vue.js

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:

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>
Enter fullscreen mode Exit fullscreen mode

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:''});
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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:

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>
Enter fullscreen mode Exit fullscreen mode

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];
        }
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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.

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>
Enter fullscreen mode Exit fullscreen mode

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

  }
})
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
straleb profile image
Strahinja Babić • Edited

Awesome, i recently finished lessons on vue.js i keep learning more from seing projects like this and the approach other devs have.
For now Vue does not stop amazing me :)

Great article

Collapse
 
raymondcamden profile image
Raymond Camden

I'm happy you liked it. I only recently started using Dev.to. You can find many more Vue articles (mainly me learning) at raymondcamden.com.

Collapse
 
straleb profile image
Strahinja Babić

Awesome, will surely check it out when i can, checking the approach of other devs is my main interest.
Will look forward to it :)