@@ -416,6 +416,7 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
416
416
this : HTMLElement ,
417
417
...args : ParametersOf < CustomHTMLElement [ 'connectedCallback' ] >
418
418
) {
419
+ ensureAttributesCustomized ( this ) ;
419
420
const definition = definitionForElement . get ( this ) ;
420
421
if ( definition ) {
421
422
// Delegate out to user callback
@@ -514,6 +515,7 @@ const patchAttributes = (
514
515
const setAttribute = elementClass . prototype . setAttribute ;
515
516
if ( setAttribute ) {
516
517
elementClass . prototype . setAttribute = function ( n : string , value : string ) {
518
+ ensureAttributesCustomized ( this ) ;
517
519
const name = n . toLowerCase ( ) ;
518
520
if ( observedAttributes . has ( name ) ) {
519
521
const old = this . getAttribute ( name ) ;
@@ -527,6 +529,7 @@ const patchAttributes = (
527
529
const removeAttribute = elementClass . prototype . removeAttribute ;
528
530
if ( removeAttribute ) {
529
531
elementClass . prototype . removeAttribute = function ( n : string ) {
532
+ ensureAttributesCustomized ( this ) ;
530
533
const name = n . toLowerCase ( ) ;
531
534
if ( observedAttributes . has ( name ) ) {
532
535
const old = this . getAttribute ( name ) ;
@@ -543,19 +546,74 @@ const patchAttributes = (
543
546
n : string ,
544
547
force ?: boolean
545
548
) {
549
+ ensureAttributesCustomized ( this ) ;
546
550
const name = n . toLowerCase ( ) ;
547
551
if ( observedAttributes . has ( name ) ) {
548
552
const old = this . getAttribute ( name ) ;
549
553
toggleAttribute . call ( this , name , force ) ;
550
554
const newValue = this . getAttribute ( name ) ;
551
- attributeChangedCallback . call ( this , name , old , newValue ) ;
555
+ if ( old !== newValue ) {
556
+ attributeChangedCallback . call ( this , name , old , newValue ) ;
557
+ }
552
558
} else {
553
559
toggleAttribute . call ( this , name , force ) ;
554
560
}
555
561
} ;
556
562
}
557
563
} ;
558
564
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
+
559
617
// Helper to patch CE class hierarchy changing those CE classes created before applying the polyfill
560
618
// to make them work with the new patched CustomElementsRegistry
561
619
const patchHTMLElement = ( elementClass : CustomElementConstructor ) : unknown => {
@@ -587,17 +645,17 @@ const customize = (
587
645
new definition . elementClass ( ) ;
588
646
}
589
647
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
+ }
601
659
}
602
660
if ( isUpgrade && definition . connectedCallback && instance . isConnected ) {
603
661
definition . connectedCallback . call ( instance ) ;
0 commit comments