6 điểm bởi GN⁺ 2024-04-27 | 3 bình luận | Chia sẻ qua WhatsApp
  • Dù cùng gắn trên một phần tử, HTML attributesDOM properties có thể hoạt động như hai vùng lưu trữ riêng biệt, nên việc thay đổi một bên không nhất thiết tự động phản ánh sang bên kia
  • Attribute xuất hiện trong quá trình tuần tự hóa HTML và trong bảng phần tử của DevTools, còn property là trạng thái chỉ tồn tại trong DOM nên sẽ không xuất hiện trong kết quả như outerHTML
  • Giá trị attribute luôn là chuỗi và tên không phân biệt hoa thường, trong khi property có thể mang nhiều kiểu khác nhau như object và có phân biệt hoa thường
  • Có những property phản ánh attribute như id, className, htmlFor, ariaLabel, nhưng cũng có ngoại lệ như input.value nơi trạng thái hiện tại và cấu hình mặc định được tách riêng
  • Mỗi framework nhắm tới đối tượng thiết lập khác nhau: Preact và VueJS nhìn chung ưu tiên property, React nhìn chung ưu tiên attribute, còn Lit phân biệt rõ ràng bằng cú pháp .value

Attribute và property có thể khác nhau dù cùng tên

  • Attributesproperties có thể hoạt động như những vùng lưu trữ khác nhau
    • Trong ``, getAttribute('foo') có thể trả về 'bar', nhưng div.foo có thể là undefined
    • Sau đó, kể cả khi đặt div.foo = 'hello world', giá trị attribute foo vẫn giữ là 'bar'
  • Trong template của framework, ngay cả khi dùng cú pháp trông giống attribute như ``, bên trong vẫn có thể thiết lập property tùy trường hợp
    • Việc khi nào thiết lập property là khác nhau giữa các framework
    • Một số thiết lập có thể tạo ra tác dụng phụ làm thay đổi cả property lẫn attribute

Thứ còn lại trong HTML serialization là attribute

  • Attribute tồn tại ở cả HTML và DOM, còn property chỉ tồn tại trong DOM
  • Dù cùng chạy div.setAttribute('foo', 'bar')div.hello = 'world', div.outerHTML vẫn chỉ hiển thị attribute như `

`

  • Bảng phần tử trong công cụ DevTools của trình duyệt hiển thị attributes của phần tử, chứ không hiển thị toàn bộ properties

Kiểu giá trị và cách xử lý hoa thường

  • Giá trị attribute luôn là chuỗi vì nó phải hoạt động trong dạng đã được tuần tự hóa
    • Nếu truyền object vào setAttribute, nó sẽ được lưu dưới dạng chuỗi "[object Object]"
    • Nếu gán cùng object đó vào property, nó có thể giữ nguyên kiểu object
  • Tên attribute không phân biệt hoa thường
    • HeLlO="world" có thể được chuẩn hóa thành attribute hello trong DOM
    • setAttribute('FOO', 'bar') cũng sẽ xuất hiện thành attribute foo
  • Tên property có phân biệt hoa thường
    • Dù đặt div.TeSt = 'value', div.test vẫn có thể là undefined
  • Không giống tên attribute, giá trị của attribute vẫn phân biệt hoa thường

Property phản ánh attribute

  • Một số property phản ánh (reflect) một attribute cụ thể
    • div.id hoạt động như getter/setter để đọc và ghi attribute id
    • Khi chạy div.id = 'bar', attribute id cũng đổi thành 'bar'
  • Trong quan hệ phản ánh này, attribute là nguồn dữ liệu gốc
    • Khi đặt property, nó sẽ cập nhật attribute
    • Khi đọc property, nó sẽ đọc giá trị từ attribute
  • Các attribute được định nghĩa trong đặc tả thường có property tương ứng để thuận tiện hơn
    • Attribute tùy ý như foo không phải là attribute được định nghĩa trong đặc tả, nên sẽ không tự động có property foo tương ứng
  • Đặc tả của `` định nghĩa attribute trong phần “Content attributes” và property trong phần “DOM interface”
    • Các IDL attribute reversedtype phải reflect các content attribute cùng tên

Quan hệ phản ánh khi tên thay đổi

  • Có những property phản ánh attribute nhưng tên không giống nhau
  • Đôi khi tên thay đổi để phù hợp với phong cách property của JavaScript
    • el.crossOrigin của `` phản ánh attribute crossorigin
    • el.ariaLabel của mọi phần tử phản ánh attribute aria-label
    • Các ARIA reflection property chỉ mới hoạt động xuyên trình duyệt vào cuối năm 2023; trước đó chỉ có thể dùng attribute
  • Cũng có trường hợp tên đổi vì các từ khóa dành riêng trước đây của JavaScript
    • el.className của mọi phần tử phản ánh attribute class
    • el.htmlFor của `` phản ánh attribute for

Validation, ép kiểu và giá trị mặc định

  • Property có thể đi kèm validation và giá trị mặc định, còn bản thân attribute thì không có các hành vi đó
  • input.type là ví dụ điển hình cho khác biệt giữa attribute và property
    • input.getAttribute('type')null, input.type vẫn trả về giá trị mặc định 'text'
    • input.type = 'number' sẽ trông là 'number' ở cả attribute lẫn property
    • input.type = 'foo' sẽ ghi 'foo' vào attribute, nhưng getter của property sẽ thấy đó là giá trị không hợp lệ và trả về 'text'
  • Một số property thực hiện ép kiểu
    • Trong ``, details.getAttribute('open') là chuỗi rỗng còn details.opentrue
    • details.open = false sẽ xóa attribute open
    • details.open = 'hello' không ghi nguyên chuỗi đó vào attribute mà chuyển sang boolean, khiến attribute open tồn tại
  • Các property như img.height sẽ chuyển giá trị attribute sang số
    • Setter sẽ đổi giá trị đầu vào thành số, và số âm sẽ được xử lý thành 0

input.value tách giá trị hiện tại và giá trị mặc định

  • input có cả property value và attribute value, nhưng property value không phản ánh attribute value
  • Property phản ánh attribute value thực ra là defaultValue
    • Getter của defaultValue trả về attribute value hoặc chuỗi rỗng
    • Setter của defaultValue đặt attribute value thành một chuỗi
  • Property value ban đầu sẽ đi theo defaultValue, nhưng khi được đặt bởi JavaScript hoặc tương tác người dùng thì sẽ dùng giá trị nội bộ riêng
    • Với `` ban đầu, value, defaultValue, và getAttribute('value') đều là 'default'
    • Khi chạy input.defaultValue = 'new default', cả attribute và value sẽ cùng trông như 'new default'
    • Sau đó nếu chạy input.value = 'hello!', value sẽ thành 'hello!', còn attribute và defaultValue vẫn là 'new default'
    • Ngay cả khi chạy setAttribute('value', 'another new default'), giá trị value hiện tại vẫn giữ là 'hello!'
  • Khi form được reset, trạng thái value nội bộ có thể được khởi tạo lại theo cách này

Attribute phù hợp hơn cho cấu hình, property phù hợp hơn cho trạng thái

  • Cách phân chia tự nhiên là dùng attribute cho cấu hình (configuration) và property cho trạng thái (state)
  • `` là ví dụ phù hợp với cách phân chia này nếu bỏ qua tên gọi
    • Attribute value dùng để đặt giá trị mặc định
    • Property value cung cấp trạng thái hiện tại
  • Việc validation áp dụng khi đọc/ghi property nhưng không áp dụng khi đọc/ghi attribute cũng phù hợp với cách phân chia đó
  • biểu diễn trạng thái mở bằng attribute open, và trình duyệt có thể tự thêm hoặc xóa attribute này theo tương tác người dùng
    • Thiết kế này phá vỡ cách phân biệt rằng attribute là để cấu hình
    • Framework quản lý DOM hay mã JavaScript thông thường đều phải chuẩn bị cho tình huống DOM tự thay đổi
  • Một thiết kế thay thế có thể tách thành attribute cấu hình mặc định như defaultopen, property details.open để đọc/ghi trạng thái hiện tại, và pseudo-class CSS để nhắm tới trạng thái
  • Simon Peters đã tìm lại thảo luận thiết kế ban đầu liên quan đến vấn đề này
  • contenteditable cũng phá vỡ cách phân chia này, nhưng thường được xem là một kiểu opt-in chấp nhận nhiều hệ quả

Cách các framework xử lý

  • Preact và VueJS, trừ một số ngoại lệ được định nghĩa sẵn, sẽ thiết lập property khi propName in element là đúng; nếu không thì thiết lập attribute
    • Mặc định chúng ưu tiên property hơn attribute
    • Các phương thức render chuỗi thì làm ngược lại và bỏ qua những mục chỉ dành cho property
    • Triển khai liên quan: setProperty của Preact, shouldSetAsProp của VueJS
  • React, trừ một số ngoại lệ được định nghĩa sẵn, sẽ thiết lập attribute
    • Cách này tạo ra logic gần giống với các phương thức render chuỗi
    • Property của custom element không có trong danh sách định nghĩa sẵn của React nên sẽ bị đặt thành attribute, và các API chỉ hỗ trợ property có thể không hoạt động
    • Trong React 19, mô hình này dự kiến sẽ chuyển sang kiểu của Preact/VueJS đối với custom element
    • React phổ biến hóa tên property className qua cú pháp trông như attribute, nhưng bên trong nó vẫn thiết lập attribute class
    • Triển khai liên quan: setProp của React
  • lit-html giữ sự khác biệt giữa attribute và property ngay trong cú pháp
    • Nếu thêm dấu . trước tên như ``, nó sẽ thiết lập property chứ không phải attribute
    • Tài liệu liên quan: Lit expressions

3 bình luận

 
GN⁺ 2024-04-27
Ý kiến trên Hacker News
  • Dùng thuộc tính data-* sẽ là một phương án dung hòa khá ổn giữa các cách được đề cập trong bài
    Các thuộc tính này được tự động phơi ra trong JavaScript dưới dạng thuộc tính đọc/ghi của dataset, nên có thể truy cập như document.getElementById('myDiv').dataset.payload == "something"
    Tuy nhiên cần chú ý rằng thuộc tính HTML dùng kebab-case, còn JavaScript dùng camelCase, nên data-my-cool-data-attribute sẽ trở thành dataset.myCoolDataAttribute

    • Tôi thật sự ghét việc tự động chuyển đổi kiểu viết hoa/thường
      Trong nhiều hệ sinh thái dùng camelCase, không có quy ước đã được xác lập về cách xử lý các từ viết tắt viết hoa, nên những trường hợp như dataset.myID trở nên mập mờ
    • Để tránh chuyển đổi giữa kebab-casecamelCase, tôi dùng các phương thức như Element.hasAttribute('data-thing'), Element.getAttribute('data-thing'), Element.setAttribute('data-thing', '...')
      Khi kiểm tra hoặc debug trong DOM, không phải đi qua phép chuyển đổi gượng gạo nào, nên ý đồ rõ ràng hơn nhiều
    • Tài liệu data attributes của MDN đề cập đến cú pháp HTML, cách truy cập bằng JavaScript, cách truy cập bằng CSS và các lưu ý liên quan đến khả năng tiếp cận: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
      Ngoài ra, tại https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att..., thuộc tính toàn cục data-* được mô tả là “các thuộc tính dữ liệu tùy chỉnh cho phép script trao đổi thông tin riêng giữa HTML và biểu diễn DOM”
    • Các thuộc tính này cũng có thể được truy cập từ CSS ở một mức độ nào đó
    • Đây cũng là một cách thú vị khác để giao tiếp giữa các ngữ cảnh thực thi JavaScript
      Ngữ cảnh ở đây không nhất thiết là các iframe khác nhau, mà là các trường hợp tạo ra những ngữ cảnh thực thi JavaScript riêng biệt cùng truy cập một DOM, như tiện ích mở rộng trình duyệt
      Theo thuật ngữ của giao thức công cụ dành cho nhà phát triển, những thứ như vậy cũng được gọi là isolated worlds
  • Những điều được đề cập trong bài thể hiện sự phức tạp và hành vi chẳng giúp ích gì nhiều cho đời sống
    Những hành vi này chỉ có nhược điểm mà không có ưu điểm; đặc tả lẽ ra nên đơn giản định nghĩa đối tượng DOM và đối tượng JavaScript là cùng một thứ, khiến thuộc tính HTML và property cũng là cùng một khái niệm

    • Nếu “đơn giản” có hạn chế như vậy thì hầu hết các UI framework hiện đại đã hoàn toàn không hoạt động được
      Việc có thể lưu giá trị không phải chuỗi trong node DOM rõ ràng là một lợi thế, và không phải chuyện có thể xem nhẹ
    • Tôi đồng ý, trừ phần “đơn giản”
      Nếu không yêu cầu mọi property chỉ có thể là chuỗi thì chuyện đó hoàn toàn không đơn giản
      Cần cân nhắc cả việc chuyển đổi với số, các property có thể được gán đối tượng, và cả các property không tương ứng với thuộc tính HTML
      Dù vậy, nếu có một quy ước hoặc quy tắc cố định 100% thì vẫn tốt hơn, nhưng đó không phải là việc đơn giản
    • Làm vậy thì không thể đặt giá trị không phải chuỗi trong interface DOM, mà vì còn bao gồm cả phương thức, phần lớn chức năng DOM sẽ biến mất
    • Việc làm cho thuộc tính HTML và property đối xứng với nhau là khó trong DOM API hiện có
      Các phương thức như addEventListener là property trên prototype của từng phần tử DOM, nhưng nếu nhìn như thuộc tính chuỗi thì không hợp lý
      Khi đó các phương thức của phần tử DOM sẽ phải trở thành hàm tự do như HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));; tuy đây là vấn đề sở thích, nhưng trong JavaScript, có thể dùng lời gọi phương thức trên đối tượng vẫn tốt hơn
    • Tôi không rõ vì sao hai thứ này phải khác nhau
      Phần tử DOM có một interface rất cụ thể để thay đổi trạng thái bằng setAttribute, và tôi thắc mắc vì sao khi chỉ gắn property vào đối tượng JavaScript được tạo ra để gửi thông điệp tới DOM qua interface đó, ta lại nên kỳ vọng nó được tự động định tuyến
      Một API như vậy có thể mang lại trải nghiệm tốt hơn, nhưng có vài vấn đề khó, và dù sao nó có lẽ vẫn sẽ gọn gàng hơn
      Có lẽ vì tôi đã viết mã backend quá lâu, nên khi nhìn các interface như thế này, tôi cảm thấy bên trong có nhiều thứ kỳ lạ, và như thể một lập trình viên tử tế ngày xưa đã cung cấp cho ta wrapper an toàn nhất có thể
  • Một góc nhìn hữu ích để hiểu vấn đề là, theo nghĩa hẹp, thuộc tính là node DOM, tức node Attr
    Node này có thể có các property như name, value, ownerElement, prefix, và xét tổng thể thì có thể truy cập dưới dạng giá trị property attributes của phần tử sở hữu, ở dạng NamedNodeMap chứ không phải mảng
    Vì vậy tên getAttribute(name) dễ gây hiểu nhầm; trên thực tế nó không trả về chính node thuộc tính, mà trả về property value của node thuộc tính đó, nên gần với getAttributeValue(name) hơn
    Tham khảo: 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

    • Giải thích đó đúng, nhưng tôi không rõ có việc thực tế nào không thể làm bằng setAttribute, getAttribute, getAttributeNames, hasAttribute mà chỉ có thể làm bằng node thuộc tính hoặc el.attributes không
      Cũng không thể chuyển node thuộc tính giữa các phần tử, và cũng không thể thay đổi thứ tự node thuộc tính bên trong phần tử
  • Jake, vậy cái này thì sao: https://jakearchibald.com/2023/against-self-closing-tags-in-...

    • “Bạn sẽ thấy cú pháp này trên blog của tôi, vì Prettier làm như vậy và tôi thật sự rất thích Prettier”
  • Tôi hiểu lập luận rằng không nên cập nhật property, nhưng việc có thể nhắm mục tiêu bằng CSS thì hữu ích

    • Vì vậy mới nói rằng cũng nên có pseudo-class, tương tự :checked của phần tử nhập liệu
    • Với tôi, trường hợp này cũng quan trọng hơn mọi lý do khác
      Kiểm tra DOM ngay lập tức dễ debug hơn nhiều so với phải đi qua debugger
  • Có lẽ khác biệt quan trọng nhất là tính bền vững, nhưng bài viết không đề cập
    Attribute được duy trì miễn là còn có thể truy cập cùng root object của cây DOM đó, chẳng hạn khi chưa rời khỏi trang
    Property của object chỉ được duy trì cho đến trước khi object đó bị garbage collection

    • Object tồn tại chừng nào DOM node có attribute còn tồn tại, nên vòng đời là như nhau
      Nếu DOM node được clone hoặc trải qua serialize/deserialize, nó sẽ trở thành một DOM node hoàn toàn khác nhưng có cùng attribute, nên sẽ không có các property không phải giá trị mặc định của node khác
    • Ở đây không có khác biệt hành vi thực chất nào ngoài khác biệt vốn có giữa attribute và property
    • Tôi tò mò không biết khác biệt đó thực tế sẽ biểu hiện như thế nào
  • Bài viết thú vị
    Tôi luôn xem phần tử DOM trong JavaScript như một proxy
    Nó cho phép làm việc với phần tử DOM thật, nhưng đồng thời cũng chỉ là một JavaScript object, nên ngoài việc dùng các hàm tương tác với phần tử thật, ta vẫn có thể gán giá trị và làm những gì mình muốn

  • Được giải thích rõ ràng kèm ví dụ nên đọc rất tốt, đặc biệt có nhiều chi tiết thường dễ bị bỏ sót, có lẽ sẽ hữu ích cho nhiều người

  • DOM property cũng được gọi là IDL attributes

    • Đúng, nhưng tôi cố ý tránh thuật ngữ đó
      Ngoài những người viết spec, hiếm ai gọi là IDL attributes, và nếu gọi cả hai đều là attribute thì rất dễ gây nhầm lẫn
  • Bài viết hay, và khiến tôi tò mò cách biểu đạt tốt hơn sẽ trông như thế nào
    Sẽ thú vị nếu HTML có thể có attribute không phải chuỗi, và giá trị đó tham chiếu đến giá trị local của một thẻ script có scope
    Cách nhìn attribute như thiết lập mặc định có vẻ hơi lạ
    Ví dụ, nếu attribute class chỉ hiển thị giá trị mặc định, thì khi toggle class, active class sẽ không hiện trong DOM inspector hoặc HTML đã stringify, hoặc inactive class lại hiện ra, khá kỳ lạ

    • Tôi không nói là thiết lập mặc định, mà chỉ là thiết lập
      Attribute class hoạt động tốt trong hệ thống này
      Tôi, với tư cách chủ sở hữu của tài liệu, đặt attribute class, và theo thời gian có thể thay đổi thiết lập đó, nhưng chỉ tôi mới làm việc đó
      Trong một hệ thống thay thế, trình duyệt sẽ tự thêm/xóa class để phản ứng với trạng thái hover hoặc focus, nhưng may là nó không làm vậy mà dùng pseudo-class
 
superwoou 2024-04-27

Có vẻ đã dịch cả attribute và property thành cùng một từ "thuộc tính" rồi..