Introducción

Esta sencilla aplicación está cogida del libro "Pro Angular 6" (ver los enlaces al final), el objetivo del tutorial es desarrollar una lista de tareas.

Creando el proyecto

Creamos un nuevo proyecto con el nombre "todo" con el comando ng de angular-cli:

ng new todo

El comando crea el proyecto con la estructura de carpetas, los ficheros de configuración y paquetes NPM necesarios.

Instalamos un paquete adicional para dar estilo con CSS a la página HTML usando Bootstrap.

cd todo
npm install bootstrap@4.1.1

Para poder usar Bootstrap hay que añadir una entrada en el fichero [angular.json] en la sección styles:

"styles": [
      "src/styles.css",
      "node_modules/bootstrap/dist/css/bootstrap.min.css"
],

La ruta apunta al directorio "node_modules" dentro del proyecto donde se almacenan los paquetes de NodeJS.

Ahora ya podemos ejecutar el proyecto (Angular incluye un servidor HTTP para probar el desarrollo):

ng serve --port 3000 --open

Diseño de nuestra aplicación ToDo

Cuando creamos el proyecto Angular por defecto crea algunos contenidos, vamos a editar [src/index.html]:

<!DOCTYPE html>
<html>
  <head>
    <title>ToDo</title>
    <meta charset="utf-8" />
  </head>

  <body class="m-1 p-1">
    <h3 class="bg-primary text-white p-3">Adam's To Do List</h3>
    <div class="my-1">
      <input class="form-control" />
      <button class="btn btn-primary mt-1">Add</button>
    </div>
    <table class="table table-striped table-bordered">
      <thead>
        <tr>
          <th>Description</th>
          <th>Done</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Buy Flowers</td>
          <td>No</td>
        </tr>
        <tr>
          <td>Get Shoes</td>
          <td>No</td>
        </tr>
        <tr>
          <td>Collect Tickets</td>
          <td>Yes</td>
        </tr>
        <tr>
          <td>Call Joe</td>
          <td>No</td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

Si trabajamos con Visual Studio Code podemos usar el atajo [Shift + Alt + F] para indentar y formatear el código (también se puede hacer con [Ctrl + Shift + P] y buscar "Format Document").

Hemos diseñado un pequeño formulario compuesto por un campo de edición y un botón para añadir nuevas tareas a la tabla inferior.

Los atributos de las etiquetas HTML como class="m-1 p-1 (margen y espacio de relleno o padding) hacen referencia a los estilos CSS de Bootstrap.

Ejemplo: El título de tercer nivel h3 tiene tres clases asignadas. bg-primary establece el color de fondo del elemento al tema Bootstrap, text-white es fácil de deducir que fija el color de letra blanco, p-3 establece el tamaño del espacio de relleno.

<h3 class="bg-primary text-white p-3">Adam's To Do List</h3>

Por ahora nuestra aplicación no hace nada, ahora añadiremos las funciones principales para que el usuario pueda interactuar con la lista de tareas. Más adelante también se dividirá la aplicación en varios componentes Web.

Añadiendo funcionalidad

Reemplazamos todo el contenido de [src/index.html] por el siguiente:

<!DOCTYPE html>
<html>
  <head>
    <title>ToDo</title>
    <meta charset="utf-8" />
  </head>

  <body class="m-1">
    <todo-app>Angular placeholder</todo-app>
  </body>
</html>

La etiqueta <todo-app> es ignorada por el momento y no tiene ningún efecto.

Creando un componente para el modelo de datos

Añado un nuevo fichero [src/app/model.ts] con el contenido de abajo:

var model = {
  user: "Adam",
  items: [
    { action: "Buy Flowers", done: false },
    { action: "Get Shoes", done: false },
    { action: "Collect Tickets", done: true },
    { action: "Call Joe", done: false }
  ]
};

La sintaxis de arriba es puro JavaScript, he creado un objeto con un atributo "user" para el nombre y "items" para la relación de tareas como un array de objetos.

Reescribo el código JavaScript de arriba usando las características introducidas en ECMAScript 6.

export class Model {
  user;
  items;
  constructor() {
    this.user = "Adam";
    this.items = [
      new TodoItem("Buy Flowers", false),
      new TodoItem("Get Shoes", false),
      new TodoItem("Collect Tickets", false),
      new TodoItem("Call Joe", false)
    ];
  }
}
export class TodoItem {
  action;
  done;
  constructor(action, done) {
    this.action = action;
    this.done = done;
  }
}

He creado dos clases, la principal que contiene el nombre de usuario y los items, cada item se representa como un objeto usando la clase TodoItem.

La palabra reservada class es lo que llaman un azucarillo sintáctico, es un termino que se emplea para sintaxis de lenguajes de programación con el único propósito de facilitar al desarrollador la lectura del código, realmente no afecta a la funcionalidad del programa y se pueden omitir (tienen como objetivo hacer "más dulce" el lenguaje para el programador).

Usamos export para que las clases sean accesibles desde otros módulos o ficheros.

Editando la plantilla del componente

Cuando creamos el proyecto al principio del tutorial Angular añadió ya un componente en [src/app], vamos a modificar el contenido HTML de la plantilla en [src/app/app.component.html]

<h3 class="bg-primary p-1 text-white">{{ getName() }}'s To Do List</h3>

Usando {{}} añado la variable con el nombre de usuario, llama a una función para obtenerlo. Este es un ejemplo de data binding donde se crea una relación entre la plantilla y el dato. La función getName() aún no está creada.

Preparando el componente

Ahora necesito editar el componente [src/app/app.component.ts] para que actue de puente y relacione la función getName() en la plantilla con el modelo de datos que hemos creado en [src/app/model.ts].

import { Component } from "@angular/core";
import { Model } from "./model";
@Component({
  selector: "todo-app",
  templateUrl: "app.component.html"
})
export class AppComponent {
  model = new Model();
  getName() {
    return this.model.user;
  }
}

¡Ya está funcionando!

Las primeras dos líneas con import se usan para declarar dependencias con otros módulos. @angular/core contiene todo el core de Angular. El segundo import se usa para acceder a la clase Model en [src/app/model.ts]. No es necesario indicar que los ficheros tienen extensión .ts.

A continuación viene el decorador con @Component({}) que proporciona metadatos relativos a la clase. La propiedad selector especifica un selector CSS que aplicará el componente en el HTML, "todo-app" lo hemos definido en[src/index.html] <todo-app>Angular placeholder</todo-app>. templateUrl define la plantilla del componente.

La parte final es la definición de clase que Angular pueda instanciar para crear el componente. La clase AppComponent define una propiedad model y un método o función getName (data binding).

Ahora para poder añadir el formulario hay que añadir el módulo necesario en [src/app/app.module.ts]

import { FormsModule } from "@angular/forms";

También el decorador imports: [BrowserModule, FormsModule],.

Añadiendo la tabla

Creamos una nueva función en la clase AppComponent de [src/app/app.component.ts] para obtener la lista de tareas.

getTodoItems() {
    return this.model.items;
  }

Ahora tenemos que modificar [src/app/app.component.html] para que muestre la tabla:

<table class="table table-striped table-bordered">
  <thead>
    <tr>
      <th></th>
      <th>Description</th>
      <th>Done</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let item of getTodoItems(); let i = index">
      <td>{{ i + 1 }}</td>
      <td>{{ item.action }}</td>
      <td [ngSwitch]="item.done">
        <span *ngSwitchCase="true">Yes</span>
        <span *ngSwitchDefault>No</span>
      </td>
    </tr>
  </tbody>
</table>

*ngFor ejecuta un bucle for recorriendo los elementos del array devuelto por getTodoItems(). El resultado es que el elemento tr y sus contenidos se duplican para cada elemento del array. La variable i contiene la posición del elemento actual en el array en cada iteración del bucle.

A continuación declaro dos variables con {{}} que proporcionan el data binding con la variable item del bucle for.

El siguiente bloque es un switch parecido a otros lenguajes de programación, [ngSwitch] evalua una condición y en base a ella selecciona una opción entre las disponibles mostrando un elemento <span> u otro en este caso. La propiedad item.done es una variable booleana que puede contener true o false.

Ahora ya tenemos algo más en nuestra aplicación:

Data Binding bidireccional

Hasta ahora sólo hemos visto el data binding unidireccional para mostrar datos que no se pueden modificar. Ahora con el formulario pondremos en práctica el data binding bidireccional.

Vamos a añadir un checkbox a cada elemento de la tabla para marcar una tarea como completada, en [src/app/app.component.html]:

<td><input type="checkbox" [(ngModel)]="item.done" /></td>

La expresión ngModel crea un data binding bidireccional con la propiedad item.done y un elemento de formulario. El modelo de datos está "vivo".

Filtrando los resultados de la tabla

La tabla sólo muestra las tareas sin completar:

return this.model.items.filter(item => !item.done);

Esto es un ejemplo de una función lambda (también se conocen como fat arrow functions =>). En notación convencional sería así:

return this.model.items.filter(function (item) { return !item.done });

Formulario para añadir tareas

Ahora sólo falta un formulario para añadir nuevas tareas. Añadimos un nuevo bloque a [src/app/app.component.html] debajo de h3:

<div class="my-1">
  <input class="form-control" #todoText />
  <button class="btn btn-primary mt-1" (click)="addItem(todoText.value)">
    Add
  </button>
</div>

En [src/app/app.component.ts] añadimos la función necesaria, primero importamos la definición de la clase TodoItem

import { Model, TodoItem } from "./model";
addItem(newItem) {
    if (newItem != "") {
      this.model.items.push(new TodoItem(newItem, false));
    }
  }

Código fuente

  • angular / src / 01-intro / todo.

Enlaces internos

Enlaces externos

Angular:

Bootstrap:

JavaScript:

Otros: