AngularJS. Framework JavaScript para Webapps

elad . lunes 4 de marzo de 2013. a las 11:06

AngularJS framework javascript para webapps

En nitsnets | studios vamos a desarrollar un nuevo gran proyecto para una importante multinacional. Un B2B (business to business) de la que pronto podremos contar más. El reto: desarrollar toda la intranet sin lenguaje servidor ya que la capa de lógica de negocios está totalmente implementada por webservices y por tanto la creación de una Webapp o Web Application con tecnología estándar HTML/CSS3/Javascript para la correcta visualización en multiplataforma y multidispositivo.

Después de tiempo investigando y planteando incluso una solución realizada a medida con nuestro jefe de proyectos Alex Such dimos con la solución: AngularJS un framework javascript MVC que se adapta a la perfección y encaja a todo lo que habíamos ideado. Para los más flasheros un sustituto ideal con tecnología estándar web de las RIAs (Rich Internet Application) creadas con Flex.

Durante este artículo os mostraremos un ejemplo de cómo empezar y una buena forma de organizarnos con AngularJS

¿Qué es AngularJS?

AngularJS es un impresionante framework javascript opensource desarrollado por Google. Un framework para crear Webapps en lenguaje cliente con Javascript ejecutándose con el conocido single-page applications (aplicación de una sóla página) que extiende el tradicional HTML con etiquetas propias (directivas) como pueden ser ng-app, ng-controller, ng-model, ng-view…

Un framework basado en MVC (Modelo-Vista-Controlador) increíblemente flexible, de muy fácil lectura y desarrollo rápido (puedes empezar en minutos).

Permite extender HTML con tags personalizados, definir y vincular (data-binding) variables vista/controllador, consultas ajax con peticiones HTTP, sistema óptimo de templating, manipulación de datos en JSON, inyección de dependencias, deep linking, formularios de validación, desacoplamiento del DOM de Javascript, internacionalización i18n y l10n, filtros, unit testing

AngularJS es compatible con los navegadores de última generación (Chrome, Firefox, Safari, Opera, Webkits, IE9+) y se puede hacer compatible para Internet Explorer 8 o anterior mediante varios hacks

Proyecto: listado de libros y acceso a su ficha


http://lostiemposcambian.com/blog/posts/angular-js/

ver demo

La aplicación final es muy sencilla y similar al tutorial oficial de AngularJS de Google, vamos a intentar hacer un paso a paso en español y explicando algunos detalles más no exclusivamente a nivel de código como la organización de los ficheros y clases del framework. La aplicación consta de un listado de libros obtenido de una petición ajax a unos datos JSON desde el servidor, además de un filtrado vía javascript y cambio de página a la ficha de cada libro.

Estructura de ficheros y organización

El framework AngularJS se carga con la inclusión de un único fichero http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js. Realmente AngularJS no indica (como en otros frameworks) cuál seria la estructura de carpetas a seguir. Nosotros hemos planteado la siguiente organización por similitud y experiencia en otros frameworks:

estructura y organización proyecto framework AngularJS

Las carpetas del raíz son:

  • css/ estilos CSS de la interfaz
  • data/ ficheros JSON de datos. No sería necesario si invocáramos a servicios web.
  • img/ imágenes de los libros
  • lib/ podría haber librerías de javascript comunes a todos los proyectos. No está en este ejemplo.
  • src/ todo el código javascript de nuestra aplicación
  • index.html HTML principal que carga toda la aplicación

Realmente la estructura importante es donde se encuentra nuestra aplicación en src/:

  • config/ constantes de configuración como rutas, urls de conexión a WS, etc… No utilizado en este ejemplo
  • controllers/ controladores de la app.
  • directives/ componentes o etiquetas extendidas de HTML. No utilizado en este ejemplo
  • filters/ filtros para búsquedas de objetos. No utilizado en este ejemplo
  • lib/ librerías javascript.
  • models/ modelos. No utilizado en este ejemplo, directamente el modelo en el ejemplo es el propio JSON pero se podría encapsular en la carpeta models
  • services/ servicios de la app. Llamadas a los webservices. No utilizado en este ejemplo, las llamadas simples las haremos desde el controlador
  • views/ vistas de la app. Vistas y parciales de vista
  • app.js inicialización de la aplicación. Donde se encuentra el routing

Inicialización de la APP. ng-app

index.html


<!doctype html>
<html lang="es" ng-app="app">
<head>
	<meta charset="utf-8"> 
    <link href="css/estilos.css" rel="stylesheet" media="screen">
    
	<title>Libros APP . AngularJS</title>
	<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script>
    
    <script src="src/app.js"></script>
    <script src="src/controllers/LibrosListController.js"></script>
    <script src="src/controllers/LibroDetailController.js"></script>
   
</head>

<body>

	<div ng-view></div>
    
</body>
</html>

Toda la aplicación pasará siempre por index.html donde cargaremos AngularJS y todas las clases javascripts necesarias. Para que AngularJS haga efecto tenemos que poner la directiva ng-app y para poder referenciarla dar un nombre a nuestra aplicación, en este caso app: <html lang=»es» ng-app=»app»>.

Cargamos los javascript de inicialización de la aplicación app.js y los controladores de la aplicación LibroDetailController.js y LibrosListController.js.

Por último definimos el cajón donde los controladores irán cambiando las vistas mediante la directiva de AngularJS ng-view: <div ng-view></div>

Routing

En la inicialización de la aplicación definimos las rutas con los pares Controlador-Vista. De esta manera podremos definir la ruta que muestra la aplicación mediante slash #

src/app.js

angular.module('app', []).

  //definimos las rutas de la 'app'
  config(['$routeProvider', function($routes) {
  
  $routes.
      when('/libros', {
		  templateUrl: 'src/views/libros-list.html',
		  controller: LibrosListController
		  }).
	  
	  //mediante dos puntos (:) definimos un parámetro
      when('/libro/:libroId', {
		  templateUrl: 'src/views/libro.html',
		  controller: LibroDetailController
		  }).
	 
	  //cualquier ruta no definida  
      otherwise({
		  redirectTo: '/libros'});

}]);

En este ejemplo tendremos dos rutas http://www.midominio.com/#/libros para el listado de libros y http://www.midominio.com/#/libro/1 para la ficha de un libro.

Mediante la llamada $routes.when definimos la url y el par controlador-vista a ejecutar. Por ejemplo, en la ruta #/libros ejecutará el controlador LibrosListController.js y cargará la vista src/views/libros-list.html en el div de index.html que tiene la directiva ng-view.

Como se puede apreciar mediante dos puntos : podemos también pasarle parámetros. En este caso a la ficha del libro le pasaremos como parámetro el identificador del mismo :libroId

Con otherwise podemos definir una ruta por defecto haciendo redirect en cualquier ruta que no corresponda a las ya definidas. En el ejemplo si la ruta no la encuentra irá a la home que es lo mismo que ir a #/libros

Vista

El listado de libros será la home de nuestra aplicación. Se llamará al controlador LibrosListController.js y se pintará la vista libros-list.html en ng-view de index.html.

src/views/libros-list.html

<h2>Libros</h2>
<span class="mini">{{var1}}</span>
 
<ul>
    <li ng-repeat="obj in libros">
    	<a href="#/libro/{{obj.id}}"><img ng-src="{{obj.img}}" width="95" height="150"></a>

         <p><a href="#/libro/{{obj.id}}">{{obj.titulo}}</a><br /><br />
               <strong>{{obj.editorial}}</strong><br />
               {{obj.descripcion}}</p>
    </li>
</ul>

Mediante las dobles llaves {{ var1 }} podremos pintar el valor de una variable definida en un controlador o en un modelo en la vista mediante ng-model. También entre llaves se pueden hacer operaciones javascript, por ejemplo {{ 2+1 }} sacaría en la vista 3.

Con la directiva ng-repeat repetimos la etiqueta donde fue definida tantas veces como objetos hayan. Lo cual lo hace muy cómodo, ya que permite foreach en la propia vista sin manchar el código: <li ng-repeat=»obj in libros»> pinta tantas etiquetas <li> con los datos como objetos libros se reciban.
La vista estará esperando recibir objetos libros en la variable libros con unos atributos id, titulo, img, editorial, descripcion tal y como han sido definidos por el modelo.

Estos objetos libros podrían ser definidos como variable en el controlador así:

...
$scope.libros = [
    {"id": "1",
     "titulo": "El Juego de Ender"
    },
    {"id": "2",
     "titulo": "Juego de tronos"
    },
   {"id": "3",
     "titulo": "I robot"
    },
  ];

o ser cargados desde un JSON como en el ejemplo data/libros.json

[
   {
	 "id": "1",
	 "titulo": "El Juego de Ender",
	 "autor": "Orson Scott Card",
	 "editorial": "Ediciones B / Zeta",
	 "descripcion": "La Tierra está amenazada por una especie extraterrestre de insectos que pretende destruir la humanidad. Para vencerlos se precisa la intervención de un genio militar, por lo cual se permite el nacimiento de Ender, tercer hijo de una pareja en un mundo que limita a dos el número de descendientes. Ender se entrenará en una estación espacial, superará a sus rivales y se convertirá en la persona capaz de dirigir las flotas terrestres contra los insectos de otros mundos.",
	 "img": "img/img1.jpg"
	},
	{
	 "id": "2",
	 "titulo": "Juego de tronos",
	 "autor": "George R. R. Martin",
	 "editorial": "Gigamesh",
	 "descripcion": "Tras el largo verano, el invierno se acerca a los Siete Reinos. Lord Eddard Stark, señor de Invernalia, deja sus dominios para unirse a la corte de su amigo el rey Robert Baratheon, llamado el Usurpador, hombre díscolo y otrora guerrero audaz cuyas mayores aficiones son comer, beber y engendrar bastardos. Eddard Stark ocupará el cargo de Mano del Rey e intentará desentrañar una maraña de intrigas que pondrá en peligro su vida y la de todos los suyos. En un mundo cuyas estaciones pueden durar decenios y en el que retazos de una magia inmemorial y olvidada surgen en los rincones más sombríos y maravillosos, la traición y la lealtad, la compasión y la sed de venganza, el amor y el poder hacen del juego de tronos una poderosa trampa que atrapará en sus fauces a los personajes... y al lector.",
	 "img": "img/img2.jpg"
	},
	{
	 "id": "3",
	 "titulo": "I robot",
	 "autor": "Isaac Asimov",
	 "editorial": "Edhasa",
	 "descripcion": "Los robots de Isaac Asimov son máquinas capaces de llevar a cabo muy diversas tareas, y que a menudo se plantean a sí mismos problemas de 'conducta humana'. Pero estas cuestiones se resuelven en Yo, robot en el ámbito de las tres leyes fundamentales de la robótica, concebidas por Asimov, y que no dejan de proponer extraordinarias paradojas que a veces se explican por errores de funcionamiento y otras por la creciente complejidad de los 'programas'. Las paradojas que se plantean en estos relatos futuristas no son sólo ingeniosos ejercicios intelectuales sino sobre todo una indagación sobre la situación del hombre actual en relación con los avances tecnológicos y con la experiencia del tiempo.",
	 "img": "img/img3.jpg"
	}
]

El buscador no lo explicaremos en este post por abreviar un poco, pero en el código es muy fácil de entender (se encuentra en el ejemplo) así como el ng-model permitiendo una limpieza de código increíble.

Vista parcial

Pero también podríamos cargar mini vistas parciales para hacer más leíble el código. Podríamos encapsular la fichita del libro en una vista parcial transformando:

...
<ul>
    <li ng-repeat="obj in libros">
    	<a href="#/libro/{{obj.id}}"><img ng-src="{{obj.img}}" width="95" height="150"></a>

         <p><a href="#/libro/{{obj.id}}">{{obj.titulo}}</a><br /><br />
               <strong>{{obj.editorial}}</strong><br />
               {{obj.descripcion}}</p>
    </li>
</ul>
...

en

...
<ul>
    <li ng-repeat="obj in libros">
    	<div ng-include src=" 'src/views/_libro-detail.html' "></div>
    </li>
</ul>
...

Donde src/views/_libro-detail.html (por best-practice los nombramos con barra baja delante _) seria el trozo de código de la fichita esperando igual la variable obj (que es un libro)

<a href="#/libro/{{obj.id}}"><img ng-src="{{obj.img}}" width="95" height="150"></a>

         <p><a href="#/libro/{{obj.id}}">{{obj.titulo}}</a><br /><br />
               <strong>{{obj.editorial}}</strong><br />
               {{obj.descripcion}}</p>

Esto se realiza mediante la directiva de AngularJS ng-include. Hay que tener en cuenta que se utilizan las comillas simples ‘ ‘ para definir el path src=» ‘src/views/_libro-detail.html’ « y que dicho path tiene que ser desde la ruta principal (index.html) y no desde el parcial cargado. De esta manera nos queda el código limpísimo.

Es interesante ver en esta parte de la vista como mediante una llamada a un enlace simple <a href=»#/libro/{{obj.id}}»> {{obj.titulo}}</a> se evaluaría el routing y en este caso cargaría el controlador LibroDetailController.js cargando la vista libro.html con el id correspondiente a cada registro.

Controlador

Mediante el controlador podemos utilizar variables definidas en la vista o crear nuevas variables que serán utilizadas en la vista.

src/controllers/LibrosListController.js

function LibrosListController($scope, $http) {
  $http.get('data/libros.json').success(function(data) {
	$scope.libros = data;
  });
 
  //defines una variable
  $scope.var1 = "variable definida desde el controlador";
  
  //selecciona el desplegable y ordena automaticamente, variable definida en la vista con ng-model
  $scope.orderField = "titulo";
  $scope.orderReverse = "true";
}

Mediante $scope tienes el ámbito de la vista. Podemos definir variables para que las utilice la vista con $scope.var1 = «mi variable» (y en la vista visualizarla con {{var1}} ) o al revés, utilizar variables definidas en la vista como es el caso de $scope.orderField = «titulo»;.

Mediante $http puedes hacer peticiones ajax y recoger sus datos. En el ejemplo solicitamos una petición a un fichero estático data/libros.json y se crea una variable llamada libros en la vista con un array de objetos libros: $scope.libros = data; para poder mostrar los libros en la vista como vimos anteriormente.

Si nos fijamos en el otro controlador: src/controllers/LibroDetailController.js

function LibroDetailController($scope, $http, $routeParams) {
  $scope.id = $routeParams.libroId;

  $http.get('data/libro'+$scope.id+'.json').success(function(data) {
	  $scope.libro = data[0];
  });
 
}

Podemos ver como mediante $routeParams podemos recuperar los parámetros dinámicos de la ruta definidos como vimos mediante dos puntos :libroId.

Seguir aprendiendo AngularJS…

Esperamos que en apenas unos minutos te hayas podido hacer con el espectacular framework de Google AngularJS. Si quieres seguir profundizando con AngularJS te recomendamos:

Conclusión: Webapps en cliente

AngularJS marca la transición entre páginas webs y aplicaciones web sin recarga de página, extendiendo las limitaciones de HTML. Una nueva tecnología que será muy utilizada en nitsnets | studios en este 2013.

Por supuesto, existen otros frameworks basados en JavaScript como podrían ser Backbone.js y el mas reciente Ember.js que comparten la misma filosofía que AngularJS, aunque personalmente nos hemos decantado por la tecnología de Google por mayor flexibilidad en conexiones REST, su potente sistema de templating y creador de tags e incluso menos código en data-binding.

Etiquetas: , , , ,

57 Comentarios
» Feed RSS de los Comentarios

  1. elad dice:

    Buenas Pablo,
    Gracias por tus comentarios y nos alegra que te haya gustado el post.

    Realmente cuando pones la dirección en http://www.dominio.com/otrapg el navegador esta intentando cargar la ruta y al no encontrar el fichero sale un 404.

    Prueba con http://www.dominio.com/#/otrapg y verás como te funciona que es lo mismo que si lo tienes en http://www.dominio.com/index.html#otrapg

    Hay una propiedad en html5 y Angularjs que te puede hacer cambiar este comportamiento y conseguir simular urls bonitas (pretty) o tradicionales. El método se encuentra en $locationProvider.html5Mode(true);
    Eso sí, depende de que el navegador interprete HTML5.

    Tienes un post muy bueno sobre esto en https://scotch.io/quick-tips/pretty-urls-in-angularjs-removing-the-hashtag

    espero que te sirva!

  2. Camilo Serna dice:

    Buen día Elad,

    Ando trabajando en Angular, estoy con la internacionalización, pero necesito que se aplique a unas variables que estoy creando dinámicamente con JQuery, es decir, tengo algo como:

    <htm..

    </htm…

    En el controlador algo como:

    $( "#miObjeto" ).append( "{{ miVariable }}" );

    miVariable sería una variable internacionalizada.

    He intentado con varios, como attr, etc.
    Lo que detecté es que Angular no coge las variables que se crean dinámicamente o por lo menos no así de sencillo.

    Podría por favor comunicarme de qué forma puedo yo realizar esto, gracias.

  3. Camilo Serna dice:

    Hola buenos días de nuevo Elad,

    Quisiera detallarle un poco sobre lo que he descubierto que en la anterior pregunta no me hice entender muy bien.

    Mi objetivo es crear un menú dinámico por medio de la base de datos, de acuerdo a lo que yo obtengo pinto las variables, por ejemplo madmin, musuario, etc. Y se deben mapear con la Internacionalización (i18n)

    Lo que observo es que si tengo esto en la vista:

    Y en el controlador:
    var app = angular.module(«myApp», []);

    app.controller(‘myCtrl’, function ($scope) {
    $(‘#myDiv’).append( ‘{{ mAdmin + » » + lastName }}’ );
    $scope.lastName = «Doe»;
    $scope.mAdmin = «Administrador»;
    });

    No me funciona, pero si el controlador está así:
    $(‘#myDiv’).append( ‘{{ mAdmin + » » + lastName }}’ );

    var app = angular.module(«myApp», []);

    app.controller(‘myCtrl’, function ($scope) {
    $scope.lastName = «Doe»;
    $scope.mAdmin = «Administrador»;
    });

    El JQuery generando el objeto antes, lo coge, en ese momento pensé que había dado con el problema y estaba seguro que eran los momentos de carga del Angular y el JQuery.

    Pero como necesito acceder a la base de datos, le hice no accediendo con el http de Angular (para no usarlo primero) sino con el $.getJSON de JQuery, pero aún así no me las toma como una variable Angular, aparecen así todavía: {{ madmin }}

    No hallo la forma aún de que Angular me coja esa variable traída desde la base de datos 🙁

    Gracias.

  4. elad dice:

    Buenas Camilo:
    No deberías utilizar jquery para manipular el DOM en los controladores de Angularjs.
    Puedes ver esto en https://docs.angularjs.org/misc/faq

    Realmente lo que tienes que hacer es cargar una variable desde el controlador y luego vincularla con la vista. Esta variable debería ser un array que es cargada a partir de una llamada ajax mediante http.get (por ejemplo).
    var app = angular.module(“myApp”, []);
    app.controller(‘myCtrl’, function ($scope, $http)) {

    $http.get(‘mi-url-php’).success(function(data) {
        $scope.myMenu = data;
      });

    Te lo pongo en modo pseudocódigo (no es código que copies y pegues :P) para que lo entiendas. Donde pone mi-url-php es la ruta donde llamas a código al servidor y allí consultas a la base de datos y cargas la información. Si quieres cargar con I18 debería enviarle el parámetro del idioma para que el servidor sepa que quieres cargar un idioma correspondiente y allí hacer el filtro por base datos con ese idioma. Puedes ver como pasar parámetros aquí: http://stackoverflow.com/questions/13760070/angularjs-passing-data-to-http-get-request

    Recuerda que deberás devolver un .json con una estructura tipo:

    [
       {
         «mAdmin»: «Administrador”,
         «lastName»: «Doe»
        },
        {
        «mAdmin»: «Administrador»,
         «lastName»: «Doe 2»    },
        {
         «mAdmin»: «Administrador»,
         «lastName»: «Doe»
        }
    ]

    Una vez tienes cargado la variable si la tenemos bindada la vista lo reproducirá automáticamente, mediante ng-repeat hacemos un bucle y cargamos el array.

    <ul>     

    <li ng-repeat="obj in myMenu">         
    <a href="#" rel="nofollow">{{obj.mAdmin + “ “ + obj.lastName }}</a>
    </li>

    </ul>

    Si haces el tutorial del artículo te quedará más claro 🙂
    Eso sí en el post se habla de llamadas a .json estático que sería sustituible por una llamada a un php (por ejemplo) con carga de datos en BD.

    Espero que te sirva

  5. Joaquín Bresan dice:

    Muy buen post, gracias por compartir.

  6. Camilo Serna dice:

    Elad buen día,

    Muchas gracias por sus oportunas respuestas, ya hice la generación dinámica de objetos con sólo Angular, volé todo indicio de JQuery, ¡y ahora si! ¡por fin! veo la diferencia de Angular y JQuery, ahora sé por qué es mucho mejor, gracias.

  7. OLGUIN LLASA dice:

    Gracias,
    me parece interesante aprender AngularJS – framework
    lo tomare mi tiempo para practicarlo, es muy interactiva.

Enviar comentario