Fechando a série

Este é o último post da série sobre a ElementComponent. Depois de cobrir a API core, hooks/cache/Rails e os componentes de conteúdo e navegação, chegamos aos componentes interativos mais elaborados — Modal, Carousel e Dropdown — e ao fluxo de desenvolvimento do projeto.

CloseButton

O mais simples da lista, usado internamente por Alert, Modal e outros:

btn = ElementComponent::Components::CloseButton.new
# => <button class="btn-close" type="button" aria-label="Close">

disabled = ElementComponent::Components::CloseButton.new(disabled: true)
# => <button class="btn-close" type="button" aria-label="Close" disabled>

O Modal é o componente mais profundamente aninhado da gem, com seis sub-componentes trabalhando juntos:

modal = ElementComponent::Components::Modal.new(id: "exampleModal") do |m|
  m.add_content(ElementComponent::Components::ModalContent.new do |content|
    content.add_content(ElementComponent::Components::ModalHeader.new do |header|
      header.add_content(ElementComponent::Components::ModalTitle.new { |t| t.add_content("Modal title") })
    end)
    content.add_content(ElementComponent::Components::ModalBody.new { |body| body.add_content("Modal body text.") })
    content.add_content(ElementComponent::Components::ModalFooter.new do |footer|
      footer.add_content(ElementComponent::Components::Button.new(variant: :secondary) { |b| b.add_content("Close") })
      footer.add_content(ElementComponent::Components::Button.new(variant: :primary) { |b| b.add_content("Save") })
    end)
  end)
end

Opções: fade, static, scrollable, centered, size (sm/lg/xl), fullscreen.

Sub-componente Tag Classe CSS
ModalDialog <div> .modal-dialog
ModalContent <div> .modal-content
ModalHeader <div> .modal-header
ModalTitle <h5> .modal-title
ModalBody <div> .modal-body
ModalFooter <div> .modal-footer

Note que os botões do rodapé são o próprio componente Button da parte 1 da série — reforçando o padrão de composição que atravessa toda a gem.

carousel = ElementComponent::Components::Carousel.new(id: "slides") do |c|
  c.add_content(ElementComponent::Components::CarouselItem.new(active: true) do |item|
    item.add_content(%(<img src="slide1.jpg" class="d-block w-100" alt="...">))
  end)
  c.add_content(ElementComponent::Components::CarouselItem.new do |item|
    item.add_content(%(<img src="slide2.jpg" class="d-block w-100" alt="...">))
  end)
end

Opções: fade (crossfade), indicators, controls — indicadores e controles de navegação são gerados automaticamente a partir dos itens adicionados, sem precisar montá-los manualmente.

Sub-componente Tag Classe CSS
CarouselItem <div> .carousel-item
CarouselCaption <div> .carousel-caption

Vale mencionar que o Carousel teve um changeset dedicado só para correções (fix-carousel-component, registrado no histórico de specs do OpenSpec do projeto) — é o componente mais sensível a estado (item ativo, índices de indicadores) da gem.

dropdown = ElementComponent::Components::Dropdown.new do |d|
  d.add_content(
    ElementComponent::Element.new("button",
      class: "btn btn-secondary dropdown-toggle",
      type: "button",
      "data-bs-toggle": "dropdown",
      "aria-expanded": "false") { |b| b.add_content("Dropdown") }
  )
  d.add_content(
    ElementComponent::Components::DropdownMenu.new do |menu|
      menu.add_content(ElementComponent::Components::DropdownItem.new { |i| i.add_content("Action") })
      menu.add_content(ElementComponent::Components::DropdownItem.new(active: true) { |i| i.add_content("Active") })
      menu.add_content(ElementComponent::Components::DropdownDivider.new)
      menu.add_content(ElementComponent::Components::DropdownItem.new(disabled: true) { |i| i.add_content("Disabled") })
    end
  )
end

Opções: direction (dropup/dropend/dropstart).

Sub-componente Tag Classe CSS
DropdownMenu <ul> .dropdown-menu
DropdownItem <li><a>/<button> .dropdown-item
DropdownDivider <li><hr> .dropdown-divider
DropdownHeader <li><h6> .dropdown-header

Este exemplo é o único do catálogo onde o botão de acionamento é montado com Element.new puro em vez de um sub-componente dedicado — mostra bem como a API core e os componentes prontos convivem no mesmo código sem fricção.

Desenvolvimento e contribuição

O projeto segue um fluxo padrão de gem Ruby, com RSpec para testes e RuboCop para lint:

bin/setup              # instala dependências
bundle exec rspec      # roda os testes
bundle exec rubocop    # lint
bundle exec rake       # specs + rubocop
bin/console             # console interativo
ruby examples/alert_example.rb  # roda exemplos isolados por componente

Cada componente tem um arquivo de exemplo correspondente em examples/ e um spec dedicado em spec/lib/components/, o que facilita tanto aprender a API quanto garantir cobertura ao alterar um componente. Para rodar com relatório de cobertura:

COVERAGE=true bundle exec rspec

O processo de release é simples: atualizar lib/element_component/version.rb e rodar bundle exec rake release, ou empurrar uma tag de versão (vX.Y.Z) para disparar o workflow de release automatizado no GitHub Actions.

Curiosamente, o repositório também usa OpenSpec para documentar mudanças arquiteturais maiores (como a correção do Carousel e a adição de exemplos) como propostas versionadas antes de virarem código — um processo de design mais formal do que o típico fluxo de issues do GitHub.

Roadmap

Segundo o README, dois itens seguem em aberto:

  • Adicionar um identificador automático ao adicionar componentes via add_content.
  • Corrigir o componente Button, adicionando a opção de atributo type (:button/:submit) no initialize.

Encerrando a série

Com isso fechamos o tour completo pela ElementComponent: da API core de construção de HTML, passando por segurança, cache e integração com Rails, até os 17 componentes Bootstrap 5 e o fluxo de contribuição do projeto. É uma gem enxuta que resolve um problema comum — montar HTML complexo em Ruby sem recorrer a template engines — com uma API consistente do início ao fim.