Demo: App ToDo con AngularJS y Yeoman en 10 minutos


En esta ocasión te enseñaré con un ejemplo más completo el uso del framework AngularJS.

Crearé una app ToDo (se pronuncia ‘tudú’), cuyo objetivo será crear un regisro de tareas, las cuales podrás marcar como resueltas, eliminarlas y editarlas.

Con estas tareas realizaré un CRUD utilizando servicios. Sin más, comenzamos.

Paso 1: Preparar la app con Yeoman

Asumiendo que tienes Yeoman instalado, junto con el generador para angular (generator-angular), creas la app de la siguiente forma:

yo angular ToDo

Con esto se abre la interfaz interactiva de Yeoman para dicho generador, el cual realiza las siguientes preguntas:

  • ¿Quieres incluir Sass (con Compass)? No
  • ¿Quieres incluir Bootstrap?
  • ¿Qué módulos te gustaría incluir?
    1. angular-animate.js
    2. angular-route.js
    3. angular-touch.js

Los 3 módulos anteriores son los que dejaremos para utilizar en la App, los demás que aparecen dentro de las opciones, desmárcalos presionando la tecla espacio cuando te posiciones sobre ellos.

Al presionar la tecla Enter el generador ejecutará automáticamente los comandos para instalar los assets (angular, bootstrap etc) y los plugins de Grunt que automatizarán todo el flujo de trabajo.

bower install && npm install

Nota: debes tener el proxy para npm y git configurados adecuadamente, sino, dará errores de conexión.

Paso 2: Ejecutar la app

grunt serve

Abre tu navegador favorito en localhost:9000 y verás la aplicación ejecutándose con una plantilla que utiliza bootstrap por defecto (no cierres el proceso de grunt para que se recargue la página con cada cambio que realizas).

Como no tenemos que preparar nada, o sea, incluir assets, incluir el proyecto dentro de un servidor web, nada de eso, vamos directo a programar.

Paso 3: Creando el servicio ToDo

Te debes preguntar ¿por qué no comenzamos inicializando la aplicación?

Yeoman ya lo hizo por nosotros, verás, este generador crea una app por defecto que incluye los componentes usados de forma obligatoria en toda app angular:

  1. Fichero app/index.html: contiene la vista principal y los elementos comunes de toda la aplicación (como el footer y la barra de navegación). Este fichero contiene un elemento con una directiva ng-view, que es donde se insertará el contenido dinámico de la App.
  2. Fichero app/scripts/app.js: se crea el módulo principal junto con la configuración de las rutas. Por defecto el generador crea dos rutas:main y about, por lo que puedes modificarlas y/o añadir nuevas. Para este caso puntual podremos utilizar la ruta main para mostrar la lista de tareas.
  3. Fichero app/scripts/controllers/main.js: contiene el controlador asociado a la ruta main. Aquí pondré la mayoría de la lógica de la app.
  4. Fichero app/scripts/services/todo.js: Este fichero no existe aún. Lo crearé a continuación para que contenga el servicio mencionado anteriormente. Normalmente irás y crearás el fichero por ti mismo, descansa, Yeoman lo hace por ti:

yo angular:factory Todo

Abre el fichero y observa que el cascarón del servicio fue creado. Este servicio/factory devuelve un objeto, que servirá como API para que otros componentes lo consuman.

Observa que por defecto se creó una variable llamada meaningOfLife = 42, la cual puede ser consultada externamente de esta forma:

Todo.someMethod() // returns 42

Esto es así pues someMethod es una función contenida dentro del objeto que devuelve el servicio, que a su vez devuelve el contenido de la variable privada meaningOfLife.

Con esto somos capaces de utilizar el servicio dentro de CUALQUIER lugar de la aplicación, dígase controladores, configuración de las rutas y otros servicios.

Por tanto, vamos a inyectarlo dentro del MainController para utilizarlo.

angular.module('toDoApp')
  .controller('MainCtrl', function ($scope, Todo){

    // obtener el valor de meaningOfLife
    $scope.myNumber = Todo.someMethod();

    $scope.awesomeThings = [
      'HTML5 Boilerplate',
      'AngularJS',
      'Karma'
    ];
});

Como puedes obervar, lo inyecté solo con pasarlo como segundo parámetro a la función, y le asigné a $scope.myNumber el valor de la variable privada meaningOfLife del servicio Todo.

Finalmente podemos visualizar este valor en la vista (app/views/main.html) con solo escribir algo como esto:

Valor de meaningOfLife: {{ myNumber }}

Si entendiste el flujo de información desde un servicio hasta la vista, pasando por el controlador, has cumplido satisfactoriamente el objetivo de este artículo. Te invito a seguir leyendo, para ver cómo terminamos de construir la app.

Paso 4: Implementando el servicio Todo

Al programar el servicio Todo, tendremos un 70% de la aplicación hecha, pues solo restaría “unir los cables”.

El servicio tendrá una variable privada que describirá el listado de tareas registradas, cada tarea tendrá la siguiente estructura:

{
  name : 'Terminar un dibujo',
  done : false
}

donde la clave done describe si la tarea está cumplida o no.

El servicio debe tener los métodos básicos de inserción, eliminación y actualización de las tareas, por lo que una aproximación es la siguiente:

angular.module('toDoApp')
  .factory('Todo', function () {

    var todos = [
      {
        name : 'Leer un libro',
        done  : false
      },

      {
        name : 'Ir al trabajo',
        done  : false
      }
    ];

    var result = {

      getTodos : function(){
        return todos;
      },

      insertTodo : function(todo){
        todos.push(todo);
      },

      updateTodo : function(todo){
        var position = todos.indexOf(todo);
        if(position >= 0){
          todo.done = !todo.done;
          todos[position] = todo;
          return true;
        }
        return false;
      },

      deleteTodo : function(todo){
        var position = todos.indexOf(todo);
        if(position >= 0){
          todos.splice(position, 1);
          return true;
        }
        return false;
      }

    };

    return result;
});

Listo. Ahora podemos utilizar el servicio desde el controlador.

Normalmente lo que acabamos de hacer, algunos lo hacen desde el propio controlador. Esto es una mala práctica, por dos problemas principales:

  1. El código no será reutilizable en otros lugares de la aplicación.
  2. Será muy difícil mantener el código de esta forma con el crecimiento de la aplicación.

Implementando la lógica de transferencia de datos en servicios, permite encapsular la funcionalidad de comunicación con el servidor en un contenedor reutilizable desde cualquier lugar.

Así pues, ya sea mediante APIs REST, peticiones Ajax o cualquier otro mecanismo de comunicación con el servidor, lo podrás hacer desde los servicios, y no tendrás que modificar tus controladores, o sea, estos últimos no necesitarán conocer de dónde proviene y hacia dónde va la información.

Paso 5: El controlador MainCtrl

Con la premisa anterior, el controlador MainCtrl solo tendrá que manejar los eventos generados en la vista.

Estos eventos coinciden con las operaciones del CRUD, por lo que crearemos una función para cada una de la siguiente manera:

angular.module('toDoApp')
  .controller('MainCtrl', function ($scope, Todo) {

    $scope.todos = Todo.getTodos();

    $scope.addTodo  = function(){
      Todo.insertTodo($scope.newTodo);
      $scope.newTodo = {};
    };

    $scope.toggleDoneTodo = function(todo){
      Todo.updateTodo(todo);
    };

    $scope.removeTodo = function(todo){
      Todo.deleteTodo(todo);
    };
  });

Observa que limpio queda MainCtrl, hasta mi gato lo entiende.

La variable $scope.todos es la que utilizaré en la vista para mostrar el listado de las tareas, $scope.newTodo es una variable temporal que se crea desde la vista para almacenar el todo que se va a insertar, el mismo debe ser limpiado en cada operación de inserción para que no se quede con los mismos datos del anterior.

La operación de actualización en este caso (toggleDoneTodo()), se limita a solo cambiar la propiedad done del todo, pues el nombre se cambia de forma automática al modificar el input correspondiente a cada uno, gracias al comportamiento bidireccional del framework (two-way databinding).

Paso 6: La vista

Como no soy diseñador, voy a dejarle la responsabilidad de crear una vista agradable a mi viejo amigo Bootstrap.

Voy a dejar todo como está en el fichero index.html y me concentraré en el fichero app/views/main.html, el cual es inyectado dinámicamente en el index cuando se carga la aplicación, debido a que en la configuración de las rutas se encuentra especificado que se cargue esa vista con el inicio de la app.

<div class="nothing">

  <!-- añadir un Todo -->
  <div class="input-group">

    <input type="text" ng-model="newTodo.name" placeholder="What needs to be done?" class="form-control">

    <span class="input-group-btn">
      <input type="submit" ng-click="addTodo(newTodo)" ng-disabled="!newTodo.name" class="btn btn-primary" value="Add">
    </span>
  </div>

  <hr>

    <!-- lista de Todos -->
    <p class="input-group" ng-repeat="todo in todos">

    <input type="text" ng-model="todo.name" class="form-control">

    <span class="input-group-btn">
      <input type="submit" ng-click="removeTodo(todo)" class="btn btn-danger" value="X">
    </span>

    <span class="input-group-btn">
      <input type="submit" ng-click="toggleDoneTodo(todo)" class="btn btn-info" value="{{todo.done ? 'Done' : 'Not done'}}">
    </span>
  </div>

Como puedes observar, está dividida en dos partes: insertar el Todo y listar los Todos existentes.
Para insertar el Todo se utiliza la variable temporal descrita anteriormente como modelo del input (ng-model=newTodo.name) y se registra el evento ng-click en el botón asociado para que registre la operación (ver el método addTodo() del controlador).

Se utiliza además la directiva ng-disabled para garantizar que no se pueda realizar la operación de inserción si el texto del input está vacío, lo que demuestra el poder expresivo del framework.

El tipo de ese input lo puse como submit para evidenciar que se va a realizar una operación, pero puede perfectamente ser de tipo button, pues en realidad no estamos utilizando un formulario para insertar el Todo.

Para listar los Todos se utiliza la directiva ng-repeat que contiene un input con el texto del botón y otros dos con eventos para eliminar y actualizar el estado de los Todos respectivamente.

En este último input se utiliza una expresión ternaria para establecer el texto, que verifica si la propiedad done del Todo actual es verdadera o falsa.

Paso 7: Elegir un método de persistencia y aplicarlo

Paso 8: Crear una versión de la aplicación para Android utilizando Córdova.

Conclusiones

Con lo anterior tenemos una aplicación funcional que aprovecha las bondades del framework AngularJS. Los pasos 7 y 8 los abordaré en un próximo artículo (posiblemente en artículos separados), pues existen varios métodos de persistencia de datos y me gustaría abordar varios de ellos.

Sin más espero te sea de utilidad y, cualquier sugerencia, duda y/o crítica (constructiva :)), no dudes dejarla(s) en los comentarios.

Categories