Skip to content

Commit bb32c3b

Browse files
committed
refactor target into targetable
1 parent 21c81fb commit bb32c3b

6 files changed

Lines changed: 170 additions & 83 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
{
5555
"path": "lib/index.js",
5656
"import": "{controller, attr, target, targets}",
57-
"limit": "2.6kb"
57+
"limit": "2.7kb"
5858
},
5959
{
6060
"path": "lib/abilities.js",

src/findtarget.ts

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

src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
export {actionable} from './actionable.js'
22
export {register} from './register.js'
3-
export {findTarget, findTargets} from './findtarget.js'
4-
export {target, targets} from './target.js'
3+
export {
4+
target,
5+
getTarget,
6+
targets,
7+
getTargets,
8+
targetChangedCallback,
9+
targetsChangedCallback,
10+
targetable
11+
} from './targetable.js'
512
export {controller} from './controller.js'
613
export {attr, getAttr, attrable, attrChangedCallback} from './attrable.js'
714
export {lazyDefine} from './lazy-define.js'

src/target.ts

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

src/targetable.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type {CustomElementClass} from './custom-element.js'
2+
import type {ControllableClass} from './controllable.js'
3+
import {registerTag, observeElementForTags} from './tag-observer.js'
4+
import {createMark} from './mark.js'
5+
import {controllable, attachShadowCallback} from './controllable.js'
6+
import {dasherize} from './dasherize.js'
7+
import {createAbility} from './ability.js'
8+
9+
export interface Targetable {
10+
[targetChangedCallback](key: PropertyKey, target: Element): void
11+
[targetsChangedCallback](key: PropertyKey, targets: Element[]): void
12+
}
13+
export interface TargetableClass {
14+
new (): Targetable
15+
}
16+
17+
const targetChangedCallback = Symbol()
18+
const targetsChangedCallback = Symbol()
19+
20+
const [target, getTarget, initializeTarget] = createMark<Element>(
21+
({name, kind}) => {
22+
if (kind === 'getter') throw new Error(`@target cannot decorate get ${String(name)}`)
23+
},
24+
(instance: Element, {name, access}) => {
25+
const selector = [
26+
`[data-target~="${instance.tagName.toLowerCase()}.${dasherize(name)}"]`,
27+
`[data-target~="${instance.tagName.toLowerCase()}.${String(name)}"]`
28+
]
29+
const find = findTarget(instance, selector.join(', '), false)
30+
return {
31+
get: find,
32+
set: () => {
33+
if (access?.set) access.set.call(instance, find())
34+
}
35+
}
36+
}
37+
)
38+
const [targets, getTargets, initializeTargets] = createMark<Element>(
39+
({name, kind}) => {
40+
if (kind === 'getter') throw new Error(`@target cannot decorate get ${String(name)}`)
41+
},
42+
(instance: Element, {name, access}) => {
43+
const selector = [
44+
`[data-targets~="${instance.tagName.toLowerCase()}.${dasherize(name)}"]`,
45+
`[data-targets~="${instance.tagName.toLowerCase()}.${String(name)}"]`
46+
]
47+
const find = findTarget(instance, selector.join(', '), true)
48+
return {
49+
get: find,
50+
set: () => {
51+
if (access?.set) access.set.call(instance, find())
52+
}
53+
}
54+
}
55+
)
56+
57+
function setTarget(el: Element, controller: Element | ShadowRoot, tag: string, key: string): void {
58+
const get = tag === 'data-targets' ? getTargets : getTarget
59+
if (controller instanceof ShadowRoot) {
60+
controller = controllers.get(controller)!
61+
}
62+
if (controller && get(controller)?.has(key)) {
63+
;(controller as unknown as Record<PropertyKey, unknown>)[key] = {}
64+
}
65+
}
66+
67+
registerTag('data-target', (str: string) => str.split('.'), setTarget)
68+
registerTag('data-targets', (str: string) => str.split('.'), setTarget)
69+
const shadows = new WeakMap<Element, ShadowRoot>()
70+
const controllers = new WeakMap<ShadowRoot, Element>()
71+
72+
const findTarget = (controller: Element, selector: string, many: boolean) => () => {
73+
const nodes = []
74+
const shadow = shadows.get(controller)
75+
if (shadow) {
76+
for (const el of shadow.querySelectorAll(selector)) {
77+
if (!el.closest(controller.tagName)) {
78+
nodes.push(el)
79+
if (!many) break
80+
}
81+
}
82+
}
83+
if (many || !nodes.length) {
84+
for (const el of controller.querySelectorAll(selector)) {
85+
if (el.closest(controller.tagName) === controller) {
86+
nodes.push(el)
87+
if (!many) break
88+
}
89+
}
90+
}
91+
return many ? nodes : nodes[0]
92+
}
93+
94+
export {target, getTarget, targets, getTargets, targetChangedCallback, targetsChangedCallback}
95+
export const targetable = createAbility(
96+
<T extends CustomElementClass>(Class: T): T & ControllableClass & TargetableClass =>
97+
class extends controllable(Class) {
98+
constructor() {
99+
super()
100+
observeElementForTags(this)
101+
initializeTarget(this)
102+
initializeTargets(this)
103+
}
104+
105+
[targetChangedCallback]() {
106+
return
107+
}
108+
109+
[targetsChangedCallback]() {
110+
return
111+
}
112+
113+
[attachShadowCallback](root: ShadowRoot) {
114+
super[attachShadowCallback]?.(root)
115+
shadows.set(this, root)
116+
controllers.set(root, this)
117+
observeElementForTags(root)
118+
}
119+
}
120+
)

test/target.ts renamed to test/targetable.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import {expect, fixture, html} from '@open-wc/testing'
2-
import {target, targets} from '../src/target.js'
3-
import {controller} from '../src/controller.js'
2+
import {target, targets, targetable} from '../src/targetable.js'
43

54
describe('Targetable', () => {
6-
@controller
7-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8-
class TargetTestElement extends HTMLElement {
5+
@targetable
6+
class TargetTest extends HTMLElement {
97
@target foo!: Element
108
bar = 'hello'
11-
@target baz!: Element
9+
count = 0
10+
_baz!: Element
11+
@target set baz(value: Element) {
12+
this.count += 1
13+
this._baz = value
14+
}
1215
@target qux!: Element
1316
@target shadow!: Element
1417

1518
@target bing!: Element
19+
@target multiWord!: Element
1620
@targets foos!: Element[]
1721
bars = 'hello'
1822
@target quxs!: Element[]
1923
@target shadows!: Element[]
24+
@targets camelCase!: Element[]
2025
}
26+
window.customElements.define('target-test', TargetTest)
2127

22-
let instance: HTMLElement
28+
let instance: TargetTest
2329
beforeEach(async () => {
2430
instance = await fixture(html`<target-test>
2531
<target-test>
@@ -32,6 +38,10 @@ describe('Targetable', () => {
3238
<div id="el6" data-target="target-test.bar target-test.bing"></div>
3339
<div id="el7" data-target="target-test.bazbaz"></div>
3440
<div id="el8" data-target="other-target.qux target-test.qux"></div>
41+
<div id="el9" data-target="target-test.multi-word"></div>
42+
<div id="el10" data-target="target-test.multiWord"></div>
43+
<div id="el11" data-targets="target-test.camel-case"></div>
44+
<div id="el12" data-targets="target-test.camelCase"></div>
3545
</target-test>`)
3646
})
3747

@@ -72,6 +82,23 @@ describe('Targetable', () => {
7282
instance.shadowRoot!.appendChild(shadowEl)
7383
expect(instance).to.have.property('foo', shadowEl)
7484
})
85+
86+
it('dasherises target name but falls back to authored case', async () => {
87+
expect(instance).to.have.property('multiWord').exist.with.attribute('id', 'el9')
88+
instance.querySelector('#el9')!.remove()
89+
expect(instance).to.have.property('multiWord').exist.with.attribute('id', 'el10')
90+
})
91+
92+
it('calls setter when new target has been found', async () => {
93+
expect(instance).to.have.property('baz').exist.with.attribute('id', 'el5')
94+
expect(instance).to.have.property('_baz').exist.with.attribute('id', 'el5')
95+
instance.count = 0
96+
instance.querySelector('#el4')!.setAttribute('data-target', 'target-test.baz')
97+
await Promise.resolve()
98+
expect(instance).to.have.property('baz').exist.with.attribute('id', 'el4')
99+
expect(instance).to.have.property('_baz').exist.with.attribute('id', 'el4')
100+
expect(instance).to.have.property('count', 1)
101+
})
75102
})
76103

77104
describe('targets', () => {
@@ -94,5 +121,11 @@ describe('Targetable', () => {
94121
expect(instance).to.have.nested.property('foos[3]').with.attribute('id', 'el4')
95122
expect(instance).to.have.nested.property('foos[4]').with.attribute('id', 'el5')
96123
})
124+
125+
it('returns camel case and dasherised element names', async () => {
126+
expect(instance).to.have.property('camelCase').with.lengthOf(2)
127+
expect(instance).to.have.nested.property('camelCase[0]').with.attribute('id', 'el11')
128+
expect(instance).to.have.nested.property('camelCase[1]').with.attribute('id', 'el12')
129+
})
97130
})
98131
})

0 commit comments

Comments
 (0)