¡Esta es una revisión vieja del documento!


Aurelia.IO el framework

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.

Pero... ¿Que es 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:

  1. Es un SPA, el cual tiene una pagina principal (index.html) y sobre esta se cargan los elementos que componen nuestro proyecto.
  2. Cada nuevo archivo html es un template, de hecho utiliza el elemento <template> y sobre el se pone el html que forma parte de nuestro html part.
  3. Puedes tener vistas, vistas con modelos, o solo modelos, todo depende de los archivos que crees.
  4. Utiliza a NodeJs como base.
  5. Puedes usar TypeScript, ESNEXT o ES2015 como lenguaje para el modelo independiente de cual uses, estos utilizarán un transpilador que te generará un Javascript compatible con la mayoría de los navegadores.
  6. No esta casado a ningún framework de diseño.
  7. Puedes descomponer en pequeños fragmentos realmente funcionales tu aplicación y reutilizarlos a tu antojo.
  8. Puedes utilizar cualquier paquete que se encuentre en NPM o por lo menos la gran mayoría.
  9. Puedes utilizar Singleton, eventos, ruteadores, diferentes tipos de bindings, y muchas cosas wuuu.
  10. Perdón me emocione.

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.

A prepararnos...

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:

  1. NodeJS
    1. La base de Aurelia, y casi imposible trabajar adecuadamente el framework sin este funcional programa, nos descargamos la ultima versión desde el sitio oficial de NodeJS.
  2. Git
    1. Ya que todo el código esta versionado, es importante que instalen esta herramienta ya que NodeJs la utilizar para la descarga e instalación de los paquetes, se puede descargar desde su sitio oficial,
  3. Visual Studio 2017 community
    1. Como yo soy fan de Microsoft (me disculpo por quien ofenda) nos descargamos el IDE de ellos en su ultima versión, mas adelante investigare como hacer lo mismo con Visual Studio Code. la descarga del IDE se puede hacer desde su sitio oficial asegúrense de dar click sobre Descarga Gratuita.

Manos a la obra...

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:

  • Abrimos una ventana de NodeJS Command Prompt con privilegios de Administrador y ejecutamos la siguiente instrucción:
npm install aurelia-cli -g
  • Tardara un rato pero se instalará después de eso podemos escribir el comando:
au
  • Con este comando validaremos si Aurelia fue instalado al mostrarse un pantalla con algo similar a lo siguiente:

Ya que validamos que se encuentra instalado Aurelia-cli, entonces procederemos a crear nuestro proyecto.

Creando el proyecto...

Para crear el proyecto haremos lo siguiente:

  1. Abrimos nuestro VS2017:
  2. Una vez abierto, en la sección donde dice nuevo proyecto escribimos la palabra web, y seleccionamos la plantilla que dice Aplicación web ASP.NET Core (.NET Core):
  3. En la siguiente ventana le pondremos nombre a nuestro proyecto, en mi caso lo llamaré memorama, les aconsejo que seleccionen una ruta corta en mi caso C:\proyectos\aurelia, la razón es que Aurelia al ser proyecto de NodeJS creara su estructura de paquetes node_modules y esta ruta puede ser un poco extensa en su contenido y ocasionar algunos problemas con la profundidad y los nombre de las rutas en caso de usar alguna herramienta:
  4. Seleccionamos la plantilla de proyecto Vacío:
  5. Y esperamos a que se cree nuestro proyecto.

Una vez creado habrá que hacer algunas configuraciones adicionales a nuestro proyecto las cuales son las siguientes.

  1. Hacemos clic derecho sobre el proyecto y seleccionamos la opción descargar proyecto:
  2. Hacemos clic derecho sobre el proyecto descargado y seleccionamos la opción Editar {nombre de proyecto}.csproj:
  3. En la primer sección PropertyGroup agregamos una instrucción para que el compilador de VS2017 ignore todo el código TypeScript, ya que quien se encargará de transpilar el código será el motor de Aurelia, para esto agregamos el siguiente código:
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
  4. Después de guardar el cambio en el archivo de proyecto, recargamos el proyecto haciendo clic derecho sobre el proyecto descargando y seleccionando la opción Volver a cargar el proyecto:

Ya cargado nuestro proyecto cargado, nos disponemos a crear el proyecto de Aurelia.IO para eso seguiremos los siguientes pasos:

  1. Abrimos una ventana de NodeJS Command Prompt con privilegios de administrador (o usar la anterior si no la cerramos), y nos iremos a la ruta raíz de nuestro proyecto creado, en mi caso la ruta es C:\proyectos\aurelia\memorama\memorama:
  2. Escribimos el siguiente comando:
    au new --here
  3. Esto iniciara el asistente de creación de proyectos de Aurelia.IO (ojo: el orden o las opciones del asistente puede variar ligeramente) personalmente tengo mi configuración a la cual ya me acostumbre y les pondré en este tutorial, entonces seleccionamos los siguientes opciones:
  • Loader? RequireJS, es el cargador de archivos a demanda que usará por defecto.
  • Platform? ASP.NET Core, como nuestro proyecto es de este tipo, hará los cambios necesarios para ajustarse al proyecto de .Net.
  • Transpiler? TypeScript, como usaremos TypeScript como lenguaje de programación, hay que decirle a Aurelia que use el transpiladro para este lenguaje.
  • Template? Minimum Minification, para que cuando genere los archivos ya lo haga con minificación.
  • CSS Processor? None, como usaremos CSS nativo, no necesitamos ningun procesador como LESS o SASS.
  • Unit Testing? No, no usaré ningún proyecto de pruebas unitarias.

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();
}
Es muy importante respetar el orden de los factores, en este caso de las instrucciones agregadas, ya que si se ponen al revés, el servidor no funcionará adecuadamente.

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:

  • Cuando ejecutamos la aplicación de aurelia, se transpilan los archivos de origen (.html, .js, .ts, .less, .css, .json, etc.) para generar los archivos de destino (.css, .js, .html) necesarios.
  • Aurelia divide los archivos de destino de manera predeterminada en 2, vendor-bundle.js el cual contiene todos los módulos de node y framework de aurelia que usemos en la aplicación, y el app-bundle.js en el cual queda todos los archivos que nosotros generamos para nuestra aplicación.
  • Nuestros fuentes ya sea .JS si usamos ESNEXT o ES2015 por medio del transpilador de Babel, o .TS si usamos TypeScript por medio del transpilador de TypeScript, son procesados en un proceso llamado Transpilación el cual lee los archivos anteriores también llamados la nueva generación de JavaScript y genera el JavaScript tradicional el cual es compatible con la mayoría de los navegadores.
  • Posterior a la generación de los archivos destinos son cargados por el navegador y voalá nuestra aplicación funciona.
  • Una ventaja de Aurelia es que si modificamos los fuentes al momento de guardar automaticamente se vuelven a transpilar, volviendo a generar los archivos destinos y recargando el navegador con el nuevo cambio.

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:

  1. Agregamos un nuevo archivo de texto en el raíz del proyecto, al cual llamaremos watch.cmd, esto lo hacemos dando clic derecho sobre el proyecto, seleccionando la opción Agregar y después seleccionamos Nuevo elemento… en la ventana que nos abre seleccionamos Archivo de texto, escribimos el nombre del archivo y le damos clic sobre Agregar.
  2. Escribiremos las siguientes instrucciones en el archivo watch.cmd, las cuales abrirán una nueva ventana de DOS y sobre esta ejecutará el comando para levantar al servidor de Aurelia.
    set root=%~dp0
    start cmd /k "cd %root% && au run --watch"
  3. Ahora modificaremos nuestro archivo Program.cs para que cuando ejecutemos la aplicación se levante el servidor de aurelia, y cuando detengamos la depuración cierre al servidor de aurelia, para eso modificaremos el método estático main para que quede de la siguiente manera:
    public static void Main(string[] args)
    {
      var host = new WebHostBuilder()
         .UseKestrel()
         .UseContentRoot(Directory.GetCurrentDirectory())
         .UseIISIntegration()
         .UseStartup&ltstartup>()
         .UseApplicationInsights()
         .Build();
     
      using (var process = new System.Diagnostics.Process())
      {
         process.StartInfo = new System.Diagnostics.ProcessStartInfo("watch.cmd");
         process.Start();
     
         host.Run();
     
         process.Kill();
      }
    }
  4. En este punto habremos terminado de configurar nuestros servidores, cuando nosotros ejecutemos la aplicación, el IISExpress levantará su servidor en el puerto designado, mientras que Aurelia levantara los suyos en los puertos 9000 para la aplicación y 3001 para el sincronizador.

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.

Estructura del Proyecto

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:

  • wwwroot (carpeta): En esta carpeta encontraremos todos los archivos de contenido como html, scripts, hojas de estilo y multimedia que necesitemos.
    • index.html (archivo): Estructura base de html, tiene los scripts necesarios para correr aurelia, y en el elemento body veremos un atributo aurelia-app el cual nos dice cual es el módulo de configuración de aurelia, de manera predeterminada se carga el módulo main.
  • aurelia_project (carpeta): Se encuentra todos los archivos de configuración de Aurelia.IO, algunas plantillas y el motor de aurelia.
  • src (carpeta): Contiene todo el código fuente de nuestra aplicación.
    • main.ts (archivo): Es el archivo base de Aurelia el cual es especificado en el archivo index.html en el atributo aurelia-app, en este archivo es en donde se hace toda la configuración inicial sobre cómo tiene que trabajar la aplicación, aquí podemos definir las diferentes opciones sobre cómo tiene que trabajar en los diferentes ambiente productivo, prueba, etc., también podremos configurar plug-ins adicionales y hacer configuraciones sobre estos plug-ins.
    • environment.ts (archivo): Contiene las configuraciones sobre como nuestra aplicación debe de trabajar, dentro de la carpeta aurelia_project, existe una carpeta llamada environments, en esta carpeta existen 3 archivos .ts, [dev, prod, stage], la manera predeterminada de ejecutar la aplicación es dev, pero se puede especificar otro de los ambientes, el archivo environment.ts será sobre-escrito por uno de los archivos de la carpeta environments según el modo en que se haya ejecutado la aplicación.
    • app.ts (archivo): Es el primer archivo de nuestra aplicación de manera predeterminada cuando aurelia se termina de configurar ejecuta este archivo, representa al modelo dentro del patrón MVVM.
    • app.html (archivo): representa a la vista del archivo app.ts, y contiene el html que se desplegará inicialmente cuando la aplicación se cargue.
    • resources (carpeta): En esta carpeta están los recursos globales que usaremos en nuestra aplicación.
      • attributes .- Son atributos personalizados que permiten dar comportamientos diferentes a los elementos.
      • binding-behaviors (carpeta): Son componentes que tienen interacciones mas profundas con los elementos, existen los value-converters pero solo se enfocan en la burbuja de valores, mientrsa que los binding-behaviors toman todo el ciclo de vida del elemento.
      • elements (carpeta): Son los componentes de uso general que podemos reutilizar en nuestras vistas.
      • value-converters (carpeta): Son los componentes que se encargan de representar valores en otros formatos.
      • index.ts (archivo): En este archivo le decimos a Aurelia, cuales son los recursos que tenemos disponibles, en la línea “config.globalResources” pero eso lo veremos mas adelante.

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:

  1. En el explorador de soluciones en la barra de herramientas que aparece en la parte superior buscamos y hacemos clic sobre el ícono que en su tooltip nos dice “Mostrar todos los archivos”:
  2. Con esto nos mostrará los archivos ocultos que existén, estos se mostraran sin color y con bordes punteados, seleccionamos las 4 carpetas dentro de resources y le damos clic derecho, despues seleccionamos la opción “Incluir en el proyecto”:
  3. Con esto, las carpetas se mostrarán de color normal y podemos dejar de ver los archivos ocultos haciendo clic en el mismo botón del paso 1.

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.

Las reglas...

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:

  1. El memorama tendrá un botón para generar nuevo juego.
  2. Las cartas tienen que estar desordenadas de tal manera que en cada juego agarren una posición aleatoria.
  3. El memorama mostrará 8 pares, por lo tanto tendremos que acomodar 16 cartas en la pantalla para jugar con ellas.
  4. Solo se puede voltear un par a la vez, es decir, no se podrán voltear mas cartas hasta que el par abierto se haya validado como igual o se haya cerrado.
  5. Cuando un par de cartas se volteé puede suceder lo siguiente:
    1. Si son iguales se quedarán abiertas y nuestro contador de pares acertados incrementará en 1.
    2. Si no son iguales se dejarán abiertas por 1 segundo y después se volverán a cerrar.
  6. Una vez que se volteé la primer carta empezará a correr un cronometro para llevar el tiempo transcurrido.
  7. El juego termina una vez que se hayan acertado todos los pares.
  8. Al terminar el juego se almacenará en una lista con los tiempos del cual se mostrarán los mejores 20 en orden ascendente, siendo el de menor tiempo el mejor.

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:

  • La mesa tiene como responsabilidades:
    • Iniciar nuevos juegos.
    • Validar el par volteado de cartas.
  • La carta tiene como responsabilidades:
    • Abrir la carta.
    • Cerrar la carta.
  • La partida tiene como responsabilidades:
    • Llevar el conteo de pares acertados.
    • Llevar el tiempo transcurrido.
  • La lista de posiciones tiene como responsabilidades:
    • Mostrar el listado de posiciones.
    • Registrar una nueva posición.

A tirar código...

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.

La carta...

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;
    }
}
Cuando se declara un método con la palabra reservada get, realmente estamos declarando una propiedad de solo lectura, o una propiedad calculada.

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>
La vista esta relacionada al modelo por convención, es decir, al tener el mismo nombre aurelia las “amarra”, pero para que sea efectivo el “amarre” la clase debe respetar dos cosas:
  • La nomenclatura antes mencionada.
  • La declaración de la clase lleve al inicio la palabra reservada export (solo si esta será utilizada desde otro modulo).

La mesa...

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:

  • Línea 7, el método regresa una promesa, una promesa es una técnica que nos ofrece javascript para tratar los modelos asíncronos, donde le dices que realice una acción en un destino remoto y cuando la acción remota termine la promesa le informará al que realizo la petición por medio de uno de los dos métodos que nos ofrece la promesa, en el cual uno sirve para informar el resultado exitoso, y otro sirve para informar el fracaso.
  • Línea 7, La promesa tiene como parámetro un función de regreso de llamada (callback), esta función a su vez tiene como parámetro otra función de regreso de llamada la cual usaremos para devolver las cartas.
  • Línea 8, del arreglo de cartas se genera un arreglo de nombre cartasDesordenadas, este arreglo se llena en base a una función sort en la cual le indicamos que ordene los elementos en base a un número aleatorio, esto nos asegura que las cartas no quedaran en el mismo orden del arreglo original.
  • Línea 9, del arreglo de cartasDesordenadas seleccionamos las cartas que formarán parte de nuestro nuevo juego en base al parámetro numPares, estas cartas las almacenaremos en un nuevo arreglo de nombre cartasSeleccionadas.
  • Línea 10, en el arreglo cartasSeleccionadas tenemos las cartas que formarán nuestro juego, pero como son pares, entonces necesitamos duplicar esas cartas concatenando el arreglo consigo mismo.
  • Línea 11, desordenamos las cartasSeleccionadas para asegurar que el juego es realmente aleatorio.
  • Línea 12, devolvemos nuestras cartasDesordenadas por medio de la función de regreso de llamada.

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() {
 
    }
}
  • Línea 4, es un mecanismo que nos ofrece Aurelia.IO para hacer inyección de dependencias, se ejecuta de manera automática generando una instancia del tipo WebApi y colocando la instancia generada en el parámetro del constructor de la línea 8.
  • Línea 8, al declarar el parámetro con la palabra reservada private, le estamos diciendo que ese parámetro también es una variable privada de la clase.
  • Línea 13, se hace la llamada a la api.
  • Línea 14, la respuesta satisfactoria nos llega por medio del método then.
  • Línea 15 y 16, una vez que nos haya respondido el servidor remoto, haremos con esos datos lo que queramos.

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>
  • Línea 2, utilizaremos el componente carta (para Aurelia.IO nuestros modelos son componentes).
  • Línea 3, agregamos un elemento button al cual le enlazamos el método nuevoJuego.
  • Línea 4, agregamos un elemento ul.
  • Línea 5, agregamos el elemento carta que se genera a partir del componente que estamos requiriendo en la línea 2, al elemento carta le enlazamos el arreglo de cartas por medio de la instrucción repeat.for, e inmediatamente cada elemento del arreglo lo enlazamos al modelo por medio de la instrucción model.bind.

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 =&gt; {
            let cartasDesordenadas = cartas.sort(c =&gt; Math.random() - 0.364);
            let cartasSeleccionadas = cartasDesordenadas.slice(0, numPares);
            cartasSeleccionadas = cartasSeleccionadas.concat(cartasSeleccionadas);
            cartasDesordenadas = cartasSeleccionadas.sort(c =&gt; Math.random() - 0.472);
            resolve(JSON.parse(JSON.stringify(cartasDesordenadas)));
        });
    }
}
 
let numCartas = 30;
let cartas = [];
 
for (var i = 1; i &lt; numCartas; i++) {
    let carta = {
        id: i,
        imagen: "/imagenes/carta_" + ("00" + i).slice(-2) + ".png"
    };
    cartas.push(carta);
}
  • Línea 12, al serializar y deserializar los objetos rompemos toda relación con los objetos originales, y solo enviamos nuevos objetos con la información necesitada.

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"&gt;&lt;/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:

  • La creación del modelo partida.
  • La comunicación efectiva entre los modelos.

La partida...

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:

  1. Abrimos una ventana de comando de NodeJS con privilegios de administrador.
  2. Nos vamos al directorio raiz de nuestro proyecto.
  3. Escribimos la siguiente instrucción:
    npm install moment --save
  4. Abrimos el archivo aurelia_project\aurelia.json y en la sección [build\bundles\dependencies] antes de la línea “text”, agregamos la siguiente línea:
    "moment",
    "text",
  5. Abrimos el archivo src\main.ts, y modificamos para que quede el siguiente código:
    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(() =&gt; aurelia.setRoot());
    }

Y con esto finalizamos la configuración de momentjs, regresamos a nuestra programación.

La Partida - Tiempo transcurrido...

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");
    }
}
  • Línea 1, importamos el del paquete moment, el cual contienen la clase moment que nos ayuda a manipular las fechas.
  • Línea 13, generamos una instancia de moment con la hora inicial.
  • Línea 14, generamos una instancia de moment con la hora final, si la hora final no existe tomamos la hora actual.
  • Línea 15, sacamos la diferencia entre la hora final y la inicial, convertimos a utc para evitar el desfase horario y le damos el formato definido.

Comunicación efectiva...

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:

  1. Crear clases que servirán como los delegados de las notificaciones.
  2. Importar la clase EventAggregator del paquete aurelia-event-aggregator en los lugares donde se harán las suscripciones y publicaciones.
  3. Importar las clases que servirán como delegados en los lugares donde se harán las suscripciones y publicaciones.
  4. Inyectar la clase EventAggregator en las clases donde haremos las suscripciones y publicaciones.
  5. Hacer las suscripciones y/o publicaciones.
  6. En la carpeta src\componentes crearemos un nuevo archivo que le llamaremos eventos.ts, agregaremos algunas clases:
  7. Una clase que nos servirá para indicar que el juego inició.
  8. Una clase que nos servirá para indicar que el juego finalizó.
  9. Una clase que nos servirá para indicar cuando un par se ha acertado.

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 =&gt; {
            this.horaInicio = new Date();
            this.horaFin = null;
        });
        this.ea.subscribe(eventos.ParAcertado, msg =&gt; {
            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");
    }
}
  • Línea 2, importamos la clase EventAggregator.
  • Línea 3, importamos todas las clases del archivo de eventos.
  • Línea 10, establecemos la función para inyectar la clase EventAggregator.
  • Línea 12, recibimos la inyección en el constructor.
  • Línea 13-16, nos suscribimos a la clase JuegoIniciado, cuando recibamos una notificación de esta clase, estableceremos la hora actual como la hora de inicio, y limpiaremos el valor de la hora de fin.
  • Línea 17-18, nos suscribimos a la clase ParAcertado, cuando recibamos una notificación de esta clase, incrementamos el valor de los pares acertados.
  • Línea 19-20, si los pares acertados son igual a 8, entonces se establece la hora actual como hora de fin del juego.
  • Línea 21, hacemos una publicación de la clase JuegoFinalizado para notificar que el juego ha finalizado.

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 =&gt; {
            this.juegoIniciado = false;
        });
    }
 
    nuevoJuego() {
        this.juegoIniciado = false;
        this.api.obtenerCartas(this.numPares)
            .then(_cartas =&gt; {
                this.cartas.length = 0;
                this.cartas = _cartas;
            });
    }
 
    validarPar() {
 
    }
}
  • Línea 2, importamos la clase EventAggregator.
  • Línea 3, importamos todas las clases del archivo de eventos.
  • Línea 6, agregamos la clase EventAggregator en el método de inyección.
  • Línea 9, declaramos una variable para detectar la primera vez que se abra una carta.
  • Línea 11, hacemos la inyección.
  • Línea 12-14, nos suscribimos a la clase JuegoFinalizado, cuando recibimos una notificación de la esta clase, establecemos el valor de la variable juegoIniciado como falso.
  • Línea 18, establecemos el valor de la variable juegoIniciado en falso.

Ahora bien todavía tenemos cabos sueltos, para atar estos cabos, tenemos que hacer lo siguiente:

  1. Notificar cuando abrimos una carta.
  2. Notificar cuando estamos o dejamos de validar un par.
  3. Notificar cuando una nueva partida ha sido iniciada.

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() {
 
    }
}
  • Línea 1, importamos la clase Carta ya que la utilizaremos en una de las clases que declararemos.
  • Línea 21-25, declaramos la clase CartaAbierta la cual servirá para notificar la carta que ha sido abierta, en el constructor declaramos una propiedad pública llamada carta, la cual será la carta abierta.
  • Línea 27-31, declaramos la clase ValidandoPar la cual servirá para notificar el inicio y el fin de la validación de un par, en el constructor declaramos una propiedad publica llamada validando, la cual nos indicará si está validando o no.
  • Línea 33-37, declaramos la clase NuevaPartida la cual servirá para notificar cuando una nueva partida se haya convocado.

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 =&gt; {
            this.estaValidando = msg.validando;
        });
    }
 
    abrir() {
        if (!this.estaAbierta &amp;&amp; !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;
    }
}
  • Línea 1, importamos la clase EventAggregator.
  • Línea 2, importamos todas las clases del archivo de eventos.
  • Línea 9, agregamos el método para inyectar la clase EventAggregator.
  • Línea 10, declaramos una variable privada para saber si se esta validando un par.
  • Línea 12, se agrega la inyección al constructor.
  • Línea 13-14, nos suscribimos a la clase ValidandoPar y cuando recibamos una notificación de esta clase, pasaremos el valor que nos trae a la variable declarada en la línea 10.
  • Línea 19, establecemos una condición en la cual solo se seguirá con el proceso de abrir la carta si esta no esta no está abierta y no existe un proceso de validación.
  • Línea 21, hacemos una publicación de la clase CartaAbierta informando que la carta actual ha sido abierta.

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 =&gt; {
            this.juegoIniciado = false;
        });
        this.ea.subscribe(eventos.CartaAbierta, msg =&gt; {
            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 =&gt; {
                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 =&gt; {
                pares.map(carta =&gt; carta.cerrar());
                this.ea.publish(new eventos.ValidandoPar(false));
            }, 1000);
        }
    }
}
  • Línea 4, importamos la clase Carta.
  • Línea 11, declaramos un arreglo que contendrá las cartas que se vayan abriendo.
  • Línea 17, nos suscribimos a la clase CartaAbierta.
  • Línea 18-20, en caso de que el juego no haya sido iniciado, marcaremos como iniciado el juego, después publicaremos con la clase JuegoIniciado que se ha iniciado el juego, esto solo sucede con la primera carta que se abre.
  • Línea 23, la carta abierta la agregamos al arreglo de cartas abiertas.
  • Línea 24-26, cuando dos cartas se hayan abierto procederemos a validar el par.
  • Línea 31, cuando iniciemos un nuevo juego publicaremos con la clase NuevaPartida que una nueva partida se ha convocado.
  • Línea 35, después de obtener las cartas vaciaremos el arreglo de cartas abiertas.
  • Línea 42, cuando iniciamos la validación del par publicamos con la clase ValidandoPar que estamos en un proceso de validación con el parámetro true.
  • Línea 43, quitamos del arreglo de cartas abiertas el par de cartas.
  • Línea 44, comparamos si las cartas son iguales.
  • Línea 45, en caso de que las cartas sean iguales publicamos con la clase ParAcertado que se ha encontrado un par.
  • Línea 46, publicamos con la clase ValidandoPar que hemos finalizado el proceso de validación con el parámetro false.
  • Línea 48, en caso de que no sean cartas iguales iniciamos y temporizador de 1 segundo.
  • Línea 49, después de que ha transcurrido el segundo, cerramos las cartas.
  • Línea 50, publicamos con la clase ValidandoPar que hemos finalizado el proceso de validación con el parámetro false.

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 =&gt; {
            this.horaFin = this.horaInicio = null;
            this.paresAcertados = 0;
        });
        this.ea.subscribe(eventos.JuegoIniciado, msg =&gt; {
            this.horaInicio = new Date();
            this.horaFin = null;
        });
        this.ea.subscribe(eventos.ParAcertado, msg =&gt; {
            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");
    }
}
  • Línea 13, nos suscribimos a la clase NuevaPartida para saber cuando la nueva partida haya sido convocada.
  • Línea 14, cuando recibamos la notificación de nueva partida establecemos los valores de la hora de inicio y de la hora de fin en nulos.
  • Línea 15, establecemos el valor de los pares acertados en 0.

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;
}
Desconozco bien como funcionan los puntos 4, es parte del esoterismo del CSS.

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.

La API

Para agregar la API primero tendremos que configurar nuestro proyecto, pero con VS2017 es muy sencillo, basta con:

  1. Agregaremos en la raíz del proyecto una carpeta de nombre api:
    - api
  2. Dentro de la carpeta agregaremos un nuevo controlador:
  3. Nos saldrá una ventana y seleccionaremos dependencias minimas:
  4. Al terminar la configuración nos saldra un archivo de texto, habrá que seguir esas instrucciones para que quede configurado.
    1. Para el paso 1, copiamos el texto y descargamos el proyecto haciendo clic derecho sobre el mismo, y seleccionando la opción Descargar el proyecto.

- 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:

  1. Agregamos una nueva carpeta en la raíz del proyecto llamada modelos:
     - modelos 
  2. Dentro de la carpeta agregaremos una nueva clase de nombre Posicion.

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

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();
    }
  }
}