Jekyll2023-07-01T18:27:39+00:00https://hpneo.dev/feed.xmlGustavo LeonFull-stack web developer.Creando un router basado en archivos con Preact, preact-router y Parcel2023-07-01T00:30:00+00:002023-07-01T00:30:00+00:00https://hpneo.dev/2023/07/01/creando-un-router-basado-en-archivos-con-preact-y-parcel<p><img src="/assets/images/fs-router.png" alt="Creando un router basado en archivos con Preact, preact-router y Parcel" /></p> <blockquote> <p>Para este ejercicio estoy utilizando <a href="https://parceljs.org/">Parcel</a>, una <em>build tool</em> simple de usar y configurable a partir de plugins.</p> </blockquote> <p>Un <em>router</em> basado en archivos permite que una aplicación <em>client-side</em> pueda armar sus rutas utilizando archivos como base. Esta convención es utilizada por frameworks como <a href="https://nextjs.org/">Next.js</a> o <a href="https://remix.run/">Remix</a>, por nombrar algunos.</p> <p>Gracias a esta convención, organizar archivos se vuelve una tarea mucho más sencilla, ya que existe una relación un poco más directa entre las rutas que ven los usuarios en su navegador con la estructura de archivos que ven los programadores al desarrollar una aplicación web frontend.</p> <p>Como un ejercicio para aprender cómo funciona el plugin de <em>resolver</em> de <a href="https://parceljs.org/">Parcel</a>, vamos a ver cómo crear un router basado en archivos utilizando Preact y <a href="https://www.npmjs.com/package/preact-router"><code class="language-plaintext highlighter-rouge">preact-router</code></a>.</p> <blockquote> <p>Este acercamiento a tener un <em>router</em> basado en archivos es experimental y más un ejercicio inicial que una implementación lista para producción.</p> </blockquote> <p>El resultado final será que lograremos tener una aplicación con rutas completamente funcional con una estructura de archivos como esta:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./app/organizations/[subdomain]/layout.js ./app/organizations/[subdomain]/page.js ./app/organizations/[subdomain]/courses/layout.js ./app/organizations/[subdomain]/courses/page.js ./app/organizations/[subdomain]/courses/[identifier]/layout.js ./app/organizations/[subdomain]/courses/[identifier]/page.js ./app/organizations/[subdomain]/courses/[identifier]/cohorts/page.js ./app/organizations/[subdomain]/courses/[identifier]/edit/page.js ./app/organizations/[subdomain]/users/page.js ./app/organizations/new/page.js ./app/organizations/<span class="o">(</span>course_editor<span class="o">)</span>/[subdomain]/courses/[identifier]/[section]/[lesson]/layout.js ./app/organizations/<span class="o">(</span>course_editor<span class="o">)</span>/[subdomain]/courses/[identifier]/[section]/[lesson]/page.js </code></pre></div></div> <p>Y un archivo <code class="language-plaintext highlighter-rouge">index.js</code> como este:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">preact</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">Router</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@hpneo/router</span><span class="dl">"</span><span class="p">;</span> <span class="nx">render</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">Router</span> <span class="o">/&gt;</span><span class="p">,</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span> </code></pre></div></div> <h2 id="creando-un-resolver-de-parcel">Creando un <em>resolver</em> de Parcel</h2> <p>Un <em>resolver</em> en Parcel toma un specifier (lo que comúnmente es la ruta de un archivo o el nombre de un módulo de NPM) y devuelve un resultado que luego es usado por otros <a href="https://parceljs.org/features/plugins/#resolvers"><em>resolvers</em></a> o por <a href="https://parceljs.org/features/plugins/#transformers"><em>transformers</em></a>.</p> <p>El <em>resolver</em> que escribiremos interceptará los <code class="language-plaintext highlighter-rouge">import</code> a <code class="language-plaintext highlighter-rouge">@hpneo/router</code> y devolverá el código que le indiquemos, también llamado <a href="https://parceljs.org/plugin-system/resolver/#virtual-modules"><em>virtual module</em></a>, porque el módulo definido por el <em>specifier</em> no existe “físicamente” como un archivo en la carpeta del proyecto.</p> <p>Lo primero que haremos será escribir la estructura básica de un <em>resolver</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ./parcel-resolver-router/index.mjs</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Resolver</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@parcel/plugin</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">path</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="k">new</span> <span class="nx">Resolver</span><span class="p">({</span> <span class="k">async</span> <span class="nx">resolve</span><span class="p">({</span> <span class="nx">specifier</span><span class="p">,</span> <span class="nx">options</span> <span class="p">})</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">specifier</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">@hpneo/router</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="s2">``</span><span class="p">;</span> <span class="k">return</span> <span class="p">{</span> <span class="na">filePath</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="dl">"</span><span class="s2">router.js</span><span class="dl">"</span><span class="p">),</span> <span class="na">code</span><span class="p">:</span> <span class="nx">code</span> <span class="p">};</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <p>Este <em>resolver</em> actualmente solo devuelve un <em>virtual module</em> vacío. Sin embargo, contiene 2 detalles importantes a tener en cuenta: <code class="language-plaintext highlighter-rouge">specifier</code> es el nombre del módulo que estamos interceptando, y <code class="language-plaintext highlighter-rouge">options</code> contiene una propiedad llamada <code class="language-plaintext highlighter-rouge">projectRoot</code>, que devuelve la ruta desde donde Parcel está ejecutándose.</p> <p>El siguiente paso es obtener todos los archivos que nos servirán para armar nuestras rutas. En este caso usaremos la convención de Next.js y su nuevo <a href="https://nextjs.org/docs/app/building-your-application/routing">App Router</a>, y usaremos <a href="https://www.npmjs.com/package/fast-glob"><code class="language-plaintext highlighter-rouge">fast-glob</code></a> para obtener todos los archivos llamados <code class="language-plaintext highlighter-rouge">page.js</code>.</p> <p>El siguiente bloque de código va dentro del método <code class="language-plaintext highlighter-rouge">resolve</code> de nuestro plugin:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">glob</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">fast-glob</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">files</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">glob</span><span class="p">(</span><span class="dl">"</span><span class="s2">./app/**/page.js</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">ignore</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">node_modules</span><span class="dl">"</span><span class="p">],</span> <span class="na">cwd</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="p">});</span> <span class="c1">// [</span> <span class="c1">// './app/organizations/[subdomain]/page.js',</span> <span class="c1">// './app/organizations/new/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/courses/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/users/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/courses/[identifier]/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/courses/[identifier]/cohorts/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/courses/[identifier]/edit/page.js',</span> <span class="c1">// './app/organizations/[subdomain]/courses/[identifier]/[section]/[lesson]/page.js'</span> <span class="c1">// ]</span> </code></pre></div></div> <p>Una vez que tenemos las rutas de todos los archivos, vamos a iterar por cada uno de ellos para obtener la ruta de la aplicación a partir de la ruta del archivo:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ./parcel-resolver-router/index.mjs</span> <span class="kd">const</span> <span class="nx">DYNAMIC_ROUTE_SEGMENT_PATTERN</span> <span class="o">=</span> <span class="sr">/</span><span class="se">\[([</span><span class="sr">a-zA-Z</span><span class="se">]</span><span class="sr">*</span><span class="se">)\]</span><span class="sr">/</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">CATCH_ALL_SEGMENT_PATTERN</span> <span class="o">=</span> <span class="sr">/</span><span class="se">\[(\.\.\.([</span><span class="sr">a-zA-Z</span><span class="se">]</span><span class="sr">*</span><span class="se">))\]</span><span class="sr">/</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">ROUTE_GROUP_PATTERN</span> <span class="o">=</span> <span class="sr">/</span><span class="se">\(([</span><span class="sr">a-zA-Z_-</span><span class="se">]</span><span class="sr">*</span><span class="se">)\)</span><span class="sr">/</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">buildRouteFromFilePath</span><span class="p">(</span><span class="nx">filePath</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">pageRoute</span> <span class="o">=</span> <span class="nx">filePath</span> <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/^</span><span class="se">\.\/</span><span class="sr">app/</span><span class="p">,</span> <span class="dl">""</span><span class="p">)</span> <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\/</span><span class="sr">page</span><span class="se">\.</span><span class="sr">js$/</span><span class="p">,</span> <span class="dl">""</span><span class="p">)</span> <span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">segment</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">segment</span> <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">CATCH_ALL_SEGMENT_PATTERN</span><span class="p">,</span> <span class="dl">"</span><span class="s2">:$2*</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">DYNAMIC_ROUTE_SEGMENT_PATTERN</span><span class="p">,</span> <span class="dl">"</span><span class="s2">:$1</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">ROUTE_GROUP_PATTERN</span><span class="p">,</span> <span class="dl">""</span><span class="p">)</span> <span class="p">)</span> <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">)</span> <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// Las rutas de aplicación deben empezar con '/'</span> <span class="k">return</span> <span class="nx">pageRoute</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">)</span> <span class="p">?</span> <span class="nx">pageRoute</span> <span class="p">:</span> <span class="s2">`/</span><span class="p">${</span><span class="nx">pageRoute</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Como lo que estamos retornando en el <em>resolver</em> es un <em>virtual module</em>, vamos a actualizar el código dentro del método <code class="language-plaintext highlighter-rouge">resolve</code> de nuestro plugin:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">files</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">glob</span><span class="p">(</span><span class="dl">"</span><span class="s2">./app/**/page.js</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">ignore</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">node_modules</span><span class="dl">"</span><span class="p">],</span> <span class="na">cwd</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="s2">`const pages = [ </span><span class="p">${</span><span class="nx">files</span> <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pagePath</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">pageRoute</span> <span class="o">=</span> <span class="nx">buildRouteFromFilePath</span><span class="p">(</span><span class="nx">pagePath</span><span class="p">);</span> <span class="k">return</span> <span class="s2">`{ route: "</span><span class="p">${</span><span class="nx">pageRoute</span><span class="p">}</span><span class="s2">", component: require("</span><span class="p">${</span><span class="nx">pagePath</span><span class="p">}</span><span class="s2">") }`</span><span class="p">;</span> <span class="p">})</span> <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">,</span><span class="se">\n</span><span class="dl">"</span><span class="p">)}</span><span class="s2"> ];`</span><span class="p">;</span> </code></pre></div></div> <p>Con estos cambios, la variable <code class="language-plaintext highlighter-rouge">pages</code> de nuestro <em>virtual module</em> contiene un <em>array</em> de objetos con 2 propiedades: <code class="language-plaintext highlighter-rouge">route</code> y <code class="language-plaintext highlighter-rouge">component</code>, que es como vamos a importar cada página.</p> <h2 id="soporte-para-layouts">Soporte para layouts</h2> <p>Una vez que tenemos las rutas con sus respectivos módulos importados, vamos a obtener los <em>layouts</em>, si existieran. Un <em>layout</em> es un componente que envuelve al componente de una página y a todos los componentes hijo que compartan la misma ruta.</p> <p>En nuestro caso, si tenemos un <em>layout</em> en <code class="language-plaintext highlighter-rouge">./app/organizations/[subdomain]/layout.js</code>, el componente que se exporta desde ese archivo va a estar presente al renderizar las siguientes rutas:</p> <ul> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/courses</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/courses/:identifier</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/courses/:identifier/cohorts</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/courses/:identifier/edit</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/courses/:identifier/:section/:lesson</code></li> <li><code class="language-plaintext highlighter-rouge">/organizations/:subdomain/users</code></li> </ul> <p>Sin embargo, el <em>layout</em> no se renderizará en la siguiente ruta:</p> <ul> <li><code class="language-plaintext highlighter-rouge">/organizations/new</code></li> </ul> <p>Para obtener los <em>layouts</em> para cada página, haremos lo siguiente:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">fsSync</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">fs</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="s2">`const pages = [ </span><span class="p">${</span><span class="nx">files</span> <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pagePath</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">pageRoute</span> <span class="o">=</span> <span class="nx">buildRouteFromFilePath</span><span class="p">(</span><span class="nx">pagePath</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">layoutPath</span> <span class="o">=</span> <span class="nx">pagePath</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/page</span><span class="se">\.</span><span class="sr">js$/</span><span class="p">,</span> <span class="dl">"</span><span class="s2">layout.js</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">hasLayout</span> <span class="o">=</span> <span class="nx">fsSync</span><span class="p">.</span><span class="nx">existsSync</span><span class="p">(</span><span class="nx">layoutPath</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">layout</span> <span class="o">=</span> <span class="nx">hasLayout</span> <span class="p">?</span> <span class="s2">`require("</span><span class="p">${</span><span class="nx">layoutPath</span><span class="p">}</span><span class="s2">")`</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">null</span><span class="dl">"</span><span class="p">;</span> <span class="k">return</span> <span class="s2">`{ route: "</span><span class="p">${</span><span class="nx">pageRoute</span><span class="p">}</span><span class="s2">", component: require("</span><span class="p">${</span><span class="nx">pagePath</span><span class="p">}</span><span class="s2">"), layout: </span><span class="p">${</span><span class="nx">layout</span><span class="p">}</span><span class="s2"> }`</span><span class="p">;</span> <span class="p">})</span> <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">,</span><span class="se">\n</span><span class="dl">"</span><span class="p">)}</span><span class="s2"> ];`</span><span class="p">;</span> </code></pre></div></div> <h2 id="soporte-para-route-groups">Soporte para route groups</h2> <p>Los <em>route groups</em> permiten agrupar rutas de manera lógica sin afectar la URL final en la aplicación, y sirven para poder tener <a href="https://nextjs.org/docs/app/building-your-application/routing/route-groups#creating-multiple-root-layouts">múltiples <em>root layouts</em></a>.</p> <p>Dado que los <em>route groups</em> se definen con carpetas con nombre siguiente el formato <code class="language-plaintext highlighter-rouge">(nombre)</code>, solo tenemos que verificar si cumple esa condición con una expresión regular:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ROUTE_GROUP_PATTERN</span> <span class="o">=</span> <span class="sr">/</span><span class="se">\(([</span><span class="sr">a-zA-Z_-</span><span class="se">]</span><span class="sr">*</span><span class="se">)\)</span><span class="sr">/</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="s2">`const pages = [ </span><span class="p">${</span><span class="nx">files</span> <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pagePath</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">pageRoute</span> <span class="o">=</span> <span class="nx">buildRouteFromFilePath</span><span class="p">(</span><span class="nx">pagePath</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">layoutPath</span> <span class="o">=</span> <span class="nx">pagePath</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/page</span><span class="se">\.</span><span class="sr">js$/</span><span class="p">,</span> <span class="dl">"</span><span class="s2">layout.js</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">hasLayout</span> <span class="o">=</span> <span class="nx">fsSync</span><span class="p">.</span><span class="nx">existsSync</span><span class="p">(</span><span class="nx">layoutPath</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">layout</span> <span class="o">=</span> <span class="nx">hasLayout</span> <span class="p">?</span> <span class="s2">`require("</span><span class="p">${</span><span class="nx">layoutPath</span><span class="p">}</span><span class="s2">")`</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">null</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">isPartOfRouteGroup</span> <span class="o">=</span> <span class="o">!!</span><span class="nx">pagePath</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="nx">ROUTE_GROUP_PATTERN</span><span class="p">);</span> <span class="k">return</span> <span class="s2">`{ route: "</span><span class="p">${</span><span class="nx">pageRoute</span><span class="p">}</span><span class="s2">", component: require("</span><span class="p">${</span><span class="nx">pagePath</span><span class="p">}</span><span class="s2">"), layout: </span><span class="p">${</span><span class="nx">layout</span><span class="p">}</span><span class="s2">, isPartOfRouteGroup: </span><span class="p">${</span><span class="nx">isPartOfRouteGroup</span><span class="p">}</span><span class="s2"> }`</span><span class="p">;</span> <span class="p">})</span> <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">,</span><span class="se">\n</span><span class="dl">"</span><span class="p">)}</span><span class="s2"> ];`</span><span class="p">;</span> </code></pre></div></div> <h2 id="renderizando-rutas-con-preact-router">Renderizando rutas con <code class="language-plaintext highlighter-rouge">preact-router</code></h2> <p>Ahora que ya tenemos una colección de rutas, vamos a usar <code class="language-plaintext highlighter-rouge">preact-router</code> para renderizarlas en una aplicación de Preact.</p> <p>Pero antes de hacer eso debemos entender que hay rutas que pueden estar anidadas, por lo que debemos convertir nuestro <em>array</em> en un árbol. Para eso, agregamos la siguiente función al código del <em>virtual module</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Este bloque de código va dentro de la variable `code`, como parte del código del virtual module:</span> <span class="k">import</span> <span class="nx">sortBy</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">lodash/sortBy</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">partition</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">lodash/partition</span><span class="dl">"</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">createRoutesFromPages</span><span class="p">(</span><span class="nx">pages</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">routes</span> <span class="o">=</span> <span class="p">[];</span> <span class="c1">// Dado que las rutas son anidadas, es mejor si ordenamos nuestra colección de rutas de menor a mayor cantidad de caracteres.</span> <span class="c1">// Esto nos sirve para poder trabajar primero las rutas padre y luego las rutas hijas.</span> <span class="kd">let</span> <span class="nx">sortedPagesByRouteLength</span> <span class="o">=</span> <span class="nx">sortBy</span><span class="p">(</span><span class="nx">pages</span><span class="p">,</span> <span class="p">(</span><span class="nx">page</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span> <span class="k">while</span> <span class="p">(</span><span class="nx">sortedPagesByRouteLength</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="nx">sortedPagesByRouteLength</span><span class="p">.</span><span class="nx">shift</span><span class="p">();</span> <span class="c1">// Usamos la función de Lodash llamada `partition` para dividir las rutas restantes entre</span> <span class="c1">// las que sí son hijas de la ruta actual y las que no</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">childrenPages</span><span class="p">,</span> <span class="nx">otherPages</span><span class="p">]</span> <span class="o">=</span> <span class="nx">partition</span><span class="p">(</span> <span class="nx">sortedPagesByRouteLength</span><span class="p">,</span> <span class="p">(</span><span class="nx">childPage</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Si una ruta es parte de un route group, la ponemos al mismo nivel que la ruta actual</span> <span class="k">if</span> <span class="p">(</span><span class="nx">childPage</span><span class="p">.</span><span class="nx">isPartOfRouteGroup</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">childPage</span><span class="p">.</span><span class="nx">route</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">);</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">sortedPagesByRouteLength</span> <span class="o">=</span> <span class="nx">otherPages</span><span class="p">;</span> <span class="nx">routes</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span> <span class="p">...</span><span class="nx">page</span><span class="p">,</span> <span class="na">children</span><span class="p">:</span> <span class="nx">createRoutesFromPages</span><span class="p">(</span><span class="nx">childrenPages</span><span class="p">),</span> <span class="p">});</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">routes</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Dado que las rutas van a estar anidadas, necesitamos un componente especial que pueda ser usando del router de <code class="language-plaintext highlighter-rouge">preact-router</code>. Este componente se llama <code class="language-plaintext highlighter-rouge">Route</code> y va a manejar todos los posibles escenarios de nuestra aplicación:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Este bloque de código va dentro de la variable `code`, como parte del código del virtual module:</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">h</span><span class="p">,</span> <span class="nx">Fragment</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">preact</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">Router</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">preact-router</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// El componente `&lt;Router /&gt;` de `preact-router` espera que sus componentes hijo tengan un prop `path`, así que lo pasamos acá</span> <span class="kd">function</span> <span class="nx">Route</span><span class="p">({</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">component</span><span class="p">,</span> <span class="nx">layout</span><span class="p">,</span> <span class="nx">childRoutes</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">...</span><span class="nx">routeProps</span> <span class="p">})</span> <span class="p">{</span> <span class="nx">childRoutes</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">childRoutes</span><span class="p">)</span> <span class="p">?</span> <span class="nx">childRoutes</span> <span class="p">:</span> <span class="p">[</span><span class="nx">childRoutes</span><span class="p">];</span> <span class="c1">// Creamos un elemento de Preact usando `h` de `preact`.</span> <span class="c1">// Como veremos luego, `component` es el componente que exporta cada archivo page.js</span> <span class="kd">const</span> <span class="nx">element</span> <span class="o">=</span> <span class="nx">h</span><span class="p">(</span><span class="nx">component</span><span class="p">,</span> <span class="p">{</span> <span class="nx">path</span> <span class="p">});</span> <span class="c1">// `layout` también es un componente, pero si no existe usamos `&lt;Fragment /&gt;` de `preact`</span> <span class="kd">const</span> <span class="nx">LayoutOrFragment</span> <span class="o">=</span> <span class="nx">layout</span> <span class="p">??</span> <span class="nx">Fragment</span><span class="p">;</span> <span class="c1">// Si esta ruta no tiene rutas hija, simplemente renderizamos el componente con su layout</span> <span class="k">if</span> <span class="p">(</span><span class="nx">childRoutes</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="o">&lt;</span><span class="nx">LayoutOrFragment</span> <span class="p">{...</span><span class="nx">routeProps</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">element</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/LayoutOrFragment&gt;</span><span class="err">; </span> <span class="p">}</span> <span class="c1">// Pero si la ruta tiene rutas hija, creamos un nested router, y le pasamos las rutas hija como un array de `&lt;Route /&gt;`</span> <span class="k">return</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">LayoutOrFragment</span> <span class="p">{...</span><span class="nx">routeProps</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">Router</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">element</span><span class="p">}</span> <span class="p">{</span><span class="nx">childRoutes</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">page</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">Route</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">}</span> <span class="c1">// Si una de las rutas hija tiene más rutas hija, debemos hacer que el path soporte routers anidados</span> <span class="nx">path</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span> <span class="p">?</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span> <span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">}</span><span class="s2">/:rest*`</span><span class="p">}</span> <span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">component</span><span class="p">.</span><span class="k">default</span><span class="p">}</span> <span class="nx">layout</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">layout</span><span class="p">?.</span><span class="k">default</span><span class="p">}</span> <span class="nx">childRoutes</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">children</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/Router</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/LayoutOrFragment</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Finalmente, creamos nuestro componente <code class="language-plaintext highlighter-rouge">&lt;ApplicationRouter /&gt;</code>, el cual tendrá todas las rutas creadas con <code class="language-plaintext highlighter-rouge">&lt;Route /&gt;</code> en base al resultado de <code class="language-plaintext highlighter-rouge">createRoutesFromPages</code>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Este bloque de código va dentro de la variable `code`, como parte del código del virtual module:</span> <span class="kd">function</span> <span class="nx">ApplicationRouter</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">routes</span> <span class="o">=</span> <span class="nx">createRoutesFromPages</span><span class="p">(</span><span class="nx">pages</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">routes</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">Router</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">routes</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">page</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">Route</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">}</span> <span class="c1">// Si una de las rutas hija tiene más rutas hija, debemos hacer que el path soporte routers anidados</span> <span class="nx">path</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span> <span class="p">?</span> <span class="nx">page</span><span class="p">.</span><span class="nx">route</span> <span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">route</span><span class="p">}</span><span class="s2">/:rest*`</span><span class="p">}</span> <span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">component</span><span class="p">.</span><span class="k">default</span><span class="p">}</span> <span class="nx">layout</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">layout</span><span class="p">?.</span><span class="k">default</span><span class="p">}</span> <span class="nx">childRoutes</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">.</span><span class="nx">children</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/Router</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">ApplicationRouter</span><span class="p">;</span> </code></pre></div></div> <p>Y si bien con esto ya tenemos nuestro propio router basado en archivos, hay un último punto que debemos tener en cuenta: Si levantamos el proyecto con Parcel y agregamos luego nuevos archivos <code class="language-plaintext highlighter-rouge">page.js</code> o <code class="language-plaintext highlighter-rouge">layout.js</code>, nuestro router no reflejará las nuevas rutas. Esto es debido a que los <em>plugins</em> de Parcel usan una caché, por lo que debemos invalidar la caché de Parcel bajo ciertos escenarios.</p> <h2 id="invalidar-caché-de-parcel">Invalidar caché de Parcel</h2> <p>Para invalidar la caché de nuestro <em>resolver</em>, debemos agregar ciertas propiedades al objeto que retorna la función <code class="language-plaintext highlighter-rouge">resolve</code>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Resolver</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@parcel/plugin</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">path</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="k">new</span> <span class="nx">Resolver</span><span class="p">({</span> <span class="k">async</span> <span class="nx">resolve</span><span class="p">({</span> <span class="nx">specifier</span><span class="p">,</span> <span class="nx">options</span> <span class="p">})</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">specifier</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">@hpneo/router</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">glob_pattern</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">./app/**/page.js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">glob_layout_pattern</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">./app/**/layout.js</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">files</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">glob</span><span class="p">(</span><span class="nx">glob_pattern</span><span class="p">,</span> <span class="nx">fs</span><span class="p">,</span> <span class="p">{</span> <span class="na">ignore</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">node_modules</span><span class="dl">"</span><span class="p">],</span> <span class="na">cwd</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">layouts</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">glob</span><span class="p">(</span><span class="nx">glob_layout_pattern</span><span class="p">,</span> <span class="nx">fs</span><span class="p">,</span> <span class="p">{</span> <span class="na">ignore</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">node_modules</span><span class="dl">"</span><span class="p">],</span> <span class="na">cwd</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="s2">`...`</span><span class="p">;</span> <span class="k">return</span> <span class="p">{</span> <span class="na">filePath</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="dl">"</span><span class="s2">router.js</span><span class="dl">"</span><span class="p">),</span> <span class="na">code</span><span class="p">:</span> <span class="nx">code</span><span class="p">,</span> <span class="c1">// Le decimos a Parcel que invalide cualquier archivo que cumpla con los patrones glob para `page.js` o `layout.js`</span> <span class="na">invalidateOnFileCreate</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">glob</span><span class="p">:</span> <span class="nx">glob_pattern</span> <span class="p">},</span> <span class="p">{</span> <span class="na">glob</span><span class="p">:</span> <span class="nx">glob_layout_pattern</span> <span class="p">},</span> <span class="p">],</span> <span class="c1">// Le decimos a Parcel que invalide cualquier cambio en los archivos `page.js` o `layout.js` dentro de "./app":</span> <span class="na">invalidateOnFileChange</span><span class="p">:</span> <span class="p">[</span> <span class="p">...</span><span class="nx">files</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">filePath</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="nx">filePath</span><span class="p">)),</span> <span class="p">...</span><span class="nx">layouts</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">filePath</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">projectRoot</span><span class="p">,</span> <span class="nx">filePath</span><span class="p">)),</span> <span class="p">],</span> <span class="p">};</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <p>Si quisieramos soportar otras <a href="https://nextjs.org/docs/app/building-your-application/routing#file-conventions">convenciones de nombres de archivos</a>, como <code class="language-plaintext highlighter-rouge">error.js</code> o <code class="language-plaintext highlighter-rouge">template.js</code>, también debemos incluirlos aquí en <code class="language-plaintext highlighter-rouge">invalidateOnFileCreate</code> y <code class="language-plaintext highlighter-rouge">invalidateOnFileChange</code>.</p> <hr /> <p>El código completo del plugin de Parcel lo puedes encontrar en este Gist: <a href="https://gist.github.com/hpneo/c9e9e61e9d530d6c412163f20d8a7df4">https://gist.github.com/hpneo/c9e9e61e9d530d6c412163f20d8a7df4</a></p>Con un plugin de Parcel podemos tener nuestro propio router basado en archivos, como los usados en Next.js o Remix.Creando un clon de Storybook desde 0 con Parcel2023-06-28T01:10:00+00:002023-06-28T01:10:00+00:00https://hpneo.dev/2023/06/28/creando-un-storybook-desde-cero<p><img src="/assets/images/storybook.png" alt="Creando un clon de Storybook desde 0 con Parcel" /></p> <p><a href="https://storybook.js.org/">Storybook</a> permite mostrar en un solo lugar los componentes que utilizas en tu aplicación. Esto es útil si tienes componentes reutilizables y quieres documentar cómo se ven y cómo se usan sin necesidad de ir al código.</p> <p>Como un ejercicio para aprender cómo funcionan el plugin de <em>transformer</em> de <a href="https://parceljs.org/">Parcel</a>, vamos a crear un clon de Storybook.</p> <h2 id="component-story-format-o-csf">Component Story Format (o CSF)</h2> <p>El primer paso es entender que Storybook funciona leyendo archivos llamados <em>stories</em> (o historias), los cuales son archivos de JavaScript cuyo nombre termina en <code class="language-plaintext highlighter-rouge">.stories.js</code>. Esto es una convención y puede cambiarse en la configuración de Storybook; pero lo importante es que, al fin y al cabo, estos archivos siguen un formato llamado <a href="https://storybook.js.org/docs/react/api/csf"><strong>Component Story Format</strong>, o CSF</a>.</p> <p>Un ejemplo básico es CSF es este archivo que llamaremos <code class="language-plaintext highlighter-rouge">button.stories.js</code>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Importamos el componente que usaremos en nuestras stories</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../src/ui</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Definimos un objeto llamado meta (el nombre no importa), que luego exportaremos como default.</span> <span class="kd">const</span> <span class="nx">meta</span> <span class="o">=</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Components/Button</span><span class="dl">"</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">Button</span><span class="p">,</span> <span class="p">};</span> <span class="c1">// Definimos una variable Basic que también exportaremos.</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">Basic</span> <span class="o">=</span> <span class="p">{</span> <span class="na">args</span><span class="p">:</span> <span class="p">{</span> <span class="na">variant</span><span class="p">:</span> <span class="dl">"</span><span class="s2">primary</span><span class="dl">"</span><span class="p">,</span> <span class="na">children</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Click Me!</span><span class="dl">"</span><span class="p">,</span> <span class="na">loading</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">},</span> <span class="nx">render</span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="o">&lt;</span><span class="nx">Button</span> <span class="p">{...</span><span class="nx">args</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="err">; </span> <span class="p">},</span> <span class="p">};</span> <span class="c1">// Finalmente exportamos meta como default.</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">meta</span><span class="p">;</span> </code></pre></div></div> <p>La <em>story</em> más simple posible, siguiendo el Component Story Format, tiene 2 exports: un <em>default export</em> y un <em>named export</em>. El <em>default export</em> contiene meta datos sobre la <em>story</em>, como el componente del que se está escribiendo, o el título de la <em>story</em>.</p> <p>Por otro lado, el <em>named export</em> es un objeto que tiene 2 propiedades: <code class="language-plaintext highlighter-rouge">args</code> y <code class="language-plaintext highlighter-rouge">render</code>. A su vez, <code class="language-plaintext highlighter-rouge">args</code> es un objeto que contiene los <em>props</em> del componente que el usuario puede configurar mediante un panel de control, y <code class="language-plaintext highlighter-rouge">render</code> es una función que retorna el componente del que estamos escribiendo, y que también recibe una copia de <code class="language-plaintext highlighter-rouge">args</code> como argumento. Cada vez que los valores de <code class="language-plaintext highlighter-rouge">args</code> cambian, Storybook vuelve a llamar a <code class="language-plaintext highlighter-rouge">render</code> y el componente se re-renderiza.</p> <p>Ahora que ya sabemos como funciona una <em>story</em>, vamos a crear nuestro propio Storybook.</p> <h2 id="obteniendo-todos-los-archivos-storiesjs">Obteniendo todos los archivos <code class="language-plaintext highlighter-rouge">.stories.js</code></h2> <blockquote> <p>Para este ejercicio estoy utilizando <a href="https://parceljs.org/">Parcel</a>, una <em>build tool</em> simple de usar y configurable a partir de plugins.</p> </blockquote> <p>Para poder listar todas las <em>stories</em> que existen en nuestro proyecto, necesitamos primero importar todos los archivos JavaScript que terminen en <code class="language-plaintext highlighter-rouge">.stories.js</code>. Para lograr esta primera tarea usaremos un paquete llamado <a href="https://www.npmjs.com/package/@parcel/resolver-glob"><code class="language-plaintext highlighter-rouge">@parcel/resolver-glob</code></a>.</p> <p>Los <em>resolvers</em> se encargan de resolver un <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import"><em>import</em></a>, ya sea convirtiendo el <em>specifier</em> de un <code class="language-plaintext highlighter-rouge">import</code> (lo que comunmente es el nombre del módulo) en la ruta absoluta de un archivo o retornando código. Esto último se conoce como <em>virtual module</em>, porque el módulo definido por el <em>specifier</em> no existe “físicamente” como un archivo.</p> <p>Lo que hace <code class="language-plaintext highlighter-rouge">@parcel/resolver-glob</code> es utilizar el <em>specifier</em> como si fuera un patrón llamado <em>glob</em>, y retorna un objeto con todos los módulos que cumplen con el patrón.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">stories</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../stories/**/*.stories.js</span><span class="dl">"</span><span class="p">;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">stories</span><span class="p">);</span> <span class="c1">// {</span> <span class="c1">// button: {</span> <span class="c1">// default: { title: "Components/Button", component: ... },</span> <span class="c1">// Basic: { args: { variant: "primary", children: "Click Me!", loading: false }, render: ... }</span> <span class="c1">// }</span> <span class="c1">// }</span> </code></pre></div></div> <p>Para poder utilizar un <em>resolver</em> dentro de nuestra aplicación, debemos agregarlo en la configuración de Parcel (<code class="language-plaintext highlighter-rouge">.parcelrc</code>):</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"extends"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@parcel/config-default"</span><span class="p">,</span><span class="w"> </span><span class="nl">"resolvers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"@parcel/resolver-glob"</span><span class="p">,</span><span class="w"> </span><span class="s2">"..."</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Una vez que ya tenemos los módulos importados en nuestro archivo, debemos obtener más información sobre los componentes que vamos a probar en las <em>stories</em>.</p> <h2 id="obteniendo-información-de-los-componentes-con-react-docgen">Obteniendo información de los componentes con <code class="language-plaintext highlighter-rouge">react-docgen</code></h2> <p>Si bien ya importamos los módulos de nuestras <em>stories</em>, es muy poco lo que podemos hacer con ellas, excepto quizá renderizar los <em>named exports</em>.</p> <p>Si quisieramos crear una interfaz gráfica que nos permita cambiar los valores de las propiedades definidas en <code class="language-plaintext highlighter-rouge">args</code>, necesitamos saber de qué tipo de dato es cada atributo de ese objeto. Dado que <code class="language-plaintext highlighter-rouge">args</code> en realidad representa los valores que tienen los <em>props</em> de un componente, usamos <a href="https://www.npmjs.com/package/react-docgen"><code class="language-plaintext highlighter-rouge">react-docgen</code></a> para obtener la información de los props directamente del componente. ¿Cómo podemos hacer eso?</p> <p>En la sección anterior vimos los <em>resolvers</em> de Parcel, pero Parcel tiene otro tipo de plugin que es igual o más útil: los <em>transformers</em>. Un <em>transformer</em> toma un <em>asset</em> (por ejemplo: un módulo de JavaScript) y lo transforma, pudiendo retornar código que es cualquier otra cosa excepto el código original del asset. En este caso, vamos a usar un <em>transformer</em> para obtener información del componente, mediante <code class="language-plaintext highlighter-rouge">react-docgen</code>.</p> <p>Lo que hace este <em>transformer</em> es obtener el código fuente del asset con <code class="language-plaintext highlighter-rouge">asset.getCode()</code>, para luego pasárselo a <code class="language-plaintext highlighter-rouge">ReactDocGen</code>. Luego, <code class="language-plaintext highlighter-rouge">ReactDocGen</code> analiza el código y retorna información de los <em>props</em> del componente, utilizando la propiedad <code class="language-plaintext highlighter-rouge">propTypes</code> del componente.</p> <blockquote> <p>Si deseas hacer lo mismo pero tus componentes están escritos en TypeScript, puedes usar <a href="https://www.npmjs.com/package/react-docgen-typescript"><code class="language-plaintext highlighter-rouge">react-docgen-typescript</code></a>.</p> </blockquote> <p>Una vez que obtenemos la información de los props de un componente, usamos una expresión regular para cambiar el código original e incluir lo devuelto por <code class="language-plaintext highlighter-rouge">ReactDocGen</code> como una propiedad más del component. Otra forma de lograr el mismo resultado es manipulando el AST con alguna biblioteca que permita eso, como <code class="language-plaintext highlighter-rouge">@babel/core</code>.</p> <p>Finalmente, reemplazamos el código original del <em>asset</em> por el editado, con <code class="language-plaintext highlighter-rouge">asset.setCode(output)</code>. No debemos olvidar asegurarnos que el <em>asset</em> sea de tipo <code class="language-plaintext highlighter-rouge">js</code>, para que Parcel siga procesando el archivo en caso hayan otros <em>transformers</em> en su <em>pipeline</em>.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Transformer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@parcel/plugin</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">ReactDocGen</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-docgen</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="k">new</span> <span class="nx">Transformer</span><span class="p">({</span> <span class="k">async</span> <span class="nx">transform</span><span class="p">({</span> <span class="nx">asset</span> <span class="p">})</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">source</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">asset</span><span class="p">.</span><span class="nx">getCode</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="nx">ReactDocGen</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">source</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="nx">source</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span> <span class="sr">/export default </span><span class="se">([</span><span class="sr">a-zA-Z</span><span class="se">]</span><span class="sr">*</span><span class="se">)</span><span class="sr">/</span><span class="p">,</span> <span class="p">(</span><span class="nx">substring</span><span class="p">,</span> <span class="nx">group</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">group</span><span class="p">}</span><span class="s2">.__docgenInfo = </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span> <span class="nx">code</span> <span class="p">)}</span><span class="s2">;\n\n</span><span class="p">${</span><span class="nx">substring</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">asset</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">js</span><span class="dl">"</span><span class="p">;</span> <span class="nx">asset</span><span class="p">.</span><span class="nx">setCode</span><span class="p">(</span><span class="nx">output</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{}</span> <span class="k">return</span> <span class="p">[</span><span class="nx">asset</span><span class="p">];</span> <span class="p">},</span> <span class="p">});</span> </code></pre></div></div> <p>A diferencia de los <em>resolvers</em>, los <em>transformers</em> pueden ser definidos como parte de un <em>pipeline</em> específico, los cuales son diferenciados utilizando <em>globs</em>. En este caso, quiero que mi <em>transformer</em> solo sea utilizado en los archivos <code class="language-plaintext highlighter-rouge">.js</code> dentro de la carpeta <code class="language-plaintext highlighter-rouge">src/ui</code>.</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"extends"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@parcel/config-default"</span><span class="p">,</span><span class="w"> </span><span class="nl">"resolvers"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"@parcel/resolver-glob"</span><span class="p">,</span><span class="w"> </span><span class="s2">"..."</span><span class="p">],</span><span class="w"> </span><span class="nl">"transformers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"src/ui/*.js"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"./parcel-transformer-react-docgen/index.mjs"</span><span class="p">,</span><span class="w"> </span><span class="s2">"..."</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Los <code class="language-plaintext highlighter-rouge">"..."</code> al final del pipeline le dicen a Parcel que debe seguir procesando esos archivos con el pipeline por defecto para archivos <code class="language-plaintext highlighter-rouge">.js</code>.</p> <hr /> <p>Con este pequeño ejercicio hemos aprendido a utilizar los plugins <em>resolvers</em> de Parcel, y cómo escribir nuestro propio <em>transformer</em>. Con estos 2 plugins, cubrimos la funcionalidad básica de Storybook, pero se puede extender utilizando otras bibliotecas, como <a href="https://www.npmjs.com/package/@storybook/csf-tools"><code class="language-plaintext highlighter-rouge">@storybook/csf-tools</code></a>.</p>Usando Parcel, podemos crear una versión simple de StorybookPersistiendo estados de React con Web Storage2021-02-25T13:30:00+00:002021-02-25T13:30:00+00:00https://hpneo.dev/2021/02/25/usando-storage-react-state<p><img src="/assets/images/uselocalstoragestate.png" alt="Persistiendo estados de React con Web Storage" /></p> <p>La API de Web Storage permite guardar datos dentro del navegador en forma llave/valor. Con Web Storage podamos persistir datos en el navegador y volver a leerlos incluso después de haber recargado una página, lo cual es útil si queremos guardar datos sobre configuración de usuario, o incluso usarlo como caché.</p> <p>Existen dos tipos de Web Storage, <code class="language-plaintext highlighter-rouge">localStorage</code> y <code class="language-plaintext highlighter-rouge">sessionStorage</code>, y ambos comparten las mismas propiedades, métodos y eventos. Así mismo, ambos comparten las siguientes características:</p> <ul> <li>Crean un espacio por cada origen (un origen está formado por el protocolo y el host).</li> <li>Cada espacio no comparte información con otros espacios de otros orígenes.</li> <li>Los datos persisten las recargas de página.</li> <li>Cada espacio está limitado a guardar 5MB como máximo.</li> </ul> <p>La principal diferencia entre ambos tipos es que <strong><code class="language-plaintext highlighter-rouge">sessionStorage</code> borra los datos al momento que el tab o la ventana del navegador se cierra</strong>, mientras que <code class="language-plaintext highlighter-rouge">localStorage</code> mantiene los datos guardados hasta que son manualmente eliminados.</p> <p>Como ambos tienen una interfaz similar, vamos a usar <code class="language-plaintext highlighter-rouge">localStorage</code> en este post, pero el mismo código se puede aplicar para <code class="language-plaintext highlighter-rouge">sessionStorage</code> (teniendo en cuenta sus limitaciones).</p> <h2 id="accediendo-a-elementos-del-storage">Accediendo a elementos del Storage</h2> <p>Si revisamos <code class="language-plaintext highlighter-rouge">localStorage</code> vamos a ver que parece ser solo un objeto con algunas entradas:</p> <p><img src="/assets/images/localstorage.png" alt="Pasando datos síncronamente desde Rails hacia React" /> <em>Mi <code class="language-plaintext highlighter-rouge">localStorage</code> desde https://developer.mozilla.org/</em></p> <p>Esto significa que podemos acceder a las entradas de <code class="language-plaintext highlighter-rouge">localStorage</code> como si accediéramos a cualquier objeto en JavaScript:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">localStorage</span><span class="p">[</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">];</span> <span class="c1">// "1604634596534"</span> </code></pre></div></div> <p>Sin embargo, <code class="language-plaintext highlighter-rouge">localStorage</code> también tiene un método llamado <code class="language-plaintext highlighter-rouge">getItem</code> que cumple la misma función:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "1604634596534"</span> </code></pre></div></div> <h2 id="escribiendo-en-el-storage">Escribiendo en el Storage</h2> <p>Como ya vimos, <code class="language-plaintext highlighter-rouge">localStorage</code> se comporta como un objeto en JavaScript, lo que significa que también podemos guardar valores de la misma forma que guardamos valores en un objeto:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">localStorage</span><span class="p">[</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> <span class="c1">// 1614099381524</span> </code></pre></div></div> <p>Y de igual forma, <code class="language-plaintext highlighter-rouge">localStorage</code> tiene un método <code class="language-plaintext highlighter-rouge">setItem</code> que hace exactamente lo mismo:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">,</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">());</span> <span class="c1">// undefined</span> </code></pre></div></div> <p>Hasta aquí no hay nada fuera de lo común con Web Storage, <em>excepto</em> por un tema bastante particular: <strong>Los valores son guardados como cadenas</strong>. Volviendo al ejemplo anterior:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">localStorage</span><span class="p">[</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">];</span> <span class="c1">// "1614099592460"</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">banner.developer_needs.embargoed_until</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "1614099592460"</span> </code></pre></div></div> <p>Sabemos que <code class="language-plaintext highlighter-rouge">Date.now()</code> devuelve un número, pero al acceder a la propiedad <code class="language-plaintext highlighter-rouge">banner.developer_needs.embargoed_until</code> de <code class="language-plaintext highlighter-rouge">localStorage</code>, lo que obtenemos es una cadena. ¿Qué pasa si queremos guardar un objeto dentro de <code class="language-plaintext highlighter-rouge">localStorage</code>?</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userConfiguration</span> <span class="o">=</span> <span class="p">{</span> <span class="na">receiveNotifications</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">displaySubscriptionBanner</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">enableGeolocation</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">};</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userConfiguration</span><span class="p">);</span> <span class="c1">// Ahora tratamos de leer el objeto guardado en localStorage.</span> <span class="c1">// La línea después de `localStorage.getItem...` es el resultado:</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "[object Object]"</span> </code></pre></div></div> <p>Como vemos, en vez de obtener el objeto <code class="language-plaintext highlighter-rouge">userConfiguration</code> tenemos una cadena <code class="language-plaintext highlighter-rouge">"[object Object]"</code>. Esto es por el <em>type coercion</em> de JavaScript, que convierte el objeto en una cadena. Felizmente, tenemos una forma fácil de convertir objetos en cadenas: <code class="language-plaintext highlighter-rouge">JSON.stringify</code>.</p> <p><code class="language-plaintext highlighter-rouge">JSON.stringify</code> convierte cualquier valor de JavaScript a una cadena usando su representación en JSON, que es justamente lo que necesitamos ahora.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userConfiguration</span> <span class="o">=</span> <span class="p">{</span> <span class="na">receiveNotifications</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">displaySubscriptionBanner</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">enableGeolocation</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="p">};</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">userConfiguration</span><span class="p">));</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "{"receiveNotifications":false,"displaySubscriptionBanner":true,"enableGeolocation":false}"</span> </code></pre></div></div> <p>Luego de esta extensa introducción, podemos ver el tema que da título a este post: <strong>¿Cómo podemos usar <code class="language-plaintext highlighter-rouge">localStorage</code> para persistir algunos estados de React?</strong></p> <h2 id="creando-nuestro-propio-hook-en-react">Creando nuestro propio <em>hook</em> en React</h2> <p>Los <a href="https://es.reactjs.org/docs/hooks-intro.html"><em>hooks</em></a> son una funcionalidad de las tantas ofrecidas por React para poder trabajar dentro de los componentes creados a través de funciones. En particular nos interesa usar el <em>hook</em> <code class="language-plaintext highlighter-rouge">useState</code>, con el cual podemos leer y escribir estados dentro de un componente.</p> <p>Supongamos que queremos guardar la configuración del usuario en nuestra aplicación, y para eso tenemos el siguiente componente:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({});</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>El componente <code class="language-plaintext highlighter-rouge">&lt;UserConfigurationDashboard /&gt;</code> está usando <code class="language-plaintext highlighter-rouge">useState</code> para crear un estado que almacenará la configuración del usuario. Este estado luego es actualizado cuando el componente <code class="language-plaintext highlighter-rouge">&lt;UserConfigurationForm /&gt;</code> lanza un evento <code class="language-plaintext highlighter-rouge">onSubmit</code>.</p> <p>Este componente funciona sin problemas pero, si refrescamos la página, el estado de <code class="language-plaintext highlighter-rouge">&lt;UserConfigurationDashboard /&gt;</code> vuelve a su valor inicial (<code class="language-plaintext highlighter-rouge">{}</code>). Para evitar esto usaremos <code class="language-plaintext highlighter-rouge">localStorage</code>.</p> <p>El primer paso será hacer que el valor inicial de el estado venga de <code class="language-plaintext highlighter-rouge">localStorage</code>:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialUserConfiguration</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialUserConfiguration</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Como ya sabemos, los valores de <code class="language-plaintext highlighter-rouge">localStorage</code> son guardados como cadenas, por lo que debemos convertirlos a objetos usando la contraparte de <code class="language-plaintext highlighter-rouge">JSON.stringy</code>: la función <code class="language-plaintext highlighter-rouge">JSON.parse</code>:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialUserConfiguration</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">));</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialUserConfiguration</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Ahora nuestro estado <code class="language-plaintext highlighter-rouge">userConfiguration</code> siempre va iniciar con el valor que venga del <code class="language-plaintext highlighter-rouge">localStorage</code>, pero ahora notamos algo: Realmente no estamos persistiendo cualquier nuevo valor ingresado por el usuario. Esto es porque en ninguna parte hemos usado <code class="language-plaintext highlighter-rouge">setItem</code>.</p> <p>Para sincronizar nuestro estado con <code class="language-plaintext highlighter-rouge">localStorage</code> vamos a usar un nuevo <em>hook</em> llamado <code class="language-plaintext highlighter-rouge">useEffect</code>. Este <em>hook</em> tiene dos partes: una función (también llamada <em>callback</em>) y un arreglo de dependencias; y ejecuta el <em>callback</em> cuando cualquier elemento del arreglo de dependencias es actualizado.</p> <p>En este caso, cada vez que el estado <code class="language-plaintext highlighter-rouge">userConfiguration</code> se actualice, lo grabaremos en el <code class="language-plaintext highlighter-rouge">localStorage</code>:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialUserConfiguration</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">));</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialUserConfiguration</span><span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Recuerda usar `JSON.stringify` para convertir un objeto en cadena con formato JSON.</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">userConfiguration</span><span class="p">));</span> <span class="p">},</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">]);</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Ahora que ya tenemos la lógica lista podemos trabajar en crear nuestro propio <em>hook</em>, y para hacer eso vamos a mover esta lógica a una nueva función:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="c1">// La convención es que todos los hooks deben empezar con `use`.</span> <span class="c1">// Le pasamos un `defaultValue` como valor por defecto, en caso la entrada no exista en `localStorage`.</span> <span class="kd">function</span> <span class="nx">useUserConfiguration</span><span class="p">(</span><span class="nx">defaultValue</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialUserConfiguration</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">))</span> <span class="o">||</span> <span class="nx">defaultValue</span><span class="p">;</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialUserConfiguration</span><span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Recuerda usar `JSON.stringify` para convertir un objeto en cadena con formato JSON.</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">userConfiguration</span><span class="p">));</span> <span class="p">},</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">]);</span> <span class="k">return</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">];</span> <span class="p">}</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useUserConfiguration</span><span class="p">({});</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>De esta forma hemos encapsulado parte de la lógica del componente dentro de un <em>hook</em> e incluso aprovechamos en agregar soporte para valores por defecto. Pero como eso no es suficiente, vamos a hacerlo un poco más genérico y reutilizable:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">UserConfigurationForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./UserConfigurationForm</span><span class="dl">'</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">useLocalStorageState</span><span class="p">(</span><span class="nx">localStorageKey</span><span class="p">,</span> <span class="nx">defaultValue</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialValue</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="nx">localStorageKey</span><span class="p">))</span> <span class="o">||</span> <span class="nx">defaultValue</span><span class="p">;</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">setValue</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialValue</span><span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="nx">localStorageKey</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">value</span><span class="p">));</span> <span class="p">},</span> <span class="p">[</span><span class="nx">value</span><span class="p">]);</span> <span class="k">return</span> <span class="p">[</span><span class="nx">value</span><span class="p">,</span> <span class="nx">setValue</span><span class="p">];</span> <span class="p">}</span> <span class="kd">function</span> <span class="nx">UserConfigurationDashboard</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userConfiguration</span><span class="p">,</span> <span class="nx">setUserConfiguration</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useLocalStorageState</span><span class="p">(</span><span class="dl">'</span><span class="s1">userConfiguration</span><span class="dl">'</span><span class="p">,</span> <span class="p">{});</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nc">UserConfigurationForm</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">setUserConfiguration</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <hr /> <p>Con este <em>hook</em> propio hemos aprendido a trabajar con la API de Web Storage dentro de React. Si bien el ejemplo usa <code class="language-plaintext highlighter-rouge">localStorage</code>, el mismo código puede usarse con <code class="language-plaintext highlighter-rouge">sessionStorage</code>; e incluso se puede extender <code class="language-plaintext highlighter-rouge">useLocalStorageState</code> para poder elegir entre una y otra opción para persistir nuestros estados.</p>Podemos usar localStorage y sessionStorage para guardar estados de React que persistan luego de recargar una página.Pasando datos síncronamente desde Rails hacia React2021-02-23T00:30:00+00:002021-02-23T00:30:00+00:00https://hpneo.dev/2021/02/23/integrando-rails-con-react<p><img src="/assets/images/domstate.png" alt="Pasando datos síncronamente desde Rails hacia React" /></p> <p>Una de las cosas que más me gustan de Webpacker es lo fácil es que es integrar React en una aplicación Rails. Si bien hay otras formas de tener una aplicación de React, como CRA, manejar el frontend y el backend en una sola aplicación sigue siendo el enfoque con el que más me siento cómodo.</p> <p>Una de las ventajas de tener React integrado con Rails es que podemos enviar datos en la carga inicial de la página sin necesidad de hacer peticiones asíncronas. De esta forma podemos, por ejemplo, enviar datos sobre el usuario actual, una lista de categorías, configuración inicial, etc.</p> <p>Para lograr esto, vamos a necesitar trabajar tanto por el lado de Rails como por el lado de React; y para eso usaremos el DOM y JSON.</p> <h2 id="renderizando-datos-para-react-desde-rails">Renderizando datos para React desde Rails</h2> <p>Al estar en el lado del cliente, podemos usar JavaScript para leer el HTML que devuelve Rails, que luego podemos incluir en cualquier componente. Y para seguir aprovechando las ventajas del lenguaje, usaremos JSON para pasar datos de un ambiente (backend con Rails) a otro (frontend con React).</p> <p>Para conseguir esto, vamos a crear una etiqueta <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> dentro de una vista de Rails:</p> <div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"application/json"</span> <span class="na">id=</span><span class="s">"root-initial-state-data"</span><span class="nt">&gt;</span> <span class="nt">&lt;/script&gt;</span> <span class="c">&lt;!-- Nuestro nodo raíz, donde React montará la aplicación que hemos construido. --&gt;</span> <span class="nt">&lt;main</span> <span class="na">id=</span><span class="s">"root"</span><span class="nt">&gt;&lt;/main&gt;</span> </code></pre></div></div> <p>Este <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> es diferente al que escribimos usualmente, por dos motivos:</p> <ul> <li>Su atributo <code class="language-plaintext highlighter-rouge">type</code> es <code class="language-plaintext highlighter-rouge">application/json</code>, en vez de <code class="language-plaintext highlighter-rouge">text/javascript</code> (o simplemente no lleva).</li> <li>Tiene un atributo <code class="language-plaintext highlighter-rouge">id</code>.</li> </ul> <p>Esta forma de utilizar <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> nos permite incluir código JSON dentro de una página HTML de tal forma que: a) no es visible para el usuario final, y b) puede ser leído por nuestro código en el frontend, o por otras máquinas.</p> <p>Una vez que tenemos esta etiqueta <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> dentro de una vista, podemos incluir código JSON usando algunos métodos de Ruby y Rails:</p> <div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"application/json"</span> <span class="na">id=</span><span class="s">"root-initial-state-data"</span><span class="nt">&gt;</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">currentUser</span><span class="dl">"</span><span class="p">:</span> <span class="cp">&lt;%=</span> <span class="vi">@current_user</span><span class="p">.</span><span class="nf">to_json</span><span class="p">.</span><span class="nf">html_safe</span> <span class="cp">%&gt;</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span> </code></pre></div></div> <p>En este caso, tenemos una propiedad <code class="language-plaintext highlighter-rouge">currentUser</code>, cuyo valor es una variable <code class="language-plaintext highlighter-rouge">@current_user</code> que viene de un <em>controller</em> de Rails. Para poder convertir esta variable en JSON, usamos el método <code class="language-plaintext highlighter-rouge">to_json</code>, y <code class="language-plaintext highlighter-rouge">html_safe</code> para asegurarnos que el valor JSON devuelto pueda ser insertado dentro de HTML sin problemas.</p> <blockquote> <p><strong>Nota:</strong> Si quieres aprender sobre otros usos de esta técnica, puedes leer sobre <a href="https://json-ld.org/">JSON+LD</a>.</p> </blockquote> <p>Ahora que ya tenemos los datos disponibles para ser leídos por React, vamos a ver cómo leerlos desde un componente.</p> <h2 id="leyendo-datos-del-dom-con-react">Leyendo datos del DOM con React</h2> <p>Como mencioné líneas arriba, React tiene acceso al DOM, por lo que leer el HTMl renderizado por Rails es relativamente sencillo.</p> <p>Lo primero que debemos hacer es obtener acceso al elemento que queremos leer, para eso usaremos <code class="language-plaintext highlighter-rouge">getElementById</code>:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root-initial-state-data</span><span class="dl">'</span><span class="p">);</span> </code></pre></div></div> <p>Para poder leer el contenido de <code class="language-plaintext highlighter-rouge">initialDataElement</code> usamos la propiedad <code class="language-plaintext highlighter-rouge">textContent</code>, lo que nos devuelve una cadena con el contenido de la etiqueta:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root-initial-state-data</span><span class="dl">'</span><span class="p">);</span> <span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">;</span> <span class="c1">// '{ "currentUser": {...} }'</span> </code></pre></div></div> <p>Llegado a este punto, solo tenemos una cadena con el contenido, y para poder trabajar con ese contenido como un objeto en JavaScript necesitamos convertirlo usando <code class="language-plaintext highlighter-rouge">JSON.parse</code>:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root-initial-state-data</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">initialData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">);</span> <span class="nx">initialData</span><span class="p">.</span><span class="nx">currentUser</span> <span class="c1">// { id: 1, username: ... }</span> </code></pre></div></div> <p>Podemos agrupar esta lógica en una función, y quedaría así:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getDOMState</span><span class="p">(</span><span class="nx">elementID</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">elementID</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">initialData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">);</span> <span class="k">return</span> <span class="nx">initialData</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Hasta aquí, solo hemos usado JavaScript para obtener los datos que han sido renderizados por Rails, pero nos falta usarlos en React. Para lograr eso podemos llamar a la función <code class="language-plaintext highlighter-rouge">getDOMState</code> dentro del componente, como una variable más:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getDOMState</span><span class="p">(</span><span class="nx">elementID</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">elementID</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">initialData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">);</span> <span class="k">return</span> <span class="nx">initialData</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">initialData</span> <span class="o">=</span> <span class="nx">getDOMState</span><span class="p">(</span><span class="dl">'</span><span class="s1">root-initial-state-data</span><span class="dl">'</span><span class="p">);</span> <span class="kd">function</span> <span class="nx">Home</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">currentUser</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">initialData</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="nx">currentUser</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="o">&lt;</span><span class="nx">Dashboard</span> <span class="o">/&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="o">&lt;</span><span class="nx">SignIn</span> <span class="o">/&gt;</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Si queremos ir un paso más allá, podemos hacer algunas mejoras a <code class="language-plaintext highlighter-rouge">getDOMState</code>, para asegurarnos que <code class="language-plaintext highlighter-rouge">JSON.parse</code> no falle si el elemento del DOM no existe o está vacío. Para eso, haremos los siguientes cambios:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getDOMState</span><span class="p">(</span><span class="nx">elementID</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialDataElement</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">elementID</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">initialDataElement</span> <span class="o">&amp;&amp;</span> <span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">initialData</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">initialDataElement</span><span class="p">.</span><span class="nx">textContent</span><span class="p">);</span> <span class="k">return</span> <span class="nx">initialData</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Con esta condición aprovechamos el <em>type coercion</em> de JavaScript donde un objeto del DOM (<code class="language-plaintext highlighter-rouge">initialDataElement</code>) y una cadena que no está vacía (<code class="language-plaintext highlighter-rouge">initialDataElement.textContent</code>) son considerados como <code class="language-plaintext highlighter-rouge">true</code>.</p> <hr /> <p>Esta técnica nos permite aprovechar las funcionalidades que nos ofrece un stack donde el backend y el frontend coexisten en una sola aplicación. Expandiendo un poco más esta idea podemos tratar al DOM como un <em>state</em> de React, que no solo pueda ser leído si no re-escrito.</p>En aplicaciones Rails+React es posible mandar datos en la carga inicial de la página utilizando vistas de Rails y el DOM.Creando bookmarks de CodeMirror con Preact2020-07-25T21:30:00+00:002020-07-25T21:30:00+00:00https://hpneo.dev/2020/07/25/codemirror-preact<p><img src="/assets/images/codemirror-preact.png" alt="Creando bookmarks de CodeMirror con Preact" /></p> <p><a href="https://codemirror.net/">CodeMirror</a> es una de mis bibliotecas favoritas y la que más utilizo cuando se trata de implementar un editor de texto plano. Una de las funcionalidades que estoy explorando ahora es la de los llamados <em>bookmarks</em>.</p> <p>Un <em>bookmark</em> es una marca dentro del editor que está asociado a una posición específica (línea y columna) y puede contener un nodo del DOM. Los <em>bookmarks</em> son útiles para extender la funcionalidad del editor y proveer acciones dentro de un contexto específico (por ejemplo <strong><a href="https://github.com/easylogic/codemirror-colorpicker"><code class="language-plaintext highlighter-rouge">codemirror-colorpicker</code></a></strong> agrega un <em>color picker</em> en un <em>bookmark</em> cuando encuentra un color dentro del texto).</p> <p>Lo interesante es que, al contener un nodo del DOM, el <em>bookmark</em> puede recibir eventos, con lo que podemos usar JavaScript para <a href="https://cevichejs.com/3-dom-cssom.html#eventos">agregarle eventos</a>. O, podríamos usar una biblioteca que nos permita simplificar ese trabajo, e incluso reutilizar otras partes de nuestro código.</p> <p>Aquí es donde entra <a href="https://preactjs.com/">Preact</a>. Con solo <a href="https://bundlephobia.com/[email protected]">4kB de tamaño</a> es ideal para crear componentes como React, pero más ligeros.</p> <p>Digamos que para el caso que nos ocupa, vamos a hacer un editor de texto que, cuando encuentre un texto <strong><code class="language-plaintext highlighter-rouge">date:</code></strong>, agregue un botón al costado derecho que permita incrustar la fecha y hora local.</p> <h2 id="creando-el-editor-con-codemirror-y-preact">Creando el editor con CodeMirror (y Preact)</h2> <p>Primero debemos crear un editor de texto con CodeMirror. En mi caso, voy a utilizar Preact para crear el <em>bookmark</em> y el editor.</p> <p>Para esto creo un <code class="language-plaintext highlighter-rouge">&lt;div /&gt;</code> con un <code class="language-plaintext highlighter-rouge">&lt;textarea /&gt;</code>. El <code class="language-plaintext highlighter-rouge">&lt;textarea /&gt;</code> debe tener un <code class="language-plaintext highlighter-rouge">ref</code>, que luego voy a usar dentro de un hook para crear la instancia de <code class="language-plaintext highlighter-rouge">CodeMirror</code>.</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useRef</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">preact/hooks</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">CodeMirror</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">codemirror/lib/codemirror</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="dl">"</span><span class="s2">codemirror/lib/codemirror.css</span><span class="dl">"</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> <span class="na">lineNumbers</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Editor</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">ref</span> <span class="o">=</span> <span class="nx">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">CodeMirror</span><span class="p">.</span><span class="nx">fromTextArea</span><span class="p">(</span><span class="nx">ref</span><span class="p">.</span><span class="nx">current</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span> <span class="p">},</span> <span class="p">[]);</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="p">=</span><span class="s">"editor"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">textarea</span> <span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="nx">ref</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Este primer paso es bastante directo, y si no utilizas Preact (o React), también puedes usarlo con cualquier otra biblioteca, o incluso con JavaScript puro.</p> <h2 id="creando-el-component-del-bookmark-con-preact">Creando el component del <em>bookmark</em> con Preact</h2> <p>El siguiente paso es crear el componente que irá dentro del <em>bookmark</em>. En este caso, el componente es un simple botón que, al hacer click, llamará al prop <code class="language-plaintext highlighter-rouge">onClick</code>, pasándole la fecha y hora actual como una cadena.</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">NowButton</span><span class="p">({</span> <span class="nx">onClick</span> <span class="p">})</span> <span class="p">{</span> <span class="kd">function</span> <span class="nx">handleOnClick</span><span class="p">()</span> <span class="p">{</span> <span class="nx">onClick</span><span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toString</span><span class="p">());</span> <span class="p">}</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">handleOnClick</span><span class="si">}</span><span class="p">&gt;</span>Now<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;;</span> <span class="p">}</span> </code></pre></div></div> <h2 id="creando-el-bookmark">Creando el <em>bookmark</em></h2> <p>Este paso es el más complejo, porque requiere conocer un poco del API de CodeMirror.</p> <p>Lo primero que tenemos que hacer es crear la función <code class="language-plaintext highlighter-rouge">createBoomarks</code>. Esta función va a usar el API de CodeMirror para crear los bookmarks en base a nuestra condición inicial (que haya una línea que empiece con <strong><code class="language-plaintext highlighter-rouge">date:</code></strong>).</p> <p>Para poder hacer esto debemos permitir que <code class="language-plaintext highlighter-rouge">createBoomarks</code> reciba como argumento, la instancia de CodeMirror que corresponde al editor.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span> </code></pre></div></div> <p>Luego, para poder aplicar los bookmarks sin bloquear el editor, lo que evitaría que el usuario pueda escribir, usaremos el método <code class="language-plaintext highlighter-rouge">operation</code> de CodeMirror.</p> <p>Este método permite pasarle un <em>callback</em> que puede contener varios cambios y operaciones en el editor, pero que se aplicará como un solo cambio dentro de CodeMirror, mejorando la performance de nuestras operaciones.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">operation</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{});</span> <span class="p">}</span> </code></pre></div></div> <p>El siguiente paso es iterar por cada línea del editor para agregar nuestro <em>bookmark</em>, si cumple con la condición:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">operation</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">eachLine</span><span class="p">(</span><span class="nx">lineHandle</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Si no cumple la condición, ignoramos el resto del callback.</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">lineHandle</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">date:</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// Obtenemos el número de línea y caracter para saber dónde posicionar el bookmark.</span> <span class="kd">const</span> <span class="nx">line</span> <span class="o">=</span> <span class="nx">lineHandle</span><span class="p">.</span><span class="nx">lineNo</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">ch</span> <span class="o">=</span> <span class="nx">lineHandle</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">:</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// Creamos el "widget", que será el contenedor de nuestro componente.</span> <span class="kd">const</span> <span class="nx">widget</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">);</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">inline</span><span class="dl">"</span><span class="p">;</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">verticalAlign</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">middle</span><span class="dl">"</span><span class="p">;</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">14px</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// Creamos el bookmark con cm.doc.setBookmark, pasándole la línea y caracter, y el widget.</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">line</span><span class="p">]</span> <span class="o">=</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">doc</span><span class="p">.</span><span class="nx">setBookmark</span><span class="p">(</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">widget</span><span class="p">,</span> <span class="na">handleMouseEvents</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">);</span> <span class="p">});</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div></div> <p>Aquí hay algo importante a considerar. Dado que el texto de nuestro editor puede cambiar varias veces (porque el usuario edita el texto), es necesario guardar una referencia de nuestros <em>bookmarks</em> en caso necesitemos eliminarlos antes del siguiente cambio.</p> <p>Es por eso que CodeMirror nos da un objeto <code class="language-plaintext highlighter-rouge">cm.state</code>, donde podemos definir estados para el editor. En nuestro caso, usaremos <code class="language-plaintext highlighter-rouge">state</code> para crear un objeto <code class="language-plaintext highlighter-rouge">bookmarks</code> donde las llaves serán los números de línea, y los valores serán los <em>bookmarks</em>.</p> <blockquote> <p><strong>Nota:</strong> En nuestro caso, las llaves del objeto <code class="language-plaintext highlighter-rouge">cm.state.bookmarks</code> son los números de línea porque se asume que nuestros <em>bookmarks</em> solo aparecen una vez por línea. Pero en otros casos podría ser una combinación de línea y caracter u otro.</p> </blockquote> <p>Hasta aquí hemos creado una función que crea un <em>bookmark</em> con un elemento <code class="language-plaintext highlighter-rouge">&lt;div /&gt;</code> que no hace nada. Ahora toca agregarle el componente:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">operation</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">eachLine</span><span class="p">(</span><span class="nx">lineHandle</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// if (!lineHandle.text.startsWith("date:")) {</span> <span class="c1">// return;</span> <span class="c1">// }</span> <span class="c1">// const line = lineHandle.lineNo();</span> <span class="c1">// const ch = lineHandle.text.indexOf(":");</span> <span class="c1">// const widget = document.createElement("div");</span> <span class="c1">// widget.style.display = "inline";</span> <span class="c1">// widget.style.verticalAlign = "middle";</span> <span class="c1">// widget.style.height = "14px";</span> <span class="c1">// Aquí definimos el valor del prop `onClick`. Recordemos que esta función recibe la fecha y hora local como una cadena.</span> <span class="kd">function</span> <span class="nx">setDate</span><span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// `cm.doc.replaceRange` va a reemplazar cualquier texto que exista luego de "date:" con el valor que reciba la función `setDate`.</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">doc</span><span class="p">.</span><span class="nx">replaceRange</span><span class="p">(</span> <span class="nx">date</span><span class="p">,</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="nx">date</span><span class="p">.</span><span class="nx">length</span> <span class="p">}</span> <span class="p">);</span> <span class="p">}</span> <span class="c1">// Aquí es donde creamos el componente que definimos algunos párrafos arriba.</span> <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="p">&lt;</span><span class="nc">NowButton</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">setDate</span><span class="si">}</span> <span class="p">/&gt;;</span> <span class="c1">// cm.state.bookmarks[line] = cm.doc.setBookmark(</span> <span class="c1">// { line, ch: ch + 1 },</span> <span class="c1">// { widget, handleMouseEvents: true }</span> <span class="c1">// );</span> <span class="c1">// Por último, montamos y renderizamos nuestro componente usando la función `render` de Preact.</span> <span class="nx">render</span><span class="p">(</span><span class="nx">button</span><span class="p">,</span> <span class="nx">widget</span><span class="p">);</span> <span class="p">});</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div></div> <p>Por último, debemos definir un <em>init hook</em>. Un <em>init hook</em> en CodeMirror es una función que se va a llamar al crear un editor. Esto es útil si sabemos que el editor tendrá un valor inicial y queremos crear nuestros <em>bookmarks</em> inmediatamente después de crear el editor. Pero no solo eso, también es aquí donde guardaremos un estado con los <em>bookmarks</em>.</p> <p>También vamos a requerir asociarnos a un evento <code class="language-plaintext highlighter-rouge">change</code>, que permitirá ejecutar nuestra función <code class="language-plaintext highlighter-rouge">createBoomarks</code> (la que va a crear los <em>bookmarks</em>) cada vez que haya un cambio en el editor.</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">CodeMirror</span><span class="p">.</span><span class="nx">defineInitHook</span><span class="p">(</span><span class="nx">cm</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span> <span class="o">=</span> <span class="p">{};</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">);</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">change</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">));</span> <span class="p">});</span> </code></pre></div></div> <p>Líneas arriba había mencionado que podemos requerir borrar todos los <em>bookmarks</em> antes del siguiente cambio. Esto es posible agregando el siguiente código dentro del <em>callback</em> de <code class="language-plaintext highlighter-rouge">cm.operation</code>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">operation</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">lineNumber</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Desmontamos el componente al llamar a la función `render` de preact con un `null`.</span> <span class="kd">const</span> <span class="nx">widget</span> <span class="o">=</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">].</span><span class="nx">replacedWith</span><span class="p">;</span> <span class="nx">render</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">widget</span><span class="p">);</span> <span class="c1">// Llamamos al método `clear` de los bookmarks y eliminamos `cm.state.bookmarks[lineNumber]` de la memoria.</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">].</span><span class="nx">clear</span><span class="p">();</span> <span class="k">delete</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">];</span> <span class="p">});</span> <span class="c1">// cm.eachLine(lineHandle =&gt; {</span> <span class="c1">// ...</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div></div> <hr /> <p><strong>¿Por qué es importante desmontar los componentes antes de llamar a <code class="language-plaintext highlighter-rouge">clear()</code>?</strong> Por dos motivos:</p> <ol> <li>Si solo llamamos a <code class="language-plaintext highlighter-rouge">clear()</code>, CodeMirror eliminará el nodo del DOM (el famoso <code class="language-plaintext highlighter-rouge">widget</code>), pero Preact seguirá manteniendo una instancia del componente para ese nodo y no liberará memoria (los ya conocidos <em>memory leaks</em>).</li> <li>Al desmontar los componentes con <code class="language-plaintext highlighter-rouge">render(null, widget)</code> también logramos que los componentes se desuscriban de sus propios <em>side effects</em>. Si no desmontamos el componente, un (hipotético) <code class="language-plaintext highlighter-rouge">setInterval</code> seguirá llamándose incluso luego de haber eliminado el <em>bookmark</em>, o un <code class="language-plaintext highlighter-rouge">fetch</code> podría seguir en ejecución tratando de finalizar un request aunque el editor esté completamente vacío.</li> </ol> <hr /> <p>El código completo quedaría así:</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">preact</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">CodeMirror</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">codemirror/lib/codemirror</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="nx">NowButton</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./nowButton</span><span class="dl">"</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">operation</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">lineNumber</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">widget</span> <span class="o">=</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">].</span><span class="nx">replacedWith</span><span class="p">;</span> <span class="nx">render</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">widget</span><span class="p">);</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">].</span><span class="nx">clear</span><span class="p">();</span> <span class="k">delete</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">lineNumber</span><span class="p">];</span> <span class="p">});</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">eachLine</span><span class="p">(</span><span class="nx">lineHandle</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">lineHandle</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">date:</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">line</span> <span class="o">=</span> <span class="nx">lineHandle</span><span class="p">.</span><span class="nx">lineNo</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">ch</span> <span class="o">=</span> <span class="nx">lineHandle</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">:</span><span class="dl">"</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">widget</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">);</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">inline</span><span class="dl">"</span><span class="p">;</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">verticalAlign</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">middle</span><span class="dl">"</span><span class="p">;</span> <span class="nx">widget</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">14px</span><span class="dl">"</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">setDate</span><span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">doc</span><span class="p">.</span><span class="nx">replaceRange</span><span class="p">(</span> <span class="nx">date</span><span class="p">,</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="nx">date</span><span class="p">.</span><span class="nx">length</span> <span class="p">}</span> <span class="p">);</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="p">&lt;</span><span class="nc">NowButton</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">setDate</span><span class="si">}</span> <span class="p">/&gt;;</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">[</span><span class="nx">line</span><span class="p">]</span> <span class="o">=</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">doc</span><span class="p">.</span><span class="nx">setBookmark</span><span class="p">(</span> <span class="p">{</span> <span class="nx">line</span><span class="p">,</span> <span class="na">ch</span><span class="p">:</span> <span class="nx">ch</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">widget</span><span class="p">,</span> <span class="na">handleMouseEvents</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">render</span><span class="p">(</span><span class="nx">button</span><span class="p">,</span> <span class="nx">widget</span><span class="p">);</span> <span class="p">});</span> <span class="p">});</span> <span class="p">}</span> <span class="nx">CodeMirror</span><span class="p">.</span><span class="nx">defineInitHook</span><span class="p">(</span><span class="nx">cm</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span> <span class="o">=</span> <span class="p">{};</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">);</span> <span class="nx">cm</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">change</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">createBoomarks</span><span class="p">(</span><span class="nx">cm</span><span class="p">));</span> <span class="p">});</span> </code></pre></div></div> <p>Y aquí un ejemplo en vivo:</p> <iframe src="https://codesandbox.io/embed/magical-dawn-3hw6z?fontsize=14&amp;hidenavigation=1&amp;theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="magical-dawn-3hw6z" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe> <hr /> <p>Esto es solo una exploración de lo que podría hacerse con los <em>bookmarks</em> de CodeMirror y una biblioteca de componentes como Preact. Algo que creo que vale la pena seguir revisando es ver cómo lograr mantener un estado entre <em>unmounts</em> del mismo componente, o en su defecto hacer un <em>diff</em> inteligente para borrar solo algunos <em>bookmarks</em> y no todos en cada operación.</p>Con CodeMirror puedes crear marcas dentro del texto en el editor. Con Preact, puedes agregar interactividad a bajo costo.Estructuras de datos para React2020-07-15T01:30:00+00:002020-07-15T01:30:00+00:00https://hpneo.dev/2020/07/15/estructuras-datos-react<p><img src="/assets/images/data-structures.png" alt="Estructuras de datos para React" /></p> <p>React <a href="https://es.reactjs.org/docs/update.html#overview"><em>recomienda</em></a> <a href="https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data">evitar mutar los datos</a> de una aplicación. Es por eso que técnicas como usar el <em>spread operator</em> son bastante utilizadas para manejar datos en React, pero conforme pasamos del código de ejemplo, nos podemos dar cuenta de lo complicado que puede llegar a ser manipular la data en cada vista.</p> <p>Por ejemplo, tenemos el clásico <em>to-do list</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">TodoList</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">items</span><span class="p">,</span> <span class="nx">setItems</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">([]);</span> <span class="k">return</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">ul</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=&gt;</span> <span class="o">&lt;</span><span class="nx">li</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">item</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">item</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/li&gt;</span><span class="se">)</span><span class="err">} </span> <span class="o">&lt;</span><span class="sr">/ul</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>En este ejemplo, los elementos de esta lista están guardados en un array, o arreglo, y para poder agregar un elemento a <code class="language-plaintext highlighter-rouge">items</code>, podemos hacer:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">items</span><span class="p">,</span> <span class="nx">setItems</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">([]);</span> <span class="kd">function</span> <span class="nx">addItem</span><span class="p">(</span><span class="nx">newItem</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">updatedItems</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">items</span><span class="p">,</span> <span class="nx">newItem</span><span class="p">];</span> <span class="k">return</span> <span class="nx">setItems</span><span class="p">(</span><span class="nx">updatedItems</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Y si queremos eliminar un elemento nos topamos con un problema. <strong>¿Cómo se hace?</strong></p> <h2 id="eliminar-un-elemento-de-una-colección">Eliminar un elemento de una colección:</h2> <p>Para eliminar un elemento de una colección tenemos 2 opciones:</p> <h3 id="usando-splice">Usando <code class="language-plaintext highlighter-rouge">splice</code></h3> <p><a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/splice"><code class="language-plaintext highlighter-rouge">[].splice</code></a> permite eliminar elementos de un array en base a su índice.</p> <p>El primer parámetro de <code class="language-plaintext highlighter-rouge">splice</code> es el índice o posición donde se empieza a contar los elementos a eliminar, mientras que el segundo parámetro es el número de elementos a eliminar:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">removeItem</span><span class="p">(</span><span class="nx">indexToRemove</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">updatedItems</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">items</span><span class="p">];</span> <span class="nx">updatedItems</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">indexToRemove</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="k">return</span> <span class="nx">setItems</span><span class="p">(</span><span class="nx">updatedItems</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <h3 id="usando-filter">Usando <code class="language-plaintext highlighter-rouge">filter</code></h3> <p><a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/filter"><code class="language-plaintext highlighter-rouge">[].filter</code></a> permite filtrar los elementos de un array en base a una condición.</p> <p>En este caso, la condición es <em>“todos los elementos excepto el que tenga el índice a eliminar”</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">removeItem</span><span class="p">(</span><span class="nx">indexToRemove</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">updatedItems</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">_item</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">index</span> <span class="o">!==</span> <span class="nx">indexToRemove</span><span class="p">);</span> <span class="k">return</span> <span class="nx">setItems</span><span class="p">(</span><span class="nx">updatedItems</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <h2 id="reemplazar-un-elemento-de-la-colección">Reemplazar un elemento de la colección</h2> <p>Para reemplazar un elemento dentro de una colección también tenemos 2 opciones, muy similar a la forma como eliminamos elementos.</p> <h3 id="usando-splice-1">Usando <code class="language-plaintext highlighter-rouge">splice</code></h3> <p><code class="language-plaintext highlighter-rouge">splice</code> tiene un tercer parámetro que puede ser uno o más elementos que se van a insertar en la posición definida por el primer parámetro (en nuestro ejemplo, <code class="language-plaintext highlighter-rouge">indexToReplace</code>).</p> <p>En este caso, lo que le decimos a <code class="language-plaintext highlighter-rouge">splice</code> es: <em>“elimina <strong>1</strong> elemento en la posición <code class="language-plaintext highlighter-rouge">indexToReplace</code>, y además agrega <code class="language-plaintext highlighter-rouge">newItem</code> en la posición <code class="language-plaintext highlighter-rouge">indexToReplace</code>“</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">replaceItem</span><span class="p">(</span><span class="nx">indexToReplace</span><span class="p">,</span> <span class="nx">newItem</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">updatedItems</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">items</span><span class="p">];</span> <span class="nx">updatedItems</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">indexToReplace</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">newItem</span><span class="p">);</span> <span class="k">return</span> <span class="nx">setItems</span><span class="p">(</span><span class="nx">updatedItems</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <h3 id="usando-map">Usando <code class="language-plaintext highlighter-rouge">map</code></h3> <p><a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/map"><code class="language-plaintext highlighter-rouge">[].map</code></a> toma un array, itera por todos sus elementos, aplicando una función sobre cada uno de los elementos, y guarda el resultado en un nuevo array.</p> <p>En este caso, lo que le decimos a <code class="language-plaintext highlighter-rouge">map</code> es: <em>“Devuelve los mismos elementos, a menos que el índice de la iteración actual sea el índice a reemplazar”</em>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">replaceItem</span><span class="p">(</span><span class="nx">indexToReplace</span><span class="p">,</span> <span class="nx">newItem</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">updatedItems</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">item</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">index</span> <span class="o">===</span> <span class="nx">indexToReplace</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">newItem</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">item</span><span class="p">;</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">setItems</span><span class="p">(</span><span class="nx">updatedItems</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <h2 id="reemplazar-la-propiedad-de-un-objeto">Reemplazar la propiedad de un objeto</h2> <p>Ahora supongamos que los elementos del <em>to-do list</em> son objetos que no solo guardan el texto de cada tarea, si no otras propiedades, como fecha de creación, si está marcado como completado o no, etc.</p> <p>Para poder cambiar las propiedades de un elemento de esa lista, vamos a tener que manipular un nuevo tipo de estructura de datos, que en este caso es un objeto.</p> <p>Asumiendo que cada elemento de la lista tiene esta estructura:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nl">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Aprendiendo sobre estructuras de datos</span><span class="dl">"</span><span class="p">,</span> <span class="nx">isCompleted</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">createdAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2020-07-15T00:42:03.338Z</span><span class="dl">"</span> <span class="p">}</span> </code></pre></div></div> <p>Podemos modificar una propiedad de un objeto usando el <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator"><em>spread operator</em></a>, que nos permite crear una copia del objeto que queremos modificar, sin cambiar el objeto anterior:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">markAsCompleted</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span> <span class="nx">isCompleted</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">newItem</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">item</span> <span class="p">};</span> <span class="nx">newItem</span><span class="p">.</span><span class="nx">isCompleted</span> <span class="o">=</span> <span class="nx">isCompleted</span><span class="p">;</span> <span class="k">return</span> <span class="nx">newItem</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <h2 id="relacionar-dos-colecciones">Relacionar dos colecciones</h2> <p>Vayamos un paso más allá. Ahora la data viene de una API y tienes 2 recursos: <code class="language-plaintext highlighter-rouge">todoLists</code> y <code class="language-plaintext highlighter-rouge">todoItems</code>.</p> <p>Los <code class="language-plaintext highlighter-rouge">todoLists</code> tienen la siguiente estructura:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">todoLists</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Aprendiendo React</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Aprendiendo Ruby</span><span class="dl">"</span> <span class="p">}</span> <span class="p">]</span> </code></pre></div></div> <p>Y los <code class="language-plaintext highlighter-rouge">todoItems</code> tienen la siguiente estructura:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">todoItems</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">todoListId</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Aprendiendo sobre estructuras de datos</span><span class="dl">"</span><span class="p">,</span> <span class="na">isCompleted</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2020-07-15T00:42:03.338Z</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">todoListId</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">React hooks</span><span class="dl">"</span><span class="p">,</span> <span class="na">isCompleted</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2020-07-15T00:45:03.338Z</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">todoListId</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">React Context</span><span class="dl">"</span><span class="p">,</span> <span class="na">isCompleted</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2020-07-15T00:46:03.338Z</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="na">todoListId</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Metaprogramación en Ruby</span><span class="dl">"</span><span class="p">,</span> <span class="na">isCompleted</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2020-07-15T00:48:03.338Z</span><span class="dl">"</span> <span class="p">}</span> <span class="p">]</span> </code></pre></div></div> <p>En este caso, tenemos tres elementos para la lista con ID 1, y un elemento para el ID 2. <strong>¿Cómo podríamos hacer para poder mostrar ambos recursos en una vista?</strong></p> <p>Supongamos que queremos mostrar algo así:</p> <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">*</span> Aprendiendo React <span class="p"> -</span> [ ] Aprendiendo sobre estructuras de datos <span class="p"> -</span> [ ] React hooks <span class="p"> -</span> [ ] React Context <span class="p">*</span> Aprendiendo Ruby <span class="p"> -</span> [ ] Metaprogramación en Ruby </code></pre></div></div> <p>Para lograr esto podemos usar <a href="https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Array/filter"><code class="language-plaintext highlighter-rouge">[].filter</code></a>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">todoListsWithItems</span> <span class="o">=</span> <span class="nx">todoLists</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">todoList</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">listWithItems</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">todoList</span> <span class="p">};</span> <span class="nx">listWithItems</span><span class="p">.</span><span class="nx">todoItems</span> <span class="o">=</span> <span class="nx">todoItems</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">todoItem</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">todoItem</span><span class="p">.</span><span class="nx">todoListId</span> <span class="o">===</span> <span class="nx">todoList</span><span class="p">.</span><span class="nx">id</span> <span class="p">);</span> <span class="k">return</span> <span class="nx">listWithItems</span><span class="p">;</span> <span class="p">});</span> </code></pre></div></div> <p>De esta forma, podemos tener una estructura de datos mucho más fácil de usar dentro de un componente de React, como por ejemplo:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">TodoListsSummary</span><span class="p">()</span> <span class="p">{</span> <span class="c1">// const todoLists = [...];</span> <span class="c1">// const todoItems = [...];</span> <span class="c1">// const todoListsWithItems = todoLists.map((todoList) =&gt; {...;</span> <span class="k">return</span> <span class="nx">todoListsWithItems</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">listWithItems</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">article</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">listWithItems</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">h3</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">listWithItems</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/h3</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">ul</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">listWithItems</span><span class="p">.</span><span class="nx">todoItems</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">todoItem</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">li</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">todoItem</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">todoItem</span><span class="p">.</span><span class="nx">text</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/li</span><span class="err">&gt; </span> <span class="p">))}</span> <span class="o">&lt;</span><span class="sr">/ul</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/article</span><span class="err">&gt; </span> <span class="p">));</span> <span class="p">}</span> </code></pre></div></div> <hr /> <p>Hay varias bibliotecas que permiten manejar estructuras de datos de manera inmutable, como <a href="https://immutable-js.github.io/">Immutable</a> o <a href="https://immerjs.github.io/immer/">Immer</a>, pero es interesante conocer cómo podemos manejar este tipo de estructuras de manera relativamente sencilla sin depender de bibliotecas de terceros.</p>Manejar estructuras de datos complejas en React puede ser complicado, aquí algunos tips.Web scraping y análisis de datos con Ruby2020-07-12T18:30:00+00:002020-07-12T18:30:00+00:00https://hpneo.dev/2020/07/12/web-scraping-analisis-ruby<p><img src="/assets/images/m2-cli.png" alt="Web scraping y análisis de datos con Ruby" /></p> <blockquote> <p><strong>Nota:</strong> Lo utilizado en este post es estrictamente de uso personal.</p> </blockquote> <p>Buscar un lugar para vivir siempre es complicado, porque hay tantas variables a considerar antes de tomar una decisión. No solo es importante el precio, y la zona, si no también el número de habitaciones o incluso la cantidad de baños y el área construida.</p> <p>Me he mudado varias veces en los últimos años, y algo que trato de hacer antes de elegir un lugar es analizar todas las ofertas disponibles.</p> <p>Para esto, lo que hacía era buscar anuncios de lugares para vivir y volcar la información a mano en una hoja de cálculo, luego ir descartando los lugares más alejados, o con peor relación de m2/precio. Pero este es un proceso manual y tedioso que puede automatizarse. Y para automatizarlo me apoyo en 2 bibliotecas que construí en Ruby: <strong><a href="https://hpneo.dev/scrap_kit">ScrapKit</a></strong> y <strong><a href="https://hpneo.dev/active_worksheet">ActiveWorksheet</a></strong>.</p> <p><a href="https://hpneo.dev/scrap_kit">ScrapKit</a> permite hacer web scraping y mapear selectores del DOM a propiedades de un objeto plano, mientras que <a href="https://hpneo.dev/active_worksheet">ActiveWorksheet</a> lee un archivo CSV o XLS/XLSX y lo convierte a objetos de Ruby, donde cada fila es un registro y las columnas se vuelven propiedades de estos registros.</p> <h2 id="usando-scrapkit">Usando ScrapKit</h2> <p>Esta es la parte más <em>delicada</em> del proceso, ya que implica hacer web scraping, que no es más que automatizar la extracción de información de una página web. Algunos sitios web no permiten usar esta técnica, pero ya que es para uso personal, espero que no haya problemas.</p> <p>En mi caso, estoy usando <a href="http://nexoinmobiliario.pe/">Nexo Inmobiliario</a> para obtener la información de las ofertas inmobiliarias que hay actualmente.</p> <p>ScrapKit en su forma más básica me permite mapear selectores del DOM a atributos. Por ejemplo, puedo mapear el selector <code class="language-plaintext highlighter-rouge">.Project-header h1</code> al atributo <code class="language-plaintext highlighter-rouge">title</code>.</p> <p>ScrapKit tiene una característica más poderosa, que permite mapear estructuras complejas usando el atributo especial <code class="language-plaintext highlighter-rouge">selector</code>, el cual es una colección de selectores que se va internando en el DOM hasta que cumpla una condición establecida.</p> <p>Por ejemplo, en <code class="language-plaintext highlighter-rouge">[".Project-available-model", { ".name_tipology": "Departamento tipo A" }]</code>, ScrapKit busca dentro de <code class="language-plaintext highlighter-rouge">.Project-available-model</code> algún elemento que cumpla con el selector <code class="language-plaintext highlighter-rouge">.name_tipology</code> y que tenga el valor <code class="language-plaintext highlighter-rouge">Departamento tipo A</code>.</p> <p>Sabiendo todo esto, podemos crear una “receta” de ScrapKit de la siguiente forma:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">url</span> <span class="o">=</span> <span class="s2">"https://nexoinmobiliario.pe/proyecto/venta-de-departamento-123-en-lima"</span> <span class="n">type</span> <span class="o">=</span> <span class="s2">"Departamento tipo A"</span> <span class="c1"># Defino el mapeo de selectores a atributos a través de una "receta".</span> <span class="n">recipe</span> <span class="o">=</span> <span class="no">ScrapKit</span><span class="o">::</span><span class="no">Recipe</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span> <span class="ss">url: </span><span class="n">url</span><span class="p">,</span> <span class="ss">attributes: </span><span class="p">{</span> <span class="ss">title: </span><span class="s2">".Project-header h1"</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"#project_id"</span><span class="p">,</span> <span class="ss">stage: </span><span class="s2">".bx-data-project.box-st &gt; table &gt; tbody &gt; tr:nth-child(4) &gt; td:nth-child(2)"</span><span class="p">,</span> <span class="ss">due_date: </span><span class="s2">".bx-data-project.box-st &gt; table &gt; tbody &gt; tr:nth-child(5) &gt; td:nth-child(2)"</span><span class="p">,</span> <span class="ss">latitude: </span><span class="s2">"#latitude"</span><span class="p">,</span> <span class="ss">longitude: </span><span class="s2">"#longitude"</span><span class="p">,</span> <span class="ss">info: </span><span class="p">{</span> <span class="ss">selector: </span><span class="p">[</span><span class="s2">".Project-available-model"</span><span class="p">,</span> <span class="p">{</span> <span class="s2">".name_tipology"</span><span class="p">:</span> <span class="n">type</span> <span class="p">}],</span> <span class="ss">children_attributes: </span><span class="p">{</span> <span class="ss">tipology: </span><span class="s2">"span.name_tipology"</span><span class="p">,</span> <span class="ss">bedrooms: </span><span class="s2">"span.bedroom"</span><span class="p">,</span> <span class="ss">area: </span><span class="s2">"span.area"</span><span class="p">,</span> <span class="ss">price: </span><span class="s2">"span.price"</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">)</span> <span class="c1"># Obtengo el resultado ejecutando la receta.</span> <span class="n">output</span> <span class="o">=</span> <span class="n">recipe</span><span class="p">.</span><span class="nf">run</span> </code></pre></div></div> <p>Dado que es un proceso que toma tiempo, es recomendable hacerlo una vez y guardar estos datos en algún lado para procesarla después. Y aquí es donde entra <a href="https://hpneo.dev/active_worksheet">ActiveWorksheet</a>.</p> <h2 id="usando-activeworksheet">Usando ActiveWorksheet</h2> <p>ActiveWorksheet permite definir una clase en Ruby que se comporta de manera similar a un objeto de ActiveResource o ActiveRecord, con la diferencia que, mientras ActiveResource lee los datos desde un endpoint y ActiveRecord hace lo mismo desde una base de datos, ActiveWorksheet lo hace desde un archivo CSV, XLS o XLSX.</p> <p>Los datos obtenidos por ScrapKit fueron guardados en un archivo en la ruta <code class="language-plaintext highlighter-rouge">~/departamentos.csv</code>, que tiene este formato:</p> <pre><code class="language-csv">Date,ID,Project,Model,Bedrooms,Area,Price,Due Date,Stage,Latitude,Longitude 2020-07-12,1823,SARAY 2,TIPO K,3,70.24 m2,"S/ 347,568","15 de Diciembre, 2021",En construcción,-12.087613139816469,-77.07117197819613 </code></pre> <p>Con ActiveWorksheet creo una clase llamada <code class="language-plaintext highlighter-rouge">Project</code> que herede de <code class="language-plaintext highlighter-rouge">ActiveWorksheet::Base</code>, donde cada columna del CSV es automáticamente mapeado a un atributo:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"active_worksheet"</span> <span class="k">class</span> <span class="nc">Project</span> <span class="o">&lt;</span> <span class="no">ActiveWorksheet</span><span class="o">::</span><span class="no">Base</span> <span class="nb">self</span><span class="p">.</span><span class="nf">source</span> <span class="o">=</span> <span class="s2">"~/departamentos.csv"</span> <span class="c1"># Estos métodos son usados para obtener y calcular valores en base a lo que viene del archivo CSV.</span> <span class="k">def</span> <span class="nf">currency</span> <span class="nb">self</span><span class="p">.</span><span class="nf">price</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">).</span><span class="nf">first</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">price_as_number</span> <span class="nb">self</span><span class="p">.</span><span class="nf">price</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">).</span><span class="nf">last</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="s2">","</span><span class="p">,</span> <span class="s2">""</span><span class="p">).</span><span class="nf">to_f</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">area_as_number</span> <span class="nb">self</span><span class="p">.</span><span class="nf">area</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="s2">" m2"</span><span class="p">,</span> <span class="s2">""</span><span class="p">).</span><span class="nf">to_f</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">price_per_m2</span> <span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="nf">price_as_number</span> <span class="o">/</span> <span class="nb">self</span><span class="p">.</span><span class="nf">area_as_number</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div></div> <p>Con el método de clase <code class="language-plaintext highlighter-rouge">all</code>, ActiveWorksheet devuelve una colección de instancias de <code class="language-plaintext highlighter-rouge">Project</code>, una por cada fila del archivo CSV. Así, puedo filtrar por los registros que no cumplen los requisitos mínimos que busco:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">projects</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">all</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">project</span><span class="o">|</span> <span class="n">project</span><span class="p">.</span><span class="nf">date</span> <span class="o">==</span> <span class="no">Date</span><span class="p">.</span><span class="nf">today</span> <span class="p">}</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">project</span><span class="o">|</span> <span class="n">project</span><span class="p">.</span><span class="nf">bedrooms</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">project</span><span class="o">|</span> <span class="n">project</span><span class="p">.</span><span class="nf">price_as_number</span> <span class="o">&lt;=</span> <span class="mi">380_000</span> <span class="p">}</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">project</span><span class="o">|</span> <span class="n">project</span><span class="p">.</span><span class="nf">area_as_number</span> <span class="o">&gt;=</span> <span class="mi">60</span> <span class="p">}</span> <span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">project</span><span class="o">|</span> <span class="n">project</span><span class="p">.</span><span class="nf">stage</span> <span class="o">!=</span> <span class="s2">"En planos"</span> <span class="p">}</span> <span class="n">sorted_projects</span> <span class="o">=</span> <span class="n">projects</span><span class="p">.</span><span class="nf">sort_by</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:price_as_number</span><span class="p">)</span> </code></pre></div></div> <p>Los siguientes pasos serían: empezar a recopilar los datos de manera semanal, si se quiere hacer un análisis de los precios en el tiempo, y presentar los datos en un formato más legible para poder decidir mejor. Para ello desarrollé una herramienta de línea de comandos que está alojada en este <a href="https://github.com/hpneo/m2">repositorio de GitHub</a>.</p>Con ActiveWorksheet y ScrapKit pude construir un pequeño análisis de ofertas inmobiliarias en las últimas semanas.Usando OpenStruct2020-02-16T16:30:00+00:002020-02-16T16:30:00+00:00https://hpneo.dev/2020/02/16/usando-openstruct<p><img src="/assets/images/openstruct.png" alt="Usando OpenStruct" /></p> <p>Ruby tiene varias formas de simplificar algunas partes del desarrollo, como por ejemplo el manejo de atributos en un objeto.</p> <p>Nos podemos encontrar en la situación donde queremos dejar de usar hashes y manejar la data como si fueran objetos de Ruby. De esta forma podemos hacer el código un poco más simple o legible.</p> <p>Por ejemplo, teniendo este array:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pages</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Aprendiendo a usar arrays en JavaScript"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2020-02-15-javascript-arrays.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"APIs de Internacionalización en JavaScript"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-05-13-apis-internacionalizacion.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Ejecutando comandos desde Ruby"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-03-25-ejecutar-comandos-ruby.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Usando Higher-Order Components"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-03-19-usando-hocs.html"</span> <span class="p">}</span> <span class="p">]</span> </code></pre></div></div> <p>Podríamos hacer:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pages</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">page</span><span class="o">|</span> <span class="n">page</span><span class="p">[</span><span class="ss">:slug</span><span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>O, si convertimos <code class="language-plaintext highlighter-rouge">pages</code> a un array de <code class="language-plaintext highlighter-rouge">OpenStruct</code>, podríamos hacer:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pages</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:slug</span><span class="p">)</span> </code></pre></div></div> <hr /> <p>OpenStruct permite crear objetos que leen y guardan atributos de manera dinámica, utilizando metaprogramación.</p> <p>Para usar OpenStruct, creamos una instancia de <code class="language-plaintext highlighter-rouge">OpenStruct</code>:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"ostruct"</span> <span class="n">page</span> <span class="o">=</span> <span class="no">OpenStruct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">""</span><span class="p">)</span> <span class="c1">#=&gt; #&lt;OpenStruct title="", slug=""&gt;</span> </code></pre></div></div> <p>Con la estructura anterior, podemos hacer:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"ostruct"</span> <span class="n">pages</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Aprendiendo a usar arrays en JavaScript"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2020-02-15-javascript-arrays.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"APIs de Internacionalización en JavaScript"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-05-13-apis-internacionalizacion.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Ejecutando comandos desde Ruby"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-03-25-ejecutar-comandos-ruby.html"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">title: </span><span class="s2">"Usando Higher-Order Components"</span><span class="p">,</span> <span class="ss">slug: </span><span class="s2">"/2019-03-19-usando-hocs.html"</span> <span class="p">}</span> <span class="p">]</span> <span class="n">pages</span> <span class="o">=</span> <span class="n">pages</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">page</span><span class="o">|</span> <span class="no">OpenStruct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">page</span><span class="p">)</span> <span class="k">end</span> <span class="n">pages</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:slug</span><span class="p">)</span> <span class="c1">#=&gt; ["/2020-02-15-javascript-arrays.html", "/2019-05-13-apis-internacionalizacion.html", "/2019-03-25-ejecutar-comandos-ruby.html", "/2019-03-19-usando-hocs.html"]</span> </code></pre></div></div> <p>Si queremos ir más allá, podemos crear una clase que herede de <code class="language-plaintext highlighter-rouge">OpenStruct</code>:</p> <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"ostruct"</span> <span class="nb">require</span> <span class="s2">"date"</span> <span class="k">class</span> <span class="nc">Page</span> <span class="o">&lt;</span> <span class="no">OpenStruct</span> <span class="k">def</span> <span class="nf">created_at</span> <span class="n">date_as_string</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">slug</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="n">date_parts</span> <span class="o">=</span> <span class="n">date_as_string</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">"-"</span><span class="p">).</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:to_i</span><span class="p">)</span> <span class="no">Date</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">date_parts</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">date_parts</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">date_parts</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="k">end</span> <span class="k">end</span> <span class="n">pages</span> <span class="o">=</span> <span class="p">[</span> <span class="c1">#...</span> <span class="p">]</span> <span class="n">pages</span> <span class="o">=</span> <span class="n">pages</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">page</span><span class="o">|</span> <span class="no">Page</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">page</span><span class="p">)</span> <span class="k">end</span> <span class="n">pages_created_in_2019</span> <span class="o">=</span> <span class="n">pages</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="o">|</span><span class="n">page</span><span class="o">|</span> <span class="n">page</span><span class="p">.</span><span class="nf">created_at</span><span class="p">.</span><span class="nf">year</span> <span class="o">==</span> <span class="mi">2019</span> <span class="p">}</span> <span class="c1">#=&gt; [#&lt;Page title="APIs de Internacionalizacion en JavaScript", slug="/2019-05-13-apis-internacionalizacion.html"&gt;, #&lt;Page title="Ejecutando comandos desde Ruby", slug="/2019-03-25-ejecutar-comandos-ruby.html"&gt;, #&lt;Page title="Usando Higher-Order Components", slug="/2019-03-19-usando-hocs.html"&gt;]</span> </code></pre></div></div> <p>De esta forma, podemos hacer más escribiendo un poco menos, ahorrándonos varios <code class="language-plaintext highlighter-rouge">attr_accessor</code> y tener que definir un método <code class="language-plaintext highlighter-rouge">initialize</code> para <code class="language-plaintext highlighter-rouge">Page</code>.</p>OpenStruct es una estructura de datos de Ruby que permite crear objetos que reciban cualquier atributo de manera dinámica.Aprendiendo a usar arrays en JavaScript2020-02-15T23:15:00+00:002020-02-15T23:15:00+00:00https://hpneo.dev/2020/02/15/javascript-arrays<p><img src="/assets/images/arrays-js.png" alt="Aprendiendo a usar arrays en JavaScript" /></p> <p>Los <em>arrays</em> (o arreglos) son un tipo especial de objetos y representan colecciones que pueden guardar cualquier tipo de dato en JavaScript.</p> <p>Los arrays tienen una propiedad llamada <code class="language-plaintext highlighter-rouge">length</code>, que guarda el tamaño, o número de elementos que contiene dicho array, y utilizan los corchetes (<code class="language-plaintext highlighter-rouge">[]</code>) para acceder a un elemento del arreglo a través de su índice. En JavaScript, el índice de los arrays empieza en 0.</p> <h2 id="trabajando-con-arrays">Trabajando con arrays</h2> <p>Al ser objetos, los arrays tienen una serie de métodos que sirven para manipularlos:</p> <ol> <li><a href="#join"><code class="language-plaintext highlighter-rouge">join</code></a></li> <li><a href="#pop"><code class="language-plaintext highlighter-rouge">pop</code></a></li> <li><a href="#push"><code class="language-plaintext highlighter-rouge">push</code></a></li> <li><a href="#indexOf"><code class="language-plaintext highlighter-rouge">indexOf</code></a></li> <li><a href="#reverse"><code class="language-plaintext highlighter-rouge">reverse</code></a></li> <li><a href="#concat"><code class="language-plaintext highlighter-rouge">concat</code></a></li> <li><a href="#find"><code class="language-plaintext highlighter-rouge">find</code></a></li> <li><a href="#forEach"><code class="language-plaintext highlighter-rouge">forEach</code></a></li> <li><a href="#map"><code class="language-plaintext highlighter-rouge">map</code></a></li> <li><a href="#filter"><code class="language-plaintext highlighter-rouge">filter</code></a></li> <li><a href="#reduce"><code class="language-plaintext highlighter-rouge">reduce</code></a></li> </ol> <h3 id="join"><code class="language-plaintext highlighter-rouge">join</code></h3> <p>Une todos los elementos de un array en una cadena, utilizando un <em>separador</em>.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="c1">// 3</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// "2020-2-15"</span> </code></pre></div></div> <h3 id="pop"><code class="language-plaintext highlighter-rouge">pop</code></h3> <p>Quita el último elemento del array y retorna su valor.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">pop</span><span class="p">();</span> <span class="c1">// 15</span> <span class="nx">dateParts</span><span class="p">;</span> <span class="c1">// [2020, 2]</span> </code></pre></div></div> <blockquote> <p><strong>Nota:</strong>: <code class="language-plaintext highlighter-rouge">pop</code> es uno de los tantos métodos que modifican el array original.</p> </blockquote> <h3 id="push"><code class="language-plaintext highlighter-rouge">push</code></h3> <p>Agrega un elemento al final del array original y retorna el nuevo tamaño.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">pop</span><span class="p">();</span> <span class="c1">// 15</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">14</span><span class="p">);</span> <span class="c1">// 3</span> <span class="nx">dateParts</span><span class="p">;</span> <span class="c1">// [2020, 2, 14]</span> </code></pre></div></div> <h3 id="indexof"><code class="language-plaintext highlighter-rouge">indexOf</code></h3> <p>Devuelve el índice (o posición) en el array del primer elemento que sea igual al argumento que recibe.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span> <span class="c1">// 1</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="mi">15</span><span class="p">);</span> <span class="c1">// 2</span> </code></pre></div></div> <h3 id="reverse"><code class="language-plaintext highlighter-rouge">reverse</code></h3> <p>Modifica el array invirtiendo sus elementos y retorna el array invertido.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">reverse</span><span class="p">();</span> <span class="c1">// [15, 2, 2020]</span> <span class="nx">dateParts</span><span class="p">;</span> <span class="c1">// [15, 2, 2020]</span> </code></pre></div></div> <h3 id="concat"><code class="language-plaintext highlighter-rouge">concat</code></h3> <p>Agrega elementos a una copia del array original y devuelve la copia con los nuevos elementos agregados.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="mi">18</span><span class="p">,</span> <span class="mi">15</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="c1">// [2020, 2, 15, 18, 15, 0]</span> <span class="nx">dateParts</span><span class="p">;</span> <span class="c1">// [2020, 2, 15]</span> </code></pre></div></div> <h3 id="find"><code class="language-plaintext highlighter-rouge">find</code></h3> <p>Devuelve el primer elemento que cumpla con una condición pasada como función.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">part</span> <span class="o">&gt;</span> <span class="mi">1900</span><span class="p">);</span> <span class="c1">// 2020</span> </code></pre></div></div> <h3 id="foreach"><code class="language-plaintext highlighter-rouge">forEach</code></h3> <p>Recorre cada elemento del array a través de una función.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">part</span><span class="p">));</span> <span class="c1">// 2020</span> <span class="c1">// 2</span> <span class="c1">// 15</span> </code></pre></div></div> <h3 id="map"><code class="language-plaintext highlighter-rouge">map</code></h3> <p>Devuelve un nuevo array, donde cada elemento es el resultado de aplicar una función sobre cada elemento del array original.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">part</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span> <span class="c1">// [2022, 4, 17]</span> </code></pre></div></div> <h3 id="filter"><code class="language-plaintext highlighter-rouge">filter</code></h3> <p>Devuelve un nuevo array, cuyos elementos son aquellos elementos del array original que cumplen con una condición pasada como función.</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">part</span> <span class="o">=&gt;</span> <span class="nx">part</span> <span class="o">&lt;</span> <span class="mi">1990</span><span class="p">);</span> <span class="c1">// [2, 15]</span> </code></pre></div></div> <h3 id="reduce"><code class="language-plaintext highlighter-rouge">reduce</code></h3> <p><code class="language-plaintext highlighter-rouge">reduce</code> permite trabajar sobre todos los elementos de un array y devolver un resultado, que puede ser <em>cualquier cosa</em>.</p> <p>Como en los métodos anteriores, recibe una función como argumento, solo que en esta ocasión el parámetro es llamado <em>acumulador</em>. El <em>acumulador</em> empieza con un valor inicial y acumula el valor retornado por cada iteración. Al final, <code class="language-plaintext highlighter-rouge">reduce</code> retorna el valor del <em>acumulador</em>.</p> <p>Un ejemplo bastante común es sumar un array de números:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">];</span> <span class="nx">numbers</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">accumulator</span><span class="p">,</span> <span class="nx">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">accumulator</span> <span class="o">+</span> <span class="nx">number</span><span class="p">);</span> <span class="c1">// 21</span> </code></pre></div></div> <p>El valor inicial de un acumulador es pasado como segundo parámetro, luego de la función. Si no se pasa ningún valor inicial, el valor inicial es el primer elemento del array.</p> <p>Para nuestro ejemplo, ya que <code class="language-plaintext highlighter-rouge">dateParts</code> es un array que contiene año, mes y día, podemos generar un objeto que tenga la misma información pero estructurada de otra forma. Para eso creamos <code class="language-plaintext highlighter-rouge">dateKeys</code>, un array que contiene los nombres de las propiedades asociados al año, mes y día:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">dateParts</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2020</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">15</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">dateKeys</span> <span class="o">=</span> <span class="p">[</span><span class="dl">"</span><span class="s2">year</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">month</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">day</span><span class="dl">"</span><span class="p">];</span> <span class="nx">dateParts</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">accumulator</span><span class="p">,</span> <span class="nx">part</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">dateKeys</span><span class="p">[</span><span class="nx">index</span><span class="p">];</span> <span class="nx">accumulator</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">part</span><span class="p">;</span> <span class="k">return</span> <span class="nx">accumulator</span><span class="p">;</span> <span class="p">},</span> <span class="p">{});</span> <span class="c1">// {year: 2020, month: 2, day: 15}</span> </code></pre></div></div> <blockquote> <p><strong>Nota:</strong>: <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">forEach</code>, <code class="language-plaintext highlighter-rouge">map</code> y <code class="language-plaintext highlighter-rouge">filter</code> tienen una función como primer parámetro, que a su vez puede recibir hasta 3 argumentos, en el siguiente orden: <code class="language-plaintext highlighter-rouge">item</code>, <code class="language-plaintext highlighter-rouge">index</code> y <code class="language-plaintext highlighter-rouge">array</code>. <code class="language-plaintext highlighter-rouge">reduce</code> tiene un argumento adicional (el <em>acumulador</em>) que va antes de <code class="language-plaintext highlighter-rouge">item</code>.</p> </blockquote>Los arrays (o arreglos) son un tipo especial de objetos y representan colecciones que pueden guardar cualquier tipo de dato.APIs de Internacionalización en JavaScript2019-05-13T14:15:57+00:002019-05-13T14:15:57+00:00https://hpneo.dev/2019/05/13/apis-internacionalizacion<p><img src="/assets/images/intl.png" alt="APIs de Internacionalización en JavaScript" /></p> <p>Cuando hablamos de internacionalización en front-end nos encontramos con algunos retos. Dejando de lado la traducción de etiquetas, botones y textos en general, nos podemos topar con escenarios donde necesitemos mostrar fechas o monedas para diferentes idiomas o países.</p> <p>Felizmente existe un conjunto de APIs que nos facilitan este trabajo, y si antes necesitábamos bibliotecas de terceros para manejar estas tareas, con <code class="language-plaintext highlighter-rouge">Intl</code> podemos hacerlo nativamente, sin necesidad de agregar más dependencias a nuestra aplicación.</p> <h2 id="intldatetimeformat"><code class="language-plaintext highlighter-rouge">Intl​.Date​Time​Format</code></h2> <p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat"><code class="language-plaintext highlighter-rouge">Intl​.Date​Time​Format</code></a> nos permite formatear una fecha según el idioma que elijamos. Por ejemplo, las fechas en Estados Unidos tienen el formato <code class="language-plaintext highlighter-rouge">mes/día/año</code>, mientras que en Gran Bretaña y países de Hispanoamérica, el formato es <code class="language-plaintext highlighter-rouge">día/mes/año</code>.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">date</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">formatterUnitedStates</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">DateTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">formatterGreatBritan</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">DateTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-GB</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">formatterPeru</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">DateTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es-PE</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">formatterSpain</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">DateTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es-ES</span><span class="dl">'</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterUnitedStates</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">date</span><span class="p">));</span> <span class="c1">// 5/13/2019</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterGreatBritan</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">date</span><span class="p">));</span> <span class="c1">// 13/05/2019</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterPeru</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">date</span><span class="p">));</span> <span class="c1">// 13/5/2019</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterSpain</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">date</span><span class="p">));</span> <span class="c1">// 13/5/2019</span> </code></pre></div></div> <h2 id="intlrelativetimeformat"><code class="language-plaintext highlighter-rouge">Intl​.Relative​Time​Format</code></h2> <p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RelativeTimeFormat"><code class="language-plaintext highlighter-rouge">Intl​.Relative​Time​Format</code></a> es una API interesante que nos permite formatear cantidades de tiempo relativas. Por ejemplo, si queremos mostrar <em>“2 días antes”</em> o <em>“1 día después”</em> de manera más <em>humana</em> podemos hacerlo con esta API.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">formatterEN</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">RelativeTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">formatterENAuto</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">RelativeTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">numeric</span><span class="p">:</span> <span class="dl">'</span><span class="s1">auto</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterES</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">RelativeTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">formatterESAuto</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">RelativeTimeFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">numeric</span><span class="p">:</span> <span class="dl">'</span><span class="s1">auto</span><span class="dl">'</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterEN</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">day</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// in 1 day</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterENAuto</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">day</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// tomorrow</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterEN</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">week</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// in 1 week</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterENAuto</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">week</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// next week</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterES</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">day</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// dentro de 1 día</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterESAuto</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">day</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// mañana</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterES</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">week</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// dentro de 1 semana</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterESAuto</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="dl">'</span><span class="s1">week</span><span class="dl">'</span><span class="p">));</span> <span class="c1">// la próxima semana</span> </code></pre></div></div> <h2 id="intlnumberformat"><code class="language-plaintext highlighter-rouge">Intl​.Number​Format</code></h2> <p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat"><code class="language-plaintext highlighter-rouge">Intl​.Number​Format</code></a> es un API que nos ayuda a formatear números, monedas y porcentajes. El caso de uso más común es cuando tenemos una aplicación que realiza cálculos de montos y la moneda varía según el cliente. Por ejemplo, si queremos mostrar un monto en dólares, en pesos mexicanos o en soles peruanos.</p> <blockquote> <p><strong>Nota:</strong>: Esta API no realiza conversión entre monedas, solo se encarga de formatear el monto que se le pase.</p> </blockquote> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">total</span> <span class="o">=</span> <span class="mf">145.50</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">formatterDollars</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span> <span class="na">currency</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USD</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterSoles</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es-PE</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span> <span class="na">currency</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PEN</span><span class="dl">'</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterDollars</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">total</span><span class="p">));</span> <span class="c1">// $145.50</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterSoles</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">total</span><span class="p">));</span> <span class="c1">// S/ 145.50</span> </code></pre></div></div> <blockquote> <p><strong>Nota:</strong>: El resultado de <code class="language-plaintext highlighter-rouge">format</code> depende no solo de <code class="language-plaintext highlighter-rouge">currency</code>, si no también del idioma que le pases, por ejemplo:</p> </blockquote> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">total</span> <span class="o">=</span> <span class="mf">145.50</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">formatterDollars</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span> <span class="na">currency</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USD</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterSoles</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span> <span class="na">currency</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PEN</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterPesos</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span> <span class="na">currency</span><span class="p">:</span> <span class="dl">'</span><span class="s1">MXN</span><span class="dl">'</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterDollars</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">total</span><span class="p">));</span> <span class="c1">// $145.50</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterSoles</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">total</span><span class="p">));</span> <span class="c1">// PEN 145.50</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterPesos</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">total</span><span class="p">));</span> <span class="c1">// MX$145.50</span> </code></pre></div></div> <h2 id="intllistformat"><code class="language-plaintext highlighter-rouge">Intl​.List​Format</code></h2> <p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ListFormat"><code class="language-plaintext highlighter-rouge">Intl​.List​Format</code></a> nos ayuda a mostrar una lista de elementos de manera <em>humana</em>. Por ejemplo, si queremos convertir <code class="language-plaintext highlighter-rouge">['manzana', 'uva', 'sandía']</code> a <em>“manzana, uva y sandía”</em> o <em>“manzana, uva o sandía”</em>, esta API nos facilita esta tarea.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Iron Man</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Captain America</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Thor</span><span class="dl">'</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">formatterENAnd</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">ListFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">conjunction</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterENOr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">ListFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">disjunction</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterESAnd</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">ListFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">conjunction</span><span class="dl">'</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">formatterESOr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">ListFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">disjunction</span><span class="dl">'</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterENAnd</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">items</span><span class="p">));</span> <span class="c1">// Iron Man, Captain America, and Thor</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterENOr</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">items</span><span class="p">));</span> <span class="c1">// Iron Man, Captain America, or Thor</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterESAnd</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">items</span><span class="p">));</span> <span class="c1">// Iron Man, Captain America y Thor</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">formatterESOr</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">items</span><span class="p">));</span> <span class="c1">// Iron Man, Captain America o Thor</span> </code></pre></div></div> <blockquote> <p><strong>Nota:</strong>: Existe <a href="https://github.com/tc39/proposal-intl-list-format/issues/45">un bug en español</a> para <em>“y”</em> u <em>“o”</em> cuando la última palabra empieza con <em>“i”</em>/<em>“hi”</em> u <em>“o”</em>/<em>“ho”</em>, respectivamente.</p> </blockquote> <hr /> <p>Usualmente, para formatear fechas o monedas teníamos que recurrir a algunas bibliotecas, o escribir código más complejo. Con las APIs de Internacionalización nos ahorramos agregar más dependencias a nuestra aplicación.</p> <p>De todas formas, es importante revisar el <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Browser_compatibility">soporte de estas APIs en los navegadores</a> antes de decidir usarlas en producción.</p>Con las APIs de Internacionalización del navegador podemos formatear fechas y números para diferentes idiomas