Práticas recomendadas para elementos personalizados

Com os elementos personalizados, você pode criar suas próprias tags HTML. Esta lista de verificação abrange as práticas recomendadas para ajudar você a criar elementos de alta qualidade.

Os elementos personalizados permitem estender o HTML e definir suas próprias tags. Eles são um recurso incrivelmente poderoso, mas também são de baixo nível, o que significa que nem sempre está claro a melhor forma de implementar seu próprio elemento.

Para ajudar você a criar as melhores experiências possíveis, elaboramos esta lista de verificação. Ele detalha tudo o que achamos necessário para ser um elemento personalizado com bom comportamento.

Lista de verificação

Shadow DOM

Criar uma raiz paralela para encapsular estilos.

Por quê? O encapsulamento de estilos na raiz paralela do elemento garante que eles funcionem independentemente de onde forem usados. Isso é especialmente importante se um desenvolvedor quiser colocar seu elemento dentro da raiz paralela de outro. Isso se aplica até mesmo a elementos simples, como uma caixa de seleção ou um botão de opção. Pode ser que o único conteúdo dentro da raiz paralela sejam os próprios estilos.
Exemplo O elemento <howto-checkbox>.

Crie sua raiz paralela no construtor.

Por quê? O construtor é quando você tem conhecimento exclusivo do elemento. Esse é um ótimo momento para configurar os detalhes de implementação sem que você queira que outros elementos me atrapalhem. Se você fizer isso em um callback posterior, como o connectedCallback, será necessário proteger contra situações em que seu elemento é removido e, em seguida, reanexado ao documento.
Exemplo O elemento <howto-checkbox>.

Coloque todos os filhos que o elemento criar na raiz paralela.

Por quê? Filhos criados pelo seu elemento fazem parte da implementação e precisam ser particulares. Sem a proteção de uma raiz paralela, o JavaScript externo pode interferir acidentalmente nesses filhos.
Exemplo O elemento <howto-tabs>.

Usar <slot> para projetar filhos do light DOM no seu shadow DOM

Por quê? Permitir que os usuários do componente especifiquem conteúdo nele porque os filhos HTML tornam o componente mais combinável. Quando um navegador não é compatível com elementos personalizados, o conteúdo aninhado permanece disponível, visível e acessível.
Exemplo O elemento <howto-tabs>.

Defina um estilo de exibição :host (por exemplo, block, inline-block, flex), a menos que você prefira o padrão de inline.

Por quê? Por padrão, os elementos personalizados são display: inline. Portanto, definir width ou height não terá efeito. Isso geralmente é uma surpresa para os desenvolvedores e pode causar problemas relacionados ao layout da página. A menos que você prefira uma tela inline, sempre defina um valor display padrão.
Exemplo O elemento <howto-checkbox>.

Adicione um estilo de exibição :host que respeite o atributo oculto.

Por quê? Um elemento personalizado com um estilo display padrão, por exemplo, :host { display: block }, vai substituir o atributo hidden integrado de especificidade mais baixa. Isso pode surpreender você se espera definir o atributo hidden no seu elemento para renderizá-lo como display: none. Além de um estilo display padrão, adicione suporte para hidden com :host([hidden]) { display: none }.
Exemplo O elemento <howto-checkbox>.

Atributos e propriedades

Não substitua atributos globais definidos pelo autor.

Por quê? Os atributos globais são aqueles presentes em todos os elementos HTML. Alguns exemplos incluem tabindex e role. Um elemento personalizado pode definir o tabindex inicial como 0 para que seja focalizável pelo teclado. No entanto, sempre verifique primeiro se o desenvolvedor que está usando o elemento definiu outro valor. Se, por exemplo, a definição tabindex foi definida como -1, é um sinal de que a pessoa não quer que o elemento seja interativo.
Exemplo O elemento <howto-checkbox>. Isso é explicado com mais detalhes em Não substituir o autor da página.

Sempre aceite dados primitivos (strings, números, booleanos) como atributos ou propriedades.

Por quê? Os elementos personalizados, como seus correspondentes integrados, devem ser configuráveis. A configuração pode ser transmitida de maneira declarativa, por atributos ou de modo imperativo usando propriedades JavaScript. O ideal é que cada atributo também seja vinculado a uma propriedade correspondente.
Exemplo O elemento <howto-checkbox>.

Procure manter os atributos e propriedades de dados primitivos em sincronia, refletindo de propriedade para atributo e vice-versa.

Por quê? Nunca se sabe como um usuário vai interagir com o elemento. Ele pode definir uma propriedade em JavaScript e esperar ler esse valor usando uma API como getAttribute(). Se cada atributo tiver uma propriedade correspondente e ambos forem refletidos, facilitará o trabalho dos usuários com seu elemento. Em outras palavras, chamar setAttribute('foo', value) também precisa definir uma propriedade foo correspondente e vice-versa. É claro que há exceções a essa regra. Não inclua propriedades de alta frequência, como currentTime, em um player de vídeo. Use o bom senso. Se parecer que o usuário vai interagir com uma propriedade ou atributo e não for trabalhoso refletir isso, faça isso.
Exemplo O elemento <howto-checkbox>. Isso é explicado com mais detalhes em Evitar problemas de reentrada.

Tente aceitar apenas dados avançados (objetos, matrizes) como propriedades.

Por quê? De modo geral, não há exemplos de elementos HTML integrados que aceitem dados avançados (objetos e matrizes JavaScript simples) por meio dos atributos. Os dados avançados são aceitos por chamadas de método ou propriedades. Há algumas desvantagens óbvias em aceitar dados avançados como atributos: pode ser caro serializar um objeto grande para uma string, e todas as referências de objeto serão perdidas nesse processo de string. Por exemplo, se você stringificar um objeto que tem uma referência a outro objeto ou talvez a um nó DOM, essas referências serão perdidas.

Não reflete propriedades de dados avançados para atributos.

Por quê? Refletir propriedades de dados avançados para atributos é desnecessariamente caro, exigindo serializar e desserializar os mesmos objetos JavaScript. A menos que você tenha um caso de uso que só possa ser resolvido com esse recurso, provavelmente é melhor evitá-lo.

Verifique se há propriedades que podem ter sido definidas antes do upgrade do elemento.

Por quê? Um desenvolvedor que usa seu elemento pode tentar definir uma propriedade no elemento antes que a definição seja carregada. Isso é válido principalmente quando o desenvolvedor usa um framework que processa o carregamento de componentes, os carimbos na página e a vinculação das propriedades a um modelo.
Exemplo O elemento <howto-checkbox>. Mais informações são explicadas em Tornar as propriedades lentas.

Não aplicar turmas automaticamente.

Por quê? Os elementos que precisam expressar o estado precisam usar atributos. O atributo class geralmente é considerado propriedade do desenvolvedor que usa seu elemento, e gravar nele por conta própria pode acabar pisando nas classes de desenvolvedor.

Eventos

Envie eventos em resposta à atividade do componente interno.

Por quê? O componente pode ter propriedades que mudam em resposta a uma atividade que somente ele conhece, por exemplo, se um timer ou uma animação for concluída ou um recurso terminar de carregar. É útil enviar eventos em resposta a essas mudanças para notificar o host de que o estado do componente é diferente.

Não envie eventos em resposta à configuração do host de uma propriedade (fluxo de dados para baixo).

Por quê? O envio de um evento em resposta a uma propriedade do host que define uma propriedade é supérfluo (o host sabe o estado atual porque apenas o definiu). O envio de eventos em resposta a uma configuração de host de uma propriedade pode causar loops infinitos com sistemas de vinculação de dados.
Exemplo O elemento <howto-checkbox>.

Vídeos de explicação

Não substituir o autor da página

É possível que um desenvolvedor que use seu elemento queira modificar parte do estado inicial dele. Por exemplo, mudando a ARIA role ou a focalização com tabindex. Verifique se esses e outros atributos globais foram definidos antes de aplicar seus próprios valores.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Tornar as propriedades lentas

Um desenvolvedor pode tentar definir uma propriedade no seu elemento antes que a definição dele seja carregada. Isso é válido principalmente quando o desenvolvedor usa um framework que gerencia componentes de carregamento, inserção na página e vinculação das propriedades a um modelo.

No exemplo a seguir, o Angular vincula de maneira declarativa a propriedade isChecked do modelo à propriedade checked da caixa de seleção. Se a definição da caixa de seleção de instruções foi carregada lentamente, é possível que o Angular tente definir a propriedade marcada antes do upgrade do elemento.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Um elemento personalizado precisa lidar com esse cenário verificando se alguma propriedade já foi definida na instância. A <howto-checkbox> demonstra esse padrão usando um método com o nome _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() captura o valor da instância sem upgrade e exclui a propriedade para não ocultar o definidor de propriedades do próprio elemento personalizado. Dessa forma, quando a definição do elemento for finalmente carregada, ela poderá refletir imediatamente o estado correto.

Evitar problemas de reentrada

É tentador usar o attributeChangedCallback() para refletir o estado para uma propriedade subjacente, por exemplo:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

No entanto, isso pode criar um loop infinito se o setter da propriedade também refletir o atributo.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Uma alternativa é permitir que o setter da propriedade reflita o atributo e fazer com que o getter determine o valor dele com base no atributo.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

get checked() {
  return this.hasAttribute('checked');
}

Nesse exemplo, adicionar ou remover o atributo também vai definir a propriedade.

Por fim, o attributeChangedCallback() pode ser usado para processar efeitos colaterais, como a aplicação de estados ARIA.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}