Skip to content

Commit 13c78ef

Browse files
keithamuskoddsson
andcommitted
initialize attrs on attributeChangedCallback
If attrs are used, and attributeChangedCallback fires before connectedCallback, attrs will be in their initially set state, rather than reflected from the attribute value, likewise attributes will be null rather than their attr value. Initializing attrs on the first attributeChangedCallback call fixes this. Co-authored-by: Kristján Oddsson <koddsson@gmail.com>
1 parent 9de0557 commit 13c78ef

File tree

3 files changed

+55
-1
lines changed

3 files changed

+55
-1
lines changed

src/controller.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {initializeInstance, initializeClass} from './core.js'
1+
import {initializeInstance, initializeClass, initializeAttributeChanged} from './core.js'
22
import type {CustomElement} from './custom-element.js'
33
/**
44
* Controller is a decorator to be used over a class that extends HTMLElement.
@@ -11,5 +11,14 @@ export function controller(classObject: CustomElement): void {
1111
classObject.prototype.connectedCallback = function (this: HTMLElement) {
1212
initializeInstance(this, connect)
1313
}
14+
const attributeChanged = classObject.prototype.attributeChangedCallback
15+
classObject.prototype.attributeChangedCallback = function (
16+
this: HTMLElement,
17+
name: string,
18+
oldValue: unknown,
19+
newValue: unknown
20+
) {
21+
initializeAttributeChanged(this, name, oldValue, newValue, attributeChanged)
22+
}
1423
initializeClass(classObject)
1524
}

src/core.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ export function initializeInstance(instance: HTMLElement, connect?: (this: HTMLE
1717
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
1818
}
1919

20+
export function initializeAttributeChanged(
21+
instance: HTMLElement,
22+
name: string,
23+
oldValue: unknown,
24+
newValue: unknown,
25+
attributeChangedCallback?: (this: HTMLElement, name: string, oldValue: unknown, newValue: unknown) => void
26+
): void {
27+
initializeAttrs(instance)
28+
if (name !== 'data-catalyst' && attributeChangedCallback) {
29+
attributeChangedCallback.call(instance, name, oldValue, newValue)
30+
}
31+
}
32+
2033
export function initializeClass(classObject: CustomElement): void {
2134
defineObservedAttributes(classObject)
2235
register(classObject)

test/controller.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {controller} from '../lib/controller.js'
2+
import {attr} from '../lib/attr.js'
23

34
describe('controller', () => {
45
let root
@@ -102,4 +103,35 @@ describe('controller', () => {
102103
// eslint-disable-next-line github/unescaped-html-literal
103104
root.innerHTML = '<parent-element><child-element></child-element></parent-element>'
104105
})
106+
107+
describe('attrs', () => {
108+
let attrValues = []
109+
class AttributeTestElement extends HTMLElement {
110+
foo = 'baz'
111+
attributeChangedCallback() {
112+
attrValues.push(this.getAttribute('data-foo'))
113+
attrValues.push(this.foo)
114+
}
115+
}
116+
controller(AttributeTestElement)
117+
attr(AttributeTestElement.prototype, 'foo')
118+
119+
beforeEach(() => {
120+
attrValues = []
121+
})
122+
123+
it('initializes attrs as attributes in attributeChangedCallback', () => {
124+
const el = document.createElement('attribute-test')
125+
el.foo = 'bar'
126+
el.attributeChangedCallback()
127+
expect(attrValues).to.eql(['bar', 'bar'])
128+
})
129+
130+
it('initializes attributes as attrs in attributeChangedCallback', () => {
131+
const el = document.createElement('attribute-test')
132+
el.setAttribute('data-foo', 'bar')
133+
el.attributeChangedCallback()
134+
expect(attrValues).to.eql(['bar', 'bar'])
135+
})
136+
})
105137
})

0 commit comments

Comments
 (0)