6 pontos por GN⁺ 2024-04-27 | 3 comentários | Compartilhar no WhatsApp
  • Mesmo quando estão no mesmo elemento, atributos HTML e propriedades DOM podem funcionar como espaços de armazenamento separados, então uma mudança em um lado pode não ser refletida automaticamente no outro
  • Os atributos aparecem na serialização HTML e no painel de elementos das ferramentas de desenvolvedor, mas as propriedades são estado exclusivo do DOM, então não aparecem em resultados como outerHTML
  • Os valores de atributos são sempre strings e seus nomes não diferenciam maiúsculas de minúsculas, enquanto propriedades podem ter vários tipos, como objetos, e seus nomes diferenciam maiúsculas de minúsculas
  • Existem propriedades que refletem atributos, como id, className, htmlFor e ariaLabel, mas também há exceções como input.value, em que estado atual e configuração padrão ficam separados
  • Cada framework define isso de forma diferente: Preact e VueJS geralmente priorizam propriedades, React geralmente prioriza atributos, e Lit distingue os dois explicitamente com a sintaxe .value

Atributos e propriedades podem ser diferentes mesmo com o mesmo nome

  • Atributos (attributes) e propriedades (properties) podem funcionar como espaços de armazenamento diferentes
    • Em ```, getAttribute('foo') retorna 'bar', mas div.foo pode ser undefined
    • Depois, mesmo que div.foo = 'hello world' seja definido, o valor do atributo foo continua sendo 'bar'
  • Em templates de frameworks, mesmo que se use uma sintaxe que parece atributo, como ````, internamente o framework pode definir uma propriedade dependendo do caso
    • Quando uma propriedade é usada depende de cada framework
    • Algumas definições podem gerar efeitos colaterais que alteram tanto a propriedade quanto o atributo

O que permanece na serialização HTML são os atributos

  • Os atributos existem tanto no HTML quanto no DOM, mas as propriedades existem apenas no DOM
  • Mesmo executando div.setAttribute('foo', 'bar') e div.hello = 'world', em div.outerHTML só aparecem os atributos, como em `

`

  • O painel de elementos das ferramentas de desenvolvedor do navegador mostra os atributos do elemento, não o conjunto completo de propriedades

Tipo dos valores e tratamento de maiúsculas e minúsculas

  • Valores de atributos são sempre strings, porque precisam funcionar em formato serializado
    • Se um objeto for passado para setAttribute, ele será armazenado como a string "[object Object]"
    • Se o mesmo objeto for atribuído a uma propriedade, ele pode continuar com o tipo objeto
  • Nomes de atributos não diferenciam maiúsculas de minúsculas
    • HeLlO="world" pode ser normalizado no DOM como o atributo hello
    • setAttribute('FOO', 'bar') também pode aparecer como o atributo foo
  • Nomes de propriedades diferenciam maiúsculas de minúsculas
    • Mesmo que div.TeSt = 'value' seja definido, div.test pode continuar undefined
  • Diferentemente do nome dos atributos, os valores dos atributos diferenciam maiúsculas de minúsculas

Propriedades que refletem atributos

  • Algumas propriedades refletem (reflect) um atributo específico
    • div.id funciona como getter/setter que lê e escreve o atributo id
    • Ao executar div.id = 'bar', o atributo id também muda para 'bar'
  • Em uma relação de reflexão, o atributo é a fonte dos dados
    • Definir a propriedade atualiza o atributo
    • Ler a propriedade lê o valor do atributo
  • Atributos definidos pela especificação frequentemente ganham uma propriedade correspondente por conveniência
    • Um atributo arbitrário foo não é um atributo definido pela especificação, então uma propriedade correspondente foo não é criada automaticamente
  • A especificação de `` define atributos em “Content attributes” e propriedades em “DOM interface”
    • As propriedades IDL reversed e type devem reflect os atributos de conteúdo com o mesmo nome

Relações de reflexão em que o nome muda

  • Algumas propriedades têm nome diferente do atributo que refletem
  • Em alguns casos, o nome muda para combinar com o estilo de propriedades do JavaScript
    • Em ```, el.crossOrigin reflete o atributo crossorigin
    • Em qualquer elemento, el.ariaLabel reflete o atributo aria-label
    • As propriedades reflexivas de ARIA passaram a funcionar entre navegadores no fim de 2023; antes disso, só era possível usar atributos
  • Em outros casos, a diferença de nome vem de palavras reservadas antigas do JavaScript
    • Em qualquer elemento, el.className reflete o atributo class
    • Em ```, el.htmlFor reflete o atributo for

Validação, conversão de tipo e valores padrão

  • Propriedades podem ter validação e valores padrão, mas atributos por si só não têm esse comportamento
  • input.type mostra bem a diferença entre atributo e propriedade
    • Mesmo que input.getAttribute('type') seja null, input.type retorna o valor padrão 'text'
    • input.type = 'number' faz com que atributo e propriedade pareçam 'number'
    • input.type = 'foo' grava 'foo' no atributo, mas o getter da propriedade detecta o valor inválido e retorna 'text'
  • Algumas propriedades fazem conversão de tipo
    • Em ```, details.getAttribute('open') é uma string vazia e details.open é true
    • details.open = false remove o atributo open
    • details.open = 'hello' não grava a string literal no atributo; em vez disso, converte para boolean e faz o atributo open existir
  • Propriedades como img.height convertem o valor do atributo para número
    • O setter converte o valor recebido em número e trata valores negativos como 0

input.value separa valor atual e valor padrão

  • input tem tanto a propriedade value quanto o atributo value, mas a propriedade value não reflete o atributo value
  • A propriedade que reflete o atributo value é defaultValue
    • O getter de defaultValue retorna o atributo value ou uma string vazia
    • O setter de defaultValue define o atributo value como string
  • A propriedade value inicialmente segue defaultValue, mas, depois que é definida por JavaScript ou por interação do usuário, passa a usar um valor interno
    • No estado inicial de ````, value, defaultValue e getAttribute('value') são todos 'default'
    • Ao executar input.defaultValue = 'new default', tanto o atributo quanto value passam a parecer 'new default'
    • Depois, ao executar input.value = 'hello!', value passa a ser 'hello!', mas o atributo e defaultValue continuam 'new default'
    • Mesmo executando setAttribute('value', 'another new default'), o value atual continua 'hello!'
  • Quando o formulário é resetado, o estado interno de value pode ser reinicializado

Atributos são mais naturais para configuração, propriedades para estado

  • É natural separar atributos para configuração (configuration) e propriedades para estado (state)
  • ``` input.value é um exemplo que se encaixa nessa divisão, tirando o nome
    • O atributo value define o valor padrão
    • A propriedade value fornece o estado atual
  • A distinção também combina com o fato de que há validação ao ler ou escrever propriedades, mas não ao ler ou escrever atributos
  • e expressam o estado aberto com o atributo open, e o navegador adiciona ou remove esse atributo diretamente conforme a interação do usuário
    • Esse design quebra a ideia de que atributos servem para configuração
    • Frameworks que mantêm o DOM, ou código JavaScript comum, precisam lidar com situações em que o DOM muda por conta própria
  • Um design alternativo poderia separar um atributo de configuração padrão como ```, uma propriedade details.open para ler e escrever o estado atual e uma pseudoclasse CSS para mirar esse estado
  • Simon Peters encontrou uma discussão inicial de design relacionada a isso
  • contenteditable também quebra essa divisão, mas é tratado como um opt-in que aceita bastante comportamento problemático

Como cada framework lida com isso

  • Preact e VueJS, exceto por algumas exceções predefinidas, definem uma propriedade quando propName in element é verdadeiro; caso contrário, definem um atributo
    • Por padrão, priorizam propriedades em vez de atributos
    • Métodos de renderização para string fazem o oposto e ignoram itens exclusivos de propriedades
    • Implementações relacionadas: setProperty do Preact, shouldSetAsProp do VueJS
  • React, exceto por algumas exceções predefinidas, define um atributo
    • Essa abordagem cria uma lógica parecida com a dos métodos de renderização em string
    • Como propriedades de elementos customizados não estão na lista predefinida do React, elas são definidas como atributos, e APIs exclusivas de propriedades podem não funcionar
    • No React 19, elementos customizados devem passar para o modelo usado por Preact/VueJS
    • React popularizou o nome de propriedade className com uma sintaxe que parece atributo, mas internamente define o atributo class
    • Implementação relacionada: setProp do React
  • lit-html mantém a diferença entre atributos e propriedades na própria sintaxe
    • Em algo como ```, colocar . antes do nome faz com que seja definida uma propriedade, e não um atributo
    • Documentação relacionada: Lit expressions

3 comentários

 
GN⁺ 2024-04-27
Opiniões do Hacker News
  • Usar atributos data-* acaba sendo um bom meio-termo entre as abordagens tratadas no texto
    Esses atributos são expostos automaticamente em JavaScript como propriedades de leitura/escrita de dataset, podendo ser acessados como em document.getElementById('myDiv').dataset.payload == "something"
    Mas é preciso tomar cuidado porque atributos HTML usam kebab-case e JavaScript usa camelCase, então data-my-cool-data-attribute vira dataset.myCoolDataAttribute

    • Eu realmente detesto a conversão automática de caixa
      Em vários ecossistemas que usam camelCase, não há uma convenção estabelecida sobre como lidar com siglas em maiúsculas, o que torna casos como dataset.myID ambíguos
    • Para evitar a conversão entre kebab-case e camelCase, uso métodos como Element.hasAttribute('data-thing'), Element.getAttribute('data-thing') e Element.setAttribute('data-thing', '...')
      Ao inspecionar ou depurar no DOM, não é preciso passar por uma conversão estranha, então a intenção fica muito mais clara
    • A documentação da MDN sobre data attributes cobre a sintaxe HTML, o acesso via JavaScript, o acesso via CSS e observações relacionadas a acessibilidade: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
      Além disso, em https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att..., os atributos globais data-* são descritos como “atributos de dados personalizados que permitem que scripts troquem informações próprias entre o HTML e a representação no DOM”
    • Esses atributos também são acessíveis, em certa medida, via CSS
    • Também são uma outra forma interessante de comunicação entre contextos de execução JavaScript
      Os contextos aqui não significam necessariamente iframes diferentes, mas casos como extensões de navegador, que criam contextos separados de execução JavaScript acessando o mesmo DOM
      Na terminologia do protocolo de ferramentas de desenvolvedor, isso às vezes é chamado de isolated worlds
  • O que o texto aborda representa complexidades e comportamentos que não acrescentam muito à vida
    Esse tipo de comportamento só traz desvantagens, sem benefícios, e a especificação deveria simplesmente ter definido objetos DOM e objetos JavaScript como a mesma coisa, tornando atributos e propriedades o mesmo conceito

    • Se essa restrição tivesse existido “simplesmente”, a maioria dos frameworks modernos de UI não funcionaria de jeito nenhum
      Poder armazenar valores que não são strings em nós DOM é uma vantagem clara, e não é algo a se descartar tão levianamente
    • Concordo, exceto pela parte do “simplesmente”
      A menos que se exija que todas as propriedades possam ser apenas strings, isso não é nada simples
      Seria preciso considerar conversões com números, propriedades em que é possível definir objetos e até propriedades que não correspondem a atributos
      Ainda assim, teria sido melhor se houvesse uma convenção ou regra 100% fixa, mas isso não é uma tarefa simples
    • Fazendo isso, não seria possível colocar valores que não são strings na interface DOM; e como ela também inclui métodos, a maior parte da funcionalidade do DOM desapareceria
    • Tornar atributos e propriedades simétricos entre si é difícil com a API DOM existente
      Métodos como addEventListener são propriedades do protótipo de elementos DOM individuais, mas não fazem sentido como atributos de string
      Nesse caso, métodos de elementos DOM teriam de virar funções livres, como HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));; embora seja uma questão de gosto, é melhor poder usar chamadas de método em objetos em JavaScript
    • Não entendo bem por que os dois precisam ser diferentes
      Elementos DOM têm uma interface muito específica para mudar estado com setAttribute, e não vejo por que deveríamos esperar que, ao anexar propriedades diretamente ao objeto JavaScript criado para enviar mensagens ao DOM por essa interface, isso fosse automaticamente roteado
      Uma API assim poderia proporcionar uma experiência melhor, mas há alguns problemas difíceis; ainda assim, acho que ficaria mais limpa
      Talvez por eu ter escrito código de backend por tempo demais, quando vejo interfaces desse tipo sinto que há muita coisa estranha por dentro, como se algum desenvolvedor gentil do passado tivesse nos dado o wrapper mais seguro possível
  • Uma perspectiva que ajuda a entender é que atributos, no sentido estrito, são nós DOM, isto é, nós Attr
    Esse nó pode ter propriedades como name, value, ownerElement e prefix, e, como um todo, pode ser acessado não como um array, mas como um NamedNodeMap, pelo valor da propriedade attributes do elemento proprietário
    Por isso, o nome getAttribute(name) é enganoso; na prática, ele não retorna o próprio nó de atributo, mas a propriedade value desse nó, então seria mais próximo de getAttributeValue(name)
    Referências: https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Attr

    • Essa explicação está correta, mas não sei se há algo prático que só possa ser feito com nós de atributo ou el.attributes, e não com setAttribute, getAttribute, getAttributeNames ou hasAttribute
      Também não dá para mover nós de atributo entre elementos, nem alterar a ordem dos nós de atributo dentro de um elemento
  • Jake, então o que fazemos com isto: https://jakearchibald.com/2023/against-self-closing-tags-in-...

    • “Você vai ver essa sintaxe no meu blog, porque é assim que o Prettier faz, e eu gosto muito do Prettier”
  • Entendo o argumento de não atualizar a propriedade, mas é útil poder direcionar o alvo via CSS

    • Por isso eu disse que também deveria haver uma pseudoclasse, parecida com :checked em elementos de entrada
    • Para mim, esse caso também pesa mais do que todos os outros motivos
      É muito mais fácil depurar verificando o DOM imediatamente do que passando pelo depurador
  • Talvez a diferença mais importante seja a persistência, mas o texto não aborda isso
    Atributos são mantidos enquanto for possível acessar o mesmo objeto raiz daquela árvore DOM, por exemplo sem sair da página
    Propriedades de objeto só são mantidas até o objeto ser coletado pelo garbage collector

    • O objeto existe enquanto existir o nó DOM que tem o atributo, então o ciclo de vida é o mesmo
      Se o nó DOM for clonado ou passar por serialização/desserialização, ele se torna um nó DOM completamente diferente com os mesmos atributos, portanto não terá propriedades de outro nó que não sejam valores padrão
    • Aqui não há diferença prática de comportamento além da diferença já existente entre atributos e propriedades
    • Fico curioso para saber como essa diferença aparece na prática
  • É um texto interessante
    Sempre vi elementos DOM em JavaScript como uma espécie de proxy
    Eles permitem trabalhar com os elementos DOM reais, mas, ao mesmo tempo, são simplesmente objetos JavaScript, então eu sentia que, além de usar funções que interagem com o elemento real, também dava para atribuir valores e fazer o que quisesse

  • É bom ver uma explicação clara com exemplos, especialmente porque há vários detalhes que muitas vezes passam batidos; deve ajudar muita gente

  • Propriedades do DOM também são chamadas de IDL attributes

    • Correto, mas evitei esse termo de propósito
      Fora autores de especificações, pouca gente chama isso de IDL attributes, e chamar ambos de atributos fica muito confuso
  • Bom texto; fiquei curioso sobre como seria uma forma melhor de expressar isso
    Seria interessante se HTML pudesse ter atributos que não fossem strings, e se seus valores pudessem referenciar valores locais de uma tag script com escopo
    Achei um pouco estranha a visão de atributos como configuração padrão
    Por exemplo, se o atributo class mostrasse apenas o valor padrão, ao alternar classes ficaria bem estranho: a classe ativa não apareceria no inspetor do DOM ou no HTML convertido em string, ou uma classe inativa apareceria

    • Não disse configuração padrão, disse apenas configuração
      O atributo class funciona bem dentro desse sistema
      Eu, como dono do documento, defino o atributo class e posso alterar essa configuração ao longo do tempo, mas só eu faço isso
      Em um sistema alternativo, o navegador adicionaria e removeria classes por conta própria em resposta a estados como hover ou focus; felizmente ele não faz isso, e são usadas pseudoclasses
 
superwoou 2024-04-27

Acabaram traduzindo tanto attribute quanto property como “atributo”...