¿Qué es AngularJS?
AngularJS es un marco de JavaScript MVC desarrollado por
Google, el cual permite construir aplicaciones front-end bien estructuradas y
fáciles de comprobar y mantener.
¿Y Por Qué Debo Utilizarlo?
Si no has probado AngularJS todavía, es una lástima. El marco consiste en un
conjunto de herramientas bien integradas que te ayudará a construir
aplicaciones del lado del cliente, bien estructuradas en un sistema modular,
con menos código y más flexibilidad.
AngularJS extiende
HTML, proporcionando directrices que añaden funcionalidad a tu margen de beneficio
y te permite crear plantillas dinámicas poderosas. También puedes crear tus
propias directrices, elaborando componentes reusables que completan tus
necesidades y abstrayendo toda la lógica de manipulación del DOM.
También implementa binding de datos de dos vías, conectando tu HTML
(vistas) a los objetos de JavaScript (modelos) sin problemas. En términos simples, esto significa que cualquier
actualización de tu modelo se reflejará inmediatamente en tu vista, sin
necesidad de ningún tipo de manipulación DOM o el control de eventos (por
ejemplo, con jQuery).
Angular presta
servicios en la parte superior de XHR que simplifican considerablemente tu
código y permite abstraer llamadas API en servicios reusables. Con esto, puedes mover tu modelo y lógica de negocio para
el front-end y construir aplicaciones web back-end independientes (agnostic).
Por último, me
encanta Angular debido a su flexibilidad en cuanto a la comunicación del
servidor. Como la mayoría de los marcos de JavaScript MVC, Angular te permite
trabajar con cualquier tecnología de servidor, siempre que puede servir a tu
aplicación a través de una API REST Web. Pero Angular también proporciona
servicios aparte de XHR, los cuales simplifican considerablemente tu código y
te permite abstraer llamadas API en servicios reusables. Como resultado, se
puede mover el modelo y la lógica de negocio para el front-end y construir
aplicaciones web back-end independientes. En este post vamos a
hacer precisamente eso: un paso a la vez.
Así que, ¿Por Dónde Empezamos?
En primer lugar, vamos a decidir la naturaleza de la aplicación que
queremos construir. En esta guía, preferimos no pasar demasiado tiempo en el
back-end, por lo que vamos a escribir algo sobre la base de datos que es fácil
de obtener en internet, ¡como una aplicación de noticias deportiva!
Ya que soy un gran
fan del automovilismo y la Fórmula 1, voy a utilizar un servicio API Autosport
como nuestro back-end. Por suerte, los chicos de Ergast son lo suficientemente amables para proporcionar
una API de automovilismo gratis, la cual es perfecta para nosotros.
Como adelanto de lo
que vamos a construir, echa un vistazo al demo en vivo. Para embellecer el demo y mostrar algunas
plantillas Angular, apliqué un tema Bootstrap de WrapBootstrap, pero ya que éste artículo no es acerca de CSS,
lo voy a abstraer de los ejemplos y dejarlo fuera por completo.
Tutorial para Comenzar
Vamos a iniciar
nuestra aplicación de ejemplo con un poco de Boilerplate. Recomiendo el
proyecto angular-seed, ya que no sólo proporciona un gran esqueleto
para bootstrapping, sino que también establece las bases para las pruebas de
unidad con Karma y Jasmine(no vamos a hacer ninguna prueba en éste demo,
así que vamos dejar eso de lado por ahora; ve la Parte 2 de éste tutorial para obtener más información
sobre la configuración de tu proyecto, para pruebas unitarias y de extremo a
extremo).
EDITADO (Mayo de
2014): Desde que escribí éste tutorial, el proyecto angular-seed ha pasado por algunos cambios importantes
(incluyendo la adición de Bower como gestor de paquetes). Si tienes alguna duda
acerca de cómo implementar el proyecto, echa un vistazo rápido a la primera
sección de su guía dereferencia. En la parte 2 del tutorial, Bower, entre otras herramientas, es explicado
en mayor detalle.
Bien, ahora que hemos clonado el repositorio e instalado las
dependencias, el esqueleto de nuestra aplicación va a tener este aspecto:
Ahora podemos empezar
a codificar. Como estamos tratando de construir aplicación de noticias de
deporte para un campeonato de carreras, vamos a empezar con la vista más
relevante: la tabla del campeonato.
Teniendo en cuenta que ya tenemos una lista de los conductores definida
dentro de nuestro alcance (Quédate conmigo – Llegaremos ahí), y haciendo caso
omiso de cualquier CSS (para facilitar la lectura), nuestra HTML podría ser:
<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}} {{driver.Driver.familyName}} </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table> </body>
La primera cosa que notarás en esta plantilla es el uso de expresiones
(“{{“ y “}}”) para regresar valores de las variables. En AngularJS, las
expresiones permiten ejecutar algunos cálculos, con el fin de regresar un valor
deseado. Algunas expresiones válidas serían:
·
{{ 1 + 1 }}
·
{{ 946757880 | date }}
·
{{ user.name }}
Efectivamente, las expresiones son fragmentos parecidos a JavaScript. A
pesar de ser muy potente, no deberías utilizar expresiones para implementar
cualquier lógica de nivel superior. Para
ello, utilizamos directrices.
La Comprensión de las Directrices Básicas
La segunda cosa que notarás es la presencia de ng-attributes, que no
verías en el marcado típico. Esas son las directrices.
En un nivel alto, las directrices son marcadores (como atributos,
etiquetas y nombres genéricos) que le ordenan a AngularJS adjuntar un
comportamiento dado a un elemento DOM (o transformarlo, reemplazarlo, etc.).
Vamos a echar un vistazo a los que ya hemos visto:
·
La directriz
ng-app
es responsable de hacer bootstrapping a tu aplicación,
para definir el ámbito de ésta. En AngularJS, puedes tener múltiples
aplicaciones dentro de la misma página, por lo que esta directriz define el
lugar donde comienza y termina cada aplicación.
·
La directriz
ng-controller
define qué controlador estará a cargo de tu
vista. En este caso, la denotamos driversController
, la cual proporcionará nuestra lista de
conductores (driversList
).
·
La directriz
ng-repeat
es una de las más utilizadas, y sirve para definir tu
alcance de plantilla al pasar a través de colecciones. En el ejemplo anterior,
repite una línea en la tabla por cada conductor en driversList
.
Añadir Controladores
Por supuesto,
nuestra vista no sirve de nada, sin un controlador. Vamos a añadir driversController a nuestros
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"} ] } ]; });
Seguro notaste la
variable $scope que estamos pasando como parámetro al controlador. La variable
$scope
se supone que debe enlazar tu controlador y
vistas. En particular, lleva todos los datos que se utilizarán dentro de la
plantilla. Todo lo que se agrega a ella (como la driversList
del ejemplo anterior) será directamente accesible
en tus vistas. Por ahora, vamos a trabajar
con una matriz de datos ficticios (estática), que vamos a sustituir más tarde
con nuestro servicio API.
Ahora, añade esto a app.js:
angular.module('F1FeederApp', [ 'F1FeederApp.controllers' ]);
Con esta línea de
código inicializamos nuestra aplicación y registramos los módulos de los cuales
depende. Volveremos a ese archivo (
app.js
) más adelante.
Ahora, vamos a poner todo junto en
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}} {{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>
Moduló errores menores, ahora puedes iniciar tu aplicación y comprobar
tu lista (estática) de los conductores.
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ónde 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.
Sign up here with your email
Déjame tus dudas y comentarios: Conversion Conversion Emoticon Emoticon