Skip to content

Instantly share code, notes, and snippets.

@gordonbrander
Last active November 6, 2022 07:36
Show Gist options
  • Save gordonbrander/ec7d2192e1534b46f4622da6372150f2 to your computer and use it in GitHub Desktop.
Save gordonbrander/ec7d2192e1534b46f4622da6372150f2 to your computer and use it in GitHub Desktop.
const $state = Symbol('state')
const $rendered = Symbol('rendered state')
const $shadow = Symbol('shadow')
const $frame = Symbol('animation frame')
// StoreElement is a deterministic web component,
// inspired loosely by Elm's App Architecture pattern.
export class StoreElement extends HTMLElement {
constructor() {
super()
this.send = this.send.bind(this)
this.handleEvent = this.handleEvent.bind(this)
// Attach *closed* shadow. We keep the insides of the component closed
// so that we can be sure no one is messing with the DOM, and DOM
// writes are deterministic functions of state.
this[$shadow] = this.attachShadow({mode: 'closed'})
const [state, fx] = this.init(this)
this[$state] = state
this.setup(this[$shadow], this[$state], this.handleEvent)
this[$rendered] = state
this.effect(fx)
}
// This lifecycle callback is called by the platform whenever an
// attribute changes on our element.
//
// Override the `attribute` method to map changes to actions.
attributeChangedCallback(name, prev, next) {
let msg = attribute(name, next)
if (msg) {
this.send(msg)
}
}
// Create initial model and effect via reading host element.
init(el) {
throw new Error('Not implemented')
}
// Update model via message, returning a new model and effect
update(prev, msg) {
throw new Error('Not implemented')
}
// Create initial HTML in shadow DOM.
setup(shadowRoot, curr, handle) {}
// Write updates to shadow DOM by comparing previous and current state.
write(shadowRoot, prev, curr, handle) {}
// Map events to actions
event(event) {
return event
}
// Map attribute changes to actions
attribute(name, value) {}
send(msg) {
const [state, fx] = this.update(this[$state], msg)
if (this[$state] !== state) {
this[$state] = state
this.render()
}
if (fx) {
this.effect(fx)
}
}
async effect(fx) {
const msg = await fx
if (msg) {
this.send(msg)
}
}
handleEvent(event) {
const msg = this.event(event)
if (msg) {
this.send(msg)
}
}
render(next) {
const frame = requestAnimationFrame(t => {
this.write(
this[$shadow],
this[$rendered],
this[$state],
this.handleEvent
)
this[$rendered] = this[$state]
})
// Avoid extra writes by cancelling any previous renders that
// queued the next frame.
cancelAnimationFrame(this[$frame])
this[$frame] = frame
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment