Skip to content

Commit 5d437d6

Browse files
committed
refactor bind into actionable
1 parent 86e4eeb commit 5d437d6

6 files changed

Lines changed: 84 additions & 120 deletions

File tree

src/actionable.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type {CustomElementClass, CustomElement} from './custom-element.js'
2+
import type {ControllableClass} from './controllable.js'
3+
import {registerTag, observeElementForTags, parseElementTags} from './tag-observer.js'
4+
import {controllable, attachShadowCallback} from './controllable.js'
5+
import {createAbility} from './ability.js'
6+
7+
const parse = (tag: string): [tagName: string, event: string, method: string] => {
8+
const eventSep = tag.lastIndexOf(':')
9+
const methodSep = Math.max(0, tag.lastIndexOf('#')) || tag.length
10+
return [tag.slice(eventSep + 1, methodSep), tag.slice(0, eventSep), tag.slice(methodSep + 1) || 'handleEvent']
11+
}
12+
registerTag(
13+
'data-action',
14+
parseActionAttribute,
15+
(el: Element, controller: Element | ShadowRoot, tag: string, event: string) => {
16+
el.addEventListener(event, handleEvent)
17+
}
18+
)
19+
20+
const actionables = new WeakSet<CustomElement>()
21+
// Bind a single function to all events to avoid anonymous closure performance penalty.
22+
function handleEvent(event: Event) {
23+
const el = event.currentTarget as Element
24+
for (const [tag, type, method] of parseElementTags(el, 'data-action', parseActionAttribute)) {
25+
if (event.type === type) {
26+
type EventDispatcher = CustomElement & Record<string, (ev: Event) => unknown>
27+
const controller = el.closest<EventDispatcher>(tag)!
28+
if (actionables.has(controller) && typeof controller[method] === 'function') {
29+
controller[method](event)
30+
}
31+
const root = el.getRootNode()
32+
if (root instanceof ShadowRoot) {
33+
const shadowController = root.host as EventDispatcher
34+
if (shadowController.matches(tag) && actionables.has(shadowController)) {
35+
if (typeof shadowController[method] === 'function') {
36+
shadowController[method](event)
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
43+
44+
export const actionable = createAbility(
45+
<T extends CustomElementClass>(Class: T): T & ControllableClass =>
46+
class extends controllable(Class) {
47+
constructor() {
48+
super()
49+
actionables.add(this)
50+
observeElementForTags(this)
51+
}
52+
53+
[attachShadowCallback](root: ShadowRoot) {
54+
super[attachShadowCallback]?.(root)
55+
observeElementForTags(root)
56+
}
57+
}
58+
)

src/bind.ts

Lines changed: 0 additions & 111 deletions
This file was deleted.

src/controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {CatalystDelegate} from './core.js'
22
import type {CustomElementClass} from './custom-element.js'
33
import {attrable} from './attrable.js'
4+
import {actionable} from './actionable.js'
5+
46
/**
57
* Controller is a decorator to be used over a class that extends HTMLElement.
68
* It will automatically `register()` the component in the customElement
79
* registry, as well as ensuring `bind(this)` is called on `connectedCallback`,
810
* wrapping the classes `connectedCallback` method if needed.
911
*/
1012
export function controller(classObject: CustomElementClass): void {
11-
new CatalystDelegate(attrable(classObject))
13+
new CatalystDelegate(actionable(attrable(classObject)))
1214
}

src/core.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ export class CatalystDelegate {
4949
connectedCallback(instance: HTMLElement, connectedCallback: () => void) {
5050
instance.toggleAttribute('data-catalyst', true)
5151
customElements.upgrade(instance)
52-
bind(instance)
5352
connectedCallback?.call(instance)
54-
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
5553
}
5654

5755
disconnectedCallback(element: HTMLElement, disconnectedCallback: () => void) {

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export {bind, listenForBind} from './bind.js'
1+
export {actionable} from './actionable.js'
22
export {register} from './register.js'
33
export {findTarget, findTargets} from './findtarget.js'
44
export {target, targets} from './target.js'

test/bind.ts renamed to test/actionable.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {expect, fixture, html} from '@open-wc/testing'
22
import {fake} from 'sinon'
3-
import {controller} from '../src/controller.js'
4-
import {bindShadow} from '../src/bind.js'
3+
import {actionable} from '../src/actionable.js'
54

65
describe('Actionable', () => {
7-
@controller
6+
@actionable
87
class BindTestElement extends HTMLElement {
98
foo = fake()
109
bar = fake()
1110
handleEvent = fake()
1211
}
12+
window.customElements.define('bind-test', BindTestElement)
1313
let instance: BindTestElement
1414
beforeEach(async () => {
1515
instance = await fixture(html`<bind-test data-action="foo:bind-test#foo">
@@ -126,7 +126,25 @@ describe('Actionable', () => {
126126
el1.setAttribute('data-action', 'click:bind-test#foo')
127127
el2.setAttribute('data-action', 'submit:bind-test#foo')
128128
const shadowRoot = instance.attachShadow({mode: 'open'})
129-
bindShadow(shadowRoot)
129+
shadowRoot.append(el1, el2)
130+
131+
// We need to wait for one microtask after injecting the HTML into to
132+
// controller so that the actions have been bound to the controller.
133+
await Promise.resolve()
134+
135+
expect(instance.foo).to.have.callCount(0)
136+
el1.click()
137+
expect(instance.foo).to.have.callCount(1)
138+
el2.dispatchEvent(new CustomEvent('submit'))
139+
expect(instance.foo).to.have.callCount(2)
140+
})
141+
142+
it('can bind elements within a closed shadowDOM', async () => {
143+
const el1 = document.createElement('div')
144+
const el2 = document.createElement('div')
145+
el1.setAttribute('data-action', 'click:bind-test#foo')
146+
el2.setAttribute('data-action', 'submit:bind-test#foo')
147+
const shadowRoot = instance.attachShadow({mode: 'closed'})
130148
shadowRoot.append(el1, el2)
131149

132150
// We need to wait for one microtask after injecting the HTML into to
@@ -158,7 +176,6 @@ describe('Actionable', () => {
158176
const el1 = document.createElement('div')
159177
const el2 = document.createElement('div')
160178
const shadowRoot = instance.attachShadow({mode: 'open'})
161-
bindShadow(shadowRoot)
162179
shadowRoot.append(el1, el2)
163180

164181
// We need to wait for one microtask after injecting the HTML into to

0 commit comments

Comments
 (0)