Un Tutorial Paso-a-Paso para tu Primera Aplicación AngularJS
¿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 de referencia.
En laparte 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ó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,
yBower y
otros puntos fuertes de éste fantástico marco front-end.
Artículo via Toptal
No hay comentarios.:
Publicar un comentario
Bienvenido a nuestro blog, para nosotros es un gusto que comente nuestro material