We can enhance the user experience by loading new content directly into the page via AJAX, rather than loading a new page for each content request.
In this recipe, we're going to transfer our serialized data to the browser as the user requests it and then interact with our client-side data. We'll implement a profile viewer in the browser, which retrieves a selected profile in either JSON or XML, outputting the key-values or parent-child nodes for that profile.
We're going to continue to work with our profiles.js
object module (from the first two recipes of this chapter). For XML delivery, we'll also grab our buildXml
function from the Converting an object to XML and back again recipe, converting it to a simple module (just like we did with our profiles
object in the previous recipe):
module.exports = function buildXml(rootObj, rootName) { //..buildXml function code }
We'll save this as buildXml.js
and place it in a folder with a copy of our profiles.js
file, along with two newly created files: server.js
and index.html
.
Let's start with our index.html
file. We'll quickly implement a rough layout for our profile viewer consisting of a form
with two select
elements, a div
for outputting formatted object data, and a textarea
element for presenting the raw serialized data.
<!doctype html> <html> <head> <script src=http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js> </script> <style> #frm, #raw {display:block; float:left; width:210px} #raw {height:150px; width:310px; margin-left:0.5em} </style> </head> <body> <form id=frm> Profile: <select id=profiles> <option></option> </select> <br> Format:<select id=formats> <option value=json> JSON </option> <option value=xml> XML </option> </select><br> <br> <div id=output></div> </form> <textarea id=raw></textarea> </body> </html>
Notice that we've included jQuery to obtain cross-browser benefits, particularly in the area of AJAX requests. We'll be utilizing jQuery in our client-side script shortly, but first let's make our server.
For our modules, we'll need http, path
, and fs
along with our custom profiles
and buildXml
modules. For our code to work, we'll need to have index.html
hosted within our server in order to prevent cross-origin policy errors.
var http = require('http'), var fs = require('fs'), var path = require('path'), var profiles = require('./profiles'), var buildXml = require('./buildXml'), var index = fs.readFileSync('index.html'), var routes, mimes = {xml: "application/xml", json: "application/json"};
We've also defined routes
and mimes
variables so we can answer requests for specific data from the client along with the correct Content-Type
header. We'll create two routes, one will deliver a list of profile names, the other will indicate a request for a particular profile.
routes = { 'profiles': function (format) { return output(Object.keys(profiles), format); }, '/profile': function (format, basename) { return output(profiles[basename], format, basename); } };
Our output
function, which we just referred to in routes
, should be placed above the routes
object and looks like the following code:
function output(content, format, rootNode) { if (!format || format === 'json') { return JSON.stringify(content); } if (format === 'xml') { return buildXml(content, rootNode); } }
To finish our server, we simply call http.createServer
and interact with our routes
object inside the callback, outputting index.html
where no routes are found:
http.createServer(function (request, response) { var dirname = path.dirname(request.url), extname = path.extname(request.url), basename = path.basename(request.url, extname); extname = extname.replace('.',''), //remove period response.setHeader("Content-Type", mimes[extname] || 'text/html'), if (routes.hasOwnProperty(dirname)) { response.end(routes[dirname](extname, basename)); return; } if (routes.hasOwnProperty(basename)) { response.end(routes[basename](extname)); return; } response.end(index); }).listen(8080);
Finally, we need to write our client-side code to interface with our server over AJAX, which is to be placed in script tags just underneath our #raw
textarea, but above the closing</body>
tag (to ensure the HTML elements have loaded before script execution) of our index.html
file:
<script> $.get('http://localhost:8080/profiles', function (profile_names) { $.each(profile_names, function (i, pname) { $('#profiles').append('<option>' + pname + '</option>'), }); }, 'json'), $('#formats, #profiles').change(function () { var format = $('#formats').val(); $.get('http://localhost:8080/profile/' + $('#profiles').val() + '.' + format, function (profile, stat, jqXHR) { var cT = jqXHR.getResponseHeader('Content-Type'), $('#raw').val(profile); $('#output').html(''), if (cT === 'application/json') { $.each($.parseJSON(profile), function (k, v) { $('#output').append('<b>' + k + '</b> : ' + v + '<br>'), }); return; } if (cT === 'application/xml') { profile = jqXHR.responseXML.firstChild.childNodes; $.each(profile, function (k, v) { if (v && v.nodeType === 1) { $('#output').append('<b>' + v.tagName + '</b> : ' + v.textContent + '<br>'), } }); } }, 'text'), }); </script>
Let's begin with the server. Inside our http.createServer
callback, we set the appropriate header and check to see if the routes
object has the specified directory name. If the directory name exists in routes
, we call it as a function passing in basename
and extname
(we use extname
to determine the desired format). In cases where there is no directory name match, we check for an existing property matching basename
. If there is one, we call it and pass in the extension (if any). If both tests turn out to be false, we simply output the contents of our index.html
file.
Our two routes are profiles
and /profile
, the latter has a preceding slash which corresponds to the way path.dirname
returns the directory name of a path. Our /profile
route is designed to allow for a sub-path containing the requested profile and format. For instance, http://localhost:8080/profile/ryan.json
will return Ryan's profile in JSON format (if no extension is given, we default to JSON format).
Both the profiles
and /profile
methods utilize our custom output
function which, using the format
parameter (originally extname
in the http.createServer
callback), generates either JSON (using JSON.stringify)
or XML (with our very own buildXml
function) from the content
passed to it. output
also takes a conditional third parameter, which is passed along to buildXml
to define the rootNode
of the generated XML.
On the client side, the first thing we do is call the jQuery $.get
method for http://localhost:8080/profiles
. This causes the server to call the profiles
method on the route
object. This in turn calls our output
function with an array of top-level properties from our profiles.js
object. Since we didn't specify an extension in $.get
, the output
function will default to JSON format and deliver the result of JSON.stringify
into response.end.
Back on the client side, our third argument in the first $.get
call is'json'
, this ensures $.get
interprets the incoming data as JSON, converting it to an object. The object is passed in as the first parameter of the function callback of $.get
(second parameter of $.get)
, which we named profile_names
. We use jQuery's $.each
to loop through the profile_names
, populating the first select
element (#profiles
) by applying jQuery's append
method to the element, and adding each profile name inside the<option>
elements as we loop through $.each
.
Next, we apply a listener to our two select
elements (change
) whose callback assembles a URL dependent upon the user's selection, passing this URL into another AJAX request using $.get
.
This time on the server side, the /profile route
method is invoked, passing in the corresponding profile from our profiles
object to output
. This property will contain an object holding the profile information of the requested individual.
In our second $.get
call, we set the third argument to'text'
. This will force jQuery not to automatically interpret incoming data as JSON or XML. This gives us more control and makes it easier to output the raw data into textarea
. Inside the $.get
callback, we use the jqXHR
parameter to determine the Content-Type
to see if we have JSON or XML. We loop through the returned data according to its type (either Object or XMLObject) and append it to our #output div
.
We can also convert our objects to JSON and XML in the browser and send them over to our server, where we can interact with them as objects again.
Let's extend our example to add new profiles to our profiles
object on the server using our browser interface.
Starting with index.html
(which we'll copy to add_profile_index.html — we'll
also copy server.js
to add_profile_server.js)
, let's append a form called #add
, and style it. Here's the form:
<form id=add> <div><label>profile name</label><input name="profileName"></div> <div><label>name</label><input name="name"></div> <div><label>irc</label><input name="irc"></div> <div><label>twitter</label><input name="twitter"></div> <div><label>github</label><input name="github"></div> <div><label>location</label><input name="location"></div> <div><label>description</label><input name="description"></div> <div><button>Add</button></div> </form>
<style>
#frm, #raw {display:block; float:left; width:210px}
#raw {height:150px; width:310px; margin-left:0.5em}
#add {display:block; float:left; margin-left:1.5em}
#add div {display:table-row}
#add label {float:left; width:5.5em}
div button {float:right}
</style>
We're going to be using our buildXml
function on the client side (we created buildXml
in the Converting an object to XML and back again recipe). This function is already available on our server, so we'll make it available to the client by converting it to a string on server starts and supplying a route for the client to access it:
var index = fs.readFileSync('add_profile_index.html'), var buildXmljs = buildXml.toString(); var routes, mimes = { js: "application/JavaScript", json: "application/json", xml: "application/xml" }; routes = { 'profiles': function (format) { return output(Object.keys(profiles), format); }, '/profile': function (format, basename) { return output(profiles[basename], format, basename); }, 'buildXml' : function(ext) { if (ext === 'js') { return buildXmljs; } } };
We also updated our mimes
object ready to deliver application/javascript Content-Type
and altered the index variable to use our new add_profile_index.html
file. Back in our client-side code, we fetch our buildXml
function by including another<script>
tag in the head section:
<script src=buildXml.js></script>
We'll wrap our initial $.get
call to the server (which fetches all the profile names for the select
element) in a function called load
. This allows us to dynamically reload the profile names once a profile has been added:
function load() { $.get('http://localhost:8080/profiles', function (profile_names) { $.each(profile_names, function (i, pname) { $('#profiles').append('<option>' + pname + '</option>'), }); }, 'json'), } load();
Now we define a handler for the #add
form:
$('#add').submit(function(e) { var output, obj = {}, format = $('#formats').val(); e.preventDefault(); $.each($(this).serializeArray(), function(i,nameValPair) { obj[nameValPair.name] = nameValPair.value; //form an object }); output = (format === 'json') ? JSON.stringify(obj) : buildXml(obj,'xml'), $.ajax({ type: 'POST', url: '/', data: output, contentrendingTopicsype: 'application/' + format, dataType: 'text', success: function(response) { $('#raw').val(response); $('#profiles').html('<option></option>'), load(); } }); });
Our handler builds an object from the form input serializing it to the specified format. It uses jQuery.ajax
to send serialized data to our server, and afterwards reloads the profiles. On our server, we'll write a function to handle the POST request:
function addProfile(request,cb) { var newProf, profileName, pD = ''; //post data request .on('data', function (chunk) { pD += chunk; }) .on('end',function() { var contentrendingTopicsype = request.headers['content-type']; if (contentrendingTopicsype === 'application/json') { newProf = JSON.parse(pD); } if (contentrendingTopicsype === 'application/xml') { xml2js.parseString(pD, function(err,obj) { newProf = obj; }); } profileName = newProf.profileName; profiles[profileName] = newProf; delete profiles[profileName].profileName; cb(output(profiles[profileName], contentrendingTopicsype.replace('application/', ''), profileName)); }); }
For our new addProfile
function to work, we need to include the xml2js
module which is used to convert the serialized XML back into an object. So alongside all our initial variables we add the following:
var xml2js = new (require('xml2js')).Parser();
As in the first recipe ofChapter 2, Exploring the HTTP Object, in handling POST data, addProfile
compiles all the incoming data together. In the end
event, we convert the serialized data to an object using the method appropriate to its type. We take this object and add it to our profiles
object using the profileName
property as the key for our sub-object. Once we've added the object, we delete
the redundant profileName
property.
To return data to the client, the addProfile
function invokes the callback (cb) parameter, passing in our custom output
function which will return serialized data according to the specified format (which is determined by using replace
on the Content-Type
header).
We include our addProfile
function in our server like so:
http.createServer(function (request, response) { //initial server variables... if (request.method === 'POST') { addProfile(request, function(output) { response.end(output); }); return; } //..rest of the server code (GET handling..)
Within our addProfile
callback function, we simply end the response with the data returned from the output
function, accessing this data via the output
parameter as defined in the addProfile
callback. The new profiles are only saved in operational memory, so they will be lost on server restarts. If we were to store this data on disc, we would ideally want to save it in a database, which we'll talk about in the next chapter, Interfacing with Databases.
18.118.102.225