Skip to content

Commit b8be2ea

Browse files
committed
feat(bind): support binding shadowDOM elements
1 parent 105b896 commit b8be2ea

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/bind.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const controllers = new Set<string>()
66
*/
77
export function bind(controller: HTMLElement): void {
88
controllers.add(controller.tagName.toLowerCase())
9+
if (controller.shadowRoot) {
10+
bindElements(controller.shadowRoot)
11+
listenForBind(controller.shadowRoot)
12+
}
913
bindElements(controller)
1014
}
1115

@@ -48,12 +52,12 @@ interface Subscription {
4852
unsubscribe(): void
4953
}
5054

51-
function bindElements(root: Element) {
55+
function bindElements(root: Element | ShadowRoot) {
5256
for (const el of root.querySelectorAll('[data-action]')) {
5357
bindActions(el)
5458
}
5559
// Also bind the controller to itself
56-
if (root.hasAttribute('data-action')) {
60+
if (root instanceof Element && root.hasAttribute('data-action')) {
5761
bindActions(root)
5862
}
5963
}
@@ -63,10 +67,18 @@ function handleEvent(event: Event) {
6367
const el = event.currentTarget as Element
6468
for (const binding of bindings(el)) {
6569
if (event.type === binding.type && controllers.has(binding.tag)) {
66-
const controller = el.closest(binding.tag) as Element & Record<string, (ev: Event) => unknown>
70+
type EventDispatcher = Element & Record<string, (ev: Event) => unknown>
71+
const controller = el.closest(binding.tag) as EventDispatcher
6772
if (controller && typeof controller[binding.method] === 'function') {
6873
controller[binding.method](event)
6974
}
75+
const root = el.getRootNode()
76+
if (root instanceof ShadowRoot && root.host.matches(binding.tag)) {
77+
const shadowController = root.host as EventDispatcher
78+
if (typeof shadowController[binding.method] === 'function') {
79+
shadowController[binding.method](event)
80+
}
81+
}
7082
}
7183
}
7284
}

test/bind.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,45 @@ describe('bind', () => {
138138
expect(instance.foo).to.have.been.called.exactly(2)
139139
})
140140

141+
it('can bind elements within the shadowDOM', () => {
142+
const instance = document.createElement('bind-test-element')
143+
chai.spy.on(instance, 'foo')
144+
instance.attachShadow({mode: 'open'})
145+
const el1 = document.createElement('div')
146+
const el2 = document.createElement('div')
147+
el1.setAttribute('data-action', 'click:bind-test-element#foo')
148+
el2.setAttribute('data-action', 'submit:bind-test-element#foo')
149+
instance.shadowRoot.append(el1, el2)
150+
bind(instance)
151+
expect(instance.foo).to.have.not.been.called()
152+
el1.click()
153+
expect(instance.foo).to.have.been.called.exactly(1)
154+
el2.dispatchEvent(new CustomEvent('submit'))
155+
expect(instance.foo).to.have.been.called.exactly(2)
156+
})
157+
158+
it('binds elements added to shadowDOM', async () => {
159+
const instance = document.createElement('bind-test-element')
160+
chai.spy.on(instance, 'foo')
161+
instance.attachShadow({mode: 'open'})
162+
const el1 = document.createElement('div')
163+
const el2 = document.createElement('div')
164+
el1.setAttribute('data-action', 'click:bind-test-element#foo')
165+
el2.setAttribute('data-action', 'submit:bind-test-element#foo')
166+
bind(instance)
167+
instance.shadowRoot.append(el1)
168+
instance.shadowRoot.append(el2)
169+
// We need to wait for a couple of frames after injecting the HTML into to
170+
// controller so that the actions have been bound to the controller.
171+
await waitForNextAnimationFrame()
172+
await waitForNextAnimationFrame()
173+
expect(instance.foo).to.have.not.been.called()
174+
el1.click()
175+
expect(instance.foo).to.have.been.called.exactly(1)
176+
el2.dispatchEvent(new CustomEvent('submit'))
177+
expect(instance.foo).to.have.been.called.exactly(2)
178+
})
179+
141180
describe('listenForBind', () => {
142181
it('re-binds actions that are denoted by HTML that is dynamically injected into the controller', async function () {
143182
const instance = document.createElement('bind-test-element')

0 commit comments

Comments
 (0)