diff --git a/src/attr.ts b/src/attr.ts index 2761926b..15a310e3 100644 --- a/src/attr.ts +++ b/src/attr.ts @@ -34,7 +34,10 @@ export function attr(proto: Record, key: K): voi * This is automatically called as part of `@controller`. If a class uses the * `@controller` decorator it should not call this manually. */ +const initialized = new WeakSet() export function initializeAttrs(instance: HTMLElement, names?: Iterable): void { + if (initialized.has(instance)) return + initialized.add(instance) if (!names) names = getAttrNames(Object.getPrototypeOf(instance)) for (const key of names) { const value = (>(instance))[key] diff --git a/src/controller.ts b/src/controller.ts index 058bf2e6..ba900893 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -1,4 +1,4 @@ -import {initializeInstance, initializeClass} from './core.js' +import {initializeInstance, initializeClass, initializeAttributeChanged} from './core.js' import type {CustomElement} from './custom-element.js' /** * Controller is a decorator to be used over a class that extends HTMLElement. @@ -11,5 +11,14 @@ export function controller(classObject: CustomElement): void { classObject.prototype.connectedCallback = function (this: HTMLElement) { initializeInstance(this, connect) } + const attributeChanged = classObject.prototype.attributeChangedCallback + classObject.prototype.attributeChangedCallback = function ( + this: HTMLElement, + name: string, + oldValue: unknown, + newValue: unknown + ) { + initializeAttributeChanged(this, name, oldValue, newValue, attributeChanged) + } initializeClass(classObject) } diff --git a/src/core.ts b/src/core.ts index a64c7d8a..56ee7f21 100644 --- a/src/core.ts +++ b/src/core.ts @@ -17,6 +17,19 @@ export function initializeInstance(instance: HTMLElement, connect?: (this: HTMLE if (instance.shadowRoot) bindShadow(instance.shadowRoot) } +export function initializeAttributeChanged( + instance: HTMLElement, + name: string, + oldValue: unknown, + newValue: unknown, + attributeChangedCallback?: (this: HTMLElement, name: string, oldValue: unknown, newValue: unknown) => void +): void { + initializeAttrs(instance) + if (name !== 'data-catalyst' && attributeChangedCallback) { + attributeChangedCallback.call(instance, name, oldValue, newValue) + } +} + export function initializeClass(classObject: CustomElement): void { defineObservedAttributes(classObject) register(classObject) diff --git a/test/controller.js b/test/controller.js index 7b346536..23547626 100644 --- a/test/controller.js +++ b/test/controller.js @@ -1,4 +1,5 @@ import {controller} from '../lib/controller.js' +import {attr} from '../lib/attr.js' describe('controller', () => { let root @@ -102,4 +103,35 @@ describe('controller', () => { // eslint-disable-next-line github/unescaped-html-literal root.innerHTML = '' }) + + describe('attrs', () => { + let attrValues = [] + class AttributeTestElement extends HTMLElement { + foo = 'baz' + attributeChangedCallback() { + attrValues.push(this.getAttribute('data-foo')) + attrValues.push(this.foo) + } + } + controller(AttributeTestElement) + attr(AttributeTestElement.prototype, 'foo') + + beforeEach(() => { + attrValues = [] + }) + + it('initializes attrs as attributes in attributeChangedCallback', () => { + const el = document.createElement('attribute-test') + el.foo = 'bar' + el.attributeChangedCallback() + expect(attrValues).to.eql(['bar', 'bar']) + }) + + it('initializes attributes as attrs in attributeChangedCallback', () => { + const el = document.createElement('attribute-test') + el.setAttribute('data-foo', 'bar') + el.attributeChangedCallback() + expect(attrValues).to.eql(['bar', 'bar']) + }) + }) })