Blog

Scott Steinbeck

March 28, 2016

Spread the word


Share your thoughts

Today we will be making a contact database that you can quickly and easily manage using ColdBox and Vue.js. We will be using bootstrap in our project to make it the UI look a little better but it is completely optional if you want to use this in your own project.

For this project I will be using CommandBox to generate all my files.

TL;DR: View the repo here

Lets Begin.

Step 1: You can skip this step if you already have a project set up. 

From CommandBox run:

coldbox create app name=CBVue skeleton=rest --installColdBox

This will give us a minimal project with a handlers\BaseHandler.cfc (needed to make our life easy when creating a REST API) and an handlers\Echo.cfc which is an example usage to get you started.

Now that we have our project started we need to tweak a few things.

First, since this is a template that is expecting to be setup for REST only, the Echo.cfc is set to be the default entry point. Since we want to create a view that accesses a REST API we need to point that to a view.

Step 2: Create you default Layout & View.

coldbox create layout Main

This is going to create our default layout. A layout is the outer template that you content will go inside.
The layout will be created in layouts/Main.cfm. Navigate to that folder and replace the content with this:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Coldbox & Vue.js</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    <script type="text/javascript" src="http://cdn.jsdelivr.net/vue/1.0.18/vue.min.js"></script>
    <script type="text/javascript" src="https://rawgit.com/vuejs/vue-resource/master/dist/vue-resource.min.js"></script>
  </head>
  <body>
    <main>
	<cfoutput>#renderView()#</cfoutput>
    </main>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
  </body>
</html>

This contain some boilerplate code to get our page setup and includes all of our libraries that will be needed. Again, the only libraries needed for this project are vue.js & vue-resource, everything else is just for UI.

coldbox create view main\index --helper

This will create 2 files: view/main/index.cfmview/main/indexHelper.cfm.

index.cfm will contain the HTML table and form we will be interacting with. indexHelper.cfm (a file automatically included when we load index.cfm)  will contain all of our code for vue.js. The helper file is a connivence provided by ColdBox to help organize your code into separate files.

Now before we go too far, we need to change the default entry point to be our new index so that we will see the correct view. Navigate to config/ColdBox.cfc. In our settings we are going to change:

defaultEvent = "Main.index",

From this point is is time to start your server 

start --rewritesEnable --force

As a side note if your on CommandBox latest stable version you can also create a server.json file in the root of your project which will enable you to store all of your settings so when you want to start your server up in CommandBox you just type:

start

I have included my server.json in the repo if you want to take a look.

Step 3. Creating the REST API

We need a place to store our data so we will use a model, since we have been using CommandBox to generate our files we will continue to do so:

coldbox create model name=ContactService properties=contacts,lastID --accessors

This will create 2 files: models/ContactServices.cfctests/specs/unit/ContactServicesTest.cfc.

Navigate to our models/ContactServices.cfc, file and add this inside  your init function:

variables.contacts = {
	"1" : {"id": 1, "firstName": "Scott", 	"lastName": "Steinbeck", 	"phone": "661-555-5555",	"email": "scottsteinbeck@email.com"},
	"2" : {"id": 2, "firstName": "Scott", 	"lastName": "Coldwell", 	"phone": "661-555-5555",	"email": "scottcoldwell@email.com"},
	"3" : {"id": 3, "firstName": "Jon", 	"lastName": "Clausen", 		"phone": "661-555-5555",	"email": "johnclausen@email.com"},
	"4" : {"id": 4, "firstName": "Eric", 	"lastName": "Peterson", 	"phone": "661-555-5555",	"email": "ericpeterson@email.com"},
	"5" : {"id": 5, "firstName": "Andrew", 	"lastName": "Dixon", 		"phone": "661-555-5555",	"email": "andrewdixon@email.com"},
	"6" : {"id": 6, "firstName": "Gavin", 	"lastName": "Pickin", 		"phone": "661-555-5555",	"email": "gavinpickin@email.com"},
	"7" : {"id": 7, "firstName": "Brad", 	"lastName": "Wood", 		"phone": "661-555-5555",	"email": "bradwood@email.com"},
	"8" : {"id": 8, "firstName": "Luis", 	"lastName": "Majano", 		"phone": "661-555-5555",	"email": "luismajano@email.com"},
	"9" : {"id": 9, "firstName": "Jorge", 	"lastName": "Reyes", 		"phone": "661-555-5555",	"email": "jorgereyes@email.com"}
};

variables.lastID = 10;

This is just some default table rows to get us started seeing some data. Instead of adding a database into the mix i thought it would be easier to persist this in the singleton scope to make it simpler to walk though. This just makes it persist across refreshes and starts us at an increment number when we create new items. 

In case you are new to ColdBox you may be unfamiliar with the SINGLETON scope, it is one of the many scopes provided by WireBox to persist your data. For more detailed information check out: Wirebox Scopes

BTW: This model can easily be modified to use a database instead, and the best part is you can do it without ever making any changes to the front end :)

Now that our data is created lets create some helper methods for accessing & changing our data.

Below your init function paste the following code:

/**
* Get all contacts
*/
struct function getAll(){
	return variables.contacts;
}

/**
* save and return all contacts
* @contactID The id to save/update
* @data The data record
*/
struct function save( required contactID, required data ){
	// insert, move lastID
	if( arguments.contactID == 0 ){
		arguments.contactID = variables.lastID;
		arguments.data.id 	= variables.lastID;
		variables.lastID++;
	} 

	// Store data
	variables.contacts[ arguments.contactID ] = arguments.data;
	
	return variables.contacts;
}

/**
* Remove a contact
* @contactID The incoming ID
*/
struct function remove( required contactID ){
	structDelete( variables.contacts, arguments.contactID );
	return variables.contacts;
}

The methods should be pretty self explanatory, they allow us to keep the logic of data manipulation in the model so when we access it we can just simply call getAll(), save(), and remove(). This is extremely useful because all of your data logic is separated from the controller and in one place.

Since we already had a handler created handlers/Echo.cfc I decided to rename this handler to Contacts.cfc and changed its contents. Open up the file once you change the name and copy in the content below:

/**
* My RESTFul Contact Handler
*/
component extends="BaseHandler"{
    // Dependency Injection
    property name="contactService" inject="ContactService";

}

This is the base code that will extend our BaseHandler.cfc so we get all our RESTful helpers. Additionally, we inject our ContactService.cfc into our handler to use the helper methods we defined earlier.

The first API action is view which will list all of the contacts we have in our data:

/**
* List All Contacts
*/
any function view( event, rc, prc ){
	prc.response.setData( contactService.getAll() );
}

This will grab the data from the session and send it back as JSON

Our next action is save which, as it suggests, will save an existing row or add a new one.

/**
* Save A Contact
*/
any function save( event, rc, prc ){
	var requestBody = deserializeJSON( toString( getHTTPRequestData().content ) );
	var sContacts = contactService.save( requestBody.id, requestBody );
	prc.response.setData( sContacts );
}

You will notice something weird here event.getHTTPContent( json=true );, normally you would grab the data from the rc scope, but the vue-resource sends the data through the headers instead of the FORM or URL scope so we have to grab it from there.

Once we have the data posted we send the contact data to the ContactService so we can check if it has an existing id, if it is 0 we know its a new record, so we will create the new record, otherwise we will just update the existing data, then we send back the new data.

Lastly we will make it so you can delete a contact:

/**
* Delete An Existing Contact
*/
any function remove( event, rc, prc ){
	var sContacts = contactService.remove( rc.contactID );
	prc.response.setData( sContacts );
}

Now that our handler is done we need to add our routes for this handler so that we can map our functions to RESTful actions.

Open up your config/Routes.cfm file and add this line right above the route that says addRoute(pattern=":handler/:action?"); -- the order is important.

addRoute(pattern = 'contacts/:contactID?',handler = 'contacts',action = {GET = 'view', POST = 'save', PUT = 'save', DELETE = 'remove'});

Ok, now that we have our RESTful functions ready, we can start working on our front end.

Step 4. Creating our view

Open up your views/main/index.cfm that you created earlier. Inside you will copy in the html below

<div id="app" class="container">
	<div class="row">
		<div class="col-sm-4">
			<div class="panel panel-default">
			  <div class="panel-heading">
			    <strong>Add/Edit Contact</strong>
			  </div>
			  <div class="panel-body">
			    <div>
			      <div class="form-group"><input v-model="contactItem.firstName"    class="form-control" value="" placeholder="First Name"></div>
			      <div class="form-group"><input v-model="contactItem.lastName"     class="form-control" value="" placeholder="Last Name"></div>
			      <div class="form-group"><input v-model="contactItem.phone" 	class="form-control" value="" placeholder="Phone"></div>
			      <div class="form-group"><input v-model="contactItem.email" 	class="form-control" value="" placeholder="Email"></div>
			      <button class="btn btn-primary"  @click="saveContact()">Submit</button>
			      <button class="btn btn-warning"  @click="cancelSave()">Cancel</button>
			    </div>
			  </div>
			</div>
		</div>
		<div class="col-sm-8">
			<table class="table">
			  <thead>
			    <tr>
			      <th>First Name</th>
			      <th>Last Name</th>
			      <th>Phone</th>
			      <th>Email</th>
			      <th></th>
			    </tr>
			  </thead>
			  <tbody>
			    <tr v-for="contact in contacts">
			      <td>{{contact.firstName}}</td>
			      <td>{{contact.lastName}}</td>
			      <td>{{contact.phone}}</td>
			      <td>{{contact.email}}</td>
			      <td><button @click="loadContact(contact)"  type="button" class="btn btn-primary">Edit</button></td>
			      <td><button @click="deleteContact(contact)"  type="button" class="btn btn-danger">X</button></td>
			    </tr>
			  </tbody>
			</table>
		</div>
	</div>
</div>

This creates the html for our Contact Table & Contact Editor. You will notice some odd looking code if you are not familar with Vue.js or Angular JS:

v-model="contactItem.firstName"
v-for="contact in contacts"
@click="saveContact()"
{{contact.email}}

This is how Vue.js communicates with your code..

v-model="contactItem.firstName"

v-model, which is part of the form, binds together the form field and the Vue.js controller so they are aware of eachother.

v-for="contact in contacts"

v-for, is how Vue.js loops through a list of data, this can be used for any type of list.

@click="saveContact()"

@click, is basically equivalent to the onclick method, accept, this way the even is registered with the Vue.js controller so it can do something when you click it.

{{contact.email}}

{{contact.email}}, is known as handlebar syntax. This is how you define variables so Vue.js knows to replace them with that contact email for instance.

Step 4. Continued The Vue.js Controller

We will copy the following code into the views/main/indexHelper.cfm file. This file gets appended after our views/main/index.cfm.

<script>
$( document ).ready(function() {
new Vue({
  // Where the app will be instantiated on the page
  el: '#app',

  // Variables that you want it to have access to or start out with
  data: {
    contactItem: {
    	id:0,
    	firstName:'',
    	lastName:'',
    	phone:'',
    	email:''
    },
    contacts: []
  },
  
  // When this module is ready run this
  ready: function() {
    this.loadContacts();
  },

  // All the methods you want the view to have access to, basically an object of functions
  methods: {

    loadContacts: function() {
      var _this = this;
      // Get the list of contacts
      this.$http.get('/Contacts').then(function(result) {
      	// Update the list with the new data
        _this.$set('contacts', result.data.data);
      });
    },

    loadContact: function(contact) {
   	// Set the form with the contact row information
      	this.contactItem = contact;
    },

    saveContact: function() {
      var _this = this;
      // Save the new contact information
      this.$http.post('Contacts',_this.contactItem).then(function(result) {
      	// Reset the form to detault
      	_this.contactItem = {id:0,	firstName:'',	lastName:'',	phone:'',	email:''};
      	// Update the list with the new data
        return _this.$set('contacts', result.data.data);
      });
    },

    cancelSave: function(){
        // Reset the form to detault
        return this.contactItem = {id:0,	firstName:'',	lastName:'',	phone:'',	email:''};
    },
 
    deleteContact: function(contact) {
      var _this = this;
      //Delete the contact
      this.$http.delete('/Contacts/' + contact.id).then(function(result) {
      	// Update the list with the new data
        _this.$set('contacts', result.data.data);
      });
    }
  }
});
});
</script>

Lastly we have the controller. This is where all the magic happens.....

el: '#app',

Tells Vue that all of our logic will be nested within the <div id="app"></div>

  data: {
    contactItem: {
    	id:0,
    	firstName:'',
    	lastName:'',
    	phone:'',
    	email:''
    },
    contacts: []
  },

data is a struct of information it is pulling from, here we are providing defaults but you can also populate the data statically and it will use that information.

  // When this module is ready run this
  ready: function() {
    this.loadContacts();
  },
    loadContacts: function() {
      var _this = this;
      // Get the list of contacts
      this.$http.get('/Contacts').then(function(result) {
      	// Update the list with the new data
        _this.$set('contacts', result.data.data);
      });
    },

ready is fired once the Vue Component is loaded. What we are doing here is calling the ajax function that will load our table with data from our ColdBox RESTful API.

 

Gotcha** - Vue only knows about nested data changes if you use its built in functions (.$set, .$delete) otherwise you will be scratching your head for a while

 

_this.$set('contacts', result.data.data);

Load Contact - will load in our contact data into the form for editing. Notice the this.contactItem is being set to contact.

    loadContact: function(contact) {
   	// Set the form with the contact row information
      	this.contactItem = contact;
    },

Here the entire row of data is being sent as an argument when you click on the Edit button.

 <td><button @click="loadContact(contact)"  type="button" class="btn btn-primary">Edit</button></td>

So we are setting the 

this.contactItem = contact;

Now in our code we have the form set up with:

v-model="contactItem.firstName"

which means as soon as the this.contactItem has updated data it is going to update the v-model that is connected to that data.

Once the data is populated in the form we can make edits to it and then click either the submit or the cancel button. The respective code is below.

    saveContact: function() {
      var _this = this;
      // Save the new contact information
      this.$http.post('Contacts',_this.contactItem).then(function(result) {
      	// Reset the form to detault
      	_this.contactItem = {id:0,	firstName:'',	lastName:'',	phone:'',	email:''};
      	// Update the list with the new data
        return _this.$set('contacts', result.data.data);
      });
    },

    cancelSave: function(){
        // Reset the form to detault
        return this.contactItem = {id:0,	firstName:'',	lastName:'',	phone:'',	email:''};
    },

Since the default data sends an id = 0 we can decide if we need to create a new item or edit an existing item.

Last on our list for the Vue Controller is the Delete. Basically here we are just sending back the id to our API with the delete verb and our API will take care of deleting the entry and returning our new data set back to us.

deleteContact: function(contact) {
  var _this = this;
  //Delete the contact
  this.$http.delete('/Contacts/' + contact.id).then(function(result) {
  	// Update the list with the new data
    _this.$set('contacts', result.data.data);
  });
}

So thats it. All you need is a few files (Most of which can be generated by CommandBox) and you have got yourself a fully functional contacts manager.

The Code: View the repo here

Add Your Comment

(3)

Jul 19, 2016 06:33:11 UTC

by Don Bellenger

This is VERY interesting. I have a direct need for this at work right now. I went through it, until "All you need is a few files (Most of which can be generated by CommandBox) and you have got yourself a fully functional contacts manager.". In any event, what I have right now is not working. Is there any chance you could tweak/check the writeup to work with latest Command Box, and have step-by-step (i.e. steps in order) instructions for going from an empty directory to a "fully functional contacts manager"? Also, some commends seemed not to work, e.g. "coldbox create view main\index --helper". Perhaps a useful addition would be to give a single command that would install a fully functional contacts manager from Github, using command box. Then I could examine the code, in light of the excellent tutorial you have written. Thank you for all this work.

Oct 20, 2016 19:27:34 UTC

by Dave Merrill

Thanks for this, interested in exactly these two technologies. But you skipped an inconvenient but about doing things like this for real, like with a db and queries. As I'm sure you know, CD's serializeJSON doesn't pristine output that, as far as I know, Vue can consume directly. I've written a js tool to process cf's format on the client, but I'm not sure if that can be tied into vue-resource, which is my preference over bootstrap/jquery. Any thoughts on how you'd approach that?

Nov 04, 2016 10:35:06 UTC

by Luis Majano

@Dave You can convert your query to array of structs to make it easier.

Recent Entries

Hackers demand a ransom to restore data from my ColdFusion web applications!

Hackers demand a ransom to restore data from my ColdFusion web applications!

Hackers demand a ransom to restore data from my ColdFusion web applications!

Unfortunately, we often hear this message from clients who thought it would never happen to them... until it did. Some believed they could delay the expense of Implementing ColdFusion security best practices for one year, while others were tempted to put it off for just a few months. However, in today's rapidly evolving digital landscape, the security of web applications, including ColdFusio...

Cristobal Escobar
Cristobal Escobar
April 16, 2024
Ortus March Newsletter

Ortus March Newsletter

Welcome to Ortus Solutions’ monthly roundup, where we're thrilled to showcase cutting-edge advancements, product updates, and exciting events! Join us as we delve into the latest innovations shaping the future of technology.

Maria Jose Herrera
Maria Jose Herrera
April 01, 2024
Into the Box 2024 Last Early Bird Days!

Into the Box 2024 Last Early Bird Days!

Time is ticking, with less than 60 days remaining until the excitement of Into the Box 2024 unfolds! Don't let this golden opportunity slip away; our exclusive Early Bird Pricing is here for a limited time only, available until March 31st. Why wait? Secure your seat now and take advantage of this steal!

Maria Jose Herrera
Maria Jose Herrera
March 20, 2024