Integrar Trix Editor con Phoenix

on Iván Hernández Cazorla - Blog

Trix es un editor de texto enriquecido programado en CoffeeScript. ¿Cómo se puede integrar en una instancia de Phoenix? ¡No es magia, son hooks!
#phoenix, #elixir, #javascript

Llevaba tiempo dándole vueltas a esta idea: integrar un editor de texto enriquecido en un sitio web que utilice Phoenix. Sin embargo, hasta hace poco no me ha surgido la necesidad real y, casi siempre, si no siempre, las necesidades reales son las que imperan sobre cualquier otra.

El caso es que hace poco me propusieron un proyecto muy sencillo: un sitio web para un proyecto, en el que la usuaria pueda crear páginas a modo de memoria de proyecto. Pensé en diferentes opciones basadas en Elixir, entre ellas probar BeaconCMS, que no terminó con buenos resultados. Luego le di vueltas a PardallMarkdown, sugerido por un amigo, y si bien está bastante interesante y guay ese proyecto, no puedo proponerle a mi clienta que que trabaje con archivos Markdown y se adapte a ese modus operandi. Por lo que al final desistí y decidí crear un sitio web con Phoenix, con el que simplemente gestionar la creación de páginas y la navegación por medio de formularios sencillos.

Una vez tengo la estructura del formulario me encuentro con el problema: integrar un editor de texto enriquecido.

# Editores de texto enriquecido

Generalmente no me gustan. Suelo preferir a utilizar algún lenguaje de marcado, como por ejemplo Markdown, Wikitexto cuando trabajo en alguna wiki MediaWiki o incluso el propio HTML.

Entre los editores de texto enriquecido, el nombre que más resonaba en mis oídos era CKEditor, pero me parecía matar moscas a cañonazos. Por ello seguí buscando y me encontré con Trix.

# Trix Editor

Trix es un editor de texto enriquecido programado en CoffeeScript por el equipo de Basecamp, que suele programar en Ruby, lo que me animó aún más a echarle un vistazo profundo.

Pero tampoco podía observarlo muy profundamente: Trix es lo que es, un editor sencillo que ofrece suficientes características para crear un documento con un buen formato, pero sin caer en tediosas configuraciones o incontables opciones que apenas se suelen usar.

# Integrar Trix en Phoenix con un hook

Para integrar Trix en Phoenix debemos recurrir a la interoperabilidad de Phoenix con JavaScript. Si no sabes de lo que estoy hablando, te recomiendo leer la sección JavaScript interoperability de la documentación de Phoenix LiveView. ¡Vamos a empezar!

Lo primero que tenemos que hacer es añadir Trix como dependencia. Podemos descargarla de su repositorio y añadirla a nuestra carpeta vendor dentro de los assets de Phoenix. Pero yo iba con prisa y preferí utilizar cdnjs para realizar la prueba de fuego, por lo que añadí las siguientes en mi app_web/templates/layout/root.html.heex:

1<head>
2  <script src="https://cdnjs.cloudflare.com/ajax/libs/trix/1.3.1/trix.min.js" integrity="sha512-2RLMQRNr+D47nbLnsbEqtEmgKy67OSCpWJjJM394czt99xj3jJJJBQ43K7lJpfYAYtvekeyzqfZTx2mqoDh7vg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
3  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/trix/1.3.1/trix.min.css" integrity="sha512-5m1IeUDKtuFGvfgz32VVD0Jd/ySGX7xdLxhqemTmThxHdgqlgPdupWoSN8ThtUSLpAGBvA8DY2oO7jJCrGdxoA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
4</head>

Luego, en el componente del formulario, PageLive.FormComponent en este caso, introduje lo siguiente:

1<%= label f, :body %>
2<%= hidden_input f, :body, rows: 9, phx_hook: "TrixHook" %>
3<div id="page-form_body-editor" phx-update="ignore">
4  <trix-editor input="page-form_body"></trix-editor>
5</div>
6<%= error_tag f, :body %>

¿Qué es cada parte?

Una vez tenemos esta estructura, ya podemos visualizar el editor en el formulario.

Trix Editor en el formulario.

Ya solo queda darle forma a ese hook que hemos utilizado. En mi caso yo tengo la siguiente estructura en mis assets:

assets
|-- css
|-- js
    |-- app.js
    |-- hooks.js
    |-- hooks
        |-- trix_hook.js

trix_hook.js es el hook que hemos creado para vincular los diferentes eventos de Trix y Phoenix.

 1const TrixHook = {
 2  initListener(trix, event) {
 3    trix.addEventListener(event, function() {
 4      console.log(`event ${event} fired!`)
 5    })
 6  },
 7  bindTrixEditor() {
 8    /*
 9    this.el = elemento en el que está el hook
10    el siguiente elemento sería el contenedor "page-form_body-editor"
11    el hijo "1" se refiere a la etiqueta <trix-editor>, el hijo "0" es la barra
12    de tareas del editor
13    */
14    let trix = this.el.nextElementSibling.children[1]
15    this.initListener(trix, "trix-change")
16  },
17  mounted() {
18    console.log("Hello, Trix!");
19    this.bindTrixEditor()
20  }
21}
22
23export default TrixHook;

En el objeto TrixHook creamos el método initListener(trix, event), que simplemente nos ayuda a añadir el event listener al editor. En el método bindTrixEditor() vinculamos el editor por medio de this.el.nextElementSibling.children[1] (véanse los comentarios en el código para entender el porqué del índice 1. Yo me he decantado por esta opción para trabajar directamente con el elemento en el que se utiliza el hook, lo que conlleva respetar la estructura:

hidden_input
div
|-- trix-editor

Pero también se podría utilizar document.querySelector("trix-editor") o el método que se les ocurra. ¡Lo importante es llegar hasta <trix-editor>!.

Si quisiésemos ejecutar una función concreta según el evento que se haya iniciado, se podría determinar en el initListener(trix, event) o crear un método que gestione cada evento con su propia lógica.

Finalmente, en TrixHook añadimos la callback mounted(), en la que se hace uso del método bindTrix. En hooks.js se importa el hook recien cocinado:

1import TrixHook from "./hooks/trix_hook";
2
3let Hooks = {
4  TrixHook: TrixHook
5}
6
7export default Hooks;

Y en app.js se importan los hooks y se añaden al socket:

1...
2
3import Hooks from "./hooks"
4
5...
6
7let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

Si ahora vamos a nuestro formulario y añadimos un texto formateado, nuestra vista mostrará el texto con las etiquetas HTML, pero no el formato. Para poder mostrar el texto formateado tenemos que hacer unos pequeños cambios en la plantilla de la LiveView, tanto en la dedicada al listado de páginas como a la de cada página, en mi caso, PageLive.Index y PageLive.Show respectivamente. En app_web/live/page_live/show.html.heex tengo lo siguiente (es un proyecto recién generado):

 1<ul>
 2
 3  <li>
 4    <strong>Title:</strong>
 5    <%= @page.title %>
 6  </li>
 7
 8  <li>
 9    <strong>Body:</strong>
10    <%= @page.body %>
11  </li>
12
13  <li>
14    <strong>Author:</strong>
15    <%= @page.author %>
16  </li>
17
18  <li>
19    <strong>Slug:</strong>
20    <%= @page.slug %>
21  </li>
22
23</ul>

Aquí hay que cambiar <%= @page.body %> por <%= @page.body |> raw() |> html_escape() %>. raw/1 hace que la cadena no escape las etiquetas HTML y html_escape/1 devuelve una cadena segura.

Cuando llegué a este punto me surgieron dudas sobre seguridad, sobre todo con si este sería el método correcto a aplicar. En estos momentos no se me ocurre otra opción que marcar la cadena del campo con raw/1 y posteriormente utilizar html_escape/1.

# Adjuntar archivos

Gracias al hook que hemos montado podemos crear métodos dentro del propio objeto para interactuar con cada uno de los botones. Si bien he podido comprobar que la mayoría funcionan sin que tengamos que hacer nada por nuestra parte, si hay un botón que si pulsamos funcionará a medias: el clip para adjuntar archivos.

Si hacemos clic en ese botón, saltará una ventana para que seleccionemos el archivo que queremos adjuntar, pero se nos cerrará el editor y volveremos a la visualización de la página.

Para hacer funcionar este botón debemos integrar el event listener trix-attachment-add. Por suerte, los programadores de Trix han previsto que esta función es algo que muchos usuarios probablemente quieran integrar en su editor, por lo que proporcionan un ejemplo con el que podemos realizar esta integración. Véase Storing Attached Files en su repositorio.

Ahora mismo no lo tengo integrado, pero pronto lo intentaré. Cuando haya conseguido integrar esta función de Trix, escribiré otra entrada en la que explicaré el cómo y los aspectos a tener en cuenta.

# Eventos de Trix

Por supuesto, trix-change y trix-attachment-add no son los únicos eventos que emite Trix. Les recomiendo echarle un vistazo a los eventos que mencionan en su repositorio.


Cualquier comentario, duda o sugerencia me la pueden hacer llegar por Telegram, ya sea por privado o mencionarme (@ivanhercaz) en el grupo elixirES.


Formo parte de Molécula, un grupo de tres cowboys del espacio y alquimistas que programan mucho e intentan contribuir a la comunidad del software libre. Si te gusta lo que escribo o lo que contribuyo, puedes invitarnos a un café, té o maté. ¡Estamos en proceso de liberar varios proyectos!

Todas las entradas de este blog están bajo la licencia CC BY-SA 4.0. Las imágenes y otros recursos que no son de mi autoría tienen especificada su respectiva licencia. En caso de no tenerla, no dudes en reportármelo.

We are a team of three space cowboys and alchemists who develop software and try to contribute to the free software community. If you like what I write or what I contribute, you can invite us for a coffee, tea or maté. I still have have to finish developing several projects and, of course, release them!

All blog posts are licensed under CC BY-SA 4.0. The images or other resources that are not my autorship have their licenses specified. In case there is something it hasn't, don't hesitate to report it to me.