Comparing version 4.6.0-beta.0 to 4.6.0
925
haunted.js
import { directive, render } from 'https://unpkg.com/lit-html@^1.0.0/lit-html.js'; | ||
export { html, render } from 'https://unpkg.com/lit-html@^1.0.0/lit-html.js'; | ||
const symbolFor = typeof Symbol === 'function' ? Symbol.for : str => str; | ||
const phaseSymbol = symbolFor('haunted.phase'); | ||
const hookSymbol = symbolFor('haunted.hook'); | ||
const updateSymbol = symbolFor('haunted.update'); | ||
const commitSymbol = symbolFor('haunted.commit'); | ||
const effectsSymbol = symbolFor('haunted.effects'); | ||
const contextSymbol = symbolFor('haunted.context'); | ||
const contextEvent = 'haunted.context'; | ||
let current; | ||
let currentId = 0; | ||
function setCurrent(element) { | ||
current = element; | ||
function setCurrent(state) { | ||
current = state; | ||
} | ||
function clear() { | ||
current = null; | ||
currentId = 0; | ||
current = null; | ||
currentId = 0; | ||
} | ||
function notify() { | ||
let id = currentId; | ||
currentId++; | ||
return id; | ||
return currentId++; | ||
} | ||
//import { render, html } from './lit.js'; | ||
const phaseSymbol = Symbol('haunted.phase'); | ||
const hookSymbol = Symbol('haunted.hook'); | ||
const updateSymbol = Symbol('haunted.update'); | ||
const commitSymbol = Symbol('haunted.commit'); | ||
const effectsSymbol = Symbol('haunted.effects'); | ||
const layoutEffectsSymbol = Symbol('haunted.layoutEffects'); | ||
const contextEvent = 'haunted.context'; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
function scheduler() { | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for(var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
class State { | ||
constructor(update, host) { | ||
this.update = update; | ||
this.host = host; | ||
this[hookSymbol] = new Map(); | ||
this[effectsSymbol] = []; | ||
this[layoutEffectsSymbol] = []; | ||
} | ||
} | ||
return function(task) { | ||
tasks.push(task); | ||
if(id == null) { | ||
id = defer(runTasks); | ||
run(cb) { | ||
setCurrent(this); | ||
let res = cb(); | ||
clear(); | ||
return res; | ||
} | ||
}; | ||
_runEffects(phase) { | ||
let effects = this[phase]; | ||
setCurrent(this); | ||
for (let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
runEffects() { | ||
this._runEffects(effectsSymbol); | ||
} | ||
runLayoutEffects() { | ||
this._runEffects(layoutEffectsSymbol); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach(hook => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
} | ||
} | ||
function makeContainer(render$$1) { | ||
const read = scheduler(); | ||
const write = scheduler(); | ||
class Container { | ||
constructor(renderer, frag, host) { | ||
this.renderer = renderer; | ||
this.frag = frag; | ||
this.host = host || frag; | ||
this[hookSymbol] = new Map(); | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
function runner() { | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for (var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
} | ||
} | ||
return function (task) { | ||
tasks.push(task); | ||
if (id == null) { | ||
id = defer(runTasks); | ||
} | ||
}; | ||
} | ||
const read = runner(); | ||
const write = runner(); | ||
class BaseScheduler { | ||
constructor(renderer, host) { | ||
this.renderer = renderer; | ||
this.host = host; | ||
this.state = new State(this.update.bind(this), host); | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
} | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
if(this[effectsSymbol]) { | ||
if (this._updateQueued) | ||
return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
this.handlePhase(commitSymbol, result); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
}); | ||
}); | ||
} | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = true; | ||
this._updateQueued = true; | ||
} | ||
handlePhase(phase, arg) { | ||
this[phaseSymbol] = phase; | ||
switch(phase) { | ||
case commitSymbol: return this.commit(arg); | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
this[phaseSymbol] = phase; | ||
switch (phase) { | ||
case commitSymbol: | ||
this.commit(arg); | ||
this.runEffects(layoutEffectsSymbol); | ||
return; | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
} | ||
commit(result) { | ||
render$$1(result, this.frag); | ||
this.runEffects(commitSymbol); | ||
} | ||
render() { | ||
setCurrent(this); | ||
let result = this.args ? | ||
this.renderer.apply(this.host, this.args) : | ||
this.renderer.call(this.host, this.host); | ||
clear(); | ||
return result; | ||
return this.state.run(() => this.renderer.call(this.host, this.host)); | ||
} | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
runEffects(phase) { | ||
this.state._runEffects(phase); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
this.state.teardown(); | ||
} | ||
} | ||
return Container; | ||
} | ||
function toCamelCase(val = '') { | ||
return val.indexOf('-') === -1 ? val.toLowerCase() : val.toLowerCase().split('-').reduce((out, part) => { | ||
return out ? out + part.charAt(0).toUpperCase() + part.slice(1) : part; | ||
},'') | ||
} | ||
function makeComponent(Container) { | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const {observedAttributes = [], useShadowDOM = true, shadowRootInit = {}} = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._container = new Container(renderer, this); | ||
} else { | ||
this.attachShadow({ mode: "open", ...shadowRootInit}); | ||
this._container = new Container(renderer, this.shadowRoot, this); | ||
const toCamelCase = (val = '') => val.replace(/-+([a-z])?/g, (_, char) => char ? char.toUpperCase() : ''); | ||
function makeComponent(render) { | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, frag, host) { | ||
super(renderer, host || frag); | ||
this.frag = frag; | ||
} | ||
} | ||
connectedCallback() { | ||
this._container.update(); | ||
} | ||
disconnectedCallback() { | ||
this._container.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
} | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._container.update(); | ||
commit(result) { | ||
render(result, this.frag); | ||
} | ||
}) | ||
} | ||
const proto = new Proxy(BaseElement.prototype, { | ||
set(target, key, value, receiver) { | ||
if(key in target) { | ||
Reflect.set(target, key, value); | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const { observedAttributes = [], useShadowDOM = true, shadowRootInit = {} } = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._scheduler = new Scheduler(renderer, this); | ||
} | ||
else { | ||
this.attachShadow({ mode: 'open', ...shadowRootInit }); | ||
this._scheduler = new Scheduler(renderer, this.shadowRoot, this); | ||
} | ||
} | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
connectedCallback() { | ||
this._scheduler.update(); | ||
} | ||
disconnectedCallback() { | ||
this._scheduler.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
} | ||
let desc; | ||
if(typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} else { | ||
desc = reflectiveProp(value); | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._scheduler.update(); | ||
} | ||
}); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if(desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
const proto = new Proxy(BaseElement.prototype, { | ||
getPrototypeOf(target) { | ||
return target; | ||
}, | ||
set(target, key, value, receiver) { | ||
if (key in target) { | ||
Reflect.set(target, key, value); | ||
} | ||
let desc; | ||
if (typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} | ||
else { | ||
desc = reflectiveProp(value); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if (desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
} | ||
class Hook { | ||
constructor(id, el) { | ||
this.id = id; | ||
this.el = el; | ||
} | ||
constructor(id, state) { | ||
this.id = id; | ||
this.state = state; | ||
} | ||
} | ||
function use(Hook, ...args) { | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if(!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if (!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
} | ||
function hook(Hook) { | ||
return use.bind(null, Hook); | ||
return use.bind(null, Hook); | ||
} | ||
function setEffects(el, cb) { | ||
if(!(effectsSymbol in el)) { | ||
el[effectsSymbol] = []; | ||
} | ||
el[effectsSymbol].push(cb); | ||
function createEffect(setEffects) { | ||
return hook(class extends Hook { | ||
constructor(id, state, ignored1, ignored2) { | ||
super(id, state); | ||
setEffects(state, this); | ||
} | ||
update(callback, values) { | ||
this.callback = callback; | ||
this.lastValues = this.values; | ||
this.values = values; | ||
} | ||
call() { | ||
if (!this.values || this.hasChanged()) { | ||
this.run(); | ||
} | ||
} | ||
run() { | ||
this.teardown(); | ||
this._teardown = this.callback.call(this.state); | ||
} | ||
teardown() { | ||
if (this._teardown) { | ||
this._teardown(); | ||
} | ||
} | ||
hasChanged() { | ||
return !this.lastValues || this.values.some((value, i) => this.lastValues[i] !== value); | ||
} | ||
}); | ||
} | ||
const useEffect = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
this.values = false; | ||
setEffects(el, this); | ||
} | ||
update(callback, values) { | ||
this.callback = callback; | ||
this.lastValues = this.values; | ||
this.values = values; | ||
} | ||
call() { | ||
if(this.values) { | ||
if(this.hasChanged()) { | ||
this.run(); | ||
} | ||
} else { | ||
this.run(); | ||
} | ||
} | ||
run() { | ||
this.teardown(); | ||
this._teardown = this.callback.call(this.el); | ||
} | ||
teardown() { | ||
if(this._teardown) { | ||
this._teardown(); | ||
} | ||
} | ||
hasChanged() { | ||
return this.lastValues === false || this.values.some((value, i) => this.lastValues[i] !== value); | ||
} | ||
}); | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
} | ||
el[contextSymbol].push(consumer); | ||
function setEffects(state, cb) { | ||
state[effectsSymbol].push(cb); | ||
} | ||
const useEffect = createEffect(setEffects); | ||
const useContext = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
setContexts(el, this); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(el, this); | ||
} | ||
update(Context) { | ||
if (this.el.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
constructor(id, state, _) { | ||
super(id, state); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(state, this); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
update(Context) { | ||
if (this.state.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
return this.value; | ||
} | ||
call() { | ||
if(!this._ranEffect) { | ||
this._ranEffect = true; | ||
if(this._unsubscribe) this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.el.update(); | ||
call() { | ||
if (!this._ranEffect) { | ||
this._ranEffect = true; | ||
if (this._unsubscribe) | ||
this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.state.update(); | ||
} | ||
} | ||
} | ||
_updater(value) { | ||
this.value = value; | ||
this.el.update(); | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.el.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, // carrier | ||
bubbles: true, // to bubble up in tree | ||
cancelable: true, // to be able to cancel | ||
composed: true, // to pass ShadowDOM boundaries | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
_updater(value) { | ||
this.value = value; | ||
this.state.update(); | ||
} | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.state.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, | ||
bubbles: true, | ||
cancelable: true, | ||
composed: true, | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
} | ||
} | ||
}); | ||
function makeContext(component) { | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
if(this.listeners.has(callback)) { | ||
this.listeners.delete(callback); | ||
} | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for(let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render: render$$1 }) { | ||
const context = useContext(Context); | ||
return render$$1(context); | ||
}), | ||
defaultValue | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
this.listeners.delete(callback); | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for (let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render }) { | ||
const context = useContext(Context); | ||
return render(context); | ||
}), | ||
defaultValue, | ||
}; | ||
return Context; | ||
}; | ||
return Context; | ||
}; | ||
} | ||
const useMemo = hook(class extends Hook { | ||
constructor(id, el, fn, values) { | ||
super(id, el); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
update(fn, values) { | ||
if(this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
constructor(id, state, fn, values) { | ||
super(id, state); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
update(fn, values) { | ||
if (this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
@@ -447,145 +386,127 @@ | ||
function setLayoutEffects(state, cb) { | ||
state[layoutEffectsSymbol].push(cb); | ||
} | ||
const useLayoutEffect = createEffect(setLayoutEffects); | ||
const useState = hook(class extends Hook { | ||
constructor(id, el, initialValue) { | ||
super(id, el); | ||
this.updater = this.updater.bind(this); | ||
if(typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
constructor(id, state, initialValue) { | ||
super(id, state); | ||
this.updater = this.updater.bind(this); | ||
if (typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
update() { | ||
return this.args; | ||
} | ||
updater(value) { | ||
if (typeof value === "function") { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
update() { | ||
return this.args; | ||
} | ||
this.makeArgs(value); | ||
this.el.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
updater(value) { | ||
if (typeof value === 'function') { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
} | ||
this.makeArgs(value); | ||
this.state.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
}); | ||
const useReducer = hook(class extends Hook { | ||
constructor(id, el, _, initialState) { | ||
super(id, el); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.state = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.state, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.state = this.reducer(this.state, action); | ||
this.el.update(); | ||
} | ||
constructor(id, state, _, initialState) { | ||
super(id, state); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.currentState = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.currentState, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.currentState = this.reducer(this.currentState, action); | ||
this.state.update(); | ||
} | ||
}); | ||
const useRef = (initialValue) => { | ||
return useMemo(() => { | ||
return { | ||
current: initialValue | ||
}; | ||
}, []); | ||
}; | ||
const useRef = (initialValue) => useMemo(() => ({ | ||
current: initialValue | ||
}), []); | ||
function haunted({ render: render$$1 }) { | ||
const Container = makeContainer(render$$1); | ||
const component = makeComponent(Container); | ||
const createContext = makeContext(component); | ||
return { Container, component, createContext }; | ||
function haunted({ render }) { | ||
const component = makeComponent(render); | ||
const createContext = makeContext(component); | ||
return { component, createContext }; | ||
} | ||
const includes = Array.prototype.includes; | ||
function makeVirtual(Container) { | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
function makeVirtual() { | ||
const partToScheduler = new WeakMap(); | ||
const schedulerToPart = new WeakMap(); | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.state.virtual = true; | ||
} | ||
render() { | ||
return this.state.run(() => this.renderer.apply(this.host, this.args)); | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = schedulerToPart.get(this); | ||
partToScheduler.delete(part); | ||
} | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = containerToPart.get(this); | ||
partToContainer.delete(part); | ||
} | ||
} | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return part => { | ||
let cont = partToContainer.get(part); | ||
if(!cont) { | ||
cont = new DirectiveContainer(renderer, part); | ||
partToContainer.set(part, cont); | ||
containerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return (part) => { | ||
let cont = partToScheduler.get(part); | ||
if (!cont) { | ||
cont = new Scheduler(renderer, part); | ||
partToScheduler.set(part, cont); | ||
schedulerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
return directive(factory); | ||
} | ||
return directive(factory); | ||
} | ||
return virtual; | ||
return virtual; | ||
} | ||
function teardownOnRemove(cont, part, node = part.startNode) { | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for(let mutation of mutations) { | ||
if(includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if(node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} else { | ||
cont.teardown(); | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for (let mutation of mutations) { | ||
if (includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if (node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} | ||
else { | ||
cont.teardown(); | ||
} | ||
break; | ||
} | ||
else if (includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling || undefined); | ||
break; | ||
} | ||
} | ||
break; | ||
} else if(includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling); | ||
break; | ||
} | ||
} | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
} | ||
const { Container, component, createContext } = haunted({ | ||
render(what, where) { | ||
render(what, where); | ||
} | ||
}); | ||
const { component, createContext } = haunted({ render }); | ||
const virtual = makeVirtual(); | ||
const virtual = makeVirtual(Container); | ||
export default haunted; | ||
export { component, createContext, virtual, useCallback, useEffect, useState, useReducer, useMemo, useContext, useRef, hook, Hook }; | ||
export { BaseScheduler, Hook, State, component, createContext, hook, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, virtual }; |
@@ -1,91 +0,88 @@ | ||
function toCamelCase(val = '') { | ||
return val.indexOf('-') === -1 ? val.toLowerCase() : val.toLowerCase().split('-').reduce((out, part) => { | ||
return out ? out + part.charAt(0).toUpperCase() + part.slice(1) : part; | ||
},'') | ||
} | ||
function makeComponent(Scheduler) { | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const {observedAttributes = [], useShadowDOM = true, shadowRootInit = {}} = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._scheduler = new Scheduler(renderer, this); | ||
} else { | ||
this.attachShadow({ mode: "open", ...shadowRootInit}); | ||
this._scheduler = new Scheduler(renderer, this.shadowRoot, this); | ||
import { BaseScheduler } from './scheduler.js'; | ||
const toCamelCase = (val = '') => val.replace(/-+([a-z])?/g, (_, char) => char ? char.toUpperCase() : ''); | ||
function makeComponent(render) { | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, frag, host) { | ||
super(renderer, host || frag); | ||
this.frag = frag; | ||
} | ||
} | ||
connectedCallback() { | ||
this._scheduler.update(); | ||
} | ||
disconnectedCallback() { | ||
this._scheduler.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
}; | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._scheduler.update(); | ||
commit(result) { | ||
render(result, this.frag); | ||
} | ||
}) | ||
} | ||
const proto = new Proxy(BaseElement.prototype, { | ||
set(target, key, value, receiver) { | ||
if(key in target) { | ||
Reflect.set(target, key, value); | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const { observedAttributes = [], useShadowDOM = true, shadowRootInit = {} } = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._scheduler = new Scheduler(renderer, this); | ||
} | ||
else { | ||
this.attachShadow({ mode: 'open', ...shadowRootInit }); | ||
this._scheduler = new Scheduler(renderer, this.shadowRoot, this); | ||
} | ||
} | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
connectedCallback() { | ||
this._scheduler.update(); | ||
} | ||
disconnectedCallback() { | ||
this._scheduler.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
} | ||
let desc; | ||
if(typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} else { | ||
desc = reflectiveProp(value); | ||
; | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._scheduler.update(); | ||
} | ||
}); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if(desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
const proto = new Proxy(BaseElement.prototype, { | ||
getPrototypeOf(target) { | ||
return target; | ||
}, | ||
set(target, key, value, receiver) { | ||
if (key in target) { | ||
Reflect.set(target, key, value); | ||
} | ||
let desc; | ||
if (typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} | ||
else { | ||
desc = reflectiveProp(value); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if (desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
} | ||
export { makeComponent }; |
@@ -1,16 +0,12 @@ | ||
import { makeScheduler } from './scheduler.js'; | ||
import { makeComponent } from './component.js'; | ||
import { makeContext } from './create-context.js'; | ||
function haunted({ render }) { | ||
const Scheduler = makeScheduler(render); | ||
const component = makeComponent(Scheduler); | ||
const createContext = makeContext(component); | ||
return { Scheduler, component, createContext }; | ||
const component = makeComponent(render); | ||
const createContext = makeContext(component); | ||
return { component, createContext }; | ||
} | ||
export { haunted as default }; | ||
export { useCallback } from "./use-callback.js"; | ||
export { useCallback } from './use-callback.js'; | ||
export { useEffect } from './use-effect.js'; | ||
export { useLayoutEffect } from './use-layout-effect.js'; | ||
export { useState } from './use-state.js'; | ||
@@ -22,2 +18,3 @@ export { useReducer } from './use-reducer.js'; | ||
export { hook, Hook } from './hook.js'; | ||
export { BaseScheduler } from './scheduler.js'; | ||
export { State } from './state.js'; |
import { contextEvent } from './symbols.js'; | ||
import { useContext } from './use-context.js'; | ||
function makeContext(component) { | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
if(this.listeners.has(callback)) { | ||
this.listeners.delete(callback); | ||
} | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for(let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render }) { | ||
const context = useContext(Context); | ||
return render(context); | ||
}), | ||
defaultValue | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
this.listeners.delete(callback); | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for (let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render }) { | ||
const context = useContext(Context); | ||
return render(context); | ||
}), | ||
defaultValue, | ||
}; | ||
return Context; | ||
}; | ||
return Context; | ||
}; | ||
} | ||
export { makeContext }; | ||
export { makeContext }; |
@@ -1,60 +0,3 @@ | ||
import { html, render, TemplateResult } from 'lit-html'; | ||
import { DirectiveFactory } from "lit-html/lib/directive"; | ||
export { html, render, TemplateResult, DirectiveFactory } | ||
export type ComponentLike = HTMLElement | ShadowRoot; | ||
export type ComponentType<P, T extends ComponentLike = HTMLElement> = new(...args: any[]) => T & P; | ||
type Options = { | ||
useShadowDOM: boolean, | ||
shadowRootInit?: { | ||
mode?: string | ||
delegatesFocus?: boolean, | ||
} | ||
} | ||
export function component<P, T extends ComponentLike = HTMLElement>( | ||
renderer: (this: T, el: P & T) => TemplateResult | void, | ||
BaseElement?: new(...args: any[]) => T, | ||
options?: Options): ComponentType<P, T>; | ||
export function component<P, T extends ComponentLike = HTMLElement>( | ||
renderer: (this: T, el: P & T) => TemplateResult | void, | ||
options?: Options & { baseElement: new(...args: any[]) => T} ): ComponentType<P, T>; | ||
export function useCallback<T extends Function>(fn: T, inputs: any[]): T; | ||
export function useEffect(fn: () => void | VoidFunction, inputs?: any[]): void; | ||
export type StateUpdater<T> = (value: T | ((state?: T) => T)) => void; | ||
export function useState<T>(intialValue?: T): [T, StateUpdater<T>]; | ||
export function useReducer<S = any, A = any>(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void]; | ||
export function useMemo<T>(fn: () => T, values: any[]): T; | ||
export function useRef<T>(initialValue: T): { current: T}; | ||
export function virtual<P, T extends ComponentLike = HTMLElement>(renderer: (this: T, el: P) => TemplateResult | void): () => DirectiveFactory; | ||
export interface Context<T> { | ||
Provider: ComponentType<T>; | ||
Consumer: ComponentType<T>; | ||
defaultValue: T; | ||
} | ||
export function createContext<T = any>(defaultValue: T): Context<T> | ||
export function useContext<T>(Context: Context<T>): T | ||
export class Hook<T extends ComponentLike = HTMLElement> { | ||
id: number; | ||
el: T; | ||
constructor(id: number, el: T); | ||
} | ||
interface HookWithLifecycle<T extends ComponentLike = HTMLElement, P extends any[] = null, R = void> extends Hook<T> { | ||
update?(...args: P): R; | ||
teardown?(): void; | ||
} | ||
export function hook<T extends ComponentLike = HTMLElement>(Hook: new(id: number, el: T) => Hook<T>): () => void; | ||
export function hook<T extends ComponentLike = HTMLElement, P extends any[] = void[], R = void>(Hook: new (id: number, el: T, ...args: P) => HookWithLifecycle<T, P, R>): (...args: P) => R; | ||
export { html, render, component, createContext, virtual } from './lit-haunted'; | ||
export * from './core'; | ||
export { default } from './core'; |
import { current, notify } from './interface.js'; | ||
import { hookSymbol } from './symbols.js'; | ||
class Hook { | ||
constructor(id, state) { | ||
this.id = id; | ||
this.el = this.state = state; | ||
} | ||
constructor(id, state) { | ||
this.id = id; | ||
this.state = state; | ||
} | ||
} | ||
function use(Hook, ...args) { | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if(!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if (!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
} | ||
function hook(Hook) { | ||
return use.bind(null, Hook); | ||
return use.bind(null, Hook); | ||
} | ||
export { hook, Hook }; | ||
export { hook, Hook }; |
let current; | ||
let currentId = 0; | ||
function setCurrent(element) { | ||
current = element; | ||
function setCurrent(state) { | ||
current = state; | ||
} | ||
function clear() { | ||
current = null; | ||
currentId = 0; | ||
current = null; | ||
currentId = 0; | ||
} | ||
function notify() { | ||
let id = currentId; | ||
currentId++; | ||
return id; | ||
return currentId++; | ||
} | ||
export { clear, current, setCurrent, notify } | ||
export { clear, current, setCurrent, notify }; |
import { html, render } from 'lit-html'; | ||
import haunted from './core.js'; | ||
import { makeVirtual } from './virtual.js'; | ||
const { Scheduler, component, createContext } = haunted({ | ||
render(what, where) { | ||
render(what, where); | ||
} | ||
}); | ||
const virtual = makeVirtual(Scheduler); | ||
export { | ||
component, | ||
createContext, | ||
virtual, | ||
html, | ||
render | ||
}; | ||
const { component, createContext } = haunted({ render }); | ||
const virtual = makeVirtual(); | ||
export { component, createContext, virtual, html, render }; |
@@ -1,93 +0,69 @@ | ||
import { commitSymbol, phaseSymbol, updateSymbol, effectsSymbol } from './symbols.js'; | ||
import { State } from './state.js'; | ||
import { commitSymbol, phaseSymbol, updateSymbol, effectsSymbol, layoutEffectsSymbol } from './symbols.js'; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
function runner() { | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for(var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for (var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
} | ||
} | ||
} | ||
return function(task) { | ||
tasks.push(task); | ||
if(id == null) { | ||
id = defer(runTasks); | ||
} | ||
}; | ||
return function (task) { | ||
tasks.push(task); | ||
if (id == null) { | ||
id = defer(runTasks); | ||
} | ||
}; | ||
} | ||
function makeScheduler(render) { | ||
const read = runner(); | ||
const write = runner(); | ||
class Scheduler { | ||
constructor(renderer, frag, host) { | ||
this.renderer = renderer; | ||
this.frag = frag; | ||
this.host = host || frag; | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
this.state = new State(this.update.bind(this), host); | ||
const read = runner(); | ||
const write = runner(); | ||
class BaseScheduler { | ||
constructor(renderer, host) { | ||
this.renderer = renderer; | ||
this.host = host; | ||
this.state = new State(this.update.bind(this), host); | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
} | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
if(this.state[effectsSymbol]) { | ||
if (this._updateQueued) | ||
return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
this.handlePhase(commitSymbol, result); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
}); | ||
}); | ||
} | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = true; | ||
this._updateQueued = true; | ||
} | ||
handlePhase(phase, arg) { | ||
this[phaseSymbol] = phase; | ||
switch(phase) { | ||
case commitSymbol: return this.commit(arg); | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
this[phaseSymbol] = phase; | ||
switch (phase) { | ||
case commitSymbol: | ||
this.commit(arg); | ||
this.runEffects(layoutEffectsSymbol); | ||
return; | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
} | ||
render() { | ||
return this.state.run(() => { | ||
return this.args ? | ||
this.renderer.apply(this.host, this.args) : | ||
this.renderer.call(this.host, this.host); | ||
}); | ||
return this.state.run(() => this.renderer.call(this.host, this.host)); | ||
} | ||
runEffects() { | ||
this.state.runEffects(); | ||
runEffects(phase) { | ||
this.state._runEffects(phase); | ||
} | ||
commit(result) { | ||
render(result, this.frag); | ||
} | ||
teardown() { | ||
this.state.teardown(); | ||
this.state.teardown(); | ||
} | ||
} | ||
return Scheduler; | ||
} | ||
export { makeScheduler }; | ||
export { BaseScheduler }; |
@@ -1,40 +0,40 @@ | ||
import { hookSymbol, effectsSymbol } from './symbols.js'; | ||
import { setCurrent, clear } from './interface.js'; | ||
import { hookSymbol, effectsSymbol, layoutEffectsSymbol } from './symbols.js'; | ||
class State { | ||
constructor(update, host) { | ||
this.update = update; | ||
this.host = host; | ||
this[hookSymbol] = new Map(); | ||
this[effectsSymbol] = []; | ||
} | ||
run(cb) { | ||
setCurrent(this); | ||
let res = cb(); | ||
clear(); | ||
return res; | ||
} | ||
runEffects() { | ||
let effects = this[effectsSymbol]; | ||
if(effects) { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
constructor(update, host) { | ||
this.update = update; | ||
this.host = host; | ||
this[hookSymbol] = new Map(); | ||
this[effectsSymbol] = []; | ||
this[layoutEffectsSymbol] = []; | ||
} | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}) | ||
} | ||
run(cb) { | ||
setCurrent(this); | ||
let res = cb(); | ||
clear(); | ||
return res; | ||
} | ||
_runEffects(phase) { | ||
let effects = this[phase]; | ||
setCurrent(this); | ||
for (let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
runEffects() { | ||
this._runEffects(effectsSymbol); | ||
} | ||
runLayoutEffects() { | ||
this._runEffects(layoutEffectsSymbol); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach(hook => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
} | ||
} | ||
export { State }; | ||
export { State }; |
@@ -1,10 +0,8 @@ | ||
const symbolFor = typeof Symbol === 'function' ? Symbol.for : str => str; | ||
export const phaseSymbol = symbolFor('haunted.phase'); | ||
export const hookSymbol = symbolFor('haunted.hook'); | ||
export const updateSymbol = symbolFor('haunted.update'); | ||
export const commitSymbol = symbolFor('haunted.commit'); | ||
export const effectsSymbol = symbolFor('haunted.effects'); | ||
export const contextEvent = 'haunted.context'; | ||
const phaseSymbol = Symbol('haunted.phase'); | ||
const hookSymbol = Symbol('haunted.hook'); | ||
const updateSymbol = Symbol('haunted.update'); | ||
const commitSymbol = Symbol('haunted.commit'); | ||
const effectsSymbol = Symbol('haunted.effects'); | ||
const layoutEffectsSymbol = Symbol('haunted.layoutEffects'); | ||
const contextEvent = 'haunted.context'; | ||
export { phaseSymbol, hookSymbol, updateSymbol, commitSymbol, effectsSymbol, layoutEffectsSymbol, contextEvent, }; |
@@ -1,5 +0,3 @@ | ||
import { useMemo } from "./use-memo.js"; | ||
import { useMemo } from './use-memo.js'; | ||
const useCallback = (fn, inputs) => useMemo(() => fn, inputs); | ||
export { useCallback }; |
@@ -0,65 +1,53 @@ | ||
import { hook, Hook } from './hook.js'; | ||
import { contextEvent } from './symbols.js'; | ||
import { hook, Hook } from './hook.js'; | ||
import { setEffects } from './use-effect.js'; | ||
const useContext = hook(class extends Hook { | ||
constructor(id, state) { | ||
super(id, state); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(state, this); | ||
} | ||
update(Context) { | ||
if (this.state.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
constructor(id, state, _) { | ||
super(id, state); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(state, this); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
update(Context) { | ||
if (this.state.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
return this.value; | ||
} | ||
call() { | ||
if(!this._ranEffect) { | ||
this._ranEffect = true; | ||
if(this._unsubscribe) this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.state.update(); | ||
call() { | ||
if (!this._ranEffect) { | ||
this._ranEffect = true; | ||
if (this._unsubscribe) | ||
this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.state.update(); | ||
} | ||
} | ||
} | ||
_updater(value) { | ||
this.value = value; | ||
this.state.update(); | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.state.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, // carrier | ||
bubbles: true, // to bubble up in tree | ||
cancelable: true, // to be able to cancel | ||
composed: true, // to pass ShadowDOM boundaries | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
_updater(value) { | ||
this.value = value; | ||
this.state.update(); | ||
} | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.state.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, | ||
bubbles: true, | ||
cancelable: true, | ||
composed: true, | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
} | ||
} | ||
}); | ||
export { useContext }; |
import { effectsSymbol } from './symbols.js'; | ||
import { hook, Hook } from './hook.js'; | ||
import { createEffect } from './create-effect.js'; | ||
function setEffects(state, cb) { | ||
if(!(effectsSymbol in state)) { | ||
state[effectsSymbol] = []; | ||
} | ||
state[effectsSymbol].push(cb); | ||
state[effectsSymbol].push(cb); | ||
} | ||
const useEffect = hook(class extends Hook { | ||
constructor(id, state) { | ||
super(id, state); | ||
this.values = false; | ||
setEffects(state, this); | ||
} | ||
update(callback, values) { | ||
this.callback = callback; | ||
this.lastValues = this.values; | ||
this.values = values; | ||
} | ||
call() { | ||
if(this.values) { | ||
if(this.hasChanged()) { | ||
this.run(); | ||
} | ||
} else { | ||
this.run(); | ||
} | ||
} | ||
run() { | ||
this.teardown(); | ||
this._teardown = this.callback.call(this.state); | ||
} | ||
teardown() { | ||
if(this._teardown) { | ||
this._teardown(); | ||
} | ||
} | ||
hasChanged() { | ||
return this.lastValues === false || this.values.some((value, i) => this.lastValues[i] !== value); | ||
} | ||
}); | ||
export { setEffects, useEffect }; | ||
const useEffect = createEffect(setEffects); | ||
export { setEffects, useEffect }; |
import { hook, Hook } from './hook.js'; | ||
const useMemo = hook(class extends Hook { | ||
constructor(id, state, fn, values) { | ||
super(id, state); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
update(fn, values) { | ||
if(this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
constructor(id, state, fn, values) { | ||
super(id, state); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
update(fn, values) { | ||
if (this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
export { useMemo }; | ||
export { useMemo }; |
import { hook, Hook } from './hook.js'; | ||
const useReducer = hook(class extends Hook { | ||
constructor(id, state, _, initialState) { | ||
super(id, state); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.state = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.state, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.state = this.reducer(this.state, action); | ||
this.state.update(); | ||
} | ||
constructor(id, state, _, initialState) { | ||
super(id, state); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.currentState = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.currentState, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.currentState = this.reducer(this.currentState, action); | ||
this.state.update(); | ||
} | ||
}); | ||
export { useReducer }; | ||
export { useReducer }; |
@@ -1,11 +0,5 @@ | ||
import { useMemo } from "./use-memo.js"; | ||
const useRef = (initialValue) => { | ||
return useMemo(() => { | ||
return { | ||
current: initialValue | ||
}; | ||
}, []); | ||
} | ||
export { useRef } | ||
import { useMemo } from './use-memo.js'; | ||
const useRef = (initialValue) => useMemo(() => ({ | ||
current: initialValue | ||
}), []); | ||
export { useRef }; |
import { hook, Hook } from './hook.js'; | ||
const useState = hook(class extends Hook { | ||
constructor(id, state, initialValue) { | ||
super(id, state); | ||
this.updater = this.updater.bind(this); | ||
if(typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
constructor(id, state, initialValue) { | ||
super(id, state); | ||
this.updater = this.updater.bind(this); | ||
if (typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
update() { | ||
return this.args; | ||
} | ||
updater(value) { | ||
if (typeof value === "function") { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
update() { | ||
return this.args; | ||
} | ||
this.makeArgs(value); | ||
this.state.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
updater(value) { | ||
if (typeof value === 'function') { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
} | ||
this.makeArgs(value); | ||
this.state.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
}); | ||
export { useState }; |
import { directive } from 'lit-html'; | ||
import { BaseScheduler } from './scheduler.js'; | ||
const includes = Array.prototype.includes; | ||
function makeVirtual(Scheduler) { | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
class DirectiveContainer extends Scheduler { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
function makeVirtual() { | ||
const partToScheduler = new WeakMap(); | ||
const schedulerToPart = new WeakMap(); | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.state.virtual = true; | ||
} | ||
render() { | ||
return this.state.run(() => this.renderer.apply(this.host, this.args)); | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = schedulerToPart.get(this); | ||
partToScheduler.delete(part); | ||
} | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = containerToPart.get(this); | ||
partToContainer.delete(part); | ||
} | ||
} | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return part => { | ||
let cont = partToContainer.get(part); | ||
if(!cont) { | ||
cont = new DirectiveContainer(renderer, part); | ||
partToContainer.set(part, cont); | ||
containerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return (part) => { | ||
let cont = partToScheduler.get(part); | ||
if (!cont) { | ||
cont = new Scheduler(renderer, part); | ||
partToScheduler.set(part, cont); | ||
schedulerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
return directive(factory); | ||
} | ||
return directive(factory); | ||
} | ||
return virtual; | ||
return virtual; | ||
} | ||
function teardownOnRemove(cont, part, node = part.startNode) { | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for(let mutation of mutations) { | ||
if(includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if(node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} else { | ||
cont.teardown(); | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for (let mutation of mutations) { | ||
if (includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if (node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} | ||
else { | ||
cont.teardown(); | ||
} | ||
break; | ||
} | ||
else if (includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling || undefined); | ||
break; | ||
} | ||
} | ||
break; | ||
} else if(includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling); | ||
break; | ||
} | ||
} | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
} | ||
export { makeVirtual }; | ||
export { makeVirtual }; |
{ | ||
"name": "haunted", | ||
"version": "4.6.0-beta.0", | ||
"version": "4.6.0", | ||
"description": "Hooks for web components", | ||
@@ -31,7 +31,8 @@ "main": "lib/haunted.js", | ||
"homepage": "https://github.com/matthewp/haunted#readme", | ||
"typings": " lib/haunted.d.ts", | ||
"typings": "lib/haunted.d.ts", | ||
"devDependencies": { | ||
"@matthewp/compile": "^2.4.3", | ||
"http-server": "^0.11.1", | ||
"mocha-headless-chrome": "^2.0.3" | ||
"mocha-headless-chrome": "^2.0.3", | ||
"typescript": "^3.6.2" | ||
}, | ||
@@ -38,0 +39,0 @@ "dependencies": { |
# Haunted 🦇 🎃 | ||
[![npm](https://img.shields.io/npm/dt/haunted)](https://npm.im/haunted) | ||
[![npm](https://img.shields.io/npm/v/haunted)](https://npm.im/haunted) | ||
React's Hooks API but for standard web components and [lit-html](https://lit-html.polymer-project.org/) or [hyperHTML](https://codepen.io/WebReflection/pen/pxXrdy?editors=0010). | ||
@@ -297,2 +300,8 @@ | ||
#### useLayoutEffect | ||
The function signature is the same as `useEffect`, but the callback is being called synchronously after rendering. Updates scheduled inside `useLayoutEffect` will therefore be flushed synchronously, before the browser has a chance to paint. | ||
Most of time, it is preferable to use `useEffect` to avoid blocking visual updates. | ||
#### useReducer | ||
@@ -299,0 +308,0 @@ |
925
web.js
import { directive, render } from '../lit-html/lit-html.js'; | ||
export { html, render } from '../lit-html/lit-html.js'; | ||
const symbolFor = typeof Symbol === 'function' ? Symbol.for : str => str; | ||
const phaseSymbol = symbolFor('haunted.phase'); | ||
const hookSymbol = symbolFor('haunted.hook'); | ||
const updateSymbol = symbolFor('haunted.update'); | ||
const commitSymbol = symbolFor('haunted.commit'); | ||
const effectsSymbol = symbolFor('haunted.effects'); | ||
const contextSymbol = symbolFor('haunted.context'); | ||
const contextEvent = 'haunted.context'; | ||
let current; | ||
let currentId = 0; | ||
function setCurrent(element) { | ||
current = element; | ||
function setCurrent(state) { | ||
current = state; | ||
} | ||
function clear() { | ||
current = null; | ||
currentId = 0; | ||
current = null; | ||
currentId = 0; | ||
} | ||
function notify() { | ||
let id = currentId; | ||
currentId++; | ||
return id; | ||
return currentId++; | ||
} | ||
//import { render, html } from './lit.js'; | ||
const phaseSymbol = Symbol('haunted.phase'); | ||
const hookSymbol = Symbol('haunted.hook'); | ||
const updateSymbol = Symbol('haunted.update'); | ||
const commitSymbol = Symbol('haunted.commit'); | ||
const effectsSymbol = Symbol('haunted.effects'); | ||
const layoutEffectsSymbol = Symbol('haunted.layoutEffects'); | ||
const contextEvent = 'haunted.context'; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
function scheduler() { | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for(var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
class State { | ||
constructor(update, host) { | ||
this.update = update; | ||
this.host = host; | ||
this[hookSymbol] = new Map(); | ||
this[effectsSymbol] = []; | ||
this[layoutEffectsSymbol] = []; | ||
} | ||
} | ||
return function(task) { | ||
tasks.push(task); | ||
if(id == null) { | ||
id = defer(runTasks); | ||
run(cb) { | ||
setCurrent(this); | ||
let res = cb(); | ||
clear(); | ||
return res; | ||
} | ||
}; | ||
_runEffects(phase) { | ||
let effects = this[phase]; | ||
setCurrent(this); | ||
for (let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
runEffects() { | ||
this._runEffects(effectsSymbol); | ||
} | ||
runLayoutEffects() { | ||
this._runEffects(layoutEffectsSymbol); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach(hook => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
} | ||
} | ||
function makeContainer(render$$1) { | ||
const read = scheduler(); | ||
const write = scheduler(); | ||
class Container { | ||
constructor(renderer, frag, host) { | ||
this.renderer = renderer; | ||
this.frag = frag; | ||
this.host = host || frag; | ||
this[hookSymbol] = new Map(); | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
function runner() { | ||
let tasks = []; | ||
let id; | ||
function runTasks() { | ||
id = null; | ||
let t = tasks; | ||
tasks = []; | ||
for (var i = 0, len = t.length; i < len; i++) { | ||
t[i](); | ||
} | ||
} | ||
return function (task) { | ||
tasks.push(task); | ||
if (id == null) { | ||
id = defer(runTasks); | ||
} | ||
}; | ||
} | ||
const read = runner(); | ||
const write = runner(); | ||
class BaseScheduler { | ||
constructor(renderer, host) { | ||
this.renderer = renderer; | ||
this.host = host; | ||
this.state = new State(this.update.bind(this), host); | ||
this[phaseSymbol] = null; | ||
this._updateQueued = false; | ||
} | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
if(this[effectsSymbol]) { | ||
if (this._updateQueued) | ||
return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
this.handlePhase(commitSymbol, result); | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
}); | ||
}); | ||
} | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = false; | ||
}); | ||
this._updateQueued = true; | ||
this._updateQueued = true; | ||
} | ||
handlePhase(phase, arg) { | ||
this[phaseSymbol] = phase; | ||
switch(phase) { | ||
case commitSymbol: return this.commit(arg); | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
this[phaseSymbol] = phase; | ||
switch (phase) { | ||
case commitSymbol: | ||
this.commit(arg); | ||
this.runEffects(layoutEffectsSymbol); | ||
return; | ||
case updateSymbol: return this.render(); | ||
case effectsSymbol: return this.runEffects(effectsSymbol); | ||
} | ||
this[phaseSymbol] = null; | ||
} | ||
commit(result) { | ||
render$$1(result, this.frag); | ||
this.runEffects(commitSymbol); | ||
} | ||
render() { | ||
setCurrent(this); | ||
let result = this.args ? | ||
this.renderer.apply(this.host, this.args) : | ||
this.renderer.call(this.host, this.host); | ||
clear(); | ||
return result; | ||
return this.state.run(() => this.renderer.call(this.host, this.host)); | ||
} | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
runEffects(phase) { | ||
this.state._runEffects(phase); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
this.state.teardown(); | ||
} | ||
} | ||
return Container; | ||
} | ||
function toCamelCase(val = '') { | ||
return val.indexOf('-') === -1 ? val.toLowerCase() : val.toLowerCase().split('-').reduce((out, part) => { | ||
return out ? out + part.charAt(0).toUpperCase() + part.slice(1) : part; | ||
},'') | ||
} | ||
function makeComponent(Container) { | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const {observedAttributes = [], useShadowDOM = true, shadowRootInit = {}} = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._container = new Container(renderer, this); | ||
} else { | ||
this.attachShadow({ mode: "open", ...shadowRootInit}); | ||
this._container = new Container(renderer, this.shadowRoot, this); | ||
const toCamelCase = (val = '') => val.replace(/-+([a-z])?/g, (_, char) => char ? char.toUpperCase() : ''); | ||
function makeComponent(render) { | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, frag, host) { | ||
super(renderer, host || frag); | ||
this.frag = frag; | ||
} | ||
} | ||
connectedCallback() { | ||
this._container.update(); | ||
} | ||
disconnectedCallback() { | ||
this._container.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
} | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._container.update(); | ||
commit(result) { | ||
render(result, this.frag); | ||
} | ||
}) | ||
} | ||
const proto = new Proxy(BaseElement.prototype, { | ||
set(target, key, value, receiver) { | ||
if(key in target) { | ||
Reflect.set(target, key, value); | ||
function component(renderer, baseElementOrOptions, options) { | ||
const BaseElement = (options || baseElementOrOptions || {}).baseElement || HTMLElement; | ||
const { observedAttributes = [], useShadowDOM = true, shadowRootInit = {} } = options || baseElementOrOptions || {}; | ||
class Element extends BaseElement { | ||
constructor() { | ||
super(); | ||
if (useShadowDOM === false) { | ||
this._scheduler = new Scheduler(renderer, this); | ||
} | ||
else { | ||
this.attachShadow({ mode: 'open', ...shadowRootInit }); | ||
this._scheduler = new Scheduler(renderer, this.shadowRoot, this); | ||
} | ||
} | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || observedAttributes || []; | ||
} | ||
connectedCallback() { | ||
this._scheduler.update(); | ||
} | ||
disconnectedCallback() { | ||
this._scheduler.teardown(); | ||
} | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
} | ||
let desc; | ||
if(typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} else { | ||
desc = reflectiveProp(value); | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._scheduler.update(); | ||
} | ||
}); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if(desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
const proto = new Proxy(BaseElement.prototype, { | ||
getPrototypeOf(target) { | ||
return target; | ||
}, | ||
set(target, key, value, receiver) { | ||
if (key in target) { | ||
Reflect.set(target, key, value); | ||
} | ||
let desc; | ||
if (typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} | ||
else { | ||
desc = reflectiveProp(value); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
if (desc.set) { | ||
desc.set.call(receiver, value); | ||
} | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return component; | ||
} | ||
class Hook { | ||
constructor(id, el) { | ||
this.id = id; | ||
this.el = el; | ||
} | ||
constructor(id, state) { | ||
this.id = id; | ||
this.state = state; | ||
} | ||
} | ||
function use(Hook, ...args) { | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if(!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
let id = notify(); | ||
let hooks = current[hookSymbol]; | ||
let hook = hooks.get(id); | ||
if (!hook) { | ||
hook = new Hook(id, current, ...args); | ||
hooks.set(id, hook); | ||
} | ||
return hook.update(...args); | ||
} | ||
function hook(Hook) { | ||
return use.bind(null, Hook); | ||
return use.bind(null, Hook); | ||
} | ||
function setEffects(el, cb) { | ||
if(!(effectsSymbol in el)) { | ||
el[effectsSymbol] = []; | ||
} | ||
el[effectsSymbol].push(cb); | ||
function createEffect(setEffects) { | ||
return hook(class extends Hook { | ||
constructor(id, state, ignored1, ignored2) { | ||
super(id, state); | ||
setEffects(state, this); | ||
} | ||
update(callback, values) { | ||
this.callback = callback; | ||
this.lastValues = this.values; | ||
this.values = values; | ||
} | ||
call() { | ||
if (!this.values || this.hasChanged()) { | ||
this.run(); | ||
} | ||
} | ||
run() { | ||
this.teardown(); | ||
this._teardown = this.callback.call(this.state); | ||
} | ||
teardown() { | ||
if (this._teardown) { | ||
this._teardown(); | ||
} | ||
} | ||
hasChanged() { | ||
return !this.lastValues || this.values.some((value, i) => this.lastValues[i] !== value); | ||
} | ||
}); | ||
} | ||
const useEffect = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
this.values = false; | ||
setEffects(el, this); | ||
} | ||
update(callback, values) { | ||
this.callback = callback; | ||
this.lastValues = this.values; | ||
this.values = values; | ||
} | ||
call() { | ||
if(this.values) { | ||
if(this.hasChanged()) { | ||
this.run(); | ||
} | ||
} else { | ||
this.run(); | ||
} | ||
} | ||
run() { | ||
this.teardown(); | ||
this._teardown = this.callback.call(this.el); | ||
} | ||
teardown() { | ||
if(this._teardown) { | ||
this._teardown(); | ||
} | ||
} | ||
hasChanged() { | ||
return this.lastValues === false || this.values.some((value, i) => this.lastValues[i] !== value); | ||
} | ||
}); | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
} | ||
el[contextSymbol].push(consumer); | ||
function setEffects(state, cb) { | ||
state[effectsSymbol].push(cb); | ||
} | ||
const useEffect = createEffect(setEffects); | ||
const useContext = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
setContexts(el, this); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(el, this); | ||
} | ||
update(Context) { | ||
if (this.el.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
constructor(id, state, _) { | ||
super(id, state); | ||
this._updater = this._updater.bind(this); | ||
this._ranEffect = false; | ||
this._unsubscribe = null; | ||
setEffects(state, this); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
update(Context) { | ||
if (this.state.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
return this.value; | ||
} | ||
call() { | ||
if(!this._ranEffect) { | ||
this._ranEffect = true; | ||
if(this._unsubscribe) this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.el.update(); | ||
call() { | ||
if (!this._ranEffect) { | ||
this._ranEffect = true; | ||
if (this._unsubscribe) | ||
this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.state.update(); | ||
} | ||
} | ||
} | ||
_updater(value) { | ||
this.value = value; | ||
this.el.update(); | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.el.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, // carrier | ||
bubbles: true, // to bubble up in tree | ||
cancelable: true, // to be able to cancel | ||
composed: true, // to pass ShadowDOM boundaries | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
_updater(value) { | ||
this.value = value; | ||
this.state.update(); | ||
} | ||
} | ||
_subscribe(Context) { | ||
const detail = { Context, callback: this._updater }; | ||
this.state.host.dispatchEvent(new CustomEvent(contextEvent, { | ||
detail, | ||
bubbles: true, | ||
cancelable: true, | ||
composed: true, | ||
})); | ||
const { unsubscribe, value } = detail; | ||
this.value = unsubscribe ? value : Context.defaultValue; | ||
this._unsubscribe = unsubscribe; | ||
} | ||
teardown() { | ||
if (this._unsubscribe) { | ||
this._unsubscribe(); | ||
} | ||
} | ||
}); | ||
function makeContext(component) { | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
if(this.listeners.has(callback)) { | ||
this.listeners.delete(callback); | ||
} | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for(let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render: render$$1 }) { | ||
const context = useContext(Context); | ||
return render$$1(context); | ||
}), | ||
defaultValue | ||
return (defaultValue) => { | ||
const Context = { | ||
Provider: class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = new Set(); | ||
this.addEventListener(contextEvent, this); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this); | ||
} | ||
handleEvent(event) { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback); | ||
this.listeners.add(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
} | ||
unsubscribe(callback) { | ||
this.listeners.delete(callback); | ||
} | ||
set value(value) { | ||
this._value = value; | ||
for (let callback of this.listeners) { | ||
callback(value); | ||
} | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}, | ||
Consumer: component(function ({ render }) { | ||
const context = useContext(Context); | ||
return render(context); | ||
}), | ||
defaultValue, | ||
}; | ||
return Context; | ||
}; | ||
return Context; | ||
}; | ||
} | ||
const useMemo = hook(class extends Hook { | ||
constructor(id, el, fn, values) { | ||
super(id, el); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
update(fn, values) { | ||
if(this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
constructor(id, state, fn, values) { | ||
super(id, state); | ||
this.value = fn(); | ||
this.values = values; | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
update(fn, values) { | ||
if (this.hasChanged(values)) { | ||
this.values = values; | ||
this.value = fn(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
@@ -447,145 +386,127 @@ | ||
function setLayoutEffects(state, cb) { | ||
state[layoutEffectsSymbol].push(cb); | ||
} | ||
const useLayoutEffect = createEffect(setLayoutEffects); | ||
const useState = hook(class extends Hook { | ||
constructor(id, el, initialValue) { | ||
super(id, el); | ||
this.updater = this.updater.bind(this); | ||
if(typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
constructor(id, state, initialValue) { | ||
super(id, state); | ||
this.updater = this.updater.bind(this); | ||
if (typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
this.makeArgs(initialValue); | ||
} | ||
update() { | ||
return this.args; | ||
} | ||
updater(value) { | ||
if (typeof value === "function") { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
update() { | ||
return this.args; | ||
} | ||
this.makeArgs(value); | ||
this.el.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
updater(value) { | ||
if (typeof value === 'function') { | ||
const updaterFn = value; | ||
const [previousValue] = this.args; | ||
value = updaterFn(previousValue); | ||
} | ||
this.makeArgs(value); | ||
this.state.update(); | ||
} | ||
makeArgs(value) { | ||
this.args = Object.freeze([value, this.updater]); | ||
} | ||
}); | ||
const useReducer = hook(class extends Hook { | ||
constructor(id, el, _, initialState) { | ||
super(id, el); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.state = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.state, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.state = this.reducer(this.state, action); | ||
this.el.update(); | ||
} | ||
constructor(id, state, _, initialState) { | ||
super(id, state); | ||
this.dispatch = this.dispatch.bind(this); | ||
this.currentState = initialState; | ||
} | ||
update(reducer) { | ||
this.reducer = reducer; | ||
return [this.currentState, this.dispatch]; | ||
} | ||
dispatch(action) { | ||
this.currentState = this.reducer(this.currentState, action); | ||
this.state.update(); | ||
} | ||
}); | ||
const useRef = (initialValue) => { | ||
return useMemo(() => { | ||
return { | ||
current: initialValue | ||
}; | ||
}, []); | ||
}; | ||
const useRef = (initialValue) => useMemo(() => ({ | ||
current: initialValue | ||
}), []); | ||
function haunted({ render: render$$1 }) { | ||
const Container = makeContainer(render$$1); | ||
const component = makeComponent(Container); | ||
const createContext = makeContext(component); | ||
return { Container, component, createContext }; | ||
function haunted({ render }) { | ||
const component = makeComponent(render); | ||
const createContext = makeContext(component); | ||
return { component, createContext }; | ||
} | ||
const includes = Array.prototype.includes; | ||
function makeVirtual(Container) { | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
function makeVirtual() { | ||
const partToScheduler = new WeakMap(); | ||
const schedulerToPart = new WeakMap(); | ||
class Scheduler extends BaseScheduler { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.state.virtual = true; | ||
} | ||
render() { | ||
return this.state.run(() => this.renderer.apply(this.host, this.args)); | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = schedulerToPart.get(this); | ||
partToScheduler.delete(part); | ||
} | ||
} | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = containerToPart.get(this); | ||
partToContainer.delete(part); | ||
} | ||
} | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return part => { | ||
let cont = partToContainer.get(part); | ||
if(!cont) { | ||
cont = new DirectiveContainer(renderer, part); | ||
partToContainer.set(part, cont); | ||
containerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
function virtual(renderer) { | ||
function factory(...args) { | ||
return (part) => { | ||
let cont = partToScheduler.get(part); | ||
if (!cont) { | ||
cont = new Scheduler(renderer, part); | ||
partToScheduler.set(part, cont); | ||
schedulerToPart.set(cont, part); | ||
teardownOnRemove(cont, part); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
return directive(factory); | ||
} | ||
return directive(factory); | ||
} | ||
return virtual; | ||
return virtual; | ||
} | ||
function teardownOnRemove(cont, part, node = part.startNode) { | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for(let mutation of mutations) { | ||
if(includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if(node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} else { | ||
cont.teardown(); | ||
let frag = node.parentNode; | ||
let mo = new MutationObserver(mutations => { | ||
for (let mutation of mutations) { | ||
if (includes.call(mutation.removedNodes, node)) { | ||
mo.disconnect(); | ||
if (node.parentNode instanceof ShadowRoot) { | ||
teardownOnRemove(cont, part); | ||
} | ||
else { | ||
cont.teardown(); | ||
} | ||
break; | ||
} | ||
else if (includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling || undefined); | ||
break; | ||
} | ||
} | ||
break; | ||
} else if(includes.call(mutation.addedNodes, node.nextSibling)) { | ||
mo.disconnect(); | ||
teardownOnRemove(cont, part, node.nextSibling); | ||
break; | ||
} | ||
} | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
}); | ||
mo.observe(frag, { childList: true }); | ||
} | ||
const { Container, component, createContext } = haunted({ | ||
render(what, where) { | ||
render(what, where); | ||
} | ||
}); | ||
const { component, createContext } = haunted({ render }); | ||
const virtual = makeVirtual(); | ||
const virtual = makeVirtual(Container); | ||
export default haunted; | ||
export { component, createContext, virtual, useCallback, useEffect, useState, useReducer, useMemo, useContext, useRef, hook, Hook }; | ||
export { BaseScheduler, Hook, State, component, createContext, hook, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, virtual }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
73362
46
1708
0
553
4