Skip to content

Commit afbd129

Browse files
authored
fix(runtime) prevent shadowing on non-upgraded compoents
* fix an issue in the custom elements bundle where setting a property on an element programatically before it was upgraded it would shadow the accessors * this would additionally break any watchers as well.
1 parent 7f7571a commit afbd129

File tree

2 files changed

+40
-1
lines changed

2 files changed

+40
-1
lines changed

src/runtime/connected-callback.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const connectedCallback = (elm: d.HostElement) => {
6868

6969
// Lazy properties
7070
// https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties
71-
if (BUILD.prop && BUILD.lazyLoad && !BUILD.hydrateServerSide && cmpMeta.$members$) {
71+
if (BUILD.prop && !BUILD.hydrateServerSide && cmpMeta.$members$) {
7272
Object.entries(cmpMeta.$members$).map(([memberName, [memberFlags]]) => {
7373
if (memberFlags & MEMBER_FLAGS.Prop && elm.hasOwnProperty(memberName)) {
7474
const value = (elm as any)[memberName];

src/runtime/proxy-component.ts

+39
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,45 @@ export const proxyComponent = (Cstr: d.ComponentConstructor, cmpMeta: d.Componen
6262
prototype.attributeChangedCallback = function (attrName: string, _oldValue: string, newValue: string) {
6363
plt.jmp(() => {
6464
const propName = attrNameToPropName.get(attrName);
65+
66+
// In a webcomponent lifecyle the attributeChangedCallback runs prior to connectedCallback
67+
// in the case where an attribute was set inline.
68+
// ```html
69+
// <my-component some-attribute="some-value"></my-component>
70+
// ```
71+
//
72+
// There is an edge case where a developer sets the attribute inline on a custom element and then programatically
73+
// changes it before it has been upgraded as shown below:
74+
//
75+
// ```html
76+
// <!-- this component has _not_ been upgraded yet -->
77+
// <my-component id="test" some-attribute="some-value"></my-component>
78+
// <script>
79+
// // grab non-upgraded component
80+
// el = document.querySelector("#test");
81+
// el.someAttribute = "another-value";
82+
// // upgrade component
83+
// cutsomElements.define('my-component', MyComponent);
84+
// </script>
85+
// ```
86+
// In this case if we do not unshadow here and use the value of the shadowing property, attributeChangedCallback
87+
// will be called with `newValue = "some-value"` and will set the shadowed property (this.someAttribute = "another-value")
88+
// to the value that was set inline i.e. "some-value" from above example. When
89+
// the connectedCallback attempts to unshadow it will use "some-value" as the intial value rather than "another-value"
90+
//
91+
// The case where the attribute was NOT set inline but was not set programmatically shall be handled/unshadowed
92+
// by connectedCallback as this attributeChangedCallback will not fire.
93+
//
94+
// https://developers.google.com/web/fundamentals/web-components/best-practices#lazy-properties
95+
//
96+
// TODO(STENCIL-16) we should think about whether or not we actually want to be reflecting the attributes to
97+
// properties here given that this goes against best practices outlined here
98+
// https://developers.google.com/web/fundamentals/web-components/best-practices#avoid-reentrancy
99+
if (this.hasOwnProperty(propName)) {
100+
newValue = this[propName];
101+
delete this[propName];
102+
}
103+
65104
this[propName] = newValue === null && typeof this[propName] === 'boolean' ? false : newValue;
66105
});
67106
};

0 commit comments

Comments
 (0)