Skip to content

Commit f3c8e75

Browse files
authored
Merge pull request #103 from github/imperative-and-shadow-element-definitions
Imperative and shadow element definitions
2 parents a058977 + f14f04d commit f3c8e75

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

src/auto-complete-element.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,40 @@ const state = new WeakMap()
66

77
// eslint-disable-next-line custom-elements/file-name-matches-element
88
export default class AutocompleteElement extends HTMLElement {
9-
connectedCallback(): void {
10-
const listId = this.getAttribute('for')
11-
if (!listId) return
9+
#forElement: HTMLElement | null = null
10+
get forElement(): HTMLElement | null {
11+
if (this.#forElement?.isConnected) {
12+
return this.#forElement
13+
}
14+
const id = this.getAttribute('for')
15+
const root = this.getRootNode()
16+
if (id && (root instanceof Document || root instanceof ShadowRoot)) {
17+
return root.getElementById(id)
18+
}
19+
return null
20+
}
1221

13-
// eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
14-
const input = this.querySelector('input')
15-
const results = document.getElementById(listId)
16-
if (!(input instanceof HTMLInputElement) || !results) return
17-
const autoselectEnabled = this.getAttribute('data-autoselect') === 'true'
18-
state.set(this, new Autocomplete(this, input, results, autoselectEnabled))
19-
results.setAttribute('role', 'listbox')
22+
set forElement(element: HTMLElement | null) {
23+
this.#forElement = element
24+
this.setAttribute('for', '')
25+
}
26+
27+
#inputElement: HTMLInputElement | null = null
28+
get inputElement(): HTMLInputElement | null {
29+
if (this.#inputElement?.isConnected) {
30+
return this.#inputElement
31+
}
32+
return this.querySelector<HTMLInputElement>('input')
33+
}
34+
35+
set inputElement(input: HTMLInputElement | null) {
36+
this.#inputElement = input
37+
this.#reattachState()
38+
}
39+
40+
connectedCallback(): void {
41+
if (!this.isConnected) return
42+
this.#reattachState()
2043
}
2144

2245
disconnectedCallback(): void {
@@ -27,6 +50,15 @@ export default class AutocompleteElement extends HTMLElement {
2750
}
2851
}
2952

53+
#reattachState() {
54+
state.get(this)?.destroy()
55+
const {forElement, inputElement} = this
56+
if (!forElement || !inputElement) return
57+
const autoselectEnabled = this.getAttribute('data-autoselect') === 'true'
58+
state.set(this, new Autocomplete(this, inputElement, forElement, autoselectEnabled))
59+
forElement.setAttribute('role', 'listbox')
60+
}
61+
3062
get src(): string {
3163
return this.getAttribute('src') || ''
3264
}
@@ -66,7 +98,7 @@ export default class AutocompleteElement extends HTMLElement {
6698
fetchResult = fragment
6799

68100
static get observedAttributes(): string[] {
69-
return ['open', 'value']
101+
return ['open', 'value', 'for']
70102
}
71103

72104
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
@@ -75,6 +107,10 @@ export default class AutocompleteElement extends HTMLElement {
75107
const autocomplete = state.get(this)
76108
if (!autocomplete) return
77109

110+
if (this.forElement !== state.get(this)?.results || this.inputElement !== state.get(this)?.input) {
111+
this.#reattachState()
112+
}
113+
78114
switch (name) {
79115
case 'open':
80116
newValue === null ? autocomplete.close() : autocomplete.open()

src/autocomplete.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export default class Autocomplete {
2828
this.input = input
2929
this.results = results
3030
this.combobox = new Combobox(input, results)
31-
this.feedback = document.getElementById(`${this.results.id}-feedback`)
31+
this.feedback = (container.getRootNode() as Document).getElementById(`${this.results.id}-feedback`)
3232
this.autoselectEnabled = autoselectEnabled
33-
this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`)
33+
this.clearButton = (container.getRootNode() as Document).getElementById(`${this.input.id || this.input.name}-clear`)
3434

3535
// check to see if there are any default options provided
3636
this.clientOptions = results.querySelectorAll('[role=option]')

test/test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,62 @@ describe('auto-complete element', function () {
314314
assert.equal(feedback.textContent, '')
315315
})
316316
})
317+
318+
describe('shadowdom', () => {
319+
let shadow = null
320+
beforeEach(function () {
321+
const fixture = document.createElement('div')
322+
fixture.id = 'mocha-fixture'
323+
document.body.append(fixture)
324+
shadow = fixture.attachShadow({mode: 'open'})
325+
shadow.innerHTML = `
326+
<auto-complete src="/search" for="popup">
327+
<input type="text">
328+
<ul id="popup"></ul>
329+
<div id="popup-feedback"></div>
330+
</auto-complete>
331+
</div>
332+
`
333+
})
334+
335+
it('uses rootNode to find idrefs', async function () {
336+
const container = shadow.querySelector('auto-complete')
337+
const input = container.querySelector('input')
338+
const popup = container.querySelector('#popup')
339+
340+
triggerInput(input, 'hub')
341+
await once(container, 'loadend')
342+
assert.equal(5, popup.children.length)
343+
})
344+
})
345+
346+
describe('redefining elements', () => {
347+
beforeEach(function () {
348+
document.body.innerHTML = `
349+
<div id="mocha-fixture">
350+
<auto-complete src="/search" data-autoselect="true">
351+
<input type="text">
352+
<input id="second" type="text">
353+
<ul></ul>
354+
<div id="popup-feedback"></div>
355+
</auto-complete>
356+
</div>
357+
`
358+
})
359+
360+
it('changes where content gets rendered based on properties', async function () {
361+
const container = document.querySelector('auto-complete')
362+
const input = container.querySelector('input#second')
363+
const list = container.querySelector('ul')
364+
container.forElement = list
365+
container.inputElement = input
366+
367+
triggerInput(input, 'hub')
368+
await once(container, 'loadend')
369+
370+
assert.equal(5, list.children.length)
371+
})
372+
})
317373
})
318374

319375
function waitForElementToChange(el) {

0 commit comments

Comments
 (0)