Como construímos componentes de UI em Rails

Publicados: 2024-06-28

Manter a consistência visual em uma grande aplicação web é um problema compartilhado por muitas organizações. A principal aplicação web por trás do nosso produto Flywheel é construída com Ruby on Rails, e temos vários desenvolvedores Rails e três desenvolvedores front-end comprometendo código nele em um determinado dia. Também somos grandes em design (é um dos nossos valores fundamentais como empresa) e temos três designers que trabalham em estreita colaboração com os desenvolvedores em nossas equipes Scrum.

duas pessoas colaboram no design de um site

Um dos nossos principais objetivos é garantir que qualquer desenvolvedor possa construir uma página responsiva sem quaisquer obstáculos. Os obstáculos geralmente incluem não saber quais componentes existentes usar para construir uma maquete (o que leva a inflar a base de código com componentes redundantes muito semelhantes) e não saber quando discutir a reutilização com os designers. Isso contribui para experiências inconsistentes dos clientes, frustração dos desenvolvedores e uma linguagem de design díspar entre desenvolvedores e designers.

Passamos por várias iterações de guias de estilo e métodos de construção/manutenção de padrões e componentes de UI, e cada iteração ajudou a resolver os problemas que enfrentávamos naquele momento. Estamos confiantes de que nossa nova abordagem nos preparará por muito tempo. Se você enfrentar problemas semelhantes em sua aplicação Rails e quiser abordar os componentes do lado do servidor, espero que este artigo possa lhe dar algumas idéias.

um homem barbudo sorri para a câmera enquanto está sentado em frente a um monitor de computador que exibe linhas de código

Neste artigo, vou me aprofundar em:

  • O que estamos resolvendo
  • Restringindo componentes
  • Renderizando componentes no lado do servidor
  • Onde não podemos usar componentes do lado do servidor

O que estamos resolvendo

Queríamos restringir completamente nossos componentes de UI e eliminar a possibilidade de a mesma UI ser criada de mais de uma maneira. Embora um cliente possa não ser capaz de saber (a princípio), não ter restrições nos componentes leva a uma experiência confusa para o desenvolvedor, torna as coisas muito difíceis de manter e dificulta a realização de alterações globais no design.

A maneira tradicional como abordamos os componentes foi por meio de nosso guia de estilo, que listava toda a marcação necessária para construir um determinado componente. Por exemplo, esta é a aparência da página do guia de estilo do nosso componente slat:

página do guia de estilo para componente slat

Isso funcionou bem por vários anos, mas os problemas começaram a surgir quando adicionamos variantes, estados ou formas alternativas de usar o componente. Com uma interface de usuário complexa, tornou-se complicado consultar o guia de estilo para saber quais classes usar e quais evitar, e em que ordem a marcação precisava estar para gerar a variação desejada.

Muitas vezes, os designers faziam pequenos acréscimos ou ajustes em um determinado componente. Como o guia de estilo não suportava isso, hacks alternativos para fazer com que esse ajuste fosse exibido corretamente (como canibalizar inapropriadamente parte de outro componente) tornaram-se irritantemente comuns.

Exemplo de componente irrestrito

Para ilustrar como as inconsistências surgem ao longo do tempo, usarei um exemplo simples (e inventado), mas muito comum, de um de nossos componentes no aplicativo Flywheel: cabeçalhos de cartão.

Começando com uma maquete de design, esta era a aparência de um cabeçalho de cartão. Era bem simples, com um título, um botão e uma borda inferior.

 .card__header
  .card__header-esquerdo
    %h2 Backups
  .card__header-direita
    = link_to "#" faça
      = ícone("mais_pequeno")

Depois de codificado, imagine um designer querendo adicionar um ícone à esquerda do título. Fora da caixa, não haverá margem entre o ícone e o título.

 ...
  .card__header-esquerdo
    = ícone("seta_backup", cor: "cinza25")
    %h2 Backups
...

Idealmente, resolveríamos isso no CSS para cabeçalhos de cartão, mas para este exemplo, digamos que outro desenvolvedor pensou “Ah, eu sei! Temos alguns ajudantes de margem. Vou apenas dar um tapa no título para uma classe auxiliar.

 ...
  .card__header-esquerdo
    = ícone("seta_backup", cor: "cinza25")
    %h2.--ml-10 Backups
...

Bem, isso tecnicamente se parece com a maquete, certo?! Claro, mas digamos que um mês depois outro desenvolvedor precise de um cabeçalho de cartão, mas sem o ícone. Eles encontram o último exemplo, copiam/colam e simplesmente removem o ícone.

Novamente parece correto, certo? Fora do contexto, para quem não tem um olhar apurado para design, claro! Mas olhe ao lado do original. Essa margem esquerda no título ainda está lá porque eles não perceberam que a margem esquerda auxiliar precisava ser removida!

Levando este exemplo um passo adiante, digamos que outra maquete solicitasse um cabeçalho de cartão sem borda inferior. Pode-se encontrar um estado que temos no guia de estilo chamado “sem fronteiras” e aplicá-lo. Perfeito!

Outro desenvolvedor pode então tentar reutilizar esse código, mas neste caso, ele realmente precisa de uma borda. Digamos hipoteticamente que eles ignoram o uso adequado documentado no guia de estilo e não percebem que a remoção da classe sem borda lhes dará sua borda. Em vez disso, eles adicionam uma regra horizontal. Acaba havendo algum preenchimento extra entre o título e a borda, então eles aplicam uma classe auxiliar ao hr e pronto!

Com todas essas modificações no cabeçalho original do cartão, agora temos uma bagunça no código.

 .card__header.--sem borda
  .card__header-esquerdo
    %h2.--ml-10 Backups
  .card__header-direita
    = link_to "#" faça
      = ícone("mais_pequeno")
  %h.--mt-0.--mb-0

Tenha em mente que o exemplo acima é apenas para ilustrar como os componentes irrestritos podem se tornar confusos com o tempo. Se alguém em nossa equipe tentar enviar uma variação do cabeçalho de um cartão, isso deverá ser detectado por uma revisão de design ou de código. Mas coisas como essa às vezes escapam, daí a nossa necessidade de tornar as coisas à prova de balas!


Restringindo Componentes

Você pode estar pensando que os problemas listados acima já foram claramente resolvidos com componentes. Essa é uma suposição correta! Frameworks front-end como React e Vue são muito populares exatamente para esse propósito; são ferramentas incríveis para encapsular a UI. No entanto, há um problema com eles que nem sempre gostamos: eles exigem que sua IU seja renderizada por JavaScript.

Nosso aplicativo Flywheel é muito pesado em termos de back-end, principalmente com HTML renderizado pelo servidor - mas, felizmente para nós, os componentes podem vir de várias formas. No final das contas, um componente de UI é um encapsulamento de estilos e regras de design que gera marcação para um navegador. Com essa percepção, podemos adotar a mesma abordagem para os componentes, mas sem a sobrecarga de uma estrutura JavaScript.

Veremos como construímos componentes restritos abaixo, mas aqui estão alguns dos benefícios que encontramos ao usá-los:

  • Nunca existe uma maneira errada de montar um componente.
  • O componente faz todo o design thinking para você. (Basta passar as opções!)
  • A sintaxe para criar um componente é muito consistente e fácil de raciocinar.
  • Se for necessária uma alteração no design de um componente, podemos alterá-la uma vez no componente e ter certeza de que ela será atualizada em todos os lugares.

Renderizando componentes no lado do servidor

Então, do que estamos falando ao restringir componentes? Vamos cavar!

Conforme mencionado anteriormente, queremos que qualquer desenvolvedor que trabalhe no aplicativo seja capaz de olhar para uma maquete de design de uma página e construir imediatamente essa página sem impedimentos. Isso significa que o método de criação da UI deve ser A) muito bem documentado e B) muito declarativo e livre de suposições.

Parciais para o resgate (ou assim pensávamos)

Uma primeira tentativa que tentamos no passado foi usar parciais do Rails. Parciais são a única ferramenta que o Rails oferece para reutilização em modelos. Naturalmente, eles são a primeira coisa que todos buscam. Mas há desvantagens significativas em confiar neles porque se você precisar combinar a lógica com um modelo reutilizável, você terá duas opções: duplicar a lógica em cada controlador que usa o parcial ou incorporar a lógica no próprio parcial.

Parciais evitam erros de duplicação de copiar/colar e funcionam bem nas primeiras vezes em que você precisa reutilizar algo. Mas, pela nossa experiência, os parciais logo ficam confusos com suporte para cada vez mais funcionalidade e lógica. Mas a lógica não deveria viver em modelos!

Introdução às células

Felizmente, existe uma alternativa melhor aos parciais que nos permite reutilizar o código e manter a lógica fora da visualização. Chama-se Cells, uma joia Ruby desenvolvida pela Trailblazer. As células já existiam muito antes do aumento da popularidade em estruturas de front-end como React e Vue e permitem escrever modelos de visualização encapsulados que lidam com lógica e modelos. Eles fornecem uma abstração de modelo de visualização, que o Rails simplesmente não possui imediatamente. Na verdade, já usamos Cells no aplicativo Flywheel há algum tempo, mas não em uma escala global e superreutilizável.

No nível mais simples, Cells nos permite abstrair um pedaço de marcação como este (usamos Haml para nossa linguagem de modelagem):

 %div
  %h1 Olá, mundo!

Em um modelo de visualização reutilizável (muito semelhante aos parciais neste ponto) e transforme-o assim:

 =célula("olá_mundo")

Em última análise, isso nos ajuda a restringir o componente onde classes auxiliares ou componentes filhos incorretos não podem ser adicionados sem modificar a própria célula.

Construindo Células

Colocamos todas as nossas UI Cells em um diretório app/cells/ui. Cada célula deve conter apenas um arquivo Ruby, com o sufixo _cell.rb. Tecnicamente, você pode escrever os modelos diretamente em Ruby com o auxiliar content_tag, mas a maioria de nossas células também contém um modelo Haml correspondente que fica em uma pasta nomeada pelo componente.

Uma célula super básica sem lógica se parece com isto:

 //células/ui/slat_cell.rb
interface do módulo
  classe SlatCell <ViewModel
    definitivamente mostrar
    fim
  fim
fim

O método show é o que é renderizado quando você instancia a célula e procurará automaticamente um arquivo show.haml correspondente na pasta com o mesmo nome da célula. Neste caso, é app/cells/ui/slat (escopo todas as nossas células UI para o módulo UI).

No template, você pode acessar as opções passadas para a célula. Por exemplo, se a célula for instanciada em uma view como = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”), podemos acessar essas opções através do objeto options.

 //células/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= opções[:título]
      %p= opções[:subtítulo]
      = ícone(opções[:ícone], cor: "azul")

Muitas vezes moveremos elementos simples e seus valores para um método na célula para evitar que elementos vazios sejam renderizados se uma opção não estiver presente.

 //células/ui/slat_cell.rb
título de definição
  retorne a menos que opções[:título]
  content_tag:h4, opções[:título]
fim
legenda def
  retorne a menos que opções[:subtitle]
  content_tag :p, opções[:subtitle]
fim
 //células/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = título
      = legenda

Envolvendo células com um utilitário de UI

Depois de provar o conceito de que isso poderia funcionar em larga escala, eu quis abordar a marcação externa necessária para chamar uma célula. Simplesmente não flui direito e é difícil de lembrar. Então fizemos um ajudante para isso! Agora podemos apenas chamar = ui “name_of_component” e passar as opções inline.

 = ui "slat", título: "Título", subtítulo: "Subtítulo", rótulo: "Rótulo"

Passando opções como um bloco em vez de inline

Levando o utilitário UI um pouco mais longe, rapidamente ficou claro que uma célula com um monte de opções em uma linha seria muito difícil de seguir e simplesmente feia. Aqui está um exemplo de célula com muitas opções definidas in-line:

 = ui “slat", título: “Título”, subtítulo: “Subtítulo”, rótulo: “Rótulo”, link: “#”, tertiary_title: “Terciário”, desativado: verdadeiro, lista de verificação: [“Item 1”, “Item 2”, “Item 3”]

É muito complicado, o que nos leva a criar uma classe chamada OptionProxy que intercepta os métodos setter de Cells e os traduz em valores hash, que são então mesclados em opções. Se isso parece complicado, não se preocupe – é complicado para mim também. Aqui está uma visão geral da classe OptionProxy que Adam, um de nossos engenheiros de software sênior, escreveu.

Aqui está um exemplo de uso da classe OptionProxy dentro de nossa célula:

 interface do módulo
  classe SlatCell <ViewModel
    definitivamente mostrar
      OpçãoProxy.new(self).yield!(opções, &bloco)
      super()
    fim
  fim
fim

Agora com isso implementado, podemos transformar nossas complicadas opções inline em um bloco mais agradável!

 = ui "slat" do |slat|
  -slat.title = "Título"
  -slat.subtitle = "Legenda"
  - slat.label = "Etiqueta"
  -slat.link = "#"
  - slat.tertiary_title = "Terciário"
  -slat.disabled = verdadeiro
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Apresentando Lógica

Até este ponto, os exemplos não incluíram nenhuma lógica em torno do que a visualização exibe. Essa é uma das melhores coisas que o Cells oferece, então vamos conversar sobre isso!

Mantendo nosso componente slat, às vezes precisamos renderizar tudo como um link e às vezes como um div, com base na presença ou não de uma opção de link. Acredito que este é o único componente que temos que pode ser renderizado como um div ou um link, mas é um bom exemplo do poder das células.

O método abaixo chama um auxiliar link_to ou content_tag dependendo da presença de opções [:link] .

 def contêiner(&bloco)
  etiqueta =
    se opções[:link]
      [:link_to, opções[:link]]
    outro
      [:content_tag,:div]
    fim
  enviar(*tag, classe: “slat__inner”, &bloco)
fim

Isso nos permite substituir o elemento .slat__inner no modelo por um bloco contêiner:

 .slat
  = recipiente fazer
  ...

Outro exemplo de lógica em Cells que usamos muito é a saída condicional de classes. Digamos que adicionamos uma opção desabilitada à célula. Nada mais muda na invocação da célula, a não ser que agora você pode passar uma opção disabled: true e observar como tudo se transforma em um estado desabilitado (esmaecido com links não clicáveis).

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

Quando a opção desabilitada for verdadeira, podemos definir classes nos elementos do modelo que são necessários para obter a aparência desabilitada desejada.

 .slat{ classe: Possible_classes("--disabled": opções[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ classe: Possible_classes("--alt": opções[:disabled]) }= opções[:title]
      %p{classe: Possible_classes("--alt": opções[:disabled]) }=
      opções[:subtítulo]
      = ícone(opções[:ícone], cor: "cinza")

Tradicionalmente, teríamos que lembrar (ou consultar o guia de estilo) quais elementos individuais precisavam de classes adicionais para fazer tudo funcionar corretamente no estado desativado. As células nos permitem declarar uma opção e então fazer o trabalho pesado para nós.

Nota: Possible_classes é um método que criamos para permitir a aplicação condicional de classes em Haml de uma maneira agradável.


Onde não podemos usar componentes do lado do servidor

Embora a abordagem celular seja extremamente útil para nossa aplicação específica e para a maneira como trabalhamos, seria negligente dizer que ela resolveu 100% dos nossos problemas. Ainda escrevemos JavaScript (muito) e construímos algumas experiências em Vue em todo o nosso aplicativo. 75% do tempo, nosso modelo Vue ainda reside em Haml e vinculamos nossas instâncias Vue ao elemento que o contém, o que ainda nos permite aproveitar as vantagens da abordagem de célula.

No entanto, em lugares onde faz mais sentido restringir completamente um componente como uma instância Vue de arquivo único, não podemos usar Cells. Nossas listas de seleção, por exemplo, são todas Vue. Mas acho que está tudo bem! Na verdade, não tivemos a necessidade de ter versões duplicadas de componentes nos componentes Cells e Vue, então está tudo bem que alguns componentes sejam 100% construídos com Vue e outros com Cells.

Se um componente for construído com Vue, significa que é necessário JavaScript para construí-lo no DOM e aproveitamos o framework Vue para fazer isso. Porém, para a maioria de nossos outros componentes, eles não exigem JavaScript e, se precisarem, exigem que o DOM já esteja construído e nós apenas conectamos e adicionamos ouvintes de eventos.

À medida que continuamos progredindo com a abordagem celular, definitivamente experimentaremos a combinação de componentes celulares e componentes Vue para que tenhamos uma e apenas uma maneira de criar e usar componentes. Ainda não sei como é, então cruzaremos aquela ponte quando chegarmos lá!


Nossa conclusão

Até agora, convertemos cerca de trinta dos nossos componentes visuais mais usados ​​em células. Isso nos proporcionou uma enorme explosão de produtividade e dá aos desenvolvedores uma sensação de validação de que as experiências que estão construindo estão corretas e não foram hackeadas em conjunto.

Nossa equipe de design está mais confiante do que nunca de que os componentes e as experiências em nosso aplicativo são iguais ao que foram projetados no Adobe XD. Alterações ou adições a componentes agora são tratadas exclusivamente por meio de uma interação com um designer e desenvolvedor front-end, o que mantém o restante da equipe focado e livre de preocupações em saber como ajustar um componente para corresponder a um modelo de design.

Estamos constantemente iterando nossa abordagem para restringir componentes de UI, mas espero que as técnicas ilustradas neste artigo lhe dêem uma ideia do que está funcionando bem para nós!


Venha trabalhar conosco!

Cada departamento que trabalha com nossos produtos tem um impacto significativo em nossos clientes e nos resultados financeiros. Quer se trate de suporte ao cliente, desenvolvimento de software, marketing ou qualquer coisa intermediária, estamos todos trabalhando juntos em direção à nossa missão de construir uma empresa de hospedagem pela qual as pessoas possam realmente se apaixonar.

Pronto para se juntar à nossa equipe? Estamos contratando! Inscreva-se aqui.