En estos tiempos es común acercarse a algún framework que nos facilite la interacción entre nuestra programación de JavaScript y el DOM, entre los mas populares están Angular, KnockOutJS, Ember, etc, pero existe uno que en lo personal me está gustando y se llama Aurelia.IO.
Bueno Aurelia.IO es un MVVM el cual nos obliga a programar de una manera ordenada al separar la vista del modelo, como otros lenguajes lo hacen, entre las características que podemos ver están:
La verdad es que no hay mejor manera de explicar Aurelia que tirando código así que haremos una pequeña aplicación desde cero, incluyendo la instalación y configuración del mismo.
Nuestra primera parte consistirá en instalar y configurar Aurelia.IO en un proyecto de AspNet.Core en visual studio 2017 community, ya existen varios tutoriales que te dicen como hacerlo, os dejo aquí mi versión.
Antes que nada quiero mencionar que todo esto está sobre Windows, Windows 10 para ser mas precisos, por lo tanto no daré instrucciones sobre otros sistemas operativos.
Como primer paso nos dedicaremos a reunir las herramientas necesarias para tirar código, asumiendo que no tenemos nada instalado, tendremos que conseguir lo siguiente:
Lo primero que haremos es instalar el Visual Studio ya que este es el mas tardado (por mucho) de instalarse, ahora bien, la razón de instalar este primero es que dependiendo la configuración que uses puedes instalar un NodeJs de versión anterior a la mas reciente.
Desafortunadamente no tengo pantallas para llevar un paso a paso, pero les dejo un Screenshot con las opciones que deben tener instaladas.
Una vez instalado el VS2017, procedemos a instalar NodeJS, para esto ejecutamos el instalador y usamos la técnica legendaria del NEXT NEXT NEXT…
Una vez que NodeJS se instale, procedemos a instalar Git, usando la misma técnica que usamos en NodeJS.
y con eso tenemos las herramientas instaladas, para nuestra suerte las herramientas se configuran solas, así que no es necesario que les hagamos cosas adicionales.
Nuestro siguiente paso es instalar Aurelia.IO y eso se hace de la siguiente manera:
npm install aurelia-cli -g
au
Ya que validamos que se encuentra instalado Aurelia-cli, entonces procederemos a crear nuestro proyecto.
Para crear el proyecto haremos lo siguiente:
Una vez creado habrá que hacer algunas configuraciones adicionales a nuestro proyecto las cuales son las siguientes.
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
Ya cargado nuestro proyecto cargado, nos disponemos a crear el proyecto de Aurelia.IO para eso seguiremos los siguientes pasos:
au new --here
Luego nos dice que estamos creando el proyecto sobre un directorio con archivos, nos pide confirmación para crear el proyecto en este directorio, le decimos que sí.
Por ultimo paso nos pregunta si queremos descargar las dependencias y le decimos que sí.
Cuando acabe Aurelia su configuración nos dirá Happy Codding :')
Al final de la creación del proyecto, nos habrá creado muchas carpetas y archivos y estos ya estarán dentro de nuestro proyecto de VS2017.
Todavía no acabamos, hay que hacer ciertas configuraciones extras, para que nuestro proyecto funcione adecuadamente.
En el raíz del proyecto, existe un archivo tsconfig.json, este archivo define la configuración sobre como VS2017 debe interpretar el typescript, en este archivo cambiaremos el valor de la propiedad target por esnext:
{ "compileOnSave": false, "compilerOptions": { "sourceMap": true, "target": "es5", "module": "amd", "declaration": false, "noImplicitAny": false, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "moduleResolution": "node", "lib": ["es2017", "dom"] },
quedando así:
{ "compileOnSave": false, "compilerOptions": { "sourceMap": true, "target": "esnext", "module": "amd", "declaration": false, "noImplicitAny": false, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "moduleResolution": "node", "lib": ["es2017", "dom"] },
Abrimos el archivo aurelia_project\aurelia.json y revisamos que en la sección build\targets esté de la siguiente manera:
"build": { "targets": [ { "id": "aspnetcore", "displayName": "ASP.NET Core", "output": "wwwroot/scripts", "index": "wwwroot/index.html", "baseDir": "./wwwroot", "baseUrl": "scripts" } ],
En caso de que no lo esté, hay que hacer las modificaciones correspondientes para que esté, con esto le decimos a Aurelia.IO que genere el proyecto en la misma ruta que usa el VS2017 que en este caso es la carpeta wwwroot del proyecto.
Ahora hay que instalar algunos paquetes, para eso nos apoyamos de la Consola del Administrador de Paquetes y escribimos la siguiente instrucción:
Install-Package Microsoft.AspNetCore.StaticFiles -version 1.1.0
Hay que tener noción de la versión de paquetes a instalar, en mi caso mi proyecto esta configurado con .NetAppCore 1.1, por eso instalo la versión 1.1.0 del paquete de StaticFiles, si quieres revisar que versión de Core es tu proyecto dar clic derecho sobre el proyecto y selecciona propiedades, en la ventana que te abre ve a la pestaña de aplicación y busca el valor de Plataforma de Destino:
Hay que seguir con las modificaciones, ahora le toca el turno al archivo Startup.cs en este archivo cambiaremos la configuración sobre como se levanta el servidor IIS, en ASP .NET Core, tiene una configuración distinta a los proyectos tradicionales, en este caso, el servidor web funciona como un módulo el cual se carga en el archivo Program.cs y en este módulo se le dice que use la configuración del archivo que vamos a modificar, entre esas configuraciones esta que cuando la aplicación se cargue escriba un Hello World, nuestra modificación consistirá en decirle que use los archivos para eso modificamos las siguientes líneas:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }
Por las siguientes:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseDefaultFiles(); app.UseStaticFiles(); }
Ahora bien nos queda generar un pequeño truco que también es parte de otros tutoriales, como ya mencionamos Aurelia.IO utiliza a NodeJS para generar el código que será reconocido por los navegadores, también levanta un servidor de NodeJS en el puerto 9000 donde la aplicación es ejecutada, y otro en el puerto 3001 donde levanta un componente para sincronizar el puerto 9000 en caso de que los fuentes cambien.
Explicando lo anterior de manera detallada sucede lo siguiente:
En resumen a lo anterior es por eso que deshabilitamos el Compilador de TypeScript de VS y se lo dejamos a Aurelia, tambien por eso debemos hacer una configuración para que se levanten dos servidores, el de Aurelia y el ASP.NET Core, mientras que el de Aurelia transpilara los fuentes, el ASP.Net Core lo usaremos como servidor y para depuración.
Para hacer la configuración para levantar ambos servidores haremos lo siguiente:
set root=%~dp0 start cmd /k "cd %root% && au run --watch"
public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<startup>() .UseApplicationInsights() .Build(); using (var process = new System.Diagnostics.Process()) { process.StartInfo = new System.Diagnostics.ProcessStartInfo("watch.cmd"); process.Start(); host.Run(); process.Kill(); } }
Si ejecutamos veremos la siguiente pantalla:
Ahora en plena ejecución, nosotros modificaremos el archivo src\app.ts dejándolo de la siguiente manera:
export class App { message = 'Memorama!'; }
Después de guardar los cambios, esperamos unos segundos y actualizamos el navegador y veremos reflejado nuestro cambio:
Al detener la depuración se cerrará el servidor de Aurelia.
Antes de empezar es necesario conocer el lugar donde trabajaremos, en este caso el proyecto y su estructura, así que la estructura del proyecto es la siguiente:
En el directorio raíz tenemos diferentes archivos como son archivos de proyecto, de configuración, de paquete, de arranque de aplicación, entre otros.
Prácticamente nuestro trabajo estará en la carpeta src, y en una que otra ocasión modificaremos el archivo aurelia.json, para agregar alguna configuración adicional.
Si no ves las carpetas dentro de la carpeta src\resources entonces debes hacer lo siguiente:
Con respecto a DotNet, solamente tendremos como regla no crear ningun elemento de DotNet, dentro de las carpetas wwwroot, aurelia_project y src, fuera de estas carpetas podemos generar las estructuras que nosotros deseemos.
Empezaremos por definir la aplicación a crear y las reglas que contendrá, lo que haremos será un memorama.
Las reglas que contendrá son las siguientes:
Una vez definida las reglas tenemos que analizar un poco y separar las responsabilidades, en este caso tenemos el memorama, pero para enfocarlo mas a la vida real, lo llamare la mesa, también tenemos a la carta, tenemos la partida (que es el juego en curso), y la lista de posiciones.
Dejaremos las responsabilidades de la siguiente manera:
Ahora sí, empezaremos con lo que “nos divierte”, así que iremos paso a paso creando nuestros componentes. De aquí en adelante la organización varía dependiendo el programador, así que la estructura que crearé para el proyecto no es obligatoria para otros proyectos, aunque sí para efectos de este tutorial.
Ahora bien pondremos que tenemos 3 formas de programar: la fea, la mala y la buena, haciendo referencia en cuanto a técnicas, limpieza, orden entre otras cosas y no tanto al conocimiento del framework de Aurelia.IO.
En la fea se programaría de la siguiente manera: crearía una vista y un modelo, en la vista pondría toda la estructura html que necesitara, mientras que en el modelo pondría toda la programación necesaria para que la aplicación funcione.
En la mala se programaría de la siguiente manera: crearía una vista, un modelo, varias clases, en la vista pondría toda la estructura html que necesitara, en las clases pondría los métodos según sus responsabilidades, en el modelo referenciaría todas las clases y después haría todos los bindings en la vista.
En la buena se programaría de la siguiente manera: crearía varias vistas, varios modelos, cada uno con sus responsabilidades debidamente definidas y sus bindings correspondientes.
Vamos a programar usando la buena y tratando de poner buenas practicas, y explicando cada punto para que quede claro.
Con respecto a las nomenclaturas seremos sencillos, los archivos o carpetas que creemos serán siempre en minúsculas, en caso de ser palabras compuestas las separaremos con guiones medios (-), por ejemplo: componentes, sala-posiciones.
Las clases se nombraran en PascalCase, es decir, las palabras siempre llevan la primer letra en mayúscula, el resto de las letras son en minúsculas, en caso de ser palabras compuestas, cada palabra lleva la primer letra en mayúscula y el resto en minúsculas, se omiten espacios y caracteres especiales.
Normalmente se crea un archivo por cada clase si el archivo se llama comodin, la clase se deberá llamar Comodin, si el archivo se llama torre-control, la clase se deberá llamar TorreControl.
Un archivo puede tener mas de una clase.
Dentro de la carpeta src crearemos una carpeta llamada modelos, en esta carpeta crearemos nuestros elementos que formarán parte del memorama, los cuales mencionamos en las reglas y responsabilidades.
También crearemos una carpeta llamada componentes, en esta carpeta agregaremos algunos extras que nos ayudarán a tener la aplicación en buen estado.
En la carpeta src/modelos agregamos dos archivos [carta.html, carta.ts] estos archivos representarán a nuestra carta.
- src | +- modelos | +- carta.html (+) | +- carta.ts (+)
En el archivo carta.ts escribiremos el siguiente código:
export class Carta { constructor() { } abrir() { } cerrar() { } }
El archivo contiene las clase Carta, la cual contiene dos métodos uno para abrir y otro para cerrar, estos métodos harán la acción de voltear la carta para que sea visible y cerrarla para que no sea visible.
Para controlar la acción de abrir y cerrar aplicamos encapsulamiento, es decir, manejaremos una variable privada la cual nos indicará si la carta esta abierta o no, y esta variable solamente podrá ser modificada por los métodos abrir y cerrar, nuestro código debería quedar así:
export class Carta { private estaAbierta: boolean = false; constructor() { } abrir() { this.estaAbierta = true; } cerrar() { this.estaAbierta = false; } }
Nuestra carta ya controla su estado, pero este no es visible al ser privada, por lo tanto ocupamos un mecanismo para que la vista conozca el estado, agregaremos un método el cual en base a la variable privada nos regresara una clase la cual nos indicará el estatus de la carta, nuestro código debería quedar así:
export class Carta { private estaAbierta: boolean = false; constructor() { } abrir() { this.estaAbierta = true; } cerrar() { this.estaAbierta = false; } get estatusCarta(): string { return this.estaAbierta ? 'abierta' : 'cerrada'; } }
Ahora nos falta la información, la carta hará referencia a una imagen, por lo tanto deberá tener una propiedad en la cual nos diga el nombre de la imagen, y otra propiedad, la cual nos de un identificador de carta, nuestro código debería quedar así:
export class Carta { private estaAbierta: boolean = false; id: number; imagen: string; constructor() { } abrir() { this.estaAbierta = true; } cerrar() { this.estaAbierta = false; } get estatusCarta(): string { return this.estaAbierta ? 'abierta' : 'cerrada'; } }
Por último, nuestra carta nos debe decir que imagen debemos mostrar, pero no siempre lo debe hacer, o dicho de otra forma, solo nos mostrará la imagen si la carta esta abierta, en caso contrario nos deberá mostrar una imagen genérica, nuestro código debería quedar así:
export class Carta { private estaAbierta: boolean = false; id: number; imagen: string; imagenReverso: string = "url de imagen de reverso"; constructor() { } abrir() { this.estaAbierta = true; } cerrar() { this.estaAbierta = false; } get estatusCarta(): string { return this.estaAbierta ? 'abierta' : 'cerrada'; } get mostrarImagen(): string { return this.estaAbierta ? this.imagen : this.imagenReverso; } }
En el archivo src\modelos\carta.html, borraremos el contenido actual y en su lugar escribiremos el siguiente código:
<template> <li class.bind="estatusCarta" click.delegate="abrir()"> <img alt="carta" src.bind="mostrarImagen" /> </li> </template>
En la carpeta src/modelos agregamos dos archivos [mesa.html, mesa.ts] estos archivos representarán a nuestra mesa.
- src | +- modelos | +- carta.html | +- carta.ts | +- mesa.html (+) | +- mesa.ts (+)
En el archivo mesa.ts escribiremos el siguiente código:
export class Mesa { constructor() { } nuevoJuego() { } validarPar() { } }
La mesa tiene cartas las cuales son las que mostrará, estas cartas deben ser obtenidas al iniciar un nuevo juego, si ya había un juego entonces las cartas existentes deberán ser eliminadas para dar paso a las nuevas cartas.
Necesitaremos obtener las cartas de algún lugar así que en la carpeta src\componentes crearemos un archivo llamado web-api.ts:
- src | +- componentes | +- web-api.ts (+)
Este archivo tendrá el papel de servirnos las cartas, asumiendo que estas se obtienen desde un origen remoto, por lo tanto tendremos en nuestro código de la siguiente manera:
export class WebApi { constructor() { } obtenerCartas(numPares: number): Promise<any> { return new Promise<any>(resolve => { }); } }
Ahora bien si queremos ser ordenados, tendríamos que tener las cartas bajo una nomenclatura, para que de esta manera sea fácil obtenerlas, nuestra nomenclatura de cartas sera la siguiente carta_xx.png, donde xx va ser un número consecutivo a dos dígitos, partiendo del 01 hasta el número de imágenes que vayamos a utilizar, en mi caso usaré 30 imágenes distintas, de una vez le daremos la ruta donde estarán las imágenes de las cartas, y las almacenaremos en wwwroot\imagenes, para hacer referencia a esta ruta sera por medio de la ruta './imagenes/'
La nomenclatura nos facilitará la creación de un algoritmo que nos generará las cartas de manera rápida sin que tengamos que registrarlas una a una. nuestro código del src\componentes\archivo web-api.ts debería quedar así:
export class WebApi { constructor() { } obtenerCartas(numPares: number): Promise<any> { return new Promise<any>(resolve => { }); } } let numCartas = 30; let cartas = []; for (var i = 1; i < numCartas; i++) { let carta = { id: i, imagen: "/imagenes/carta_" + ("00" + i).slice(-2) + ".png" }; cartas.push(carta); }
Como ya tenemos nuestro arreglo de cartas, nuestro siguiente paso es obtener las cartas de manera desordenada, para que estas puedan ser mostradas en la mesa, para esto nuestro código debería quedar así:
export class WebApi { constructor() { } obtenerCartas(numPares: number): Promise<any> { return new Promise<any>(resolve => { let cartasDesordenadas = cartas.sort(c => Math.random() - 0.364); let cartasSeleccionadas = cartasDesordenadas.slice(0, numPares); cartasSeleccionadas = cartasSeleccionadas.concat(cartasSeleccionadas); cartasDesordenadas = cartasSeleccionadas.sort(c => Math.random() - 0.472); resolve(cartasDesordenadas); }); } } let numCartas = 30; let cartas = []; for (var i = 1; i < numCartas; i++) { let carta = { id: i, imagen: "/imagenes/carta_" + ("00" + i).slice(-2) + ".png" }; cartas.push(carta); }
Lo que se hizo dentro del método obtenerCartas fue lo siguiente:
De vuelta al archivo src\modelos\mesa.ts, haremos la llamada para obtener las cartas, para esto, deberemos importar la clase del archivo web-api, la inyectaremos en nuestra clase Mesa, y la usaremos en el método nuevoJuego, cuando la obtención de cartas termine las pasaremos a una propiedad de la clase Mesa, nuestro código debería quedar así:
import { WebApi } from '../componentes/web-api'; export class Mesa { static inject() { return [WebApi]; } private numPares = 8; cartas: any[] = []; constructor(private api: WebApi) { } nuevoJuego() { this.api.obtenerCartas(this.numPares) .then(_cartas => { this.cartas.length = 0; this.cartas = _cartas; }); } validarPar() { } }
En el archivo src\modelos\mesa.html, borraremos el contenido actual y en su lugar escribiremos el siguiente código:
<template> <require from="./carta"></require> <button type="button" click.delegate="nuevoJuego()">Nuevo Juego</button> <ul> <carta repeat.for="carta of cartas" model.bind="carta"></carta> </ul> </template>
Ahora vamos al archivo src\app.html y escribimos el siguiente código:
<template> <require from="./modelos/mesa"></require> <h1>${message}</h1> <mesa></mesa> </template>
Le decimos a la vista de la app, que usaremos al componente de mesa, y después del encabezado 1, agregamos al elemento mesa.
Ahora agregaremos nuestra carpeta de imágenes dentro de la carpeta wwwroot, agregaremos las 30 imágenes las cuales deben respetar la nomenclatura mencionada antes.
- wwwroot | +- Imagenes | +- //Aquí irán las 30 imágenes//
Con respecto a las imágenes, quería hacer el memorama de algún anime, pero no quise entrar en situaciones de derecho de autor, así que mejor lo haré de algo mas convencional, en este enlace les dejo un paquete de imágenes por si las quieren usar.
Ahora que tenemos nuestras imágenes, yo he decidido ponerle a la imágen que servirá como reverso carta_00.png, así que modificaré el archivo src\modelos\carta.ts, para hacer referencia a esta imagen, el código lo vemos así:
export class Carta { private estaAbierta: boolean = false; id: number; imagen: string; imagenReverso: string = "./imagenes/carta_00.png"; constructor() { } abrir() { this.estaAbierta = true; } cerrar() { this.estaAbierta = false; } get estatusCarta(): string { return this.estaAbierta ? 'abierta' : 'cerrada'; } get mostrarImagen(): string { return this.estaAbierta ? this.imagen : this.imagenReverso; } bind(bindingContext, overrideContext) { this.id = bindingContext.carta.id; this.imagen = bindingContext.carta.imagen; } }
Si ya ejecutaron el código, se habrán dado cuenta que cuando le damos nuevo juego después de haber abierto todas las cartas, algunas cartas se mantienen abierta, no estoy seguro, pero creo que esto se debe al paso de objetos y que estos quedan como referencia y no como valor, para corregir este detalle abriremos nuestro archivo src\componentes\web-api.ts, dejando el siguiente código:
export class WebApi { constructor() { } obtenerCartas(numPares: number): Promise<any> { return new Promise<any>(resolve => { let cartasDesordenadas = cartas.sort(c => Math.random() - 0.364); let cartasSeleccionadas = cartasDesordenadas.slice(0, numPares); cartasSeleccionadas = cartasSeleccionadas.concat(cartasSeleccionadas); cartasDesordenadas = cartasSeleccionadas.sort(c => Math.random() - 0.472); resolve(JSON.parse(JSON.stringify(cartasDesordenadas))); }); } } let numCartas = 30; let cartas = []; for (var i = 1; i < numCartas; i++) { let carta = { id: i, imagen: "/imagenes/carta_" + ("00" + i).slice(-2) + ".png" }; cartas.push(carta); }
Después de una breve revisada, me di cuenta que en el archivo src\modelos\mesa.html no era necesario el atributo model.bind, por lo tanto se lo podemos eliminar y esto se debe a que el modelo de la carta está trabajando bajo el contexto de la mesa, y al estar en un repeat.for toma el valor de cada carta 👍, por lo tanto nuestro código quedaría así:
<template> <require from="./carta"></require> <button click.delegate="nuevoJuego()" type="button">Nuevo Juego</button> <ul> <carta repeat.for="carta of cartas"></carta> </ul> </template>
A continuación veremos lo siguiente:
Cada que iniciemos un nuevo juego o partida, necesitaremos saber cierta información, para no darle esta responsabilidad a la mesa, entonces se la pasamos a la partida, la partida lo manejaremos como otro modelo más.
En la carpeta src\modelos agregamos dos archivos [partida.html, partida.ts] estos archivos representarán a nuestra partida.
- src | +- modelos | +- carta.html | +- carta.ts | +- mesa.html | +- mesa.ts | +- partida.html (+) | +- partida.ts (+)
En el archivo src\modelos\partida.ts escribiremos el siguiente código:
export class Partida { constructor() { } }
Nuestra partida tiene dos importantes responsabilidades, la primera es mostrarnos el número de pares acertados, así que agregaremos una propiedad numérica para que nos de esta información, nuestro código quedaría así:
export class Partida { paresAcertados: number; constructor() { } }
La segunda responsabilidad consiste en decirnos el tiempo transcurrido, existen muchas maneras de calcular este tiempo, nosotros veremos una manera “sencilla”, agregaremos 2 propiedades, una para registrar la hora en que inicia el juego, y otra para registrar la hora en que acaba, la diferencia entre estas 2 nos deberá dar el tiempo transcurrido, nuestro código quedaría así:
export class Partida { paresAcertados: number; horaInicio: Date; horaFin: Date; constructor() { } get tiempoTranscurrido() { return this.horaFin - this.horaInicio; } }
Hasta aquí tenemos un posible error de transpilación, por la diferencia de los tipos que se manejan entre la operación y el resultado y ha decir verdad eso no nos da el resultado que nosotros esperamos tan fácil, en este punto vamos apoyarnos de un framework para manipulación de fechas llamado momentjs.
===== Configurando MomentJS.. =====.
En este punto haremos un paréntesis a la programación y veremos como instalar un paquete, configurar dicho paquete en Aurelia.IO y usarlo.
Para instalar momentjs haremos lo siguiente:
npm install moment --save
"moment", "text",
import {Aurelia} from 'aurelia-framework' import environment from './environment'; export function configure(aurelia: Aurelia) { aurelia.use .standardConfiguration() .feature('resources'); aurelia.use.plugin("moment"); if (environment.debug) { aurelia.use.developmentLogging(); } if (environment.testing) { aurelia.use.plugin('aurelia-testing'); } aurelia.start().then(() => aurelia.setRoot()); }
Y con esto finalizamos la configuración de momentjs, regresamos a nuestra programación.
De vuelta al archivo src\modelos\partida.ts, haremos los cambios necesarios para que nuestro tiempo transcurrido funcione adecuadamente, el formato en el que mostraremos el tiempo transcurrido será el de HORAS:MINUTOS:SEGUNDOS.MILESIMAS “00:00:00.000”, por lo tanto nuestro código debería quedar así:
import * as moment from 'moment'; export class Partida { paresAcertados: number; horaInicio: Date; horaFin: Date; constructor() { } get tiempoTranscurrido() { let _horaInicio = moment(this.horaInicio); let _horaFin = moment(this.horaFin || new Date()); return moment(_horaFin.diff(_horaInicio)).utc().format("HH:mm:ss.SSS"); } }
Ahora tenemos que hacer que los parámetros de la partida tengan vida, para esto vamos a entrar a las notificaciones de Aurelia.IO.
Para explicar las notificaciones de Aurelia.IO, nos enfocaremos en una revista, la cual es publicada por un editor, y los clientes tienen suscripciones a la revista para recibirla cuando salgan nuevas.
Cada que el editor publica una revista, les llega una copia a todos los suscriptores, prácticamente este es el mismo concepto que se manejan en las notificaciones y en Aurelia.IO no es la excepción.
Para usar las notificaciones de Aurelia.IO, haremos lo siguiente:
Nuestro código debería quedar así:
export class JuegoIniciado { constructor() { } } export class JuegoFinalizado { constructor() { } } export class ParAcertado { constructor() { } }
Ahora necesitamos hacer la suscripción para saber cuando inició un juego, adicional a esto también necesitamos hacer otra suscripción para saber cuando un par es acertado, por ultimo necesitamos hacer una publicación para informar cuando el juego se haya finalizado, esto lo haremos en la partida, en el archivo src\modelos\partida.ts, y nuestro código debería quedar así:
import * as moment from 'moment'; import { EventAggregator } from 'aurelia-event-aggregator'; import * as eventos from '../componentes/eventos'; export class Partida { paresAcertados: number; horaInicio: Date; horaFin: Date; static inject() { return [EventAggregator]; } constructor(private ea: EventAggregator) { this.ea.subscribe(eventos.JuegoIniciado, msg => { this.horaInicio = new Date(); this.horaFin = null; }); this.ea.subscribe(eventos.ParAcertado, msg => { this.paresAcertados++; if (this.paresAcertados == 8) { this.horaFin = new Date(); this.ea.publish(new eventos.JuegoFinalizado()); } }); } get tiempoTranscurrido() { let _horaInicio = moment(this.horaInicio); let _horaFin = moment(this.horaFin || new Date()); return moment(_horaFin.diff(_horaInicio)).utc().format("HH:mm:ss.SSS"); } }
Ahora solo nos queda definir la vista de la partida, por lo tanto abrimos el archivo src\modelos\partida.html, nuestro código deberá quedar así:
<template> <dl> <dt>Pares Acertados</dt> <dd>${paresAcertados}</dd> <dt>Tiempo Transcurrido</dt> <dd>${tiempoTranscurrido}</dd> </dl> </template>
Ahora necesitamos hacer una publicación para informar que el juego ha iniciado, adicional a esto también necesitamos hacer una publicación para informar que un par ha sido acertado y por último necesitamos hacer una suscripción para saber cuando el juego ha finalizado, esto lo haremos en la mesa, por lo tanto abrimos el archivo src\modelos\mesa.ts, nuestro código debería quedar así:
import { WebApi } from '../componentes/web-api'; import { EventAggregator } from 'aurelia-event-aggregator'; import * as eventos from '../componentes/eventos'; export class Mesa { static inject() { return [WebApi, EventAggregator]; } private numPares = 8; cartas: any[] = []; private juegoIniciado: boolean = false; constructor(private api: WebApi, private ea: EventAggregator) { this.ea.subscribe(eventos.JuegoFinalizado, msg => { this.juegoIniciado = false; }); } nuevoJuego() { this.juegoIniciado = false; this.api.obtenerCartas(this.numPares) .then(_cartas => { this.cartas.length = 0; this.cartas = _cartas; }); } validarPar() { } }
Ahora bien todavía tenemos cabos sueltos, para atar estos cabos, tenemos que hacer lo siguiente:
De nueva cuenta modificaremos el archivo src\componentes\eventos.ts para agregar las nuevas clases, nuestro código quedaría así:
import { Carta } from '../modelos/carta'; export class JuegoIniciado { constructor() { } } export class JuegoFinalizado { constructor() { } } export class ParAcertado { constructor() { } } export class CartaAbierta { constructor(public carta: Carta) { } } export class ValidandoPar { constructor(public validando: boolean) { } } export class NuevaPartida { constructor() { } }
Ahora necesitamos hacer una suscripción para saber cuando el proceso de validación de par este activo o inactivo, también necesitamos controlar la acción para impedir que se abra una carta si el proceso de validación esta activo, y por último necesitamos publicar la carta que se ha abierto, esto lo haremos en la carta, por lo tanto abrimos nuestro archivo src\modelos\carta.ts y nuestro código debería quedar así:
import { EventAggregator } from 'aurelia-event-aggregator'; import * as eventos from '../componentes/eventos'; export class Carta { private estaAbierta: boolean = false; id: number; imagen: string; imagenReverso: string = "./imagenes/carta_00.png"; static inject() { return [EventAggregator]; } private estaValidando: boolean = false; constructor(private ea: EventAggregator) { this.ea.subscribe(eventos.ValidandoPar, msg => { this.estaValidando = msg.validando; }); } abrir() { if (!this.estaAbierta && !this.estaValidando) { this.estaAbierta = true; this.ea.publish(new eventos.CartaAbierta(this)); } } cerrar() { this.estaAbierta = false; } get estatusCarta(): string { return this.estaAbierta ? 'abierta' : 'cerrada'; } get mostrarImagen(): string { return this.estaAbierta ? this.imagen : this.imagenReverso; } bind(bindingContext, overrideContext) { this.id = bindingContext.carta.id; this.imagen = bindingContext.carta.imagen; } }
Ahora necesitamos suscribirnos para saber cuando una carta haya sido abierta, también necesitamos publicar cuando iniciamos y terminemos una validación, y por último publicar cuando una nueva partida haya sido iniciada, esto lo haremos en la mesa, abrimos el archivo src\modelos\mesa.ts y nuestro código debería quedar así:
import { WebApi } from '../componentes/web-api'; import { EventAggregator } from 'aurelia-event-aggregator'; import * as eventos from '../componentes/eventos'; import { Carta } from './carta'; export class Mesa { static inject() { return [WebApi, EventAggregator]; } private numPares = 8; cartas: any[] = []; private juegoIniciado: boolean = false; cartasAbieras: any[] = []; constructor(private api: WebApi, private ea: EventAggregator) { this.ea.subscribe(eventos.JuegoFinalizado, msg => { this.juegoIniciado = false; }); this.ea.subscribe(eventos.CartaAbierta, msg => { if (!this.juegoIniciado) { this.juegoIniciado = true; this.ea.publish(new eventos.JuegoIniciado()); } this.cartasAbieras.push(msg.carta); if (this.cartasAbieras.length == 2) { this.validarPar(); } }) } nuevoJuego() { this.ea.publish(new eventos.NuevaPartida()); this.juegoIniciado = false; this.api.obtenerCartas(this.numPares) .then(_cartas => { this.cartasAbieras.length = 0; this.cartas.length = 0; this.cartas = _cartas; }); } validarPar() { this.ea.publish(new eventos.ValidandoPar(true)); let pares = this.cartasAbieras.splice(0, 2); if (pares[0].id === pares[1].id) { this.ea.publish(new eventos.ParAcertado()); this.ea.publish(new eventos.ValidandoPar(false)); } else { setTimeout(e => { pares.map(carta => carta.cerrar()); this.ea.publish(new eventos.ValidandoPar(false)); }, 1000); } } }
Por último nos hace falta suscribirnos para saber cuando una nueva partida haya sido convocada y poder preparar los datos para la nueva partida, esto lo haremos en la partida, por lo tanto en src\modelos\partida.ts, deberíamos tener el siguiente código:
import * as moment from 'moment'; import { EventAggregator } from 'aurelia-event-aggregator'; import * as eventos from '../componentes/eventos'; export class Partida { paresAcertados: number = 0; horaInicio: Date; horaFin: Date; static inject() { return [EventAggregator]; } constructor(private ea: EventAggregator) { this.ea.subscribe(eventos.NuevaPartida, msg => { this.horaFin = this.horaInicio = null; this.paresAcertados = 0; }); this.ea.subscribe(eventos.JuegoIniciado, msg => { this.horaInicio = new Date(); this.horaFin = null; }); this.ea.subscribe(eventos.ParAcertado, msg => { this.paresAcertados++; if (this.paresAcertados == 8) { this.horaFin = new Date(); this.ea.publish(new eventos.JuegoFinalizado()); } }); } get tiempoTranscurrido() { let _horaInicio = moment(this.horaInicio); let _horaFin = moment(this.horaFin || new Date()); return moment(_horaFin.diff(_horaInicio)).utc().format("HH:mm:ss.SSS"); } }
Estamos a 2 pasos de acabar, y como primer paso modificaremos el archivo src\app.html quedando nuestro código de la siguiente manera:
<template> <require from="./modelos/mesa"></require> <require from="./modelos/partida"></require> <h1>${message}</h1> <partida></partida> <mesa></mesa> </template>
Hasta aquí se ve funcional, pero la apariencia deja que desear, así que agregaremos algunos cuantos estilos.
En la carpeta src, agregamos un archivo app.css:
- src | +- app.html | +- app.ts | +- app.css (+)
Después volvemos a abrimos la vista de app src\app.html y agregamos la referencia a la hoja de estilo:
<template> <require from="./app.css"></require> <require from="./modelos/mesa"></require> <require from="./modelos/partida"></require> <h1>${message}</h1> <partida></partida> <mesa></mesa> </template>
Y vamos meter algunos estilos.
1. Al elemento body le vamos a ocultar la barra de desplazamiento vertical, esto lo hacemos así:
body { overflow-y: hidden; }
2. A la partida la vamos hacer flotante con alineación a la izquierda y de un ancho fijo de 200px, esto lo hacemos así:
partida { float: left; width: 200px; }
3. La mesa tiene varios puntos, empezaremos dando una posición absoluta y pegada al tope de la pantalla, y a 205px de la izquierda, esto lo hacemos así:
mesa { position: absolute; top: 0; left: 205px; }
3.1 Al botón de nuevo juego, lo vamos a posicionar abajo de los datos de la partida, esto lo hacemos así:
mesa button { position: absolute; top: 200px; left: -150px; }
3.2 A la lista de la mesa le vamos a eliminar el estilo de lista, le daremos un margen de -1% para que nos de un poco de espacio, y le daremos un espaciado interno de 20px para no perderle de vista, esto lo hacemos así:
mesa ul { margin: -1%; padding: 20px; list-style: none; }
4. A la carta pero al elemento li de la carta le daremos un acomodo para que se nos muestre en una matriz de 4 x 4, esto lo hacemos así:
carta li { float: right; height: 21vh; margin: 1%; width: 16vw; }
4.1 Por ultimo a la imagen dentro de la carta le damos las mismas dimensiones:
carta img { height: 21vh; width: 16vw; }
Hasta este punto ya tenemos funcional el memorama, ya podemos jugar con el y ver que tan buena memoria tenemos, podemos iniciar nuevos juegos y ver el tiempo transcurrido, que por cierto explicaré lo del tiempo transcurrido.
Las propiedades get, a su vez funcionan como propiedades auto-calculadas, en nuestro caso, no necesitamos meter ningún timer para que nos estuviera actualizando el valor del tiempo transcurrido por que en la vista hicimos la llamada directamente a la propiedad tiempoTranscurrido, dentro de la propiedad get nosotros utilizamos una instrucción new Date(), y esta al estar cambiando genera una acción observable permanente sobre esta propiedad, es decir, que automáticamente se esta llamando para obtener el nuevo valor.
Y así terminamos la 3ra. parte de este tutorial, en el último post explicaremos sobre la lista de posiciones.
👍
Para terminar el tutorial haremos el consumo de las posiciones por medio de una API la cual vamos a desarrollar en ASP.NET Core usando WebApi, esta API nos servirá para obtener la lista de posiciones y para registrar nuevas posiciones.
Las posiciones las vamos a registrar en un archivo en el servidor, pero para hacerlo mas interesante vamos a utilizar inyección de dependencias por si en un futuro queremos cambiar por algún gestor de bases de datos.
Para registrar una posición necesitamos saber quien es el jugar de la partida, y para esto lo haremos por medio del plug-in Dialog que nos ofrece Aurelia.IO, sin mas que agregar, empezemos.
Para agregar la API primero tendremos que configurar nuestro proyecto, pero con VS2017 es muy sencillo, basta con:
- api
- Hacemos clic derecho sobre el proyecto y seleccionaremos Editar {nombre del proyecto}.csproj.
- Nos vamos al final del archivo y antes de la cierre del elemento **Project** pegamos el texto copiado. {{ VS18.png }} - Guardamos el archivo y lo cerramos. - Hacemos clic derecho sobre el proyecto y seleccionaremos la opción **Volver a cargar el proyecto**. - Para el paso 2.1, despues de pegar el texto es probable que algunas instrucciones las remarque con rojo. - Ponemos el cursor sobre el texto y presionamos las teclas **CTRL + .** - Del menú contextual seleccionaremos <code>using Microsoft.Extensions.Configuration;</code> - La instrucción del punto 2.3 la vamos a modificar un poco, ya que nosotros solo usaremos la WebApi y no nos interesa el ruteo tradicional del MVC, nuestro código quedaría asi: <code csharp>app.UseMvc();</code> - La primera vez que agregamos el controlador realmente nos configura el proyecto, por lo tanto haremos de nueva cuenta el agregado del controlador, haciendo los mismos pasos: {{ VS14.png }} - En esta ocasión la ventana que nos abre es distinta, en ella seleccionaremos **Controlador de API: en blanco**: {{ VS16.png }} - Pondremos de nombre ListaPosicionesController y le damos **Agregar**: {{ VS17.png }}
Ahora agregaremos la clase Posicion la cual tendra el nombre de la persona y el tiempo en el que completó el memorama, para eso haremos lo siguiente:
- modelos
Ahora aplicaremos lo necesario para hacer la inyección de dependencias, primero tendremos que crear una carpeta donde pondremos nuestra interface y la clase que va implementar la interface, despues habra que hacer la configuración.
El repositorio es la clase la cual nos servirá para almacenar las posiciones en el archivo de texto, para hacer que funcione dentro de la inyección de dependencias vamos aplicar el patrón de repositorio, para hacer esto haremos lo siguiente:
1. Crearemos una carpeta en la raíz del proyecto y le llamaremos repositorios.
- repositorios
2. Agregamos una interface llamada IPosicionRepositorio en la carpeta \repositorios:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace memorama.repositorios { public interface IPosicionRepositorio { List<modelos.Posicion> ObtenerPosiciones(); void GuardarPosicion(modelos.Posicion posicion); } }
3. Agregamos una clase llamada PosicionArchivoRepositorio en la misma carpeta:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using memorama.modelos; namespace memorama.repositorios { public class PosicionArchivoRepositorio : IPosicionRepositorio { public void GuardarPosicion(Posicion posicion) { throw new NotImplementedException(); } public List<Posicion> ObtenerPosiciones() { throw new NotImplementedException(); } } }