Hola a todos, ya que AngularJS está tan de moda, quiero hacer un pequeño tutorial de como crear una pequeña SPA con AngularJS que permita consumir el API de GitHub.

Así que iniciemos, lo primero es crear la página que va a contener en SPA, tambien conocida como shell view, allí referenciamos los js y css necesarios:


<!DOCTYPE html>
<html ng-app="githubSearcher">
<head lang="en">
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="content/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="content/bootstrap-superhero.min.css">
    <link rel="stylesheet" type="text/css" href="content/site.css">

    <script src="scripts/libs/angular.min.js"></script>
    <script src="scripts/libs/angular-route.min.js"></script>
    <script src="scripts/libs/angular-animate.min.js"></script>

    <script src="scripts/app.js"></script>
    <script src="scripts/Controllers/githubController.js"></script>
    <script src="scripts/Controllers/githubUserController.js"></script>
    <script src="scripts/Services/githubService.js"></script>
    <title>AngularJS - GitHub Searcher</title>
</head>
<body>
    <div class="container">
        <div class="row">
            <h1>AngularJS - GitHub Searcher</h1>
            <hr>
            <div class="page" ng-view></div>
        </div>
        <hr>
        <div class="row">
            <div class="col-xs-12 col-sm-8">
                <p><strong>@julitogtu</strong> |  <strong>http://julitogtu.com</strong></p>
            </div>
        </div>
    </div>
</body>
</html>

Lo primero a remarcar, es que usamos ng-app=”githubSearcher” para especificar que el módulo se va a llamar githubSearcher y adicionalmente para establecer el alcance o la sección en donde va a trabajar AngularJS, para ayudarnos con el diseño usaremos bootstrap, posteriormente referenciamos el core de angularjs y dos módulos adicionales:

  • angular-route: Para luego crear/definir el routing
  • angular-animate: Para tener una transición al cambiar de vista.

Adicionalmente referenciamos algunos archivos los cuales luego vamos a ir creando.

El primer archivo js que se vamos a crear es app.js, allí vamos a definir el módulo (githubSearcher), las dependencias que tiene (route y animate) y el routing:


(function(){
    var app = angular.module("githubSearcher", ["ngRoute","ngAnimate"]);

    app.config(function($routeProvider){
        $routeProvider
            .when("/search", {
                templateUrl: "search.html",
                controller: "githubController"
            })
            .when("/user/:username", {
                templateUrl: "user.html",
                controller: "githubUserController"
            })
            .otherwise({redirectTo:"/search"});
    });
}());

La aplicación solo va a tener dos vistas, es por ello que en el routeProvider solo tenemos dos reglas, una para search (que es la vista iniciar) y la otra para user que será la encargada de mostrar los datos de algún usuario.

En cada regla relacionamos el archivo html (templateUrl) y el controlador (controller) que va a estar asociado en cada vista.

Ahora vamos con el primer html, en este caso search.html, acá basicamente tenemos una caja de texto para el término a buscar y un botón que será el encargado de llamar la función de búsqueda, para los resultados tenemos un html diferente, el cual es llamado gracias a la directiva ng-include=”‘githubresults.html'”:


<div class="col-md-12">
    <div class="well profile col-md-12">
        <form class="form-horizontal" role="form">
            <div class="form-group">
                <label class="col-sm-2 control-label">Topic:</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" placeholder="Topic to search" ng-model="topic" autofocus="autofocus"/>
                </div>
            </div>

            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-10">
                    <input type="button" value="Search" class="btn btn-primary" ng-click="search(topic)" ng-disabled="!topic.length" autofocus="autofocus"/>
                </div>
            </div>
        </form>
        <br>
        <div ng-include="'githubresults.html'" ng-show="list"></div>
    </div>
</div>

El archivo githubresults.html, va a ser el encargado de listar los resultados de la búsqueda, así mismo va a tener algunas opciones adicionales que son:

  • El total de resultados de la búsqueda (en este caso se va a mostrar un máximo de 1000 resultados, esto porque el API de GitHub solo permite ver los primeros 1000)
  • Un combo que va a ser el paginador.
  • Un combo para mostrar 30 o 60 resultados por página.
  • Un combo para ordernar los resultados.

<form class="form-horizontal" role="form">
    <div class="form-group">
        <label class="col-sm-2 control-label">Total Results: </label>
        <div class="col-sm-3">
            <label class="col-sm-2 control-label">{{totalResults}}</label>
        </div>
    </div>
    <div class="form-group">
        <label class="col-sm-2 control-label">Page: </label>
        <div class="col-sm-2">
            <select class="form-control" ng-change="search(topic)" ng-model="$parent.page">
                <option ng-repeat="o in pages" value="{{o}}">{{o}}</option>
            </select>
        </div>
        <label class="col-sm-2 control-label">Items by Page: </label>
        <div class="col-sm-2">
            <select class="form-control" ng-change="search(topic)" ng-model="$parent.pageSize">
                <option value="30">30</option>
                <option value="60">60</option>
            </select>
        </div>
        <label class="col-sm-2 control-label">Order:</label>
        <div class="col-sm-2">
            <select  class="form-control" ng-model="$parent.order">
                <option value="+owner.login" selected="selected">Username [a-z]</option>
                <option value="-owner.login" selected="selected">Username [z-a]</option>
                <option value="+description" selected="selected">Description [a-z]</option>
                <option value="-description" selected="selected">Description [z-a]</option>
            </select>
        </div>
    </div>
</form>

<table class="table table-bordered table-hover table-striped">
    <thead>
        <tr class="success">
            <th></th>
            <th>Username</th>
            <th>Description</th>
            <th>URL</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="item in list | orderBy:order">
            <td>{{$index + 1}}</td>
            <td><a title="View details of {{item.owner.login}}" ng-click="userDetails(item.owner.login)">{{item.owner.login}}</a></td>
            <td>{{item.description}}</td>
            <td><a ng-href="{{item.html_url}}" target="_blank">{{item.html_url}}</a></td>
        </tr>
    </tbody>
</table>

Ya que está listo el html de búsqueda y resultados, vamos a crear el controlador githubController, el cual tiene como función principal llamar el servicio que consulta el api de GitHub y navegar hacia la vista user (utilizando location.path) para ver el detalle de algún usuario:


(function(githubSearcher) {

    var githubController = function(scope, log, location, githubService) {

        var onSearchComplete = function(data)
        {
            scope.list = data.items;
            if(data.total_count > 1000)
                scope.totalResults = 1000;
            else
                scope.totalResults = data.total_count;

            scope.totalPage = scope.totalResults/scope.pageSize;
            scope.pages = [];
            for(var i = 1; i <= scope.totalPage; i++)
            {
                scope.pages.push(i);
            }
        };

        var onError = function(err) {
            log.info(err);
        };

        scope.search = function(topic){
            githubService
                        .getResult(topic, scope.page,scope.pageSize)
                        .then(onSearchComplete, onError);
        };

        scope.userDetails = function (username) {
            location.path("/user/" + username);
        };

        scope.topic = "";
        scope.username = "";
        scope.order = "+owner.login";
        scope.page = 1;
        scope.pageSize = 30;
    };

    githubController.$inject = ['$scope','$log','$location','githubService'];

    githubSearcher.controller("githubController", githubController);

}(angular.module('githubSearcher')));

Ahora vamos a crear el servicio githubService, dicho servicio se crea para que el o los controladores no consuman directamente el api de GitHub, en cambio tenemos un servicio que realiza esa tarea, así faciliamos la mantenibilidad y separación de responsabilidades entre otros:


(function(){

    var githubService = function(http, log){

        var getResult = function(topic, page, pageSize){
            return http.get("https://api.github.com/search/repositories?q=" + topic + "&page=" + page + "&per_page=" + pageSize)
                .then(function(response){
                    return response.data;
                });
        };

        var getUser = function(username)
        {
            return http.get("https://api.github.com/users/" + username)
                .then(function(response){
                    return response.data;
                });
        };

        return {
            getResult: getResult,
            getUser: getUser
        };

    };

    githubService.$inject = ['$http','$log'];

    var module = angular.module("githubSearcher");
    module.factory("githubService", githubService);

}());

Hasta el momento, la aplicación debería ir algo así:

githubdemo1

Ya está la primera parte lista, ahora seguimos con el html de los detalles del usuario, es decir user.html:


<div class="container">
    <div class="row">
        <div class="col-md-offset-2 col-md-8 col-lg-offset-3 col-lg-6">
            <div class="well profile">
                <div class="col-sm-12">
                    <div class="col-xs-12 col-sm-8">
                        <h2>{{user.name}}</h2>
                        <p><strong>Location: </strong> {{user.location}} </p>
                        <p><strong>Email: </strong> {{user.email}} </p>
                        <p><strong>Blog: </strong> <a ng-href="http://{{user.blog}}" target="_blank">http://{{user.blog}}</a> </p>
                        <p><strong>Company: </strong> {{user.company}} </p>
                    </div>
                    <div class="col-xs-12 col-sm-4 text-center">
                        <figure>
                            <!--<img ng-src="http://www.gravatar.com/avatar/{{user.gravatar_id}}" title="{{user.name}}" class="img-circle img-responsive">-->
                            <img ng-src="{{user.avatar_url}}" title="{{user.name}}" class="img-circle img-responsive">
                        </figure>
                    </div>
                </div>
                <div class="col-xs-12 divider text-center">
                    <div class="col-xs-12 col-sm-4 emphasis">
                        <h2><strong> {{user.followers}} </strong></h2>
                        <p><small></small></p>
                        <button class="btn btn-success btn-block"><span class="fa fa-user"></span> Followers </button>
                    </div>
                    <div class="col-xs-12 col-sm-4 emphasis">
                        <h2><strong>{{user.following}}</strong></h2>
                        <p><small></small></p>
                        <button class="btn btn-success btn-block"><span class="fa fa-user"></span> Following </button>
                    </div>
                    <div class="col-xs-12 col-sm-4 emphasis">
                        <h2><strong>{{user.public_repos}}</strong></h2>
                        <p><small></small></p>
                        <button class="btn btn-success btn-block"><span class="fa fa-user"></span> Repositories </button>
                    </div>
                </div>
                <div class="col-sm-12">
                    <div class="col-xs-12 col-sm-8">
                        <br>
                        <a href="#/search">New search</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Una vez tenemos el html que va a mostrar los detalles del usuario, vamos a crear el controlador relacionado, es decir githubUserController.js:


(function(githubSearcher) {

    var githubUserController = function(scope, log, location,routeParams,githubService) {

        var onUserComplete = function(data)
        {
            scope.user = data;
        };

        var onError = function(err) {
            log.info(err);
        };

        scope.username = routeParams.username;
        githubService
                .getUser(scope.username)
                .then(onUserComplete, onError);
    };

    githubUserController.$inject = ['$scope','$log','$location','$routeParams','githubService'];

    githubSearcher.controller("githubUserController", githubUserController);

}(angular.module('githubSearcher')));

Dicho controlador, básicamente consume el servicio que hemos creado para obtener los detalles de un usuario específico, para leer el nombre del usuario que se ha enviado como parámetro usamos routeParams.username, donde username fué el nombre del parámetro que se definió en routeProvider, acá los detalles del usuario se ven cómo:

githubdemo2

Y por el momento ha sido todo, iré haciendo cambios a ejemplo y publicando dichos cambios en el blog con su explicación.



Puedes ver el código completo en: https://github.com/julitogtu/GitHubSearcher

Y el demo en: http://demogithubseracher.azurewebsites.net

Cualquier comentario/pregunta no dudes en comentar el post.

Saludos!