diff --git a/src/compiler/app-core/app-data.ts b/src/compiler/app-core/app-data.ts index ff7a7dd4059..04890373ca6 100644 --- a/src/compiler/app-core/app-data.ts +++ b/src/compiler/app-core/app-data.ts @@ -50,7 +50,7 @@ export const getBuildFeatures = (cmps: ComponentCompilerMeta[]): BuildFeatures = member: cmps.some((c) => c.hasMember), method: cmps.some((c) => c.hasMethod), mode: cmps.some((c) => c.hasMode), - observeAttribute: cmps.some((c) => c.hasAttribute), + observeAttribute: cmps.some((c) => c.hasAttribute || c.hasWatchCallback), prop: cmps.some((c) => c.hasProp), propBoolean: cmps.some((c) => c.hasPropBoolean), propNumber: cmps.some((c) => c.hasPropNumber), diff --git a/src/runtime/proxy-component.ts b/src/runtime/proxy-component.ts index a9a50b849f3..ffed91c090b 100644 --- a/src/runtime/proxy-component.ts +++ b/src/runtime/proxy-component.ts @@ -49,12 +49,12 @@ export const proxyComponent = ( ); } - if (BUILD.member && cmpMeta.$members$) { - if (BUILD.watchCallback && Cstr.watchers) { + if ((BUILD.member && cmpMeta.$members$) || (BUILD.watchCallback && (cmpMeta.$watchers$ || Cstr.watchers))) { + if (BUILD.watchCallback && Cstr.watchers && !cmpMeta.$watchers$) { cmpMeta.$watchers$ = Cstr.watchers; } // It's better to have a const than two Object.entries() - const members = Object.entries(cmpMeta.$members$); + const members = Object.entries(cmpMeta.$members$ ?? {}); members.map(([memberName, [memberFlags]]) => { if ( (BUILD.prop || BUILD.state) && diff --git a/test/wdio/watch-native-attributes/cmp-no-members.test.tsx b/test/wdio/watch-native-attributes/cmp-no-members.test.tsx new file mode 100644 index 00000000000..6bcaa8d040e --- /dev/null +++ b/test/wdio/watch-native-attributes/cmp-no-members.test.tsx @@ -0,0 +1,24 @@ +import { h } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; + +describe('watch native attributes w/ no Stencil members', () => { + beforeEach(() => { + render({ + template: () => ( + + ), + }); + }); + + it('triggers the callback for the watched attribute', async () => { + const $cmp = $('watch-native-attributes-no-members'); + await $cmp.waitForExist(); + + await expect($cmp).toHaveText('Label: myStartingLabel\nCallback triggered: false'); + + const cmp = document.querySelector('watch-native-attributes-no-members'); + cmp.setAttribute('aria-label', 'myNewLabel'); + + await expect($cmp).toHaveText('Label: myNewLabel\nCallback triggered: true'); + }); +}); diff --git a/test/wdio/watch-native-attributes/cmp-no-members.tsx b/test/wdio/watch-native-attributes/cmp-no-members.tsx new file mode 100644 index 00000000000..95cd3b7aac3 --- /dev/null +++ b/test/wdio/watch-native-attributes/cmp-no-members.tsx @@ -0,0 +1,23 @@ +import { Component, Element, forceUpdate, h, Watch } from '@stencil/core'; + +@Component({ + tag: 'watch-native-attributes-no-members', +}) +export class WatchNativeAttributesNoMembers { + @Element() el!: HTMLElement; + + private callbackTriggered = false; + + @Watch('aria-label') + onAriaLabelChange() { + this.callbackTriggered = true; + forceUpdate(this); + } + + render() { + return [ +

Label: {this.el.getAttribute('aria-label')}

, +

Callback triggered: {`${this.callbackTriggered}`}

, + ]; + } +}