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?
- La primera línea simplemente muestra la etiqueta para el campo
:body
. - En la segunda línea se utiliza
hidden_input/3
para crear área de texto invisible. De esta parte lo más importante es el atributophx_hook="TrixHook"
. - En la tercera línea iniciamos un contenedor al que le asignamos un identificador
e indicamos con un
phx-update="ignore"
que LiveView ignore los cambios en ese contenedor a la hora de actualizar DOM. - En la cuarta línea, dentro del contenedor creado, insertamos la etiquet propia
de Trix,
<trix-editor>
, a la que le añadimos el atributoinput=page-form_body
. Este atributo indica que el contenido del editor debe ser el valor del campo:body
, previamente indicado con elhidden_input
. ¿Por quépage-form-body
? Porque es el identificador final que genera Phoenix.
Una vez tenemos esta estructura, ya podemos visualizar el 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.