Skip to content

Commit

Permalink
feat(Lit): @eventlistener decorators are not supported in Controller …
Browse files Browse the repository at this point in the history
…classes. Fixes #11

feat(Lit): Add initializers to Controllers similar to Components' initializers
  • Loading branch information
a11delavar committed Mar 30, 2024
1 parent 520a6f0 commit bc2efb2
Show file tree
Hide file tree
Showing 12 changed files with 368 additions and 124 deletions.
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions packages/Lit/Controller/Controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,48 @@ class ControllerTestComponent extends Component {
}
customElements.define('controller-test-component', ControllerTestComponent)

class TestController extends Controller { }

describe('Controller', () => {
const fixture = new ComponentTestFixture<ControllerTestComponent>('controller-test-component')

it('should add itself as a controller', () => {
class TestController extends Controller { }

expect(fixture.component.controllers.size).toBe(0)

const controller = new TestController(fixture.component)

expect(fixture.component.controllers.size).toBe(1)
expect(fixture.component.controllers.has(controller)).toBeTrue()
})

describe('initializers', () => {
it('should be called when a controller is constructed', () => {
class TestController extends Controller { }
const spy = jasmine.createSpy('initializer')
TestController.addInitializer(spy)

expect(spy).not.toHaveBeenCalled()

const controller = new TestController(fixture.component)

expect(spy).toHaveBeenCalledOnceWith(controller)
})

it('should inherit initializers from parent classes', () => {
const spy1 = jasmine.createSpy('initializer1')
const spy2 = jasmine.createSpy('initializer2')
class TestController1 extends Controller { }
TestController1.addInitializer(spy1)
class TestController2 extends TestController1 { }
TestController2.addInitializer(spy2)

expect(spy1).not.toHaveBeenCalled()
expect(spy2).not.toHaveBeenCalled()

const controller = new TestController2(fixture.component)

expect(spy1).toHaveBeenCalledOnceWith(controller)
expect(spy2).toHaveBeenCalledOnceWith(controller)
})
})
})
10 changes: 9 additions & 1 deletion packages/Lit/Controller/Controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { ReactiveController, ReactiveControllerHost } from 'lit'

type Initializer = (controller: Controller) => void

export abstract class Controller implements ReactiveController {
private static _initializers?: Set<Initializer>
static addInitializer(initializer: Initializer) {
(this._initializers ??= new Set(Object.getPrototypeOf(this).initializers ?? [])).add(initializer)
}

constructor(protected readonly host: ReactiveControllerHost) {
this.host.addController(this)
this.host.addController(this);
(this.constructor as typeof Controller)._initializers?.forEach(initializer => initializer(this))
}

hostConnected?(): void
Expand Down
5 changes: 3 additions & 2 deletions packages/Lit/eventListener/EventListenerController.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentTestFixture } from '@a11d/lit-testing'
import { component, Component, EventListenerTarget, html, queryAsync } from '../index.js'
import { EventListenerController, extractEventTargets } from './EventListenerController.js'
import { EventListenerController } from './EventListenerController.js'
import { extractEventTargets } from './extractEventTargets.js'

abstract class EventListenerControllerTestComponent extends Component {
readonly fakeCall = jasmine.createSpy('fakeCall')
Expand Down Expand Up @@ -124,7 +125,7 @@ describe('EventListenerController', () => {
const event = specs.event ?? new PointerEvent('click')

const dispatchEvent = async () => {
const targets = await extractEventTargets.call(specs.fixture.component, specs.target)
const targets = await extractEventTargets(specs.fixture.component, specs.target)
targets.forEach(t => t.dispatchEvent(event))
return targets.length
}
Expand Down
39 changes: 8 additions & 31 deletions packages/Lit/eventListener/EventListenerController.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,7 @@
import { ReactiveElement } from 'lit'
import { Controller } from '../Controller/Controller.js'
import type { EventListenerTarget, EventTargets } from './EventListenerTarget.js'

export async function extractEventTargets(this: any, target: EventListenerTarget | undefined) {
const handle = (value: EventTargets) => Symbol.iterator in value ? [...value] : [value]

if (target === undefined) {
return handle(this)
}

if (typeof target === 'function') {
let eventTarget = (target as (this: any) => EventTargets | Promise<EventTargets>).call(this)

if (eventTarget instanceof Promise) {
eventTarget = await eventTarget
}

if (eventTarget instanceof EventTarget) {
return handle(eventTarget)
}

if (Symbol.iterator in eventTarget && [...eventTarget].every(t => t instanceof EventTarget)) {
return handle(eventTarget)
}

throw new TypeError(`${this.constructor}.target is not an EventTarget`)
}

return handle(target ?? this as EventTarget)
}
import { type EventListenerTarget } from './extractEventTargets.js'
import { extractEventTargets } from './extractEventTargets.js'

type Listener = EventListenerObject | ((e: any) => void)

Expand Down Expand Up @@ -64,15 +37,19 @@ export class EventListenerController extends Controller {
this.options = extractOptions(options)
}

protected get context(): object {
return this.host
}

async subscribe() {
const targets = await extractEventTargets.call(this.host, this.options.target)
const targets = await extractEventTargets.call(this.context, this.host, this.options.target)
for (const target of targets) {
target.addEventListener(this.options.type, this.options.listener, this.options.options)
}
}

async unsubscribe() {
const targets = await extractEventTargets.call(this.host, this.options.target)
const targets = await extractEventTargets.call(this.context, this.host, this.options.target)
for (const target of targets) {
target?.removeEventListener(this.options.type, this.options.listener, this.options.options)
}
Expand Down
4 changes: 0 additions & 4 deletions packages/Lit/eventListener/EventListenerTarget.ts

This file was deleted.

Loading

0 comments on commit bc2efb2

Please sign in to comment.