Cómo construimos componentes de interfaz de usuario en Rails at Flywheel

Publicado: 2019-11-16

Mantener la consistencia visual en una aplicación web grande es un problema compartido en muchas organizaciones. En Flywheel, no somos diferentes. Nuestra aplicación web principal está construida con Ruby on Rails y tenemos alrededor de 15 desarrolladores de Rails y tres desarrolladores front-end que envían código en un día determinado. También somos expertos 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.

Uno de nuestros principales objetivos es garantizar que cualquier desarrollador de Flywheel pueda crear una página receptiva sin obstáculos. Los obstáculos generalmente han incluido no saber qué componentes existentes usar para construir una maqueta (lo que lleva a inflar la base de código con componentes redundantes muy similares) y no saber cuándo discutir la reutilización con los diseñadores. Esto contribuye a que las experiencias de los clientes sean inconsistentes, la 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 la interfaz de usuario, y cada iteración ayudó a resolver los problemas a los que nos enfrentábamos en ese momento. Ahora estamos en un nuevo enfoque (para nosotros) que confío en que nos preparará durante mucho tiempo. Si enfrenta problemas similares en su aplicación Rails y desea abordar los componentes desde el lado del servidor, espero que este artículo pueda brindarle algunas ideas.

En este artículo, me sumergiré 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 por completo los componentes de nuestra interfaz de usuario y eliminar la posibilidad de que se creara la misma interfaz de usuario en más de una forma. Si bien es posible que un cliente no pueda saberlo (al principio), no tener restricciones en los componentes conduce a una experiencia de desarrollador confusa, hace que las cosas sean muy difíciles de mantener y dificulta realizar cambios de diseño global.

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 crear un componente determinado. Por ejemplo, así es como se veía la página de la guía de estilo para nuestro componente de listones:

Esto funcionó bien y nos convenía durante varios años, pero los problemas comenzaron a aparecer cuando añadimos variantes, estados o formas alternativas de usar el componente. Con una pieza compleja de interfaz de usuario, se volvió engorroso hacer referencia a 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. Y, a menudo, los diseñadores hacían pequeñas adiciones o ajustes a un componente determinado. Dado que la guía de estilo no admitía eso del todo, los trucos alternativos para lograr que ese ajuste se muestre correctamente (como canibalizar de manera inapropiada 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 tarjeta.

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__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

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

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

Idealmente, resolveríamos eso en el CSS para los encabezados de las tarjetas, pero para este ejemplo, digamos que otro desarrollador pensó “¡Oh, ya sé! Tenemos algunos ayudantes de margen. Simplemente daré una bofetada a una clase de ayuda en el título”.

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

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

Una vez más parece correcto, ¿verdad? Fuera de contexto, para alguien sin buen ojo para el diseño, ¡seguro! Pero míralo junto al original. ¡Ese margen izquierdo en el título todavía está allí porque no se dieron cuenta de que era necesario eliminar el ayudante del margen izquierdo!

Llevando este ejemplo un paso más allá, digamos que otra maqueta requiere un encabezado de tarjeta sin borde inferior. Uno podría encontrar un estado que tenemos en la guía de estilo llamado "sin bordes" 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 borde les dará su borde. En cambio, agregan una regla horizontal. Termina habiendo un relleno adicional entre el título y el borde, por lo que aplican una clase de ayuda a la hora y listo.

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

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

Tenga en cuenta que el ejemplo anterior es solo para ilustrar el punto de 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, debe ser detectado por una revisión de diseño o revisión de código. Pero cosas como esta a veces se escapan, ¡de ahí nuestra necesidad de proteger las cosas!


Componentes restrictivos

Puede estar pensando que los problemas enumerados anteriormente ya se han resuelto claramente con componentes. ¡Esa es una suposición correcta! Los marcos 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 su interfaz de usuario sea renderizada por JavaScript.

La aplicación Flywheel tiene un back-end muy pesado con HTML generado principalmente por el servidor, pero afortunadamente para nosotros, los componentes pueden venir 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. Con esta realización, podemos adoptar el mismo enfoque para los componentes, pero sin la sobrecarga de un marco de JavaScript.

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

  • Realmente nunca hay una forma incorrecta de armar un componente.
  • El componente hace todo el pensamiento de diseño por usted. (¡Simplemente pasa las opciones!)
  • La sintaxis para crear un componente es muy consistente y fácil de razonar.
  • Si se necesita 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 Flywheel pueda ver una maqueta de diseño de una página y pueda construir inmediatamente esa página sin impedimentos. Eso significa que el método de creación de la interfaz de usuario debe estar A) documentado muy bien y B) muy declarativo y libre de conjeturas.

Parciales al rescate (o eso creíamos)

Un primer intento de esto que hemos intentado en el pasado fue usar parciales de Rails. Los parciales son la única herramienta que le brinda Rails para la reutilización en las plantillas. Naturalmente, son lo primero que todos buscan. 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 todos los controladores que usan el parcial o incorporar la lógica en el propio parcial.

Los parciales SÍ previenen los errores de duplicación de copiar/pegar y funcionan bien las primeras veces que necesita reutilizar algo. Pero según nuestra experiencia, los parciales pronto se llenan de soporte para más y 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 de que aumentara la popularidad en los marcos 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 realmente no tiene listo para usar. De hecho, hemos estado usando Cells en la aplicación Flywheel desde hace un tiempo, solo que no a una escala global súper reutilizable.

En el nivel más simple, las celdas nos permiten abstraer una parte del marcado como este (usamos Haml para nuestro lenguaje de plantillas):

%div
  %h1 Hello, world!

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

= cell("hello_world")

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

Construyendo Células

Ponemos todas nuestras celdas de interfaz de usuario en un directorio app/cells/ui. Cada celda debe contener solo un archivo Ruby, con el sufijo _cell.rb. Técnicamente, puede 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 vive en una carpeta nombrada por el componente.

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

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

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 (aplicamos todas nuestras celdas de UI al módulo de UI).

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

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

Muchas veces moveremos 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.

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

Envolviendo celdas con una utilidad de interfaz de usuario

Después de probar el concepto de que esto podría funcionar a gran escala, quería abordar el marcado externo necesario para llamar a un celular. Simplemente no fluye del todo bien y es difícil de recordar. ¡Así que hicimos un pequeño ayudante para eso! Ahora podemos simplemente llamar a = ui “nombre_de_componente” y pasar las opciones en línea.

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

Pasando 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. Aquí hay un ejemplo de una celda con muchas opciones definidas en línea:

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

Es muy engorroso, lo que nos lleva a crear una clase llamada OptionProxy que intercepta los métodos setter de Cells y los traduce en valores hash, que luego se fusionan en opciones. Si eso suena complicado, no se preocupe, también lo es para mí. Aquí hay una idea general de la clase OptionProxy que escribió Adam, uno de nuestros ingenieros de software sénior.

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

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

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

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Introducción a la lógica

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

Siguiendo con nuestro componente de listones, a veces tenemos la necesidad de representar todo como un enlace y, a veces, representarlo como un div, en función de si hay una opción de enlace presente o no. Creo que este es el único componente que tenemos que se puede representar como un div o un enlace, pero es un buen ejemplo del poder de Cells.

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

Nota: Esto fue inspirado y creado por Adam Lassek, quien tuvo una gran influencia para ayudarnos a construir todo este método de desarrollo de interfaz de usuario con Cells.

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

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

.slat
  = container do
  ...

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

= ui "slat" do |slat|
  ...
  - slat.disabled = true

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

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

Tradicionalmente, habríamos tenido que recordar (o hacer referencia a 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: posibles_clases 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 de celda es extremadamente útil para nuestra aplicación particular y la forma en que trabajamos, sería negligente decir que ha resuelto el 100 % de nuestros problemas. Todavía escribimos JavaScript (mucho) y construimos bastantes experiencias en Vue a lo largo de nuestra aplicación. El 75 % de las veces, nuestra plantilla de Vue aún vive en Haml y vinculamos nuestras instancias de Vue al elemento contenedor, lo que nos permite aprovechar 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% creados con Vue y otros con Cells. Si un componente se construye con Vue, significa que se requiere JavaScript para construirlo en el DOM y aprovechamos el marco de trabajo de Vue para hacerlo. Sin embargo, para la mayoría de nuestros otros componentes, no requieren JavaScript y, si lo requieren, requieren que el DOM ya esté construido y simplemente conectamos y agregamos detectores de eventos.

A medida que avanzamos con el enfoque de celda, definitivamente vamos a experimentar con la combinación de componentes de celda y componentes de Vue para que tengamos una y solo una forma de crear y usar componentes. Todavía no sé qué aspecto tiene, ¡así que cruzaremos ese puente cuando lleguemos allí!


Nuestra conclusión

Hasta ahora, hemos convertido una treintena de nuestros componentes visuales más utilizados en Cells. Nos ha brindado una gran explosión de productividad y les da a los desarrolladores una sensación de validación de que las experiencias que están creando son correctas y no están manipuladas.

Nuestro equipo de diseño confía más 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 enfocado y libre de preocupaciones sobre cómo ajustar un componente para que coincida con una maqueta de diseño.

Estamos iterando constantemente en 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 a Flywheel!

En Flywheel, todos y cada uno de los departamentos tienen un impacto significativo en nuestros clientes y 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 alojamiento de la que la gente realmente se enamore.

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