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.

Com os elementos personalizados, você pode estender o HTML e definir suas próprias tags. Eles são incrivelmente eficiente, mas também são de baixo nível, o que significa que não são sempre claro a melhor forma de implementar seu próprio elemento.

Para ajudar você a criar as melhores experiências possíveis, reunimos esta lista de verificação. Ele detalha tudo que achamos que é preciso para ser um um elemento personalizado bem-comportado.

Crie uma raiz paralela para encapsular estilos.

Por quê? O encapsulamento de estilos na raiz paralela do elemento garante que ele funcione independentemente de onde eles são usados. Isso é importante principalmente se um desenvolvedor quer colocar seu elemento dentro da raiz paralela de outro elemento. Isso se aplica até mesmo a elementos simples, como uma caixa de seleção ou um botão de opção. Pode ser caso o único conteúdo da sua raiz paralela serão os estilos por conta própria.
Exemplo O <howto-checkbox>.

Crie sua raiz paralela no construtor.

Por quê? O construtor é quando você tem conhecimento exclusivo do seu elemento. Este é um ótimo momento para configurar detalhes de implementação que você não quer que outras com os elementos. Fazer esse trabalho em um retorno de chamada posterior, como o connectedCallback, significa que você vai precisar se proteger contra situações em que o elemento é desconectado e reanexado ao documento.
Exemplo O <howto-checkbox>.

Coloque os filhos que o elemento criar na raiz paralela.

Por quê? Os filhos criados pelo seu elemento fazem parte da implementação e devem ser privados. Sem a proteção de uma raiz paralela, o JavaScript externo pode interferir inadvertidamente nessas crianças.
Exemplo O <howto-tabs>.

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

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

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

Por quê? Os elementos personalizados são display: inline por padrão. Portanto, definir os width ou height não terão efeito. Isso muitas vezes é uma surpresa para os desenvolvedores e pode causar problemas relacionados a o layout da página. A menos que você prefira uma tela inline, precisa sempre definir um valor display padrão.
Exemplo O <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 }, substituirá a menor especificidade integrado hidden. Isso pode surpreender se você pretende definir o hidden no seu elemento para renderizá-lo como display: none. Além disso, para um estilo display padrão, adicione suporte para hidden com :host([hidden]) { display: none }.
Exemplo O <howto-checkbox>.

Atributos e propriedades

Não substitua os atributos globais definidos pelo autor.

Por quê? Atributos globais são aqueles que estão presentes em todos os elementos HTML. Algumas como tabindex e role. Um elemento personalizado pode definir o tabindex inicial como 0 para que seja teclado focalizável. Mas você deve sempre verificar primeiro se o desenvolvedor que usa o elemento definiu isso com outro valor. Se, por exemplo, ele tiver definido tabindex a -1, é um sinal de que a pessoa não quer elemento para ser interativo.
Exemplo O <howto-checkbox>. Isso é explicado em mais detalhes Não substitua o autor da página.

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

Por quê? Os elementos personalizados, assim como os integrados, precisam ser configuráveis. A configuração pode ser transmitida de maneira declarativa, por atributos ou usando propriedades JavaScript. O ideal é que todos os atributos também estejam vinculados uma propriedade correspondente.
Exemplo O <howto-checkbox>.

Procure manter os atributos e as propriedades de dados primitivos em sincronia, refletindo a partir da para atribuir a propriedade e vice-versa.

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

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

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

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

Por quê? Refletir propriedades de dados avançados em atributos é caro, exigindo a serialização e desserialização dos mesmos objetos JavaScript. A menos que você tem um caso de uso que só pode ser resolvido com esse recurso, provavelmente é melhor evitá-lo.

Verifique se há propriedades definidas antes do elemento atualizado.

Por quê? Um desenvolvedor que usa seu elemento pode tentar definir uma propriedade no elemento. antes do carregamento da definição. Isso é especialmente verdadeiro se desenvolvedor está usando um framework que lida com o carregamento de componentes, carimbando-os à página e vincular as propriedades a um modelo.
Exemplo O <howto-checkbox>. Mais explicados em Torne as propriedades lentas.

Não aplique turmas automaticamente.

Por quê? Elementos que precisam expressar o estado precisam usar atributos. A O atributo class geralmente é considerado de propriedade do usando seu elemento. Além disso, gravar nele por conta própria pode participar de aulas para desenvolvedores.

Eventos

Enviar eventos em resposta à atividade do componente interno.

Por quê? Seu componente pode ter propriedades que mudam em resposta à atividade que que somente seu componente conhece, por exemplo, se um cronômetro ou animação é concluída ou um recurso termina de carregar. É útil enviar eventos em resposta a essas alterações, para notificar o host de que o estado do componente foi diferente.

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

Por quê? Envio de um evento em resposta a uma configuração de host de uma propriedade é desnecessário (o host sabe o estado atual porque acabou de defini-lo). Eventos de envio em resposta à definição de um host, uma propriedade pode causar loops infinitos com sistemas de vinculação.
Exemplo O <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 algumas o estado inicial dela. Por exemplo, mudar a role ARIA ou a capacidade de foco 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 o foi carregada. Isso é especialmente verdadeiro se o desenvolvedor estiver usando um que lida com o carregamento de componentes, inserindo-os na página e vinculando as propriedades a um modelo.

No exemplo a seguir, o Angular vincula de forma declarativa o isChecked à propriedade checked da caixa de seleção. Se a definição de a 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 deve lidar com esse cenário verificando se alguma propriedade tem já foi definido na instância. O <howto-checkbox> demonstra esse padrão usando um método chamado _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 não atualizada e exclui a propriedade para que ela não sombre o configurador de propriedades do próprio elemento personalizado. Dessa forma, quando a definição do elemento for finalmente carregada, ela poderá refletem o estado correto.

Evitar problemas de reentrada

É tentador usar attributeChangedCallback() para refletir o estado de 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;
}

Mas 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 faça com que o getter determine seu valor 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, a adição ou remoção do atributo também definirá a propriedade.

Por fim, o attributeChangedCallback() pode ser usado para processar efeitos colaterais como aplicar 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;
    ...
  }
}