Javascript UI con Riot 5

Básicamente, si estás iniciándote en la programación web o ya has adquirido fundamentos en javascript te resultará natural poner en práctica lo aprendido, verás que Riot 5 es una librería para la UI (Interfaz de Usuario) con la que puedes conectar rapidamente suavizando la frustración como iniciado o programador interesado, en palabras más sofisticadas, menor curva de aprendizaje al guardar aspectos cercanos al estándar. Pienso que después de tener nociones y alguna practica mínima con HTML y javascript (2015+) es interesante conocer esta librería, incluso hasta puedes quedar conectado con ella si gustas de su sencillez, pequeño tamaño y su velocidad de respuesta.

Como desarrollador, llegué a utilizar la primera versión de Angular, luego un poco de las nuevas versiones en pruebas de concepto con Ionic. Estuve usando algunos meses ReactJS y fue divertido pero en ese entonces su licencia era algo peculiar (poco conveniente para algunos proyectos). Le di un vistazo a Vue, y aunque veía similitudes con Angular, en ese momento no me hacía a la idea el tener que usar plantillas con un prefijo (v-). Cuando encontré Riot fué tan simple y natural que me arriesgué entonces a darle la oportunidad hasta su evolución a la versión 5, gracias a Gianluca Guarini por su contribución. Por supuesto, Riot es similar a Vue en varios aspectos y ambas librerías corresponden a mis habilidades.

En la plataforma OnMind (de la que soy autor) el camino es bien definido, al menos para la versión actual se ha establecido la programación de nuestros componentes visuales ante todo bajo javascript, y si se pregunta por alguna librería soportamos Riot 5 como “first-class” (de primera clase) en nuestros componentes construidos para un software que es orientado principalmente a la gestión interna de negocios bajo parámetros propios. Incluso para integración nativa con móviles se puede combinar con Capacitor. Mientras alguien puede encontrarse en una investigación sobre tecnologías similares (como React, Angular, Vue u otra), esté comenzando a aprender del tema o tome sus opiniones, para OnMind es un hecho y camino elegido en nuestra primera versión, estando abierto a revisión en futura versión, que no ocupa tiempo por el momento.

El video ilustra ejemplo que se encuentra en artículo sobre Capacitor + Riot

Recordando la noción de una etiqueta HTML

Si pensamos en un botón, que se visualiza en una aplicacion o una página web, tendriamos una etiqueta como la siguiente:

<button style="color: blue" onsubmit="alert('Hello')">Press</button>

Se puede observar que:

  1. Generalmente se usa apertura y cierre con el nombre de la etiqueta entre signos menor y mayor que, el último con un “slash” (/), es decir: <button></button>
  2. La etiqueta puede reportar atributos (o propiedades), en este caso: style
  3. La etiqueta puede disparar eventos, en este caso: onsubmit
  4. La etiqueta puede tener contenido, bien sea texto o mas HTML, en este caso el texto: Press

Comprendiendo esto se construye una plantilla con las etiquetas predefinidas para HTML. Este mismo principio se aplica a componentes web que pueden verse como etiquetas pesonalizadas y pueden tener atributos, eventos y contenido (o subcomponentes).

Riot & HTML

Riot usa una plantilla HTML, en la parte inferior del archivo puede incorporar <style></style> y <script></script>

Ahora bien, ¿Si Riot 5 es cercano al estándar HTML, cual es la diferencia o qué agrega que requiera aprenderse?. En el sentido esencial de las cosas, citaría en principio sólo 3 aspectos:

  1. Expresiones entre llaves {}, las cuales son utilizadas para mostrar contenido de modo dinámico, por ejemplo un título de manera programada en una página bilingue.
  2. Condiciones if para evaluar si una etiqueta HTML se muestra o no conforme a una expresión.
  3. Ciclos each para procesar de modo dinámico datos usados frecuentemente en listas, tablas, opciones de selección o similar.

Adicionalmente, se requieren comprender aspectos como la noción de componentes web (que se presentan como etiquetas personalizadas y son piezas para la interfaz visual), el ciclo de vida (eventos para gestión del DOM de HTML), incorporación de elementos o subcomponentes en ranuras (slots), así como algo de estilos (CSS3). Presento esta guía esencial a mi manera, espero resulte de gran ayuda y agrado si suena de tu interés probar o iniciarte con RiotJS.

Ejemplo esencial

<!DOCTYPE html>
<html>
    <head>
        <title>Riot 5 - Hi there!</title>
    </head>
    <body>
        <my-tag></my-tag>

        <script src="https://cdn.jsdelivr.net/npm/riot@4/riot+compiler.min.js"></script>
        <script src="my-tag.riot" type="riot"></script>
        <script type="module">
            (async function main() {
                await riot.compile()
                riot.mount('my-tag')
            }())
        </script>
    </body>
</html>

Básicamente, se invoca primero al compilador interactivo de Riot usando riot+compiler.min.js.
Al crearse un componente, por ejemplo un archivo my-tag.riot, se incluye como script y puede usarse una estructura cercana al estándar HTML, como se verá a continuación.

Tags o Componentes Web

<my-tag>
    <h3>Hi there!</h3>
    This is { state.name }

    <script>
        export default {
            state: {
                name: 'My first tag'
            }
        }
    </script>
</my-tag>

Para propiedades internas o alterables se usa la palabra reservada state, mientras que para recibir propiedades se usa la palabra reservada props, por ejemplo:

    <my-tag title="Welcome"></my-tag>

En este caso se recibiría “Welcome” en props.title, valor que se pasa al componente y que no es alterable dentro de éste. Si bien, se pueden usar expresiones dinámicas para enviar el título pero eso sería otra cosa.

Riot vs Vue 3

Si tienes conocimientos en Vue 3, a continuación se presenta una tabla comparativa esencial de estas tecnologías para la UI.

Concepto Riot Vue
Uso de expresiones { } {{ }}
Condicional if="..." v-if="..."
Ciclo o Iterador each="..." v-for="..."
Eventos HTML onclick="..." @click="..."
Usa export default Yes Yes
Evento onBeforeMount Yes Yes
Evento onMounted Yes Yes
Evento onBeforeUpdate Yes Yes
Evento onUpdated Yes Yes
Evento onBeforeUnmount Yes Yes
Evento onUnmounted Yes Yes
Evento onErrorCaptured No Yes
Evento shouldUpdate Yes No
Método this.update() Yes No
Elemento components Yes Yes

Vue posee más características que Riot actualmente, también porque Riot busca brindar una mayor simplicidad.
Por ejemplo, Vue incorpora v-model para componentes vinculados en doble sentido y gestiona las actualizaciones, mientras en Riot debe hacerse con código y usando el método this.update().
Riot no requiere un método como setup pero requiere expresar las propiedades usando props. en el template.

Proyecto de Inicio Rápido

A continuación veremos un ejemplo en el que podemos hacer un ejercicio evitando compilar en tiempo de ejecución, es decir, incluyéndo los componentes listos para usar. Se combinarán tecnologías como Webpack (empaquetador de archivos o recursos web, prerrequisito con algún conocimiento esencial).

Para empezar se debe crear una carpeta que corresponda al proyecto de ejemplo y en la que se instalarán los modulos requeridos de NodeJS. Por tanto, allí se debe inicializar el arhivo package.json ejecutando el comando npm init e instalando los módulos de Webpack y Riot, así:

npm init
npm install riot @riotjs/webpack-loader @riotjs/compiler -D
npm install webpack webpack-cli webpack-dev-server -D

Dado que para nuestro ejercicio, Webpack requiere un archivo de configuración, generalmente denominado webpack.config.js, lo inicializamos (creándolo en un editor) con el siguiente contenido:

const path = require('path');

module.exports = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, 'web', 'dist'),
    filename: 'app.bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.riot$/,
        exclude: /node_modules/,
        use: [{
          loader: '@riotjs/webpack-loader',
          options: { hot: false }
        }]
      }
    ]
  }
}

Modificamos el archivo de configuración package.json generado por npm init para incorporar el lanzador de inicio o tarea start (bajo scripts), así:

  "scripts": {
    "start": "webpack --mode development",
    "serve": "webpack-dev-server --content-base web/ --mode development --open --inline --progress"
  },

Bajo una carpeta que nombraremos web creamos nuestro archivo index.html con el siguiente contenido:

<!DOCTYPE html>
<html>
    <head>
        <title>Riot 5 - Hi there!</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="dist/app.bundle.js"></script>
    </body>
</html>

Además, abrimos una carpeta src incluyendo el archivo del componente en Riot, por ejemplo my-tag.riot, con el siguiente contenido:

<my-tag>
    <h3>{ props.title }</h3>
    This is my first tag<br/>
    <span class="counter" if={ state.timer > 5 }>{ state.timer }</span>

    <style>
        .counter {
            font-size: 40px;
            font-family: monospace;
            font-weight: bold;
        }
    </style>

    <script>
        export default {
            state: {
                timer: 0
            },
            onMounted() {
                setInterval(this.counter, 1000)
            },
            counter() {
                this.update({ timer: ++this.state.timer })
            }
        }
    </script>
</my-tag>

Se trata de un componente sencillo que visualiza un título recibido como parámetro (props.title) y después de 5 segundos visualiza un contador, el cual se inicia bajo el método onMounted usando la función setInterval del lenguaje, que a su vez invoca a counter para actualizar una propiedad (state.timer). Aunque todo es Riot se acerca a HTML, se podría decir entonces que lo característico de Riot en este ejemplo corresponde a:

También debemos crear bajo la carpeta src un archivo app.js que será usado como script principal para armar el paquete con Webpack. Colocamos el siguiente contenido:

import { component } from 'riot'
import Tag from './my-tag.riot'

component(Tag)(document.getElementById('app'), {
  title: 'Hi there!'
})

Dado que hemos configurado webpack para ser invocado con npm, para generar nuestra versión distribuible ejecutamos:

npm start

Si bien, al tratarse de un ejercicio tan sencillo podríamos dirigirnos a la carpeta web y abrir el archivo index.html directamente desde el navegador, procedemos a iniciar un servicio web contando con webpack-dev-server y como hemos asociado un “script” en el archivo package.json, ejecutamos lo siguiente:

npm run serve

Una alternativa a webpack-dev-server sería revisar si se tiene instalado un servidor web básico de NodeJS como http-server, incluso con Python usando en la línea de comandos python -m SimpleHTTPServer 8080. Simplemente se inicia un servicio y para ver el resultado se consulta en el navegador con el puerto respectivo (generalmente 8080), ingresando en la dirección algo como localhost:8080.

Al final tendremos un proyecto inicial con el siguiente inventario de archivos:

Archivo Descripción
src/app.js Archivo de script principal para armar el paquete
src/my-tag.riot Archivo del componente web o tag
web/index.html Pagina de arranque de la aplicación web o SPA
web/dist/app.bundle.js Archivo de script trasnpilado o distribuible (generado con webpack)
package.json Archivo de configuración de paquetes o módulos NodeJS
webpack.config.js Archivo de configuración para distribución o empaquetado de la aplicación

Expresiones

Basicamente se usan expresiones del lenguaje Javascript delimitadas por signos de llaves ({}) conforme al contexto, usando por ejemplo props o state, o una propiedad exportada.

{ state.value }
{ props.value || 'its string text' }
<p class={ this.selected: true }>
<button onclick={ hint }>

<script>
    export default {
        hint (e) { console.info('Ok!') }
    }
</script>

Este último bloque de ejemplo en realidad corresponde a eventos que se manejan de modo semejante a cualquier expresión

Condicionales

Puedes manejar puntos de validación o condiciones para ocultar o mostrar algo usando if con expresiones de Javascript.

<div if={ state.ok }>...</div>

Ciclos

Si vas a procesar contenido que requiere una mayor dinámica, puedes usar each cuando se trate arreglos o iteraciones, por ejemplo, para tablas o listas.

<ol>
    <li each={ item in props.list }>{ item.name }</li>
</ol>
<ol>
    <li each={ (row,i) in state.list }>{ i } - { row.name }</li>
</ol>

Este último ejemplo ilustra el uso con un arreglo imprimiendo también su índice

Estilos (CSS)

Si te preguntabas como puedes aplicar estilos personalizados en alguna parte del componente, simplemente continúa usando <style> como en html, así que si eres un programador con algún vacío sobre css y cuentas con colaboración en equipo, un diseñador web te podría orientar.

<my-tag>
    <h1>Title</h1>

    <style>
        h1 {
            font-family: Arial;
        }
    </style>
</my-tag>

Ciclo de Vida (Eventos de Riot)

Quizás este sea uno de los puntos más claves a asimilar para un iniciado en esta librería. El ciclo de vida de un componente bajo Riot corresponde al modo de interactuar internamente con el DOM (este tipo de librerías facilitan un trabajo en el que se manipula el contenido) y las instancias por las que pasa un componente desde el momento en que se prepara para ser usado (onBeforeMount) hasta el evento en que es removido (onUnmounted), en caso de intervenir en su comportamiento de manera programada, es decir, es posible personalizar los eventos de ser necesario. Veamos entonces los 6 eventos a conocer.

Evento Descripción
onBeforeMount Antes de que el componente sea montado en la página
onMounted Justo después de que el componente es montado o renderizado por primera ocasión (o cada vez que se vuelve a montar)
shouldUpdate Punto de validación para proceder con una actualización visual o no conforme a lógica implementada. Debe retornar verdadero o falso y se evalua con los parámetros newProps para nuevos valores de propiedades recibidas, currentProps para las propiedades como se encuentran, incluso this para culquier otra propiedad interna, de modo que es posible resolver las condiciones necesarias justo antes de iniciar cualquier actualización dentro del componente.
onBeforeUpdate Justo antes de efectuar la actualización
onUpdated Justo después de que el componente es actualizado
onBeforeUnmount Antes de que el componente sea removido
onUnmounted Cuando el componente es removido
export default {
    onBeforeMount(props, state) {},
    onMounted(props, state) {},
    shouldUpdate(newProps, currentProps) {},
    onBeforeUpdate(props, state) {},
    onUpdated(props, state) {},
    onBeforeUnmount(props, state) {},
    onUnmounted(props, state) {}
}

Para refrescar el DOM o aplicar actualización se hace la siguiente invocación:

this.update()
this.update({ data: 'hi' })

Inyectando HTML dentro de un TAG (Slot)

<my-tag>
    <slot />
</my-tag>

Lo anterior se hace en la definición del TAG, en el momento de usarse se hace lo siguiente:

<my-tag>
    <span>Hi there!</span>
</my-tag>

Para multiples slots primero se deben definir éstos con name.

<my-tag>
    <h1>
        <slot name="one" />
    </h1>
    <div>
        <slot name="two" />
    </div>
</my-tag>

Al invocarse se utiliza como atributo slot del modo siguiente:

<div>
    <span slot="one">Welcome</span>
    <span slot="two">John Doe</span>
</div>

Quizás parezca que es otra manera de hacer las cosas sin necesidad de usar propiedades, pero esto se debe a que los ejemplos se encuentran en un nivel esencial. Lo que se está sugiriendo es una anatomía para escenarios distintos en dónde se inyecta todo un bloque de HTML al interior de nuestro componente y este debe saber a qué parte corresponde.

Subcomponentes

Distinto al uso de HTML5 nativo, cuando dentro de un componente lo que se requiere es usar otro dentro de éste, el archivo debe importarse e indicarse el subcomponente dentro del elemento components en la definición. Por ejemplo:

    <my-tag>
        <my-sub />
    </my-tag>

    <script>
        import MySub from './my-sub.riot'
        export default {
            components: { MySub }
        }
    </script>

Nótese que se usa una convención de nombre para las etiquetas como my-sub y se importa con un nombre distinto dónde la primera letra de cada nombre está en mayúscula y se omite el guión, usando así en el script un nombre como MySub. El archivo my-sub.riot deberá contener el código respectivo para el subcomponente.

Renderizado desde Servidor (SSR - Node.js)

Si te ha motivado usar Riot para páginas web estáticas (en las que no interviene el lado del servidor) las cosas no paran allí. Es posible devolver un plantilla desde el servidor, o que se denomina renderizado desde el servidor (SSR). Para usar Riot desde el servidor se requiere Node.js, podrías revisar la salida con el siguiente ejemplo:

<my-tag>
    <h3>Hi there!</h3>
    { props.title }
</my-tag>

El anterior contenido correspondería a un archivo para el componente o tag. Suponiendo que el anterior archivo se ha denominado my-tag.riot, se abriría un archivo Javascript (por ejemplo: ssr.js), como el siguiente:

const fs = require('fs');
const render = require('@riotjs/ssr');
const register = require('@riotjs/ssr/register');
register();
const name = 'my-tag';
const MyTag = require(`./${name}.riot`);
const tag = render.default(name, MyTag, { title: 'Hi there!' });
fs.writeFileSync(`./${name}.js`, MyTag, { encoding: 'utf8' });
const html = `<!DOCTYPE html>
<html>
    <body>
        ${tag}
        <script src="https://unpkg.com/riot@4/riot+compiler.min.js"></script>
        <script type="module">
            import MyTag from '/${name}.js';
            riot.register('${name}', MyTag)
            riot.mount('${name}')
        </script>
    </body>
</html>`;

console.log(html);  // put: <my-tag></my-tag>

Esta librería tiene un tamaño pequeño y un buen tiempo de respuesta, de modo que uno se plantearía que tan necesario sería un renderizado desde el servidor, aún así si llega a ser necesario implementar esa dinámica la función respectiva tiene un objetivo esencial. En este caso obtiene <my-tag></my-tag> de modo dinámico desde el servidor.

Una alternativa es hacer uso del compilador (@riotjs/compiler en lugar de @riotjs/ssr). Veamos el nuevo escenario:

const fs = require('fs');
const riotC = require('@riotjs/compiler');
const name = 'my-tag';
const riotFile = `./${name}.riot`;
const jsFile = `./${name}.js`;
try {
    const content = fs.readFileSync(riotFile, { encoding: 'utf8' });
    const script = content.toString();
    const { code, map } = riotC.compile(script);
    fs.writeFileSync(jsFile, code, { encoding: 'utf8' });
    const html = `<!DOCTYPE html>
<html>
    <body>
        <div id="app"></div>
        <script src="https://unpkg.com/riot@4/riot+compiler.min.js"></script>
        <script type="module">
            import MyTag from '/${name}.js';
            const createApp = window.riot.component(MyTag);
            const app = createApp(document.getElementById('app'), {})
        </script>
    </body>
</html>`;

    console.log(html);
}
catch (e) {
    console.error(e.message);
}

Esto tendría cierta semejanza al diseño web con lenguajes que posibilitan un pre-procesamiento como PHP, incluso JSP o ASP, en este caso la principal diferencia es contar con doble dinamismo al procesarse o compilarse desde el servidor e interactuar con las características de la librería una vez quede en las manos del navegador. Además, se percibe unificación al usar el mismo lenguaje Javascript (tanto en el código para cliente como en el servidor).

Siguiente proyecto

Habiendo comprendido los fundamentos de esta librería, se plantea continuar con un proyecto nuevo que tendrá sus respectivas piezas de código. Se puede copiar el archivo de configuración webpack.config.js del proyecto anterior y abriendo una nueva carpeta se puede inicializar el proyecto ejecutando los siguientes comandos (estando dentro de la carpeta):

echo {} > package.json
npm install riot @riotjs/webpack-loader @riotjs/compiler -D
npm install webpack webpack-cli -D
npm install serve-static restana -S

En el archivo package.json se incorpora el elemento scripts con lo siguiente:

  "scripts": {
    "start": "webpack --mode development",
    "serve": "node server.js"
  },

Como plantilla html tendríamos lo siguiente:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Riot Component</title>

    <!-- Semantic-UI Styles -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css" rel="stylesheet" media="screen">
    <!-- HighlightsJS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/styles/ocean.min.css">
  </head>
  <body>
    <div id="app"></div>

    <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.9/highlight.min.js"></script>
    <script defer src="dist/app.bundle.js"></script>
  </body>
</html>

Se puede copiar el archivo app.js del proyecto anterior como script principal de nuestro código que se invoca desde el navegador (cliente). Nuestro archivo my-tag.riot tendría el siguiente código:

<my-tag>
    <div class={ state.color }>
        <div class="ui text container">
            <br/>
            <form onsubmit={ onSend }>
                <div class="ui labeled action fluid input">
                    <div class="ui label">http://localhost:3000/yourname/</div>
                    <input type="text" name="parameter" placeholder={ state.name }>
                    <button class="ui icon button">
                        <i class="search icon"></i>
                    </button>
                </div>
            </form>
            <div class="ui toggle checkbox">
                <input type="checkbox" onchange={ onCheck }>
                <label><i class="adjust orange icon"></i></label>
            </div>
            <div class="ui placeholder segment" id="holder" if={ state.first }>
                <div class="ui icon header">
                    <a onclick={ onSend }><i class="search blue link icon"></i></a>
                    Try it!
                </div>
            </div>
            <pre>
                <code style="max-height: 300px; overflow-y: scroll;">{ state.code }</code>
            </pre>
        </div>
    </div>

    <style>
        .clear {
            background-color: white;
            color: black;
            height: 100vh;
        }

        .dark {
            background-color: dimgray;
            color: snow;
            height: 100vh;
        }
    </style>

    <script>
        export default {
            state: {
                first: true,
                name: 'john',
                color: 'dark'
            },
            onMounted() {  // When component is mounted or created
                this.update()
            },
            onUpdated() {  // When component is updated (after this.update)
                if (!this.state.first) hljs.initHighlighting()  // use highlight.js
            },
            onSend(e) {
                e.preventDefault()
                this.update({ first: false, code: '...' })  // update or render again HTML (DOM)
                let input = document.querySelector('[name="parameter"]').value || this.state.name
                let uri = 'http://localhost:3000/yourname/' + input

                fetch(uri)
                .then(res => res.json())
                .then(data => {
                    this.update({ code: JSON.stringify(data, null, '  ') })
                })
                .catch(err => {
                    this.update({ code: `${uri} => ${err.message}` })
                });
            },
            onCheck(e) {
                if (this.state.color == 'clear')
                    this.state.color = 'dark'
                else
                    this.state.color = 'clear'
                this.update()
            }
        }
    </script>
</my-tag>

Implementamos nuestro propio servidor web (usando restana y serve-static) nombrándolo server.js con el siguiente código:

const files = require('serve-static');
const service = require('restana')();
const port = 3000;

names = (req, res, next) => {
    if (req.params.name.length < 3)
        return res.send({ status: 'error', message: 'invalid name' });
    res.send({ yourname: req.params.name });
}

service.use(files('./web'));
service.get('/yourname/:name', names);

service.start(port).then((server) => {
    console.log(`Service listening at ${port}`);
});

Tendríamos entonces la siguiente distribución de archivos:

Se invoca a webpack con npm start y se ejecuta el servicio (ej. node server.js o npm run serve) para revisar nuestra aplicación en un navegador moderno.


© 2019 by César Arcila