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.cfm, view/main/indexHelper.cfm.
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.cfc, tests/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.
_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.