Browser-server transmission via AJAX

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.

Getting ready

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.

How to do it...

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>

How it works...

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.

There's more...

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.

Sending serialized data from client to server

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>

And some additional styles:

<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.

See also

  • Setting up a router discussed In Chapter 1,Making a Web Server
  • Processing POST data discussed In Chapter 2,Exploring the HTTP Object
  • Converting an object to JSON and back again discussed in this chapter
  • Converting an object to XML and back again discussed in this chapter
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.102.225