Un Tutorial Paso-a-Paso para tu Primera Aplicación AngularJS
Cargando Datos del Servidor
Como ya sabemos cómo mostrar los datos de nuestro controlador en nuestra vista, es momento de traer datos en vivo desde un servidor RESTful.
Para facilitar la comunicación con los servidores HTTP, AngularJS proporciona los servicios $http
y $resource
. El primero es una capa en la parte superior de XMLHttpRequest o JSONP, mientras que el último proporciona un mayor nivel de abstracción. Vamos a utilizar $http
.
Para abstraer nuestras llamadas a la API del servidor desde el controlador vamos a crear nuestro propio servicio personalizado, el cual va a capturar los datos y actuará como una envoltura alrededor de $http
al añadirlo a nuestro 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 primeras líneas, creamos un nuevo módulo (F1FeederApp.services
) y registramos un servicio dentro de ese módulo (F1FeederApp.services
). Nótese que pasamos $http como parámetro a ese servicio. Esto le dice al motor de inyección de dependenciade Angular, que nuestro nuevo servicio requiere (o depende) del servicio $http
.
De una manera similar, tenemos que decirle a Angular que incluya nuestro nuevo módulo en nuestra aplicación. Vamos a registrarlo con app.js
, reemplazando nuestro código existente con:
angular.module('F1FeederApp', [
'F1FeederApp.controllers',
'F1FeederApp.services'
]);
Ahora, lo único que tenemos que hacer es ajustar nuestra controller.js
un poco, integrar ergastAPIservice
como una dependencia, y estaremos listos para continuar:
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, recarga la aplicación y revisa el resultado. Observa que no hicimos ningún cambio en nuestra plantilla, pero añadimos una variable nameFilter
a nuestro alcance. Vamos a poner esta variable en uso.
Filtros
¡Estupendo! Tenemos un controlador funcional. Pero sólo muestra una lista de conductores. Vamos a añadir algunas funciones mediante una simple entrada de búsqueda de texto, que filtrará la lista. Vamos a añadir la siguiente línea a nuestro index.html
, justo debajo de la etiqueta <body>
:
<input type="text" ng-model="nameFilter" placeholder="Search..."/>
Ahora estamos haciendo uso de la directriz ng-model
. Esta directriz une nuestro campo de texto a la variable $scope.nameFilter
y se asegura de que su valor esté siempre al día con el valor de entrada. Ahora, vamos a visitar index.html
una vez más y hagamos un pequeño ajuste en la línea que contiene la directriz ng-repeat
:
<tr ng-repeat="driver in driversList | filter: nameFilter">
Esta línea le dice a ng-repeat
que, antes de dar salida a los datos, la matriz driversList
debe ser filtrada por el valor almacenado en nameFilter
.
En este punto, entran los datos bidireccionales binding: cada vez que un valor se introduce en el campo de búsqueda, Angular asegura inmediatamente que el $scope.nameFilter
que asociamos a él se actualice con el nuevo valor. Dado que binding funciona en ambos sentidos, el momento en el que el valor nameFilter
se actualiza, la segunda directriz asociada a la misma (es decir, ng-repeat
) también recibe el nuevo valor y la vista se actualiza inmediatamente.
Actualiza la aplicación y observa la barra de búsqueda.
Observa que éste filtro buscará la palabra clave en todos los atributos del modelo, incluyendo los que no estamos usando. Digamos que sólo queremos filtrar Driver.givenName
y Driver.familyName
: En primer lugar, añadimos a driversController
, justo por debajo de la línea $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 a index.html, actualizamos la línea que contiene la directriz ng-repeat
:
<tr ng-repeat="driver in driversList | filter: searchFilter">
Actualiza la aplicación una vez más y ahora tenemos una búsqueda por nombre.
Rutas
Nuestro próximo objetivo es crear una página de datos del conductor, la cual nos permitirá hacer clic en cada conductor y ver los detalles de su carrera.
En primer lugar, vamos a incluir el servicio $routeProvider
(en app.js
) lo que nos ayudará a lidiar con estas variadas rutas de aplicación. A continuación, añadiremos dos de estas rutas: una para la tabla del campeonato y otro para los datos del conductor. Aquí está nuestra nueva 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 éste cambio, la navegación hacia http://domain/#/drivers
cargará el driversController
y buscará la vista parcial que se va a renderizar en partials/drivers.html
. ¡Pero espera! No tenemos ninguna vista parcial todavía, ¿verdad? Vamos a tener que crearlas también.
Vistas Parciales
AngularJS te permitirá unir tus rutas a los controladores y vistas específicas.
Pero primero, tenemos que decirle a Angular dónde renderizar estas vistas parciales. Para ello, usaremos la directriz ng-view
, modificando nuestra 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 nuestras rutas de aplicaciones, Angular cargará la vista asociada y la renderizará en lugar de la etiqueta <ng-view>
. Lo único que tenemos que hacer es crear un archivo con el nombre partials/drivers.html
, y poner nuestra tabla de campeonato HTML allí. También vamos a utilizar esta oportunidad para vincular el nombre del conductor a nuestra ruta de los detalles del conductor:
<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" />
<a href="#/drivers/{{driver.Driver.driverId}}">
{{driver.Driver.givenName}} {{driver.Driver.familyName}}
</a>
</td>
<td>{{driver.Constructors[0].name}}</td>
<td>{{driver.points}}</td>
</tr>
</tbody>
</table>
Por último, vamos a decidir lo que queremos mostrar en la página de detalles. ¿Qué tal un resumen de todos los hechos relevantes sobre el conductor (por ejemplo, fecha de nacimiento, nacionalidad), junto con una tabla que contiene sus resultados recientes? Para hacer eso, añadimos a services.js
, lo siguiente:
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, proporcionamos la identificación del conductor al servicio para que podamos recuperar la información relevante de un conductor específico. Ahora, modificamos 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;
});
});
Lo importante a notar aquí es que solo inyectamos el servicio $routeParams
en el controlador del conductor. Este servicio nos permitirá acceder a nuestros parámetros de URL (para el :id, en este caso) utilizando $routeParams.id
.
Ahora que tenemos nuestros datos en el alcance, sólo necesitamos la vista parcial restante. Vamos a crear un archivo con el nombre partials/driver.html
y agregamos:
<section id="main">
<a href="./#/drivers"><- Back to drivers list</a>
<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/>
<a href="{{driver.Driver.url}}" target="_blank">Biography</a>
</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>
Observa que ahora estamos dándole buen uso a la directriz ng-show
. Esta directriz sólo mostrará el elemento HTML si la expresión proporcionada es true
(es decir, ni false
, ni null
). En este caso, el avatar sólo aparecerá una vez que el objeto conductor ha sido cargado en el alcance, por el controlador.
Últimos Toques
Añade un montón de CSS y renderiza tu página. Deberías terminar con algo como esto:
Ahora estás listo para iniciar tu aplicación y asegúrate de que ambas rutas están funcionando como deseas. También puedes añadir un menú estático a index.html
, para mejorar las capacidades de navegación del usuario. Las posibilidades son infinitas.
EDITADO (mayo de 2014): He recibido muchas peticiones para una versión descargable del código que construimos en este tutorial. Por lo tanto, he decidido hacerlo disponible aquí (despojado de cualquier CSS). Sin embargo, la verdad es que no recomiendo descargarlo, ya que ésta guía contiene cada paso que necesitas para generar la misma aplicación con tus propias manos, que será un ejercicio de aprendizaje mucho más útil y eficaz.
Conclusión
En este punto del tutorial, hemos cubierto todo lo que necesitarías para escribir una aplicación sencilla (como un informador de Fórmula 1). Cada una de las páginas restantes en el demo en vivo (por ejemplo, tabla del campeonato de constructores, detalles del equipo, calendario) comparten la misma estructura y conceptos básicos que hemos revisado.
Por último, ten en cuenta que Angular es un marco muy potente y que apenas hemos tocado la superficie, en términos de todo lo que tiene que ofrecer. En la parte 2 de éste tutorial, vamos a dar ejemplos de por qué Angular se destaca entre sus semejantes en marcos MVC front-end: capacidad de prueba. Vamos a revisar el proceso de escribir y ejecutar pruebas unitarias con Karma, lograr la integración continua con Yeomen, Grunt, y Bower y otros puntos fuertes de éste fantástico marco front-end.
El artículo original lo pueden encontrar en Totpal.