CSS

12 ago, 2015

Estendendo Estilos

Publicidade

Recentemente, o @simurai escreveu um ótimo artigo discutindo as várias estratégias para o estilo contextual em CSS. Se ainda não leu, você deveria – ele vai te dar um contexto mais completo para esta leitura, e é provável que você aprenda algo que não sabia.

O problema? Qual é a melhor maneira de abordar a alteração da aparência de um componente quando ele é descendente de outro componente?

O exemplo que ele usa é um botão que deve renderizar de forma diferente quando está dentro do cabeçalho. No artigo, @simurai delineia uma série de abordagens mais comuns, avalia os prós e contras de cada uma e, em seguida, afirma que ele não tem certeza de que existe uma opção clara. Ele conclui o artigo abrindo-o para que a comunidade possa comentar, na esperança de que um consenso possa ser alcançado.

Enquanto eu compartilho o desejo dele de encontrar a melhor estratégia (e tenho uma opinião sobre o assunto), acredito que é realmente mais importante discutir como alguém pode responder a essa pergunta e não o que a resposta real pode ser. Se você entender o como e o porquê, vai estar mais preparado para responder a perguntas semelhantes no futuro.

Critérios para a escolha

O ponto em estender estilos é a reutilização de código. Caso tenha definido alguns estilos de nível básico, você vai querer ser capaz de usar esses estilos de novo sem ter que reescrevê-los. E se precisar alterá-los em nível de base, você vai quer que essas alterações sejam propagadas por toda parte.

A reutilização de código é fácil. Mas a reutilização de código de uma maneira que seja previsível, sustentável e escalável é difícil. Felizmente, os cientistas da computação têm estudado esses problemas há décadas, e muitos dos princípios do bom design de software se aplica a CSS também.

Adesão aos princípios de design de software

Todas as opções que o @simurai relaciona no seu artigo são exemplos da modificação de uma declaração de estilo e da sua extensão. Quando somos apresentados a essas duas opções, podemos ouvir os conselhos oferecidos pelo princípio Open/Close (SOLID) de desenvolvimento de software. Ele afirma: As entidades de software (classes, módulos, funções etc.) devem estar abertas para a extensão, mas fechadas para a modificação.

Para entender o que isso significa no contexto de componentes CSS, é importante definir os termos extensão e modificação.

Modificar um componente significa que você vai mudar a definição de seu estilo – suas propriedades e seus valores. Estender um componente, por outro lado, significa que você vai pegar um componente já existente e construir em cima dele. Você não muda a definição do componente existente; em vez disso, você cria um novo que inclui os estilos originais e adiciona novos estilos (ou substituições) em cima deles.

Existem duas razões principais pelas quais os componentes devem ser estendidos em vez de modificados. Primeiramente, quando você modifica um componente, você quebra seu contrato e as expectativas dos desenvolvedores familiarizados com ele. Você também corre o risco de quebrar seu projeto existente. Para sites pequenos, esse risco é provavelmente mínimo, mas para os grandes sites com muitos componentes, você pode nem sempre saber a extensão completa de como todos os seus estilos são usados.

Uma segunda razão para preferir a extensão sobre a modificação é que, quando você modifica um componente, você limita suas opções daqui para frente. Você não pode mais usar esse componente em sua forma pré-modificada.

Compatibilidade com tecnologias futuras

Outro critério importante a se levar em consideração nas nossas opções e escolhas de melhores práticas é a forma como essas práticas se alinharão com as tecnologias futuras. Escrever CSS modular, hoje, é um desafio, porque a plataforma web não suporta um monte de recursos que viemos a desfrutar em outros ambientes que promovem o desenvolvimento modular. Mas esse não será sempre o caso.

À medida que a web evolui, vai se ficar cada vez mais fácil escrever CSS sem ter que se preocupar com todas as complicações e os efeitos colaterais que vêm de todas as regras existentes no âmbito global. Por isso, precisamos ter certeza de que as nossas escolhas de hoje não vão nos bloquear para a tecnologia ultrapassada amanhã.

Os Web Components nos dão soluções reais para quase todos os problemas que tornam difícil escrever CSS modular. E agora que todos os principais fabricantes de navegadores chegaram a um consenso sobre as partes controversas da especificação e concordaram em avançar com a implementação, nós, como desenvolvedores web, precisamos começar a pensar sobre como nossas metodologias atuais se encaixam nesse futuro.

Com essas coisas em mente, vamos considerar as opções atuais.

Opção 1 – combinador de descendente

A opção 1 é um exemplo clássico de modificação de componente – o que o princípio Open/Close diz para não fazer.

.Header .Button {
  font-size: .75em;
}

Neste exemplo, o componente .Button é definido em outro lugar na folha de estilo e, em seguida, é redefinido (modificado) aqui para todos os casos em que .Button aparece como um descendente de .Header.

Como eu mencionei acima, essa prática pode ser muito problemática. Isso torna o componente .Button menos previsível porque pode agora renderizar de forma diferente, dependendo de onde ele vive no HTML. Alguém da equipe que usou .Button no passado pode querer usá-lo novamente, mas não saber que a sua definição foi alterada fora do seu arquivo de origem.

Além disso, esta opção é limitada. Ela resolve o problema na mão, mas limita suas opções para a utilização do componente .Button no futuro. E se um novo recurso que requer botões adicionais no cabeçalho for adicionado, e os novos botões precisarem se parecer com o que o .Button fez antes de ser modificado? Como esta abordagem altera a definição de .Button, os seus estilos pré-modificados não podem mais ser usados dentro .Header, e a refatoração vai ter que acontecer, aumentando o risco de erros.

Opção 2 – Variações

Em BEM, esta opção é chamada de “modificador” (o “M” em BEM), e em SMACSS é chamada de “subclasse”. Note que, apesar de ser chamada um modificador em BEM, não é uma modificação que o princípio abrir/fechar adverte contra.

.Button--small {
  font-size: .75em;
}
<header class="Header">
  <button class="Button Button--small">Download</button>
</header>

Ao usar esta opção, você não altera a definição de estilo original, então você ainda é capaz de usar o componente original .Button dentro de .Header.

Opção 3 – filhos adotados

Com a opção filhos adotados (ou mixes, como é chamada em BEM), você denomina um elemento com duas classes de dois componentes diferentes.

Embora eu use esse padrão no meu próprio código de vez em quando, ele sempre me deixou um pouco desconfortável. O problema com esta abordagem é que, se duas ou mais classes são aplicadas ao mesmo elemento, e elas contêm algumas das mesmas declarações de propriedades, o seletor mais específico vai ganhar. Às vezes isso funciona exatamente como você deseja, mas às vezes não, e você tem que recorrer a hacks de especificidade (como você pode ver no exemplo fornecido).

Em header.css:

/*
 * Increased specificity needed so this class will win
 * when used on elements with the class "Button".
 */
.Header .Header-item {
  font-size: .75em;
}

E em button.css:

.Button {
  font-size: 1em;
}

Embora, às vezes, um comentário como o no header.css acima faça o truque, ele definitivamente não é uma solução à prova de idiota.

Sempre que você colocar mais de uma classe em um elemento, essas classes se combinam para formar o estado final renderizado. Com os modificadores, isso não é realmente um problema, porque as duas classes são definidas no mesmo arquivo, então as preferências em cascata podem ser gerenciadas facilmente por ordem de origem.

Por outro lado, ao adicionar duas classes a um elemento e essas classes forem definidas em arquivos diferentes, é aí que você esbarra nos problemas. Na maioria das vezes, tem uma classe “base” e uma ou mais classes para “estender”, e nesses casos eu acho que faz mais sentido fazer a relação explícita e as dependências claras. Falarei mais sobre isso na opção 4.

Opção 4 – @extend

A maioria dos pré-processadores CSS hoje suporta algum método de estender os estilos existentes. Na verdade, isso pode em breve ser suportado no CSS nativamente se a proposta de regra estend for aprovada.

E a maioria dos pré-processadores também suporta a declaração de dependências por meio de declarações import ou include, o que ajuda a garantir seus estilos em cascata corretamente, forçando a ordem da fonte correta em tempo de compilação.

@import './button.css';

.PromoButton {
  @extend .Button;
  /* Additional styles... */
}
<header class="Header">
  <button class="PromoButton">Download</button>
</header>

O que é legal sobre esta abordagem é que está claro para outros desenvolvedores que .PromoButton inclui os estilos de .Button, e está claro para o pré-processador (ou sistema de compilação) que button.css precisa ser incluído antes de promo-button.css quando a folha de estilo final for criada.

Se você estava usando as abordagens de mixes acima e incluindo duas ou mais classes em um único elemento HTML, o @extend pode ser uma maneira muito útil para a construção de um novo componente proveniente das partes, ao mesmo tempo em que garante que a ordem de origem está correta. No exemplo a seguir, todos os estilos aparecerão na ordem em que são importados [1].

@import './button.css';
@import './header.css';

.PromoButton {
  @extend .Button;
  @extend .Header-item;
  /* Optional additional styles... */
}

Considerações de Web Components

A principal forma como uma futura mudança para os Web Components afetará essa discussão é que os elementos de estilo deixarão de ser simplesmente uma função de adicionar classes para elementos ou seletores para suas folhas de estilo.

Com os Web Components (especificamente Shadow DOM), os únicos estilos que podem afetar o funcionamento interno de um elemento são os que o autor do componente empacotou dentro desse elemento. Da mesma forma, a única maneira de um contexto pai poder afetar o estilo de um elemento é se o autor do componente explicitamente o fizer [2].

Isso significa que se você usar as opções 1 ou 3 agora, vai ser um pouco mais difícil de fazer a transição do seu código para usar os Web Components. A opção 1 nunca vai ser capaz de trabalhar com componentes de terceiros (já que não é possível prever a sua estrutura de HTML com antecedência), e adicionar uma lista de classes a um elemento personalizado (opção 3) afetará somente esse elemento em particular. Não vai afetar seus descendentes.

As opções 2 e 4 são muito mais amigáveis para Web Components, porque se assemelham mais a um modelo de componente único. Os Web Components encapsulam estilos e funcionalidade internamente, e eles expõem isso para os desenvolvedores como um único elemento HTML. Isso significa que os componentes são sempre uma coisa única, mesmo que nos bastidores eles sejam o resultado de um monte de pequenas coisas juntas.

Considere o seguinte HTML. Existe um componente Button que deve ser exibido como bloco e assumir toda a largura de seu recipiente. Ele também deve usar o tipo de letra do logotipo da empresa:

<button class="Button FullWidthBlock LogoType">Download</button>

Converter isso para um componente web da seguinte forma (semelhante à opção 3) não vai funcionar:

<promo-button class="FullWidthBlock LogoType">Download</promo-button>

Em vez disso, você teria que adicionar estes estilos para a shadow root, como parte interna (privada) da implementação do componente:

<!-- Pseudo Code -->
<promo-button>
  #shadow-root
    <style>
      @import './button.css';
      @import './full-width-block.css';
      @import './logo-type.css';
    </style>
    <button class="Button FullWidth LogoType">
      <content></content>
    </button>
  /#shadow-root
</promo-button>

Isso pode parecer mais trabalho, mas vai acabar sendo muito mais robusto e previsível. Esse componente irá se parecer sempre exatamente como você quiser, independentemente de onde ele apareça no HTML e que existam outros estilos na página.

Isso é muito semelhante ao uso do @extend, como mostra a opção 4. Se você usar esse padrão em seu código de hoje, será muito fácil fazer a transição dos seus componentes CSS para os Web Components no futuro.

Da mesma forma, a opção 2 (variações) também se encaixa muito bem no modelo de Web Components. No entanto, em vez de classes modificadoras, é provável que a gente defina os atributos do elemento que representam as diferentes variações de nossos componentes.

<!-- Using a BEM modifier -->
<button class="Button Button--small">Download</button>

<!-- Using a Web Component with an attribute for variation -->
<my-button small>Download</my-button>

Os atributos tornam-se parte da API pública para componentes de estilo, e somente os atributos aprovados é que irão afetar a sua aparência. Os atributos sem uma regra de estilo interna correspondente simplesmente não fazem nada.

Conclusões

Dadas todas as opções discutidas até agora, sou a favor da opção 2 para extensões de estilo simples e da opção 4 para qualquer coisa mais complexa.

Se o componente em questão precisar somente de uma pequena alteração em algum novo contexto, uma variação (modificador/subclasse) é, normalmente, mais simples e faz mais sentido. Por outro lado, se o componente em questão é realmente uma coisa própria, construída em cima de um componente base, exigindo uma hierarquia de herança multinível, ou compor vários estilos complexos em conjunto, talvez seja melhor fazer essas relações conhecidas através das declarações @extend e das dependências listadas explicitamente.

No geral, diante dessas decisões, é importante não só pensar sobre como resolver o problema de imediato. Você também deve considerar como as suas escolhas vão limitar suas opções no futuro. Você está codificando sozinho em um canto, ou está deixando um espaço para construir novas funcionalidades e se adaptar às exigências futuras do projeto?

Notas de rodapé:

[1] Tecnicamente, a maioria dos pré-processadores não vai garantir realmente a ordem correta da fonte com base na ordem das declarações `@import`; `@import`significa simplesmente *este arquivo deve existir na fonte antes que eu me inclua*. Na prática, no entanto, se todos os seus arquivos de componentes fizerem um `@ import` nas suas dependências na ordem correta, a ordem do estilo final também estará correta.

[2] Isso pode ser feito pelo seletor [`:host-context()`](http://dev.w3.org/csswg/css-scoping/#host-selector), embora, sem dúvida, seu uso deve ser evitado, principalmente por todas as razões listadas neste artigo.

***

Philip Walton faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://philipwalton.com/articles/extending-styles/