In the last post, we’ve setup yeoman and karma, and then scaffold our application. If you look at app/app.js
which currently contains the routing information. Yeoman generator has automagically mapped #/main
route and created its associated files mainly main.js
and main.html
– which we do not need for this application.
We’ll do some cleanup first.
Cleanup
In index.html, remove the unnecessary Google Analytics code and main.js included. Delete following files:
$ cd eShell
$ rm app/scripts/controllers/main.js app/views/main.html test/spec/controllers/main.js
And finally replace
<div class="container"></div>
with
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container"><a class="brand" href="#">eShell - A player for eLearning Courses</a></div>
</div>
</div>
<div style="margin-top: 40px;"><div ng-view></div></div>
<div class="navbar navbar-inverse navbar-fixed-bottom ng-cloak">
<div class="navbar-inner">
<div class="container">
<div class="btn-group pull-left"><button class="btn btn-inverse input-small" type="text" disabled="disabled" value="Attempt 1 of 3"></button></div>
<div class="btn-group pull-left">
<button class="btn btn-primary" type="button"> Submit </button>
<button class="btn btn-warning" type="button"> Try Again </button>
<button class="btn btn-success" type="button">Show Answer</button>
</div>
<div class="btn-group pull-right">
<button class="btn">«</button>
<button class="btn">1 of 4</button>
<button class="btn">»</button>
</div>
</div>
</div>
</div>
First Route
Using yeoman, it takes a split second to create a new route. Just run the following command in a Terminal:
$ yo angular:route fib
create app/scripts/controllers/fib.js
create test/spec/controllers/fib.js
create app/views/fib.html
As you could see, this has created 3 files, a controller, a view and a test. If you look at app.js
now, you will see a new route information has automagically been inserted without breaking a thing. Love it!
Lets now update our app.js from:
angular.module('eShellApp', []).config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
})
.when('/fib', {
templateUrl: 'views/fib.html',
controller: 'FibCtrl'
})
.otherwise({
redirectTo: '/'
});
});
to:
angular.module('eShellApp', []).config(function ($routeProvider) {
$routeProvider
.when('/1', {
templateUrl: 'views/fib.html',
controller: 'FibCtrl'
})
.otherwise({
redirectTo: '/1'
});
});
Go and check out http://localhost:9000/
Creating our First Template – Fill in the blanks
This template creates a fill in the blank question backed by JSON data including valid answers. To read the JSON file, angular gives a nifty little service, $http that facilitates communication with the remote HTTP servers via the browser’s XMLHttpRequest object or via JSONP. The reason why I prefer to use $http service because it takes single argument (object) and returns a promise with two specific methods, success and error as it is based on Deferred/Promise APIs.
so, lets create fib.json
and put data:
$ touch app/data/fib.json
{
"text":"AngularJS is originally created in [[1]] by [[2]] and [[3]].",
"choices":[
{
"val1":"[[1]]",
"val2":"2009,09"
},
{
"val1":"[[2]]",
"val2":"misko,Misko Hevery"
},
{
"val1":"[[3]]",
"val2":"adam,adam abrons"
}
]
}
Now, lets modify our fib.js
controller to read the above JSON data. We’ll first expose $http object to our controller and $compile service to process the data before injecting into a view.
angular.module('eShellApp').controller('FibCtrl', function ($scope, $http, $compile) {
$http.get('data/fib.json').then(function(data) {
$scope.data = data.data;
var text = '<div>' + data.data.text + '</div>';
// A bit complex logic to replace every instance of [] with input boxes
data.data.choices.forEach(function(choice, index) {
text = text.replace(
new RegExp(choice.val1.replace(/\[/g, '\\[').replace(/\]/g, '\\]'), 'g'),
'<span class="control-group">' +
' <input type="text" class="input-medium" ' +
' autocorrect="off" ' +
' autocapitalize="off" ' +
' autocomplete="off" ' +
' ng-change="validate(frmFIB.$invalid)" ' +
' required ' +
' data-index="' + index + '" ' +
' ng-model="input_' + index + '" />' +
'</span>'
);
});
$scope.text = $compile(text)($scope).html();
});
});
We’re just looping through choices we’ve in JSON and replacing the occurrences of [] with input boxes. Notice that I’ve used ng-change directive that will call validate() method on every keypress to toggle submit button. You may be wondering why have I wrapped data.data.text
in a div? That’s because $compile service requires it, otherwise you will get some nasty error. Finally I’ve bound compiled text to $scope
in order to use it in a view below.
<div class="contentWrapper">
<div class="content">
<h3 class="pagination-centered">Fill in the Blanks</h3>
<form class="form-inline" name="frmFIB" novalidate="">
<div class="pagination-centered" ng-bind-html-unsafe="text"></div>
</form>
</div>
</div>
Again, ng-bind-html-unsafe evaluates the expression (text, in this case) and put it inside the element it bound to (div, in this case). In addition, it is also helpful to get rid of FOUC or precompiled values issue with angular (where you see {{expr}} before the evaluation, mostly in IE).
Enter the Dragon: Directive
Its very bad to do the DOM manipulation inside controller and directive is the best option Angular provides. Lets create our first directive.
$ yo angular:directive parseBracket
create app/scripts/directives/parseBracket.js
create test/spec/directives/parseBracket.js
Now we’ll move the entire parsing logic into above directive. But before that lets replace ng-bind-html-unsafe
we’d used earlier with parse-bracket.
<div class="contentWrapper">
<div class="content">
<h3 class="pagination-centered">Fill in the Blanks</h3>
<form class="form-inline" name="frmFIB" novalidate="">
<div class="pagination-centered" parse-bracket="{{data}}"></div>
</form>
</div>
</div>
We’re sending JSON object (data) being parsed to the directive.
According to the author, Directive is a way to teach HTML a new trick. In this case, I’m restricting the directive to be an attribute only and inside link function, $observe observes the changes of parseBracket attribute which contains interpolation. Rest is same borrowed from the controller ;-). You can learn more about directive on adobe or onehungrymind.
angular.module('eShellApp').directive('parseBracket', function ($compile) {
return {
restrict: 'A',
link: function postLink(scope, element, attrs) {
attrs.$observe('parseBracket', function(val) {
var params = angular.fromJson(val),
text = '<div>' + params.text + '</div>';
if (angular.isDefined(params.choices)) {
params.choices.forEach(function(choice, index) {
text = text.replace(
new RegExp(choice.val1.replace(/\[/g, '\\[').replace(/\]/g, '\\]'), 'g'),
'<span class="control-group">' +
'<input type="text" ' +
'class="input-medium" ' +
'autocorrect="off" ' +
'autocapitalize="off" ' +
'autocomplete="off" ' +
'ng-change="validate(frmFIB.$valid)" ' +
'ng-model="input_' + index + '"' +
'required />' +
'</span>'
);
});
}
element.html(text);
$compile(element.contents())(scope);
});
}
};
});
Lets Test ’em
You have noticed that when we created the directive using yeoman generator, it also created the test file for the same. We’re going to modify it a bit to pass through the test.
describe('Directive: parseBracket', function () {
beforeEach(module('eShellApp'));
var element, $scope;
it('should replace [] with input boxes', inject(function ($rootScope, $compile) {
$scope = $rootScope.$new();
$scope.data = {
text : 'AngularJS is [[1]]',
choices : [{
"val1": "[[1]]",
"val2": "awesome,powerful"
}]
};
element = angular.element('<div parse-bracket="{{data}}"></div>');
element = $compile(element)($scope);
$rootScope.$digest();
expect(element.html()).toBe('<div class="ng-scope">AngularJS is <span class="control-group" ng-class="setClass(input_0_valid)"><input type="text" class="input-medium ng-pristine ng-invalid ng-invalid-required" autocorrect="off" autocapitalize="off" autocomplete="off" ng-change="validate(frmFIB.$valid)" ng-readonly="input_0_readonly" ng-model="input_0" required=""></span></div>');
}));
});
Wrap up
In a next post, We’ll clean up our controller and write some tests for it too. Checkout Part 3.
If you found this article useful in anyway, feel free to
donate me and receive my dilettante painting as a token of appreciation for your donation.
19.030000
73.010000
Like this:
Like Loading...