Tutorial paso a paso para tu primera App con AngularJS

Qué es AngularJS?

Angular JS es una framework JavaScript MVC desarrollado por Google que permite construir aplicaciones “Front End” bien estructurados, fáciles de probar y mantenibles.

Y Porqué Debería Usarlo?

Si no has probado AngularJS todavía, te lo estás perdiendo. Este framework consiste en un conjunto de herramientas estrechamente integrado que te ayudará a construir aplicaciones “rich client-side” bien estructuradas de una manera modular – con menos código y más flexibilidad.

Angular JS extiende el HTML proveyendo directivas que agregan funcionalidad a tu “markup” y permitiéndote crear poderosas plantillas dinámicas. También puedes crear tus propias directivas, elaborando componentes reusables que respondan a tus necesidades y abstrayendo toda la lógica de la manipulación del DOM.

También implementa “two-way data binding” (enlace de datos bidireccional), conectando tu HTML (vistas) con tus objetos JavaScript (modelos) transparentemente. En términos simples, significa que cada actualización a tu modelo se verá reflejado inmediatamente en tu vista sin necesidad de realizar ninguna manipulación ni manejo de eventos del DOM (como en JQuery).

AngularJS provee servicios por encima del XHR que simplifican de manera dramática tu código y te permite abstraer las llamadas a una API en servicios reusables. Con eso, puedes mover tu modelo y lógica de negocio al “Front-End” y construir aplicaciones web independientes del “Back-End”.

Finalmente, me encanta AngularJS por su flexibilidad en cuanto a las comunicaciones con el servidor. Como la mayoría de los frameworks JavaScript MVC, te permite trabajar con cualquier tecnología del lado del cliente mientras pueda servir tu aplicación a través de una API web RESTful.

Entonces, Por Dónde Comienzo?

Primero, decidamos la naturaleza de la aplicación que queremos construir. En esta guía, preferimos no gastar mucho tiempo en el “Back-End”, así que escribiremos algo basado en data que sea fácilmente obtenible en la Internet.

Como sucede que soy un gran fan de las carreras de autos y de la Fórmula 1, voy a usar un servicio API de automovilismo que actuará como nuestro “Back-End”. Afortunadamente, los chicos de Ergast son lo suficientemente amables de proveer una API gratis que será perfecta para nosotros.

Para tener una idea de lo que vamos a construir, dale un vistazo a la demo en línea. Para embellecer la demo y mostrar algunas plantillas con AngularJS, he aplicado el asombroso tema Plastic Admin Bootstrap; pero como este artículo no es acerca de CSS, voy a abstraerlo de los ejemplos y dejarlo fuera.

Primeros Pasos

Vamos a comenzar nuestra aplicación de ejemplo utilizando como base el proyecto “angular-seed” ya que no sólo nos provee una estructura para comenzar, sino que también define la base para los tests unitarios con Karma y Jasmine (aunque nosotros no vamos a hacer testing en esta demo).

EDICIÓN (Mayo 2014): Desde que escribí este tutorial, el proyecto “angular-seed” ha realizado modificaciones mayores (incluyendo la adición de “Bower” como gestor de paquetes). Si tienes alguna duda acerca de cómo implementar el proyecto, revisa la primera sección de su guía de referencia.

Ok, ahora que hemos clonado el repositorio e instalado las dependencias, la estructura de nuestra aplicación se verá como ésta:

Estructura de la aplicación

Ahora podemos comenzar a codificar. Como estamos intentado construir una fuente deportiva para la carrera por el campeonato, comencemos con la vista más relevante: la tabla del campeonato.

Tabla del campeonato

Dado que ya tenemos una lista de pilotos definidos en nuestro “scope” (sigue conmigo – ya vamos a llegar a ese punto), he ignorando los CSS, nuestro HTML debería verse así:

<body ng-app="F1FeederApp" ng-controller="driversController">
  <table>
    <thead>
      <tr><th colspan="4">Drivers Championship Standings</th></tr>
    </thead>
    <tbody>
      <tr ng-repeat="driver in driversList">
        <td>{{$index + 1}}</td>
        <td>
          <img src="img/flags/{{driver.Driver.nationality}}.png" />
          {{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
        </td>
        <td>{{driver.Constructors[0].name}}</td>
        <td>{{driver.points}}</td>
      </tr>
    </tbody>
  </table>
</body>

Lo primero que notarás en esta plantilla es el uso de expresiones (“{{“ y “}}”) para retornar el valor de variables. En AngularJS, las expresiones te permiten realizar algunos cálculos para retornar el valor deseado. Algunas expresiones válidas son:

  • {{ 1 + 1 }}
  • {{ 946757880 | date }}
  • {{ user.name }}

Efectivamente, las expresiones son como “snippets” JavaScript. Pero a pesar de ser muy poderosas, no deberías usar expresiones para implementar lógicas de alto nivel. Para eso, usamos directivas.

Entendiendo Directivas Básicas

Lo segundo que notarás es la presencia de ng-attributes, los cuales no se ven típicamente. Esos atributos son directivas.

A un nivel superior, las directivas son marcas (como atributos, etiquetas o nombre de clases) que le dicen a AngularJS que debe añadir un comportamiento específico a un elemento del DOM (o transformarlo, reemplazarlo, etc.). Revisemos los que hemos visto hasta el momento:

  • La directiva ng-app es responsable de preparar tu aplicación definiendo su alcance. En AngularJS, puedes tener múltiples aplicaciones en la misma página, así esta directiva define donde comienza y termina cada una de las distintas aplicaciones.
  • La directiva ng-controller define que controlador va a estar a cargo de la vista. En este caso, designamos el driversController, quién proveerá nuestra lista de pilotos (driversList).
  • La directiva ng-repeat es una de las mas comúnmente usadas y sirve para definir tu plantilla cuando se recorren colecciones de datos. En el ejemplo de arriba, replica una línea de la tabla por cada piloto en driversList.

Agregando Controladores (Controllers)

Por supuesto, no sirve de nada nuestra vista sin un controlador. Agreguemos el controlador driversController a nuestro controllers.js:

angular.module('F1FeederApp.controllers', []).
controller('driversController', function($scope) {
    $scope.driversList = [
      {
          Driver: {
              givenName: 'Sebastian',
              familyName: 'Vettel'
          },
          points: 322,
          nationality: "German",
          Constructors: [
              {name: "Red Bull"}
          ]
      },
      {
          Driver: {
          givenName: 'Fernando',
              familyName: 'Alonso'
          },
          points: 207,
          nationality: "Spanish",
          Constructors: [
              {name: "Ferrari"}
          ]
      }
    ];
});

Se habrán dado cuenta de la variable $scope que estamos pasando como parámetro al controlador. La variable $scope se encargará de enlazar los controladores con las vistas. En particular, almacena toda la data que será usada en la plantilla. Cualquier cosa que le agregues (como el driverList en el ejemplo de arriba) va a ser directamente accesible en las vistas. Por ahora, vamos a trabajar con un arreglo de datos estático, el cual será reemplazado posteriormente con nuestro servicio API.

Ahora agrega esto al archivo app.js:

angular.module('F1FeederApp', [
  'F1FeederApp.controllers'
]);

Con esta línea de código, estamos inicializando nuestra aplicación y registramos los módulos de los que depende. Volveremos a ese archivo (app.js) mas tarde.

Ahora juntemos todo en el archivo index.html:

<!DOCTYPE html>
<html>
<head>
  <title>F-1 Feeder</title>
</head>

<body ng-app="F1FeederApp" ng-controller="driversController">
  <table>
    <thead>
      <tr><th colspan="4">Drivers Championship Standings</th></tr>
    </thead>
    <tbody>
      <tr ng-repeat="driver in driversList">
        <td>{{$index + 1}}</td>
        <td>
          <img src="img/flags/{{driver.Driver.nationality}}.png" />
          {{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
        </td>
        <td>{{driver.Constructors[0].name}}</td>
        <td>{{driver.points}}</td>
      </tr>
    </tbody>
  </table>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/services.js"></script>
  <script src="js/controllers.js"></script>
</body>
</html>

Ya puedes revisar la aplicación y ver tu lista (estática) de pilotos.

Nota: Si necesitas ayuda depurando tu aplicación y visualizando tus modelos y “scope” en el navegador, te recomiendo implementar el fabuloso plugin para Chrome llamado Batarang.

Cargando Data desde el Servidor

Dado que ya sabemos como desplegar la data de nuestro controlador en nuestra vista, es momento de obtener data real desde un servidor RESTful.

Para facilitar la comunicaciones con servidores HTTP, AngularJS provee los servicios $http y $resources. El primero no es sino una capa sobre un XMLHttpRequest o JSONP, mientras el segundo un nivel mayor de abstracción. Vamos a usar $http.

Para abstraer nuestras llamadas al servidor API del controlador, vamos a crear nuestro propio servicio personalizado que obtendrá nuestra data y actuará como un contenedor alrededor de $http agregando lo siguiente al archivo services.js:

angular.module('F1FeederApp.services', []).
  factory('ergastAPIservice', function($http) {

    var ergastAPI = {};

    ergastAPI.getDrivers = function() {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    return ergastAPI;
  });

Con las dos primers líneas, creamos un nuevo módulo (F1FeederApp.services) y registramos un servicio con ese módulo (ergastAPIservice). Fíjate que pasamos $http como un parámetro a ese servicio. Eso le dice al motor de inyección de dependencias (dependency injector) de AngularJS que nuestro nuevo servicio requiere (o depende de) del servicio $http.

De una manera similar, necesitamos decirle a AngularJS que incluya nuestro nuevo módulo en la aplicación. Registrémoslo en el archivo app.js, reemplazando nuestro código existente con:

angular.module('F1FeederApp', [
  'F1FeederApp.controllers',
  'F1FeederApp.services'
]);

Ahora lo único que tenemos que hacer es modificar levemente nuestro archivo controllers.js, incluyendo ergastAPIservice como una dependencia, y estaremos listos:

angular.module('F1FeederApp.controllers', []).
  controller('driversController', function($scope, ergastAPIservice) {
    $scope.nameFilter = null;
    $scope.driversList = [];

    ergastAPIservice.getDrivers().success(function (response) {
        //Dig into the responde to get the relevant data
        $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
    });
  });

Ahora vuelve a cargar la aplicación y revisa los resultados. Fíjate que no hemos hecho ningún cambio a la plantilla, pero agregamos una variable nameFilter a nuestro “scope”. Pongamos esa variable en uso.

Filtros

Genial! Tenemos un controlador funcionando. Pero sólo muestra una lista de pilotos. Agreguémosle un poco de funcionalidad implementando un simple campo de texto que permita filtrar nuestra lista. Agreguemos la siguiente línea al archivo index.html, justo bajo la etiqueta <body>:

<input type="text" ng-model="nameFilter" placeholder="Search..."/>

Ahora estamos haciendo uso de la directiva ng-model. Esta directiva enlaza nuestro campo de texto con la variable $scope.nameFilter y se asegura que su valor siempre esté actualizado con el valor del campo de texto. Ahora, veamos nuevamente el archivo index.html y hagamos un pequeño ajuste a la línea que contiene la directiva ng-repeat:

<tr ng-repeat="driver in driversList | filter: nameFilter">

Esta línea le dice al ng-repeat que, antes de entregar la data, el arreglo driversList debe ser filtrado por el valor guardado en nameFilter.

En este punto, en enlace bidireccional aplica: cada vez que se ingrese un valor en el campo de búsqueda, AngularJS inmediatamente se asegura que la variable $scope.nameFilter, que la asociamos previamente, sea actualizada con el nuevo valor. Como el enlace funciona en las dos direcciones, en el momento en que el valor de nameFilter es actualizado, la segunda directiva asociada a ella (ng-repeat) también obtiene el nuevo valor y la vista es actualizada inmediatamente.

Recarga la aplicación y revisa la barra de búsqueda.

Barra de búsqueda

Fíjate que este filtro busca por la palabra en todos los atributos del modelo, incluyendo aquellos que no estamos usando. Digamos que sólo queremos filtrar por Driver.givenName y Driver.familyName: Primero, agregamos a driversController, justo bajo $scope.driversList = []:

$scope.searchFilter = function (driver) {
    var keyword = new RegExp($scope.nameFilter, 'i');
    return !$scope.nameFilter || keyword.test(driver.Driver.givenName) || keyword.test(driver.Driver.familyName);
};

Ahora, de vuelta al archivo index.html, actualizamos la línea que contiene la directiva ng-repeat:

<tr ng-repeat="driver in driversList | filter: searchFilter">

Recarga la aplicación una vez mas y ahora tenemos una búsqueda por nombre.

Rutas

Nuestro siguiente objetivo es crear una página con el detalle de un piloto que nos permita presionar en cada uno de los pilotos y ver el detalle de su carrera.

Primero, incluyamos el servicio $routeProvider (en el archivo app.js) el que nos ayudará a tratar con las distintas rutas de la aplicación. Luego, agregaremos 2 rutas: una para la tabla del campeonato y otra para el detalle por piloto. Aquí está nuestro nuevo archivo app.js:

angular.module('F1FeederApp', [
  'F1FeederApp.services',
  'F1FeederApp.controllers',
  'ngRoute'
]).
config(['$routeProvider', function($routeProvider) {
  $routeProvider.
	when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}).
	when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}).
	otherwise({redirectTo: '/drivers'});
}]);

Con ese cambio, si navegamos a “http://domain/#/drivers” se cargará el controlador driversController y buscará por la vista parcial para cargar partials/drivers.html. Pero espera! no tenemos ninguna vista parcial todavía, cierto? También tenemos que crearlas.

Vistas Parciales

AngularJS te permite enlazar tus rutas a vistas y controladores específicos.

Pero primero, necesitamos decirle a AngularJS donde debe cargar estas vistas parciales. Para eso, usaremos la directiva ng-view, modificando nuestro archivo index.html para reflejar lo siguiente:

<!DOCTYPE html>
<html>
<head>
  <title>F-1 Feeder</title>
</head>

<body ng-app="F1FeederApp">
  <ng-view></ng-view>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/services.js"></script>
  <script src="js/controllers.js"></script>
</body>
</html>

Ahora, cada vez que naveguemos a través de las rutas de nuestra aplicación, AngularJS obtendrá la vista asociada y la cargará en lugar de la etiqueta <ng-view>. Lo único que tenemos que hacer es crear un archivo llamado partials/drivers.html y poner nuestra tabla HTML de campeonatos allí. También aprovecharemos esta ocasión para enlazar el nombre del piloto a nuestra ruta para el detalle de un piloto:

<input type="text" ng-model="nameFilter" placeholder="Search..."/>
<table>
<thead>
  <tr><th colspan="4">Drivers Championship Standings</th></tr>
</thead>
<tbody>
  <tr ng-repeat="driver in driversList | filter: searchFilter">
    <td>{{$index + 1}}</td>
    <td>
      <img src="img/flags/{{driver.Driver.nationality}}.png" />
      
	  	{{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
	  
	</td>
    <td>{{driver.Constructors[0].name}}</td>
    <td>{{driver.points}}</td>
  </tr>
</tbody>
</table>

Finalmente, decidamos que queremos mostrar en la página del detalle. Que tal un resumen de los datos relevantes del piloto (por ej: fecha de nacimiento, nacionalidad) junto a una tabla con sus resultados recientes? Para hacer eso, agregamos al archivo services.js:

angular.module('F1FeederApp.services', [])
  .factory('ergastAPIservice', function($http) {

    var ergastAPI = {};

    ergastAPI.getDrivers = function() {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    ergastAPI.getDriverDetails = function(id) {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    ergastAPI.getDriverRaces = function(id) {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK'
      });
    }

    return ergastAPI;
  });

Esta vez, proveemos el ID del piloto al servicio para que obtengamos sólo la información relevante al piloto específico. Ahora, modificamos el archivo controllers.js:

angular.module('F1FeederApp.controllers', []).

  /* Drivers controller */
  controller('driversController', function($scope, ergastAPIservice) {
    $scope.nameFilter = null;
    $scope.driversList = [];
    $scope.searchFilter = function (driver) {
        var re = new RegExp($scope.nameFilter, 'i');
        return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName);
    };

    ergastAPIservice.getDrivers().success(function (response) {
        //Digging into the response to get the relevant data
        $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
    });
  }).

  /* Driver controller */
  controller('driverController', function($scope, $routeParams, ergastAPIservice) {
    $scope.id = $routeParams.id;
    $scope.races = [];
    $scope.driver = null;

    ergastAPIservice.getDriverDetails($scope.id).success(function (response) {
        $scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0]; 
    });

    ergastAPIservice.getDriverRaces($scope.id).success(function (response) {
        $scope.races = response.MRData.RaceTable.Races; 
    }); 
  });

El punto importante que hay que fijarse es que hemos inyectado el servicio $routeParams en el controlador de los pilotos. Este servicio nos permitirá acceder a los parámetros de nuestra URL (en este caso para el :id) usando $routeParams.id.

Ahora que tenemos la data en el “scope”, sólo necesitamos las restantes vistas parciales. Vamos a crear un archivo llamado partials/driver.html:

<section id="main">
  Back to drivers list
  <nav id="secondary" class="main-nav">
    <div class="driver-picture">
      <div class="avatar">
        <img ng-show="driver" src="img/drivers/{{driver.Driver.driverId}}.png" />
        <img ng-show="driver" src="img/flags/{{driver.Driver.nationality}}.png" /><br/>
        {{driver.Driver.givenName}} {{driver.Driver.familyName}}
      </div>
    </div>
    <div class="driver-status">
      Country: {{driver.Driver.nationality}}   <br/>
      Team: {{driver.Constructors[0].name}}<br/>
      Birth: {{driver.Driver.dateOfBirth}}<br/>
      Biography
    </div>
  </nav>

  <div class="main-content">
    <table class="result-table">
      <thead>
        <tr><th colspan="5">Formula 1 2013 Results</th></tr>
      </thead>
      <tbody>
        <tr>
          <td>Round</td> <td>Grand Prix</td> <td>Team</td> <td>Grid</td> <td>Race</td>
        </tr>
        <tr ng-repeat="race in races">
          <td>{{race.round}}</td>
          <td><img  src="img/flags/{{race.Circuit.Location.country}}.png" />{{race.raceName}}</td>
          <td>{{race.Results[0].Constructor.name}}</td>
          <td>{{race.Results[0].grid}}</td>
          <td>{{race.Results[0].position}}</td>
        </tr>
      </tbody>
    </table>
  </div>

</section>

Fíjate que ahora le estamos dando un buen uso a la directiva ng-show. Esta directiva sólo mostrará los elementos HTML si la expresión provista es true (es decir, ni false ni null). En este caso, el avatar sólo se mostrará una vez que el objeto piloto haya sido cargado en el scope por el controlador.

Retoques Finales

Agrega un montón de CSS y carga tu página. Deberías terminar con algo como lo siguiente:

Resultado final

Ya estás listo para prender tu aplicación y asegurarte que ambas rutas estén funcionando como se espera. También puedes agregar un menú estático al archivo index.html para mejorar las posibilidades de navegación de los usuarios. Las posibilidades no tienen límite.

EDICIÓN (Mayo 2014): He recibido muchas solicitudes para una versión descargable del código que hemos construido en este tutorial. Por lo tanto he decidido liberarlo aquí (no incluye los CSS). Sin embargo, no recomiendo descargarlo, ya que esta guía contiene cada uno de los pasos necesarios para construir la misma aplicaciones con tus propias manos, lo que será un ejercicio de aprendizaje mucho mas útil y efectivo.

Conclusión

En este punto del tutorial, hemos cubierto todo lo que necesitas saber para escribir una aplicación simple. Cada una de las restantes páginas en la demo (es decir, detalle de los equipos, calendar, etc.) comparten los mismos conceptos y estructura básica que hemos revisado aquí.

Finalmente, ten en mente que AngularJS es un framework muy poderoso, y nosotros sólo hemos rasguñado la superficie en términos de todo lo que nos puede ofrecer.

Este artículo es original de Raoni Boaventura y lo puedes encontrar en este enlace. Me di el trabajo de traducirlo al español porque de todos los tutoriales que he realizado para iniciarme con AngularJS encuentro que es uno de las más claros y ordenados, entregando a la vez una buena base para seguir profundizando por cuenta propia.

Agregar un comentario

Su dirección de correo no se hará público. Los campos requeridos están marcados *