Beginning AngularJs, and the Marvel API.

February 6th, 2014

AngularJS-Shield-medium

They say that AngularJs is the:

Superheroic JavaScript MVW Framework

So why not learn the basics of Angular and play with the newly released Marvel API?!

This is a pretty high level overview of how I put together a little app, FOUND HERE. It was my first crack at AngularJS. I have some experience with Backbone, and I am working on a larger app with it I will post about later, so some of the ideas made sense to me. However others just seemed like some Dr Strange magic…see what I did there?

Disclaimer: I am very new to Angular and this is more of a documentation of how I got this running. I admit defeat in not knowing what I am doing a few times. But HEY, where is the fun in not learning a few things here and there? Also the API has a rate limit of 1000 calls a day, so if it is not working, that limit might have been reached. I have not added any error checking.

The Goal

The goal for this was for me to start learning how to use AngularJs, and the Marvel API just made it fun. The idea of the app(if you can call it that) is that we can look over all the Marvel characters and click on one of them to get a bit more info. I wanted to try and incorporate some infinite scroll in there as well, cause why not?

If you want to follow along or try it out for your self the project is on GitHub

The Shell

To start with we have a simple index.html file.


<!doctype html>
<html lang="en" ng-app="Comics">
<head>
	<meta charset="UTF-8">
	<title>Marvel Comic Book Characters</title>
	<script src="js/jquery.min.js"></script>
	<script src="js/angular.min.js"></script>
	<script src="js/angular-ui-router.min.js"></script>
	<script src="js/ng-infinite-scroll.min.js"></script>
	<script src="js/comics.js"></script>
	<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>
	<link rel="stylesheet" href="style.css">
</head>
<body ng-controller="MainCtrl">
	<div ui-view infinite-scroll="more.load()" infinite-scroll-distance="0" infinite-scroll-disabled='more.busy' class="main-container"></div>
	<div class="credit"><a href="http://marvel.com" target="_blank">Data provided by Marvel. © 2014 Marvel</a></div>
</body>
</html>

In this file we include a few scripts, namely angluar(duh), angular-ui-router, ng-infinite-scroll, and jquery(used for infinite scroll). We of course also include our own script file.

In our <html> tag we add an ng-app attribute to it with the value of Comics. This value can be anything of course, it is simply used as of way for Angular to know where to look. This sets the root element for our application.

We also have an attribute on our body tag of ng-controller="MainCtrl". This is where you will be putting our application views.

Then we have a div tag with quite a view attributes on it, but for the time being lets assume it just looks like this:

<div ui-view="" class="main-container"></div>

This will be our main view area.

The Javascript

In Angular you need to set up a module, this module is used to control and work with your app:

The angular.module is a global place for creating, registering and retrieving Angular modules

From the docs

In this case we have one module for our app.


var app = angular.module('Comics',['ui.router','infinite-scroll']);

We will just save it to a variable, you can however chain your controllers, directives, configs etc together. Say something like this:


angular.module('Comics',['ui.router','infinite-scroll']).controller().directive()....

But I found that I preferred to work from a variable and space things out a bit more, as we will see coming up.

What you see above is that the module takes the name we sets in the ng-app attribute earlier. It then takes any requirements. In this case we added angular-ui-router and ng-infinite-scroll. Note that what these modules expose are not always the same name. So check the docs on them for how to add them. I found that some pretty extensive googling really helped me out.

Routes

The first thing we will do is set up our routes. This application is pretty simple, there is a view that is all the characters that we have loaded at the moment. When you click on a character we will change the url to be the characters id, and we will show a popup with a little description and then a link to the Marvel site for some more information. So in that case we will have only two routes.

This is the first place(of many) I got stumped. I really wanted a router like Backbones, where you just create some views willy nilly like. But Angular’s router is based off of an object configuration. I was watching some video, the AngularJs In-Depth video from Front-End Masters is a pretty good starting spot, it really helped me get started. Long story short I settled on the angular-ui-router which allowed me to have nested views and states. Could I have done what I wanted with the regular router? Honestly not sure, this just seemed like a nice quick fix. Ill be looking into it more in the future for sure.

Anyways, we will configure as such:


app.config(function ($stateProvider) {
	$stateProvider.state('index', {
		url: '',
		controler: 'MainCtrl',
		templateUrl: 'templates/character.html'
	})
	.state('index.single', {
		url: '/:id',
		templateUrl: 'templates/characterPopUp.html',
		controller: 'SingleCharacter'
	});
});

The .config method takes a function where we pass a dependancy, in this case it is the $stateProvider service.

Unlike the core Angular $route service which uses .when on its $routeProvider, the angular-ui-router uses a $stateProvider and .state. In here we first set the name of the state, and then the url. The first url is going to be the main state, we set the controller we need for that, and then any templates(I will get to these in the controllers section). Note that I left the index state url as an empty string. I had made the assumption that it would have to be / but that would not load my controller, without looking too much into it I left it empty and it worked. I couldn’t find anything in the documentation about this at the time.

There is a second state after that. This is actually a child state for the main index state. I will use an :id parameter here so that if someone goes to site.com/#/1 we will have access to that 1. Also note that if that parent state was something like /characters and we had a child state that we wanted to be like /characters/1, we would only need to use the '/:id'. This is because when we named the state index.single the router realized it would be an extension of the parent. I hope that makes sense!

The controllers

So now that we have our states(routes) set up, lets take a look at the out main controller


app.controller('MainCtrl',function($scope, ComicBooks) {	
	//use this to get data
	$scope.more = new ComicBooks.LoadMore($scope);
});

This is a very simple controller, and that is fine, cause it will let me explain the templates! First, much like the .config method we use the .controller method and pass it a string with the name of the controller as well as a function with its dependencies. For this we want the $scope object and also a custom service I created called ComicBooks this is used for retrieving our information. But like many things, we will get that one later.

To talk about the $scope object, lets take a look at the template for this state, character.html.




My none expert understanding of the $scope object is that it is tied to the compiled template for said controller. If you look back to our state router, you can see that we used the character.html template. In this template we are able set up the areas we want information to go. Anywhere you see a {{insert some text here}} that is a location that the $scope can use for information.

Side-note: I was struggling with where the model was in Angular when I started. From my experience with Backbone I was looking for something like Backbone.Model that could work with. But in the case of Angular the $scope is the model. This will make more sense when we look at the single character.

On the first div you can see a ng-repeat="character in more.characters", assume that more.characters returns an array, this is essentially a for in loop. Where we can loop through the array of data we have and get the objects it contains. We can then use that object to get information from, like {{character.id}} will give us the id for that object. I hope you see how that works. Also note the url structure, this is so that it can be triggered by the angular-ui-router

Note the second <div ui-view></div> this is for when we are on the child state, our nested view will go there.

Another case where I was a little baffled as to how to do something was when it came to clicking the links. It would jump the page to the top. Normally I would do something like event.preventDefault() but I was not sure where that event would come from. In the end I found a solution with this:


app.value('$anchorScroll', angular.noop);

Solved my problem.

The second controller I have is for a single character. This is a slightly more complex controller and it also involves a directive. Lets take a look at the controller.


app.controller('SingleCharacter', function($scope, $rootScope, $stateParams, ComicBooks, $window) {
	var id = $stateParams.id;
	ComicBooks.findOne(id).then(function(result) {
		var data = result.data.results[0];
		$scope.characterName = data.name;
		$scope.characterUrl = data.urls[0].url;
		$scope.characterImg = data.thumbnail.path + '.' + data.thumbnail.extension;
		var desc = data.description;
		if(desc.length <= 0){
			desc = "No description provided";
		}
		$scope.description = desc;
		//Not quite sure this is what I am looking for, I admit defeat.
		$rootScope.$broadcast('contentLoaded');
	});
});

You can see it is similar to the main controller, but it takes a few more dependencies. Notably it takes a service from the angular-ui-router called $stateParams this is a simple object that provides the parameters from the state. Then you can see I use my ComicBooks service again to find one character using the id.

Pay attention to the $scope here, you can see that you are able to set the values for this, and looking at the template for this popup you can see the pattern.


<popup class="popup" style="background-image:url({{characterImg}})">
	<div class="bottom">
		<h1>{{characterName}}</h1>
		<p>{{description}}</p>
		<a href="{{characterUrl}}" target="_blank">
			Find out more
		</a>
		<div class="close" ng-click="close()">x</div>
	</div>
</popup>

The Directive

Now you might be wondering "popup? there is no tag popup" and you would be correct. This is an Angular directive which from my understanding creates an element and is like Shadow DOM?! You can also add it as a class or attribute on a tag. But in order to manipulate the DOM and the "Angular" way I created a directive to work with it. It is pretty simple.


app.directive('popup',function() {
	var linker = function(scope,element,attrs) {
		scope.$on('contentLoaded',function() {
			element.addClass('show');
		});
		scope.close = function() {
			element.removeClass('show');
		};
	};
	return {
		restrict: 'E',
		link: linker
	};
});

I have one function in here called linker that takes a few options. The scope, which will be the same as out SingleCharacter controller, the element which will be our popup element, and some attrs which is just any attributes on the element. I use the Revealing Module Pattern to expose the linker function. I will not to into too much detail about the directive, because I found this post by Jason More pretty helpful. Essentially this let me interact with the element the want I wanted.

The Service

And last but not least is the ComicBooks service that I use to load my data from the Marvel API. I will not show the whole thing, that can be found in the GitHub repo near the bottom of the file. The service is created using the .factory method.


app.factory('ComicBooks',function($http,$q) 

Much like everything else, we give it a name, and then it takes a function with our dependencies. In this case since we need to get some information we need the $http service as well as the $q service. $q is actually an implementation of the CommonJs Promises proposal, lets see it in action.


var findNext = function(offset) {
		var def = $q.defer();
		var url = baseUrl + 'public/characters?limit=' + limit +'&offset=' + (limit*offset) + '&apikey=' + publicKey;
		$http.get(url).success(def.resolve).error(def.reject);

		return def.promise;
	};

This is one of the functions I expose in my service. We define a new defer object, set up our url and then .get() the url. .get() has success and error methods on there. In these methods we can either resolve our deferred object, or reject it. We then return that object to be used later. If you recall back to our single character controller, used the ComicBook service like this:


ComicBooks.findOne(id).then(function(result) ...

Since we return the deferred object, we can use the .then method to ensure we get our information before we try to do anything with it.

That is about all I wanted to cover. I will point out that in my ComicBook service I have LoadMore function. This is used for the infinite scroll feature. I would check out this demo on the doc site, as that is pretty much what I ended up implementing it.

The Marvel API

Located at developer.marvel.com this was a lot of fun to work with. Getting started is pretty simple. You just need to create an account and then you are provided with your API keys. If you look at the comics.js file in the ComicBooks service you can see my usage of it. Keep in mind that my API key will not work, so you will have to use your own. Take a second to read the documentation and you will be up and running in no time.

To recap, here are some helpful links.

AngularJS In-Depth: https://frontendmasters.com/courses/angularjs-in-depth/
Jason More on directives: http://jasonmore.net/angular-js-directives-difference-controller-link/
Revealing Module Pattern: http://addyosmani.com/resources/essentialjsdesignpatterns/book/#revealingmodulepatternjavascript

I hope that helps you if you are very new to AngularJS. I know it is not an exhaustive break down of the app, but more the points I struggled with.