Thanks to visit codestin.com
Credit goes to github.com

Skip to content

refactor target into targetable #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
{
"path": "lib/index.js",
"import": "{controller, attr, target, targets}",
"limit": "2.6kb"
"limit": "2.5kb"
},
{
"path": "lib/abilities.js",
Expand Down
7 changes: 4 additions & 3 deletions src/controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {CatalystDelegate} from './core.js'
import type {CustomElementClass} from './custom-element.js'
import {targetable} from './targetable.js'
import {attrable} from './attrable.js'
import {actionable} from './actionable.js'
import {register} from './register.js'

/**
* Controller is a decorator to be used over a class that extends HTMLElement.
* It will automatically `register()` the component in the customElement
* registry, as well as ensuring `bind(this)` is called on `connectedCallback`,
* wrapping the classes `connectedCallback` method if needed.
*/
export function controller(classObject: CustomElementClass): void {
new CatalystDelegate(actionable(attrable(classObject)))
export function controller<T extends CustomElementClass>(Class: T) {
return register(actionable(attrable(targetable(Class))))
}
37 changes: 0 additions & 37 deletions src/findtarget.ts

This file was deleted.

11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export {actionable} from './actionable.js'
export {register} from './register.js'
export {findTarget, findTargets} from './findtarget.js'
export {target, targets} from './target.js'
export {
target,
getTarget,
targets,
getTargets,
targetChangedCallback,
targetsChangedCallback,
targetable
} from './targetable.js'
export {controller} from './controller.js'
export {attr, getAttr, attrable, attrChangedCallback} from './attrable.js'
2 changes: 1 addition & 1 deletion src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {dasherize} from './dasherize.js'
*
* Example: HelloController => hello-controller
*/
export function register(classObject: CustomElementClass): CustomElementClass {
export function register<T extends CustomElementClass>(classObject: T): T {
const name = dasherize(classObject.name).replace(/-(element|controller|component)$/, '')

try {
Expand Down
36 changes: 0 additions & 36 deletions src/target.ts

This file was deleted.

120 changes: 120 additions & 0 deletions src/targetable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type {CustomElementClass} from './custom-element.js'
import type {ControllableClass} from './controllable.js'
import {registerTag, observeElementForTags} from './tag-observer.js'
import {createMark} from './mark.js'
import {controllable, attachShadowCallback} from './controllable.js'
import {dasherize} from './dasherize.js'
import {createAbility} from './ability.js'

export interface Targetable {
[targetChangedCallback](key: PropertyKey, target: Element): void
[targetsChangedCallback](key: PropertyKey, targets: Element[]): void
}
export interface TargetableClass {
new (): Targetable
}

const targetChangedCallback = Symbol()
const targetsChangedCallback = Symbol()

const [target, getTarget, initializeTarget] = createMark<Element>(
({name, kind}) => {
if (kind === 'getter') throw new Error(`@target cannot decorate get ${String(name)}`)
},
(instance: Element, {name, access}) => {
const selector = [
`[data-target~="${instance.tagName.toLowerCase()}.${dasherize(name)}"]`,
`[data-target~="${instance.tagName.toLowerCase()}.${String(name)}"]`
]
const find = findTarget(instance, selector.join(', '), false)
return {
get: find,
set: () => {
if (access?.set) access.set.call(instance, find())
}
}
}
)
const [targets, getTargets, initializeTargets] = createMark<Element>(
({name, kind}) => {
if (kind === 'getter') throw new Error(`@target cannot decorate get ${String(name)}`)
},
(instance: Element, {name, access}) => {
const selector = [
`[data-targets~="${instance.tagName.toLowerCase()}.${dasherize(name)}"]`,
`[data-targets~="${instance.tagName.toLowerCase()}.${String(name)}"]`
]
const find = findTarget(instance, selector.join(', '), true)
return {
get: find,
set: () => {
if (access?.set) access.set.call(instance, find())
}
}
}
)

function setTarget(el: Element, controller: Element | ShadowRoot, tag: string, key: string): void {
const get = tag === 'data-targets' ? getTargets : getTarget
if (controller instanceof ShadowRoot) {
controller = controllers.get(controller)!
}
if (controller && get(controller)?.has(key)) {
;(controller as unknown as Record<PropertyKey, unknown>)[key] = {}
}
}

registerTag('data-target', (str: string) => str.split('.'), setTarget)
registerTag('data-targets', (str: string) => str.split('.'), setTarget)
const shadows = new WeakMap<Element, ShadowRoot>()
const controllers = new WeakMap<ShadowRoot, Element>()

const findTarget = (controller: Element, selector: string, many: boolean) => () => {
const nodes = []
const shadow = shadows.get(controller)
if (shadow) {
for (const el of shadow.querySelectorAll(selector)) {
if (!el.closest(controller.tagName)) {
nodes.push(el)
if (!many) break
}
}
}
if (many || !nodes.length) {
for (const el of controller.querySelectorAll(selector)) {
if (el.closest(controller.tagName) === controller) {
nodes.push(el)
if (!many) break
}
}
}
return many ? nodes : nodes[0]
}

export {target, getTarget, targets, getTargets, targetChangedCallback, targetsChangedCallback}
export const targetable = createAbility(
<T extends CustomElementClass>(Class: T): T & ControllableClass & TargetableClass =>
class extends controllable(Class) {
constructor() {
super()
observeElementForTags(this)
initializeTarget(this)
initializeTargets(this)
}

[targetChangedCallback]() {
return
}

[targetsChangedCallback]() {
return
}

[attachShadowCallback](root: ShadowRoot) {
super[attachShadowCallback]?.(root)
shadows.set(this, root)
controllers.set(root, this)
observeElementForTags(root)
}
}
)
47 changes: 40 additions & 7 deletions test/target.ts → test/targetable.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import {expect, fixture, html} from '@open-wc/testing'
import {target, targets} from '../src/target.js'
import {controller} from '../src/controller.js'
import {target, targets, targetable} from '../src/targetable.js'

describe('Targetable', () => {
@controller
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class TargetTestElement extends HTMLElement {
@targetable
class TargetTest extends HTMLElement {
@target foo!: Element
bar = 'hello'
@target baz!: Element
count = 0
_baz!: Element
@target set baz(value: Element) {
this.count += 1
this._baz = value
}
@target qux!: Element
@target shadow!: Element

@target bing!: Element
@target multiWord!: Element
@targets foos!: Element[]
bars = 'hello'
@target quxs!: Element[]
@target shadows!: Element[]
@targets camelCase!: Element[]
}
window.customElements.define('target-test', TargetTest)

let instance: HTMLElement
let instance: TargetTest
beforeEach(async () => {
instance = await fixture(html`<target-test>
<target-test>
Expand All @@ -32,6 +38,10 @@ describe('Targetable', () => {
<div id="el6" data-target="target-test.bar target-test.bing"></div>
<div id="el7" data-target="target-test.bazbaz"></div>
<div id="el8" data-target="other-target.qux target-test.qux"></div>
<div id="el9" data-target="target-test.multi-word"></div>
<div id="el10" data-target="target-test.multiWord"></div>
<div id="el11" data-targets="target-test.camel-case"></div>
<div id="el12" data-targets="target-test.camelCase"></div>
</target-test>`)
})

Expand Down Expand Up @@ -72,6 +82,23 @@ describe('Targetable', () => {
instance.shadowRoot!.appendChild(shadowEl)
expect(instance).to.have.property('foo', shadowEl)
})

it('dasherises target name but falls back to authored case', async () => {
expect(instance).to.have.property('multiWord').exist.with.attribute('id', 'el9')
instance.querySelector('#el9')!.remove()
expect(instance).to.have.property('multiWord').exist.with.attribute('id', 'el10')
})

it('calls setter when new target has been found', async () => {
expect(instance).to.have.property('baz').exist.with.attribute('id', 'el5')
expect(instance).to.have.property('_baz').exist.with.attribute('id', 'el5')
instance.count = 0
instance.querySelector('#el4')!.setAttribute('data-target', 'target-test.baz')
await Promise.resolve()
expect(instance).to.have.property('baz').exist.with.attribute('id', 'el4')
expect(instance).to.have.property('_baz').exist.with.attribute('id', 'el4')
expect(instance).to.have.property('count', 1)
})
})

describe('targets', () => {
Expand All @@ -94,5 +121,11 @@ describe('Targetable', () => {
expect(instance).to.have.nested.property('foos[3]').with.attribute('id', 'el4')
expect(instance).to.have.nested.property('foos[4]').with.attribute('id', 'el5')
})

it('returns camel case and dasherised element names', async () => {
expect(instance).to.have.property('camelCase').with.lengthOf(2)
expect(instance).to.have.nested.property('camelCase[0]').with.attribute('id', 'el11')
expect(instance).to.have.nested.property('camelCase[1]').with.attribute('id', 'el12')
})
})
})