Friday, April 11, 2014

Using BackboneJS and RequireJS to create simple CYU

This post explains using Backbone JavaScript framework to load a question from XML document and process it in the form of a check your understanding(cyu).

First I would list down the libraries/framework required to build this example.

Jquery - http://jquery.com/
Backbone - http://backbonejs.org/
Underscore - http://underscorejs.org/
RequireJS - http://requirejs.org/
TextJS - https://github.com/requirejs/text

Each of the libraries listed above can be downloaded or referred using CDN link. I have downloaded and placed them in the lib folder of the app structure. To begin with lets create cyu index.html page.

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CYU Example Backbone</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css" />
<script data-main="js/app" src="js/require.js"></script>
</head>
<body>
</body>
</html>

index page includes require.js along with the data-main attribute which is the first place our application boots from. It refers to the configuration file as described below:-

app.js

require.config({
baseUrl :'js/lib',

paths:{
app: '../app',
tpl: '../tpl'
},

shim:{
'backbone':{
deps:['underscore','jquery'],
exports:'Backbone'
},
'underscore':{
exports:'_'
}
}
});


require(['jquery','backbone','app/router'],function($,Backbone,Router){
var router = new Router();
Backbone.history.start();
});

Please refer to RequireJS - http://requirejs.org/ for details about the configuration. Once config is complete we load the Backbone - Router class along with its dependencies which as shown above are Jquery and Backbone. Once this is done we create new instance of Router which triggers the start of application. Lets look into Router file.

router.js

define(function(require){
"use strict";

var $ = require('jquery'),
Backbone = require('backbone'),
MainView = require('app/views/main'),
$body = $('body'),
mainView = new MainView({el:$body}).render(),
$content = $("#question_container", mainView.el)

return Backbone.Router.extend({
routes:{
"":"home"
},

home:function(){
require(["app/views/questiondisplay","app/models/quesdata"],function(QuestionDisplay,models){
var quesdata = new models.QuesData();
quesdata.fetch().then(function(){
var dataobject = {question:quesdata.pluck('question'),qoptions:quesdata.pluck('qoptions')}
var questionDisplay = new QuestionDisplay({el : $content});
questionDisplay.render({model:dataobject});
});

});
}
});
});

Router class above first of all loads the MainView. MainView in this example is the main container which holds the entire app. Let's have a look at the MainView (Backbone-View) and its template.

mainview.js

define(function(require){
"use strict";
var $ = require('jquery'),
_ = require('underscore'),
Backbone = require('backbone'),
tpl = require('text!tpl/Main.html'),

template = _.template(tpl)

return Backbone.View.extend({

render:function(){
this.$el.html(template());
return this;
}
});
});

Main.html

<div class='container'>
<div style='width:100%;text-align:center;'><h1>Check your understanding<h1></div>
<div id='question_container' style='width:100%'></div>
</div>

Once mainView gets loaded by the Router class it looks for the home route which in our example is the home function added in the Router class. The 'home' function does actually loads the XML using a Backbone model and then renders it using 'questiondisplay' view and 'quesdata' model.

First lets look into Model of this app which gets created from cyu.xml file.

cyu.xml

<?xml version='1.0' encoding='UTF-8'?>

<qdata>
<question><![CDATA[What does AJAX stand for ?]]></question>
<choices>
<opt cval='0'><![CDATA[Asynchornous JS]]></opt>
<opt cval='0'><![CDATA[Synchornous JS]]></opt>
<opt cval='0'><![CDATA[Javascript and XML]]></opt>
<opt cval='1'><![CDATA[Asynchornous Javascript and XML]]></opt>
</choices>
<feedback>
<correct><![CDATA[Congrats! Correct Answer.]]></correct>
<incorrect><![CDATA[Incorrect! Please try again.]]></incorrect>
</feedback>
</qdata>

quesdata.js

define(function(require){
"use strict";

var $ = require('jquery'),
Backbone = require('backbone'),

QuesData = Backbone.Collection.extend({
url: 'js/app/models/cyu.xml',
initialize:function(){
},

parse: function (data) {

var parsed = [];
var questionoptions = [];

$(data).find('opt').each(function (index) {
var opts = $(this).text();
questionoptions.push({option:opts,cval:$(this).attr("cval")})
});

var question = $(data).find('question').text();

parsed.push({question: question, qoptions: questionoptions});

return parsed;
},

fetch: function (options) {
options = options || {};
options.dataType = "xml";
return Backbone.Collection.prototype.fetch.call(this, options);
},


sync: function(method, model, options) {
 var params, data;  
 options || (options = {});  
 data = {xml: options.response};

 params = {
url: this.url,
type: 'POST', 
dataType: 'xml',
data: data
 };
return $.ajax(_.extend(params, options));
}

});

return{
QuesData:QuesData
}

});


Once as the model gets ready we proceed with the cyu view using 'questiondisplay' which is a Backbone-View extended class.

questiondisplay.js

define(function(require){
"use strict";
var $ = require('jquery'),
_ = require('underscore'),
Backbone = require('backbone'),
tpl = require('text!tpl/QuestionDisplay.html'),

correctAnswerIndex,
correctAnswerCode = '1',

template = _.template(tpl)

$.fn.serializeObject = function()
{
var o = {};
var a = this.serializeArray();
$.each(a, function() {
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};

return Backbone.View.extend({

render:function(options){

var o = {};

_.each(options.model.qoptions[0], function(e,i) {
o[e.option] = e.value;

if(e.cval === correctAnswerCode){
correctAnswerIndex = i;
}

});

this.$el.html(template({options:options}));
return this;
},

events:{
'submit .cyu-user-form':'submitAnswer'
},

submitAnswer:function(ev){

if($(ev.currentTarget).serializeObject().ans == correctAnswerIndex){
console.log("CORRECT ANSWER :: ");
alert("CORRECT ANSWER :: ");
}else{
console.log("INCORRECT ANSWER ::: ");
alert("INCORRECT ANSWER ::: ");
}
return false;

}


});
});

QuestionDisplay.html

<div id="question"><%= options.model.question %></div>
<div id="choices">
<form class="cyu-user-form">
<ul class="list-unstyled">
<% _.each(options.model.qoptions[0], function(e,i){ %>
<li><input type='radio' name='ans' value="<%=i%>"><span><%= e.option %></span></li>
<% }); %>
</ul>
<button class='btn btn-primary cyubtn' type="submit">Submit</button>
</form>
</div>

Backbone view classes has events property where we can add events to the buttons and other DOM elements which gets loaded via template. The 'submitAnswer' gets fired up as soon as user clicks the submit button. The correct answer is validated using the model 'quesdata' and feedback is shared in form of alert messages.

All the associated files can be downloaded and hosted on webserver/localhost to test.

Download Link:- Click Here

-Nitin

Reference Links:-

http://coenraets.org/blog/2012/02/sample-app-with-backbone-js-and-twitter-bootstrap/

http://coenraets.org/directory/


Using FormBuilder, Validators & FormGroup in Angular 2 to post data to Node server (node.js)

This blog explains usage of  FormBuilder , Validators , FormGroup classes in Angular 2 projects. In this post we will create HTML form a...