Skip to content

Commit

Permalink
fix toggleAttribute and attributes for parser created elements
Browse files Browse the repository at this point in the history
  • Loading branch information
Steve Orvell committed May 8, 2024
1 parent 59e2cfc commit f87300a
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ if (!ShadowRoot.prototype.createElement) {

connectedCallback() {
const definition = definitionForElement.get(this);
ensureAttributesCustomized(this);
if (definition) {
// Delegate out to user callback
definition.connectedCallback &&
Expand Down Expand Up @@ -354,6 +355,7 @@ if (!ShadowRoot.prototype.createElement) {
const setAttribute = elementClass.prototype.setAttribute;
if (setAttribute) {
elementClass.prototype.setAttribute = function (n, value) {
ensureAttributesCustomized(this);
const name = n.toLowerCase();
if (observedAttributes.has(name)) {
const old = this.getAttribute(name);
Expand All @@ -367,6 +369,7 @@ if (!ShadowRoot.prototype.createElement) {
const removeAttribute = elementClass.prototype.removeAttribute;
if (removeAttribute) {
elementClass.prototype.removeAttribute = function (n) {
ensureAttributesCustomized(this);
const name = n.toLowerCase();
if (observedAttributes.has(name)) {
const old = this.getAttribute(name);
Expand All @@ -380,12 +383,15 @@ if (!ShadowRoot.prototype.createElement) {
const toggleAttribute = elementClass.prototype.toggleAttribute;
if (toggleAttribute) {
elementClass.prototype.toggleAttribute = function (n, force) {
ensureAttributesCustomized(this);
const name = n.toLowerCase();
if (observedAttributes.has(name)) {
const old = this.getAttribute(name);
toggleAttribute.call(this, name, force);
const newValue = this.getAttribute(name);
attributeChangedCallback.call(this, name, old, newValue);
if (old !== newValue) {
attributeChangedCallback.call(this, name, old, newValue);
}
} else {
toggleAttribute.call(this, name, force);
}
Expand All @@ -407,6 +413,44 @@ if (!ShadowRoot.prototype.createElement) {
}
};

// Helper to defer initial attribute processing for parser generated
// custom elements.
let elementsPendingAttributes;
if (document.readyState === 'loading') {
elementsPendingAttributes = new Set();
document.addEventListener(
'readystatechange',
() => {
elementsPendingAttributes.forEach((instance) =>
customizeAttributes(instance, definitionForElement.get(instance))
);
},
{once: true}
);
}

const ensureAttributesCustomized = (instance) => {
if (!elementsPendingAttributes?.has(instance)) {
return;
}
customizeAttributes(instance, definitionForElement.get(instance));
};

// Approximate observedAttributes from the user class, since the stand-in element had none
const customizeAttributes = (instance, definition) => {
elementsPendingAttributes?.delete(instance);
definition.observedAttributes.forEach((attr) => {
if (instance.hasAttribute(attr)) {
definition.attributeChangedCallback.call(
instance,
attr,
null,
instance.getAttribute(attr)
);
}
});
};

// Helper to upgrade an instance with a CE definition using "constructor call trick"
const customize = (instance, definition, isUpgrade = false) => {
Object.setPrototypeOf(instance, definition.elementClass.prototype);
Expand All @@ -419,17 +463,14 @@ if (!ShadowRoot.prototype.createElement) {
new definition.elementClass();
}
if (definition.attributeChangedCallback) {
// Approximate observedAttributes from the user class, since the stand-in element had none
definition.observedAttributes.forEach((attr) => {
if (instance.hasAttribute(attr)) {
definition.attributeChangedCallback.call(
instance,
attr,
null,
instance.getAttribute(attr)
);
}
});
if (
elementsPendingAttributes !== undefined &&
!instance.hasAttributes()
) {
elementsPendingAttributes.add(instance);
} else {
customizeAttributes(instance, definition);
}
}
if (isUpgrade && definition.connectedCallback && instance.isConnected) {
definition.connectedCallback.call(instance);
Expand Down
21 changes: 21 additions & 0 deletions packages/scoped-custom-element-registry/test/Element.test.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
<html>
<body>
<script src="../scoped-custom-element-registry.min.js"></script>

<script>
// Test element for testing attribute processing of parser created
// elements.
customElements.define(
'parsed-el',
class extends HTMLElement {
static observedAttributes = ['a', 'b'];
attributeChanges = [];
attributeChangedCallback(name, old, value) {
this.attributeChanges.push({name, old, value});
}
}
);
const imp = document.createElement('parsed-el');
imp.setAttribute('a', 'ia');
imp.id = 'imperative-parsed-el';
document.body.append(imp);
</script>
<parsed-el id="parsed-el" a="a" b="b"></parsed-el>

<script type="module">
import {runTests} from '@web/test-runner-mocha';

Expand Down
30 changes: 27 additions & 3 deletions packages/scoped-custom-element-registry/test/Element.test.html.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ describe('Element', () => {
const $el = document.createElement(tagName);

$el.setAttribute('foo', 'bar');

expect($el.attributeChanges).to.be.deep.equal([
{name: 'foo', old: null, value: 'bar'},
]);
expect($el.getAttribute('foo')).to.equal('bar');
});

Expand All @@ -131,7 +133,10 @@ describe('Element', () => {
const $el = getHTML(`<${tagName} foo></${tagName}>`);

$el.removeAttribute('foo');

expect($el.attributeChanges).to.be.deep.equal([
{name: 'foo', old: null, value: ''},
{name: 'foo', old: '', value: null},
]);
expect($el.hasAttribute('foo')).to.be.false;
});

Expand All @@ -148,8 +153,27 @@ describe('Element', () => {

$el.setAttribute('foo', '');
$el.toggleAttribute('foo', true);

expect($el.attributeChanges).to.be.deep.equal([
{name: 'foo', old: null, value: ''},
]);
expect($el.hasAttribute('foo')).to.be.true;
});

it('should call attributeChangedCallback for parser created element', () => {
const $el = document.getElementById('parsed-el');
expect($el).to.be.ok;
expect($el.attributeChanges).to.be.deep.equal([
{name: 'a', old: null, value: 'a'},
{name: 'b', old: null, value: 'b'},
]);
});

it('should call attributeChangedCallback for imperative created element while parsing', () => {
const $el = document.getElementById('imperative-parsed-el');
expect($el).to.be.ok;
expect($el.attributeChanges).to.be.deep.equal([
{name: 'a', old: null, value: 'ia'},
]);
});
});
});
4 changes: 4 additions & 0 deletions packages/scoped-custom-element-registry/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export const getObservedAttributesTestElement = (
static get observedAttributes() {
return observedAttributeNames;
}
attributeChanges = [];
attributeChangedCallback(name, old, value) {
this.attributeChanges.push({name, old, value});
}
},
});

Expand Down

0 comments on commit f87300a

Please sign in to comment.