Cómo construimos componentes de interfaz de usuario en Rails

Publicado: 2024-06-28

Mantener la coherencia visual en una aplicación web de gran tamaño es un problema compartido en muchas organizaciones. La aplicación web principal detrás de nuestro producto Flywheel está construida con Ruby on Rails, y tenemos varios desarrolladores de Rails y tres desarrolladores front-end que le envían código en un día determinado. También somos grandes en diseño (es uno de nuestros valores fundamentales como empresa) y tenemos tres diseñadores que trabajan muy de cerca con los desarrolladores en nuestros equipos Scrum.

dos personas colaboran en el diseño de un sitio web

Uno de nuestros principales objetivos es garantizar que cualquier desarrollador pueda crear una página responsiva sin ningún obstáculo. Los obstáculos generalmente han incluido no saber qué componentes existentes usar para construir una maqueta (lo que lleva a inflar el código base con componentes redundantes muy similares) y no saber cuándo discutir la reutilización con los diseñadores. Esto contribuye a experiencias inconsistentes de los clientes, frustración de los desarrolladores y un lenguaje de diseño dispar entre desarrolladores y diseñadores.

Hemos pasado por varias iteraciones de guías de estilo y métodos para crear/mantener patrones y componentes de UI, y cada iteración ayudó a resolver los problemas que enfrentábamos en ese momento. Estamos seguros de que nuestro nuevo enfoque nos preparará para el futuro. Si enfrenta problemas similares en su aplicación Rails y le gustaría abordar los componentes desde el lado del servidor, espero que este artículo pueda brindarle algunas ideas.

un hombre barbudo sonríe a la cámara mientras está sentado frente a un monitor de computadora que muestra líneas de código

En este artículo, profundizaré en:

  • Lo que estamos resolviendo
  • Componentes restrictivos
  • Representación de componentes en el lado del servidor
  • Donde no podemos usar componentes del lado del servidor

Lo que estamos resolviendo

Queríamos restringir completamente nuestros componentes de UI y eliminar la posibilidad de que la misma UI se cree de más de una manera. Si bien es posible que un cliente no pueda saberlo (al principio), no tener restricciones en los componentes genera una experiencia confusa para el desarrollador, dificulta mucho el mantenimiento de las cosas y dificulta la realización de cambios de diseño globales.

La forma tradicional en que abordábamos los componentes era a través de nuestra guía de estilo, que enumeraba todo el marcado necesario para construir un componente determinado. Por ejemplo, así es como se veía la página de la guía de estilo de nuestro componente de lamas:

Página de guía de estilo para el componente de lamas.

Esto funcionó bien durante varios años, pero comenzaron a surgir problemas cuando agregamos variantes, estados o formas alternativas de usar el componente. Con una interfaz de usuario compleja, se volvió engorroso consultar la guía de estilo para saber qué clases usar y cuáles evitar, y en qué orden debía estar el marcado para generar la variación deseada.

A menudo, los diseñadores hacían pequeñas adiciones o ajustes a un componente determinado. Dado que la guía de estilo no soportaba eso, los trucos alternativos para lograr que ese ajuste se mostrara correctamente (como canibalizar inapropiadamente parte de otro componente) se volvieron irritantemente comunes.

Ejemplo de componente sin restricciones

Para ilustrar cómo surgen las inconsistencias con el tiempo, usaré un ejemplo simple (y artificial) pero muy común de uno de nuestros componentes en la aplicación Flywheel: encabezados de tarjetas.

A partir de una maqueta de diseño, así es como se veía el encabezado de una tarjeta. Era bastante simple con un título, un botón y un borde inferior.

 .card__encabezado
  .card__header-izquierda
    %h2 Copias de seguridad
  .card__header-derecha
    = enlace_a "#" hacer
      = icono("más_pequeño")

Después de codificarlo, imagine a un diseñador que quisiera agregar un ícono a la izquierda del título. Fuera de la caja, no habrá ningún margen entre el ícono y el título.

 ...
  .card__header-izquierda
    = icono("arrow_backup", color: "gray25")
    %h2 Copias de seguridad
...

Lo ideal sería resolver eso en el CSS para encabezados de tarjetas, pero para este ejemplo, digamos que otro desarrollador pensó “¡Oh, ya lo sé! Tenemos algunos ayudantes de margen. Simplemente pondré una clase de ayuda en el título”.

 ...
  .card__header-izquierda
    = icono("arrow_backup", color: "gray25")
    %h2.--ml-10 Copias de seguridad
...

Bueno, técnicamente eso se parece a la maqueta, ¿verdad? Claro, pero digamos que un mes después, otro desarrollador necesita un encabezado de tarjeta, pero sin el ícono. Encuentran el último ejemplo, lo copian/pegan y simplemente eliminan el icono.

De nuevo parece correcto, ¿verdad? Fuera de contexto, para alguien sin buen ojo para el diseño, ¡claro! Pero míralo al lado del original. ¡Ese margen izquierdo en el título todavía está ahí porque no se dieron cuenta de que era necesario eliminar el margen izquierdo auxiliar!

Llevando este ejemplo un paso más allá, digamos que otra maqueta requiere un encabezado de tarjeta sin borde inferior. Se podría encontrar un estado que tenemos en la guía de estilo llamado "sin fronteras" y aplicarlo. ¡Perfecto!

Otro desarrollador podría intentar reutilizar ese código, pero en este caso, en realidad necesita un borde. Digamos hipotéticamente que ignoran el uso adecuado documentado en la guía de estilo y no se dan cuenta de que eliminar la clase sin bordes les dará su borde. En cambio, añaden una regla horizontal. Termina habiendo algo de relleno adicional entre el título y el borde, por lo que aplican una clase de ayuda a hr y ¡listo!

Con todas estas modificaciones al encabezado de la tarjeta original, ahora tenemos un lío en el código.

 .card__header.--sin bordes
  .card__header-izquierda
    %h2.--ml-10 Copias de seguridad
  .card__header-derecha
    = enlace_a "#" hacer
      = icono("más_pequeño")
  %h.--mt-0.--mb-0

Tenga en cuenta que el ejemplo anterior es sólo para ilustrar cómo los componentes sin restricciones pueden volverse desordenados con el tiempo. Si alguien de nuestro equipo intentó enviar una variación del encabezado de una tarjeta, debería someterse a una revisión de diseño o de código. Pero cosas como esta a veces pasan desapercibidas, ¡de ahí nuestra necesidad de proteger las cosas!


Componentes restrictivos

Quizás esté pensando que los problemas enumerados anteriormente ya se han resuelto claramente con componentes. ¡Esa es una suposición correcta! Los frameworks front-end como React y Vue son muy populares para este propósito exacto; Son herramientas increíbles para encapsular la interfaz de usuario. Sin embargo, hay un problema con ellos que no siempre nos gusta: requieren que la interfaz de usuario se represente mediante JavaScript.

Nuestra aplicación Flywheel requiere mucho back-end, principalmente con HTML renderizado por el servidor, pero afortunadamente para nosotros, los componentes pueden presentarse en muchas formas. Al final del día, un componente de interfaz de usuario es una encapsulación de estilos y reglas de diseño que genera marcado en un navegador. Al darnos cuenta de esto, podemos adoptar el mismo enfoque para los componentes, pero sin la sobrecarga de un marco de JavaScript.

A continuación veremos cómo construimos componentes restringidos, pero estos son algunos de los beneficios que hemos encontrado al usarlos:

  • En realidad, nunca existe una forma incorrecta de ensamblar un componente.
  • El componente hace todo el diseño por usted. (¡Simplemente pasa las opciones!)
  • La sintaxis para crear un componente es muy consistente y fácil de entender.
  • Si es necesario un cambio de diseño en un componente, podemos cambiarlo una vez en el componente y estar seguros de que se actualizará en todas partes.

Representación de componentes en el lado del servidor

Entonces, ¿de qué estamos hablando al restringir componentes? ¡Vamos a profundizar en!

Como se mencionó anteriormente, queremos que cualquier desarrollador que trabaje en la aplicación pueda ver una maqueta de diseño de una página y pueda crear esa página inmediatamente sin impedimentos. Eso significa que el método de creación de la interfaz de usuario debe A) estar muy bien documentado y B) ser muy declarativo y libre de conjeturas.

Parciales al rescate (o eso pensábamos)

Un primer intento de esto que hemos intentado en el pasado fue usar parciales de Rails. Los parciales son la única herramienta que Rails le brinda para la reutilización en plantillas. Naturalmente, son lo primero que todo el mundo busca. Pero confiar en ellos tiene importantes inconvenientes porque si necesita combinar la lógica con una plantilla reutilizable, tiene dos opciones: duplicar la lógica en cada controlador que utiliza el parcial o incrustar la lógica en el propio parcial.

Los parciales SÍ evitan errores de duplicación al copiar y pegar y funcionan bien las primeras veces que necesitas reutilizar algo. Pero según nuestra experiencia, los parciales pronto se saturan con soporte para cada vez más funcionalidad y lógica. ¡Pero la lógica no debería vivir en plantillas!

Introducción a las células

Afortunadamente, existe una mejor alternativa a los parciales que nos permite reutilizar el código y mantener la lógica fuera de la vista. Se llama Cells, una gema Ruby desarrollada por Trailblazer. Las celdas han existido mucho antes del aumento de popularidad en los marcos de front-end como React y Vue y le permiten escribir modelos de vista encapsulados que manejan tanto la lógica como las plantillas. Proporcionan una abstracción del modelo de vista, que Rails simplemente no tiene lista para usar. De hecho, hemos estado usando Cells en la aplicación Flywheel por un tiempo, pero no a una escala global y súper reutilizable.

En el nivel más simple, Cells nos permite abstraer una porción de marcado como este (usamos Haml para nuestro lenguaje de plantillas):

 %div
  %h1 ¡Hola mundo!

En un modelo de vista reutilizable (muy similar a los parciales en este punto), y conviértalo en esto:

 = celda("hola_mundo")

En última instancia, esto nos ayuda a restringir el componente a donde no se pueden agregar clases auxiliares o componentes secundarios incorrectos sin modificar la celda misma.

Construyendo células

Colocamos todas nuestras celdas UI en un directorio app/cells/ui. Cada celda debe contener solo un archivo Ruby, con el sufijo _cell.rb. Técnicamente, puedes escribir las plantillas directamente en Ruby con el asistente content_tag, pero la mayoría de nuestras celdas también contienen una plantilla Haml correspondiente que se encuentra en una carpeta nombrada por el componente.

Una celda súper básica sin lógica se parece a esto:

 // celdas/ui/slat_cell.rb
interfaz de usuario del módulo
  clase SlatCell <VerModelo
    definitivamente espectáculo
    fin
  fin
fin

El método show es lo que se representa cuando crea una instancia de la celda y automáticamente buscará un archivo show.haml correspondiente en la carpeta con el mismo nombre que la celda. En este caso, es app/cells/ui/slat (amplificamos todas nuestras celdas de UI al módulo UI).

En la plantilla, puede acceder a las opciones pasadas a la celda. Por ejemplo, si se crea una instancia de la celda en una vista como = cell(“ui/slat”, título: “Título”, subtítulo: “Subtítulo”, etiqueta: “Etiqueta”), podemos acceder a esas opciones a través del objeto de opciones.

 // celdas/ui/slat/show.haml
.lama
  .slat__inner
    .slat__content
      %h4= opciones[:título]
      %p= opciones[:subtítulo]
      = icono(opciones[:icono], color: "azul")

Muchas veces movemos elementos simples y sus valores a un método en la celda para evitar que se representen elementos vacíos si no hay una opción presente.

 // celdas/ui/slat_cell.rb
título definitivo
  regresar a menos que opciones[:título]
  content_tag :h4, opciones[:título]
fin
definición de subtítulo
  regresar a menos que opciones[:subtítulo]
  content_tag :p, opciones[:subtítulo]
fin
 // celdas/ui/slat/show.haml
.lama
  .slat__inner
    .slat__content
      = título
      = subtítulo

Envolver celdas con una utilidad de interfaz de usuario

Después de demostrar el concepto de que esto podría funcionar a gran escala, quise abordar el marcado innecesario necesario para llamar a una celda. Simplemente no fluye del todo bien y es difícil de recordar. ¡Así que le hicimos una pequeña ayuda! Ahora podemos simplemente llamar = ui “nombre_del_componente” y pasar opciones en línea.

 = ui "slat", título: "Título", subtítulo: "Subtítulo", etiqueta: "Etiqueta"

Pasar opciones como un bloque en lugar de en línea

Llevando la utilidad de la interfaz de usuario un poco más allá, rápidamente se hizo evidente que una celda con un montón de opciones en una sola línea sería muy difícil de seguir y simplemente fea. A continuación se muestra un ejemplo de una celda con muchas opciones definidas en línea:

 = ui “slat", título: “Título”, subtítulo: “Subtítulo”, etiqueta: “Etiqueta”, enlace: “#”, terciario_título: “Terciario”, deshabilitado: verdadero, lista de verificación: [“Elemento 1”, “Elemento 2”, “Ítem 3”]

Es muy engorroso, lo que nos lleva a crear una clase llamada OptionProxy que intercepta los métodos de establecimiento de celdas y los traduce en valores hash, que luego se fusionan en opciones. Si eso suena complicado, no te preocupes: para mí también lo es. Aquí hay una esencia de la clase OptionProxy que escribió Adam, uno de nuestros ingenieros de software senior.

Aquí hay un ejemplo del uso de la clase OptionProxy dentro de nuestra celda:

 interfaz de usuario del módulo
  clase SlatCell <VerModelo
    definitivamente espectáculo
      OptionProxy.new(self).rendimiento!(opciones, &bloquear)
      súper()
    fin
  fin
fin

Ahora que tenemos esto en su lugar, ¡podemos convertir nuestras engorrosas opciones en línea en un bloque más agradable!

 = ui "lama" hacer |lama|
  - slat.title = "Título"
  - slat.subtitle = "Subtítulo"
  - slat.label = "Etiqueta"
  - listón.link = "#"
  - slat.tertiary_title = "Terciario"
  - listón.disabled = verdadero
  - slat.checklist = ["Elemento 1", "Elemento 2", "Elemento 3"]

Introduciendo la lógica

Hasta este punto, los ejemplos no han incluido ninguna lógica sobre lo que muestra la vista. Esa es una de las mejores cosas que ofrece Cells, ¡así que hablemos de ello!

Siguiendo con nuestro componente slat, a veces necesitamos representar todo como un enlace y otras veces como un div, dependiendo de si hay o no una opción de enlace presente. Creo que este es el único componente que tenemos que se puede representar como un div o un enlace, pero es un ejemplo bastante claro del poder de Cells.

El siguiente método llama a un ayudante link_to o content_tag dependiendo de la presencia de opciones [:link] .

 contenedor def (y bloque)
  etiqueta =
    si opciones[:enlace]
      [:enlace_a, opciones[:enlace]]
    demás
      [:content_tag, :div]
    fin
  enviar(*etiqueta, clase: “slat__inner”, &bloque)
fin

Eso nos permite reemplazar el elemento .slat__inner en la plantilla con un bloque contenedor:

 .lama
  = contenedor hacer
  ...

Otro ejemplo de lógica en Cells que usamos mucho es el de clases de salida condicional. Digamos que agregamos una opción deshabilitada a la celda. Nada más en la invocación de la celda cambia, aparte de que ahora puede pasar una opción deshabilitada: verdadera y observar cómo todo se convierte en un estado deshabilitado (atenuado con enlaces en los que no se puede hacer clic).

 = ui "lama" hacer |lama|
  ...
  - listón.disabled = verdadero

Cuando la opción deshabilitada es verdadera, podemos establecer clases en los elementos de la plantilla necesarios para obtener el aspecto deshabilitado deseado.

 .slat{ clase: posibles_clases("--disabled": opciones[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ clase: posibles_clases("--alt": opciones[:disabled]) }= opciones[:título]
      %p{ clase: posibles_clases("--alt": opciones[:disabled]) }=
      opciones[:subtítulo]
      = icono(opciones[:icono], color: "gris")

Tradicionalmente, habríamos tenido que recordar (o consultar la guía de estilo) qué elementos individuales necesitaban clases adicionales para que todo funcionara correctamente en el estado deshabilitado. Las celdas nos permiten declarar una opción y luego hacer el trabajo pesado por nosotros.

Nota: possible_classes es un método que creamos para permitir la aplicación condicional de clases en Haml de una manera agradable.


Donde no podemos usar componentes del lado del servidor

Si bien el enfoque celular es extremadamente útil para nuestra aplicación particular y la forma en que trabajamos, sería negligente al decir que ha resuelto el 100% de nuestros problemas. Todavía escribimos JavaScript (mucho) y creamos bastantes experiencias en Vue en toda nuestra aplicación. El 75% del tiempo, nuestra plantilla de Vue aún reside en Haml y vinculamos nuestras instancias de Vue al elemento contenedor, lo que nos permite seguir aprovechando el enfoque de celda.

Sin embargo, en lugares donde tiene más sentido restringir completamente un componente como una instancia de Vue de un solo archivo, no podemos usar Cells. Nuestras listas de selección, por ejemplo, son todas Vue. ¡Pero creo que está bien! Realmente no nos hemos encontrado con la necesidad de tener versiones duplicadas de componentes tanto en Cells como en Vue, por lo que está bien que algunos componentes estén 100% construidos con Vue y otros con Cells.

Si un componente está construido con Vue, significa que se requiere JavaScript para construirlo en el DOM y aprovechamos el marco de Vue para hacerlo. Sin embargo, para la mayoría de nuestros otros componentes, no requieren JavaScript y, si lo necesitan, requieren que el DOM ya esté creado y simplemente lo conectamos y agregamos detectores de eventos.

A medida que sigamos avanzando con el enfoque celular, definitivamente vamos a experimentar con la combinación de componentes celulares y componentes Vue para que tengamos una y sólo una forma de crear y usar componentes. ¡No sé cómo se ve eso todavía, así que cruzaremos ese puente cuando lleguemos allí!


Nuestra conclusión

Hasta ahora hemos convertido alrededor de treinta de nuestros componentes visuales más utilizados a Cells. Nos ha brindado un gran aumento de productividad y brinda a los desarrolladores una sensación de validación de que las experiencias que están creando son correctas y no están pirateadas.

Nuestro equipo de diseño tiene más confianza que nunca en que los componentes y las experiencias de nuestra aplicación son 1:1 con lo que diseñaron en Adobe XD. Los cambios o adiciones a los componentes ahora se manejan únicamente a través de una interacción con un diseñador y un desarrollador front-end, lo que mantiene al resto del equipo concentrado y sin preocupaciones de saber cómo modificar un componente para que coincida con una maqueta de diseño.

Estamos constantemente iterando nuestro enfoque para restringir los componentes de la interfaz de usuario, pero espero que las técnicas ilustradas en este artículo le den una idea de lo que funciona bien para nosotros.


¡Ven a trabajar con nosotros!

Todos y cada uno de los departamentos que trabajan en nuestros productos tienen un impacto significativo en nuestros clientes y en nuestros resultados. Ya sea atención al cliente, desarrollo de software, marketing o cualquier otra cosa, todos estamos trabajando juntos para lograr nuestra misión de construir una empresa de hosting de la que la gente realmente pueda enamorarse.

¿Listo para unirte a nuestro equipo? ¡Estamos contratando! Aplicar aquí.