@@ -416,6 +416,7 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
416416 this : HTMLElement ,
417417 ...args : ParametersOf < CustomHTMLElement [ 'connectedCallback' ] >
418418 ) {
419+ ensureAttributesCustomized ( this ) ;
419420 const definition = definitionForElement . get ( this ) ;
420421 if ( definition ) {
421422 // Delegate out to user callback
@@ -514,6 +515,7 @@ const patchAttributes = (
514515 const setAttribute = elementClass . prototype . setAttribute ;
515516 if ( setAttribute ) {
516517 elementClass . prototype . setAttribute = function ( n : string , value : string ) {
518+ ensureAttributesCustomized ( this ) ;
517519 const name = n . toLowerCase ( ) ;
518520 if ( observedAttributes . has ( name ) ) {
519521 const old = this . getAttribute ( name ) ;
@@ -527,6 +529,7 @@ const patchAttributes = (
527529 const removeAttribute = elementClass . prototype . removeAttribute ;
528530 if ( removeAttribute ) {
529531 elementClass . prototype . removeAttribute = function ( n : string ) {
532+ ensureAttributesCustomized ( this ) ;
530533 const name = n . toLowerCase ( ) ;
531534 if ( observedAttributes . has ( name ) ) {
532535 const old = this . getAttribute ( name ) ;
@@ -543,19 +546,74 @@ const patchAttributes = (
543546 n : string ,
544547 force ?: boolean
545548 ) {
549+ ensureAttributesCustomized ( this ) ;
546550 const name = n . toLowerCase ( ) ;
547551 if ( observedAttributes . has ( name ) ) {
548552 const old = this . getAttribute ( name ) ;
549553 toggleAttribute . call ( this , name , force ) ;
550554 const newValue = this . getAttribute ( name ) ;
551- attributeChangedCallback . call ( this , name , old , newValue ) ;
555+ if ( old !== newValue ) {
556+ attributeChangedCallback . call ( this , name , old , newValue ) ;
557+ }
552558 } else {
553559 toggleAttribute . call ( this , name , force ) ;
554560 }
555561 } ;
556562 }
557563} ;
558564
565+ // Helper to defer initial attribute processing for parser generated
566+ // custom elements. These elements are created without attributes
567+ // so attributes cannot be processed in the constructor. Instead,
568+ // these elements are customized at the first opportunity:
569+ // 1. when the element is connected
570+ // 2. when any attribute API is first used
571+ // 3. when the document becomes readyState === interactive (the parser is done)
572+ let elementsPendingAttributes : Set < CustomHTMLElement & HTMLElement > | undefined ;
573+ if ( document . readyState === 'loading' ) {
574+ elementsPendingAttributes = new Set ( ) ;
575+ document . addEventListener (
576+ 'readystatechange' ,
577+ ( ) => {
578+ elementsPendingAttributes ! . forEach ( ( instance ) =>
579+ customizeAttributes ( instance , definitionForElement . get ( instance ) ! )
580+ ) ;
581+ } ,
582+ { once : true }
583+ ) ;
584+ }
585+
586+ const ensureAttributesCustomized = (
587+ instance : CustomHTMLElement & HTMLElement
588+ ) => {
589+ if ( ! elementsPendingAttributes ?. has ( instance ) ) {
590+ return ;
591+ }
592+ customizeAttributes ( instance , definitionForElement . get ( instance ) ! ) ;
593+ } ;
594+
595+ // Approximate observedAttributes from the user class, since the stand-in element had none
596+ const customizeAttributes = (
597+ instance : CustomHTMLElement & HTMLElement ,
598+ definition : CustomElementDefinition
599+ ) => {
600+ elementsPendingAttributes ?. delete ( instance ) ;
601+ if ( ! definition . attributeChangedCallback ) {
602+ return ;
603+ }
604+ definition . observedAttributes . forEach ( ( attr : string ) => {
605+ if ( ! instance . hasAttribute ( attr ) ) {
606+ return ;
607+ }
608+ definition . attributeChangedCallback ! . call (
609+ instance ,
610+ attr ,
611+ null ,
612+ instance . getAttribute ( attr )
613+ ) ;
614+ } ) ;
615+ } ;
616+
559617// Helper to patch CE class hierarchy changing those CE classes created before applying the polyfill
560618// to make them work with the new patched CustomElementsRegistry
561619const patchHTMLElement = ( elementClass : CustomElementConstructor ) : unknown => {
@@ -587,17 +645,17 @@ const customize = (
587645 new definition . elementClass ( ) ;
588646 }
589647 if ( definition . attributeChangedCallback ) {
590- // Approximate observedAttributes from the user class, since the stand-in element had none
591- definition . observedAttributes . forEach ( ( attr ) => {
592- if ( instance . hasAttribute ( attr ) ) {
593- definition . attributeChangedCallback ! . call (
594- instance ,
595- attr ,
596- null ,
597- instance . getAttribute ( attr )
598- ) ;
599- }
600- } ) ;
648+ // Note, these checks determine if the element is being parser created.
649+ // and has no attributes when created. In this case, it may have attributes
650+ // in HTML that are immediately processed. To handle this, the instance
651+ // is added to a set and its attributes are customized at first
652+ // opportunity (e.g. when connected or when the parser completes and the
653+ // document becomes interactive).
654+ if ( elementsPendingAttributes !== undefined && ! instance . hasAttributes ( ) ) {
655+ elementsPendingAttributes . add ( instance ) ;
656+ } else {
657+ customizeAttributes ( instance , definition ) ;
658+ }
601659 }
602660 if ( isUpgrade && definition . connectedCallback && instance . isConnected ) {
603661 definition . connectedCallback . call ( instance ) ;
0 commit comments