Skip to content

Instantly share code, notes, and snippets.

@snaptopixel
Forked from WickyNilliams/component.tsx
Created December 1, 2021 16:29
Show Gist options
  • Save snaptopixel/82bc6b6e35c6a4a4c4127941242a7039 to your computer and use it in GitHub Desktop.
Save snaptopixel/82bc6b6e35c6a4a4c4127941242a7039 to your computer and use it in GitHub Desktop.
reactive controllers in stencil
import { Task } from '@lit-labs/task'
import { Component, Element, forceUpdate, h, Host, State } from '@stencil/core'
import { ComponentInterface } from '@stencil/core/internal'
import { ReactiveController, ReactiveControllerHost } from 'lit' // using the interfaces directly from lit just for this example
class LightDismissController implements ReactiveController {
host: ReactiveControllerHost
onClose: () => void
constructor(host: ReactiveControllerHost, onClose: () => void) {
this.host = host
host.addController(this)
this.onClose = onClose
}
hostConnected() {
window.addEventListener('click', this.onClose)
window.addEventListener('keyup', this.handleKeyUp)
}
hostDisconnected() {
window.removeEventListener('click', this.onClose)
window.removeEventListener('keyup', this.handleKeyUp)
}
private handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.onClose()
}
}
}
// taken directly from lit docs, no changes!
class MouseController {
host: ReactiveControllerHost
pos = { x: 0, y: 0 }
constructor(host) {
this.host = host
host.addController(this)
}
hostConnected() {
window.addEventListener('mousemove', this.onMouseMove)
}
hostDisconnected() {
window.removeEventListener('mousemove', this.onMouseMove)
}
private onMouseMove = ({ clientX, clientY }) => {
this.pos = { x: clientX, y: clientY }
this.host.requestUpdate()
}
}
class IntervalController implements ReactiveController {
host: ReactiveControllerHost
timerId: ReturnType<typeof setTimeout>
ms: number
onTick: () => void
constructor(host: ReactiveControllerHost, onTick: () => void, ms = 1000) {
this.host = host
host.addController(this)
this.ms = ms
this.onTick = onTick
}
hostConnected() {
this.timerId = setInterval(this.onTick, this.ms)
}
hostDisconnected() {
clearInterval(this.timerId)
}
}
let lastParams = {}
async function loadTodos([counter, fetchParams]) {
if (JSON.stringify(fetchParams) === JSON.stringify(lastParams)) {
return
}
lastParams = fetchParams
const params = new URLSearchParams(fetchParams)
const response = await fetch(`https://jsonplaceholder.typicode.com/todos?_limit=5&_page=${counter}?${params}`, {
headers: { 'Content-Type': 'application/json' },
})
return response.json()
}
class ControllerHost implements ReactiveControllerHost {
constructor(private host: ComponentInterface, private controllers: ReactiveController[] = []) {
const { connectedCallback, disconnectedCallback, componentWillUpdate, componentDidUpdate } = host
host.connectedCallback = () => {
this.controllers.map((ctrl) => ctrl.hostConnected?.())
connectedCallback?.apply(host)
}
host.disconnectedCallback = () => {
this.controllers.map((ctrl) => ctrl.hostDisconnected?.())
disconnectedCallback?.apply(host)
}
host.componentWillUpdate = () => {
this.controllers.map((ctrl) => ctrl.hostUpdate?.())
componentWillUpdate?.apply(host)
}
host.componentDidUpdate = () => {
this.controllers.map((ctrl) => ctrl.hostUpdated?.())
componentDidUpdate?.apply(host)
}
}
addController(ctrl: ReactiveController) {
this.controllers.push(ctrl)
}
removeController(ctrl: ReactiveController) {
this.controllers = this.controllers.filter((c) => c !== ctrl)
}
requestUpdate() {
forceUpdate(this.host)
}
updateComplete = Promise.resolve(true)
}
@Component({
tag: 'controller-demo',
})
export class Demo {
@Element() el: HTMLElement
@State() counter = 0
@State() fetchParams = { foo: 'bar' }
@State() open = false
controllerHost = new ControllerHost(this)
mousemove = new MouseController(this.controllerHost)
interval = new IntervalController(this.controllerHost, () => this.increment())
dismiss = new LightDismissController(this.controllerHost, () => this.close())
fetch = new Task(this.controllerHost, loadTodos, () => [this.counter, this.fetchParams] as any)
close = () => {
this.open = false
}
increment = () => {
this.counter++
}
render() {
return (
<Host class='p-6 bg-surface-high'>
{this.fetch.render({
pending: () => <p>loading...</p>,
complete: (todos) => {
return (
<ul>
{todos?.map((todo) => (
<li>
<input type='checkbox' checked={todo.completed} />
{todo.title}
</li>
))}
</ul>
)
},
})}
<div>Mouse Position: {JSON.stringify(this.mousemove.pos)}</div>
<button onClick={() => (this.fetchParams = { foo: 'bar' + this.counter })}>
Fetch Todos (Page {this.counter})
</button>
<button onClick={this.close}>Close</button>
</Host>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment