Comparing version 4.4.0 to 4.5.0
629
haunted.js
@@ -1,2 +0,2 @@ | ||
import { render, directive } from 'https://unpkg.com/lit-html@^1.0.0/lit-html.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'; | ||
@@ -34,2 +34,4 @@ | ||
//import { render, html } from './lit.js'; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
@@ -58,76 +60,80 @@ | ||
const read = scheduler(); | ||
const write = scheduler(); | ||
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; | ||
} | ||
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; | ||
} | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
if(this[effectsSymbol]) { | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
}); | ||
} | ||
if(this[effectsSymbol]) { | ||
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); | ||
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] = null; | ||
} | ||
commit(result) { | ||
render(result, this.frag); | ||
this.runEffects(commitSymbol); | ||
} | ||
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; | ||
} | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
render() { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
let result = this.args ? | ||
this.renderer.apply(this.host, this.args) : | ||
this.renderer.call(this.host, this.host); | ||
clear(); | ||
return result; | ||
} | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
}); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
} | ||
} | ||
return Container; | ||
} | ||
@@ -141,76 +147,80 @@ | ||
function component(renderer, BaseElement = HTMLElement, {useShadowDOM = true, shadowRootInit = {}} = {}) { | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || []; | ||
} | ||
function makeComponent(Container) { | ||
function component(renderer, BaseElement = HTMLElement, {useShadowDOM = true, shadowRootInit = {}} = {}) { | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.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); | ||
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); | ||
} | ||
} | ||
} | ||
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; | ||
connectedCallback() { | ||
this._container.update(); | ||
} | ||
}) | ||
} | ||
const proto = new Proxy(BaseElement.prototype, { | ||
set(target, key, value, receiver) { | ||
if(key in target) { | ||
Reflect.set(target, key, value); | ||
disconnectedCallback() { | ||
this._container.teardown(); | ||
} | ||
let desc; | ||
if(typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} else { | ||
desc = reflectiveProp(value); | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
} | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._container.update(); | ||
} | ||
}) | ||
} | ||
if(desc.set) { | ||
desc.set.call(receiver, value); | ||
const proto = new Proxy(BaseElement.prototype, { | ||
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; | ||
} | ||
}); | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return Element; | ||
return component; | ||
} | ||
@@ -242,24 +252,2 @@ | ||
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(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
const useCallback = (fn, inputs) => useMemo(() => fn, inputs); | ||
function setEffects(el, cb) { | ||
@@ -311,2 +299,151 @@ if(!(effectsSymbol in el)) { | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
} | ||
el[contextSymbol].push(consumer); | ||
} | ||
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'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
call() { | ||
if(!this._ranEffect) { | ||
this._ranEffect = true; | ||
if(this._unsubscribe) this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.el.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(); | ||
} | ||
} | ||
}); | ||
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 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(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
const useCallback = (fn, inputs) => useMemo(() => fn, inputs); | ||
const useState = hook(class extends Hook { | ||
@@ -316,2 +453,7 @@ constructor(id, el, initialValue) { | ||
this.updater = this.updater.bind(this); | ||
if(typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
} | ||
this.makeArgs(initialValue); | ||
@@ -358,45 +500,63 @@ } | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
const useRef = (initialValue) => { | ||
return useMemo(() => { | ||
return { | ||
current: initialValue | ||
}; | ||
}, []); | ||
}; | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
} | ||
function haunted({ render: render$$1 }) { | ||
const Container = makeContainer(render$$1); | ||
const component = makeComponent(Container); | ||
const createContext = makeContext(component); | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = containerToPart.get(this); | ||
partToContainer.delete(part); | ||
} | ||
return { Container, component, createContext }; | ||
} | ||
const includes = Array.prototype.includes; | ||
function withHooks(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); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
function makeVirtual(Container) { | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
} | ||
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); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
} | ||
return directive(factory); | ||
} | ||
return directive(factory); | ||
return virtual; | ||
} | ||
const includes = Array.prototype.includes; | ||
function teardownOnRemove(cont, part, node = part.startNode) { | ||
@@ -425,114 +585,11 @@ let frag = node.parentNode; | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
const { Container, component, createContext } = haunted({ | ||
render(what, where) { | ||
render(what, where); | ||
} | ||
el[contextSymbol].push(consumer); | ||
} | ||
const useContext = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
setContexts(el, this); | ||
this._updater = this._updater.bind(this); | ||
} | ||
update(Context) { | ||
if (this.el.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
_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(); | ||
} | ||
} | ||
}); | ||
const createContext = (defaultValue) => { | ||
const Context = {}; | ||
Context.Provider = class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = []; | ||
this.eventHandler = (event) => { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = () => { | ||
const index = this.listeners.indexOf(detail.callback); | ||
const virtual = makeVirtual(Container); | ||
if (index > -1) { | ||
this.listeners.splice(index, 1); | ||
} | ||
}; | ||
this.listeners.push(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
}; | ||
this.addEventListener(contextEvent, this.eventHandler); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this.eventHandler); | ||
} | ||
set value(value) { | ||
this._value = value; | ||
this.listeners.forEach(callback => callback(value)); | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}; | ||
Context.Consumer = component(function ({ render: render$$1 }) { | ||
const context = useContext(Context); | ||
return render$$1(context); | ||
}); | ||
Context.defaultValue = defaultValue; | ||
return Context; | ||
}; | ||
export { component, useCallback, useEffect, useState, useReducer, useMemo, withHooks, withHooks as virtual, useContext, createContext, hook, Hook }; | ||
export default haunted; | ||
export { component, createContext, virtual, useCallback, useEffect, useState, useReducer, useMemo, useContext, useRef, hook, Hook }; |
{ | ||
"name": "haunted", | ||
"version": "4.4.0", | ||
"description": "", | ||
"main": "index.js", | ||
"module": "index.js", | ||
"version": "4.5.0", | ||
"description": "Hooks for web components", | ||
"main": "lib/haunted.js", | ||
"module": "lib/haunted.js", | ||
"type": "module", | ||
@@ -16,5 +16,5 @@ "scripts": { | ||
"haunted.js", | ||
"index.js", | ||
"web.js", | ||
"index.d.ts" | ||
"core.js", | ||
"lib" | ||
], | ||
@@ -21,0 +21,0 @@ "repository": { |
# Haunted 🦇 🎃 | ||
React's Hooks API but for standard web components and [hyperHTML](https://codepen.io/WebReflection/pen/pxXrdy?editors=0010) or [lit-html](https://lit-html.polymer-project.org/). | ||
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). | ||
@@ -37,23 +37,66 @@ ```html | ||
``` | ||
For Internet Explorer 11, you'll need to use a proxy polyfill, in addition to the usual webcomponentsjs polyfills. | ||
eg. | ||
```html | ||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/proxy-polyfill@0.3.0/proxy.min.js"></script> | ||
<script src="https://cdn.jsdelivr.net/npm/proxy-polyfill@0.3.0/proxy.min.js"></script> | ||
``` | ||
For a full example see - https://github.com/crisward/haunted-ie11 | ||
For a full example with Internet Explorer 11, see - https://github.com/crisward/haunted-ie11 | ||
You can also use Custom Elements without Shadow DOM if you wish. | ||
eg. | ||
```js | ||
component(() => html`...`, HTMLElement, { useShadowDOM: false })); | ||
``` | ||
component(() => html`...`, HTMLElement, {useShadowDOM: false})) | ||
### Importing | ||
__Haunted__ can be imported just like any other library when using a bundler of your choice: | ||
```js | ||
import { component, html, useState } from 'haunted'; | ||
``` | ||
### Builds | ||
The main entry point is intended for [lit-html](https://github.com/Polymer/lit-html) users. | ||
Haunted comes in a few builds. Pick one based on your chosen environment: | ||
#### lighterhtml, etc | ||
* __index.js__ is available for bundlers such as Webpack and Rollup. Use with: `import { useState } from 'haunted';`; | ||
* __web.js__ is avaible for use with the web's native module support. Use with: `import { useState } from '../node_modules/haunted/web.js';`. | ||
* __haunted.js__ is available via the CDN [unpkg](https://unpkg.com). This is great for small apps or for local development without having to install anything. Use with `import { useState } from 'https://unpkg.com/haunted/haunted.js';` | ||
If you are using [lighterhtml](https://github.com/WebReflection/lighterhtml) or [hyperHTML](https://github.com/WebReflection/hyperHTML) then instead import `haunted/core`. This export gives you a function that creates Hooks that work with any template library. | ||
```js | ||
import haunted, { useState } from 'haunted/core'; | ||
import { html, render } from 'lighterhtml'; | ||
const { component } = haunted({ | ||
render(what, where) { | ||
render(where, () => what); | ||
} | ||
}); | ||
const App = component(() => { | ||
const [count, setCount] = useState(0); | ||
return html`Using lighterhtml! Count: ${count}`; | ||
}); | ||
``` | ||
#### Web modules | ||
__Haunted__ can work directly in the browser without using any build tools. Simply import the `haunted.js` bundle. You can use the [unpkg] or [pika](https://www.pika.dev/cdn) CDNs. This works great for demo pages and small apps. Here's an example with unpkg: | ||
```js | ||
import { html } from 'https://unpkg.com/lit-html/lit-html.js'; | ||
import { component, useState, useEffect } from 'https://unpkg.com/haunted/haunted.js'; | ||
``` | ||
If using pika then use the `html` export from Haunted, as pika bundles everything together: | ||
```js | ||
import { useState, component, html } from 'https://cdn.pika.dev/haunted'; | ||
``` | ||
If you install Haunted __locally__ this build is located at `node_modules/haunted/haunted.js`. | ||
## API | ||
@@ -77,5 +120,5 @@ | ||
const App = component(({ name }) => { | ||
const App = ({ name }) => { | ||
return html`Hello ${name}!`; | ||
}); | ||
}; | ||
@@ -169,2 +212,10 @@ customElements.define('my-app', component(App)); | ||
Additionally you can provide a function as the argument to useState, in which case the function is called to initialize the first state, but never called again. | ||
```js | ||
const [count, setCount] = useState(() => { | ||
return expensiveFunction(); | ||
}); | ||
``` | ||
#### useEffect | ||
@@ -318,2 +369,27 @@ | ||
#### useRef | ||
Create and returns an object with one property "current" which can be assigned any value and is unaffected by multiple renders. | ||
```html | ||
<!doctype html> | ||
<my-app></my-app> | ||
<script type="module"> | ||
import { html } from 'https://unpkg.com/lit-html/lit-html.js'; | ||
import { component, useRef } from 'https://unpkg.com/haunted/haunted.js'; | ||
function App() { | ||
const myRef = useRef(0); | ||
return html` | ||
${myRef.current} | ||
`; | ||
} | ||
customElements.define('my-app', component(App)); | ||
</script> | ||
``` | ||
#### useContext | ||
@@ -320,0 +396,0 @@ |
629
web.js
@@ -1,2 +0,2 @@ | ||
import { render, directive } from '../lit-html/lit-html.js'; | ||
import { directive, render } from '../lit-html/lit-html.js'; | ||
export { html, render } from '../lit-html/lit-html.js'; | ||
@@ -34,2 +34,4 @@ | ||
//import { render, html } from './lit.js'; | ||
const defer = Promise.resolve().then.bind(Promise.resolve()); | ||
@@ -58,76 +60,80 @@ | ||
const read = scheduler(); | ||
const write = scheduler(); | ||
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; | ||
} | ||
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; | ||
} | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
update() { | ||
if(this._updateQueued) return; | ||
read(() => { | ||
let result = this.handlePhase(updateSymbol); | ||
write(() => { | ||
this.handlePhase(commitSymbol, result); | ||
if(this[effectsSymbol]) { | ||
write(() => { | ||
this.handlePhase(effectsSymbol); | ||
}); | ||
} | ||
if(this[effectsSymbol]) { | ||
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); | ||
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] = null; | ||
} | ||
commit(result) { | ||
render(result, this.frag); | ||
this.runEffects(commitSymbol); | ||
} | ||
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; | ||
} | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
render() { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
let result = this.args ? | ||
this.renderer.apply(this.host, this.args) : | ||
this.renderer.call(this.host, this.host); | ||
clear(); | ||
return result; | ||
} | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
runEffects(symbol) { | ||
let effects = this[symbol]; | ||
if(effects) { | ||
setCurrent(this); | ||
for(let effect of effects) { | ||
effect.call(this); | ||
} | ||
clear(); | ||
} | ||
}); | ||
} | ||
teardown() { | ||
let hooks = this[hookSymbol]; | ||
hooks.forEach((hook) => { | ||
if (typeof hook.teardown === 'function') { | ||
hook.teardown(); | ||
} | ||
}); | ||
} | ||
} | ||
return Container; | ||
} | ||
@@ -141,76 +147,80 @@ | ||
function component(renderer, BaseElement = HTMLElement, {useShadowDOM = true, shadowRootInit = {}} = {}) { | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.observedAttributes || []; | ||
} | ||
function makeComponent(Container) { | ||
function component(renderer, BaseElement = HTMLElement, {useShadowDOM = true, shadowRootInit = {}} = {}) { | ||
class Element extends BaseElement { | ||
static get observedAttributes() { | ||
return renderer.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); | ||
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); | ||
} | ||
} | ||
} | ||
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; | ||
connectedCallback() { | ||
this._container.update(); | ||
} | ||
}) | ||
} | ||
const proto = new Proxy(BaseElement.prototype, { | ||
set(target, key, value, receiver) { | ||
if(key in target) { | ||
Reflect.set(target, key, value); | ||
disconnectedCallback() { | ||
this._container.teardown(); | ||
} | ||
let desc; | ||
if(typeof key === 'symbol' || key[0] === '_') { | ||
desc = { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value | ||
}; | ||
} else { | ||
desc = reflectiveProp(value); | ||
attributeChangedCallback(name, _, newValue) { | ||
let val = newValue === '' ? true : newValue; | ||
Reflect.set(this, toCamelCase(name), val); | ||
} | ||
Object.defineProperty(receiver, key, desc); | ||
} | ||
function reflectiveProp(initialValue) { | ||
let value = initialValue; | ||
return Object.freeze({ | ||
enumerable: true, | ||
configurable: true, | ||
get() { | ||
return value; | ||
}, | ||
set(newValue) { | ||
value = newValue; | ||
this._container.update(); | ||
} | ||
}) | ||
} | ||
if(desc.set) { | ||
desc.set.call(receiver, value); | ||
const proto = new Proxy(BaseElement.prototype, { | ||
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; | ||
} | ||
}); | ||
return true; | ||
} | ||
}); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
Object.setPrototypeOf(Element.prototype, proto); | ||
return Element; | ||
} | ||
return Element; | ||
return component; | ||
} | ||
@@ -242,24 +252,2 @@ | ||
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(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
const useCallback = (fn, inputs) => useMemo(() => fn, inputs); | ||
function setEffects(el, cb) { | ||
@@ -311,2 +299,151 @@ if(!(effectsSymbol in el)) { | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
} | ||
el[contextSymbol].push(consumer); | ||
} | ||
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'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
call() { | ||
if(!this._ranEffect) { | ||
this._ranEffect = true; | ||
if(this._unsubscribe) this._unsubscribe(); | ||
this._subscribe(this.Context); | ||
this.el.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(); | ||
} | ||
} | ||
}); | ||
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 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(); | ||
} | ||
return this.value; | ||
} | ||
hasChanged(values) { | ||
return values.some((value, i) => this.values[i] !== value); | ||
} | ||
}); | ||
const useCallback = (fn, inputs) => useMemo(() => fn, inputs); | ||
const useState = hook(class extends Hook { | ||
@@ -316,2 +453,7 @@ constructor(id, el, initialValue) { | ||
this.updater = this.updater.bind(this); | ||
if(typeof initialValue === 'function') { | ||
initialValue = initialValue(); | ||
} | ||
this.makeArgs(initialValue); | ||
@@ -358,45 +500,63 @@ } | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
const useRef = (initialValue) => { | ||
return useMemo(() => { | ||
return { | ||
current: initialValue | ||
}; | ||
}, []); | ||
}; | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
} | ||
function haunted({ render: render$$1 }) { | ||
const Container = makeContainer(render$$1); | ||
const component = makeComponent(Container); | ||
const createContext = makeContext(component); | ||
commit(result) { | ||
this.host.setValue(result); | ||
this.host.commit(); | ||
} | ||
teardown() { | ||
super.teardown(); | ||
let part = containerToPart.get(this); | ||
partToContainer.delete(part); | ||
} | ||
return { Container, component, createContext }; | ||
} | ||
const includes = Array.prototype.includes; | ||
function withHooks(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); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
function makeVirtual(Container) { | ||
const partToContainer = new WeakMap(); | ||
const containerToPart = new WeakMap(); | ||
class DirectiveContainer extends Container { | ||
constructor(renderer, part) { | ||
super(renderer, part); | ||
this.virtual = true; | ||
} | ||
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); | ||
} | ||
cont.args = args; | ||
cont.update(); | ||
}; | ||
} | ||
return directive(factory); | ||
} | ||
return directive(factory); | ||
return virtual; | ||
} | ||
const includes = Array.prototype.includes; | ||
function teardownOnRemove(cont, part, node = part.startNode) { | ||
@@ -425,114 +585,11 @@ let frag = node.parentNode; | ||
function setContexts(el, consumer) { | ||
if(!(contextSymbol in el)) { | ||
el[contextSymbol] = []; | ||
const { Container, component, createContext } = haunted({ | ||
render(what, where) { | ||
render(what, where); | ||
} | ||
el[contextSymbol].push(consumer); | ||
} | ||
const useContext = hook(class extends Hook { | ||
constructor(id, el) { | ||
super(id, el); | ||
setContexts(el, this); | ||
this._updater = this._updater.bind(this); | ||
} | ||
update(Context) { | ||
if (this.el.virtual) { | ||
throw new Error('can\'t be used with virtual components'); | ||
} | ||
if (this.Context !== Context) { | ||
this._subscribe(Context); | ||
this.Context = Context; | ||
} | ||
return this.value; | ||
} | ||
_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(); | ||
} | ||
} | ||
}); | ||
const createContext = (defaultValue) => { | ||
const Context = {}; | ||
Context.Provider = class extends HTMLElement { | ||
constructor() { | ||
super(); | ||
this.listeners = []; | ||
this.eventHandler = (event) => { | ||
const { detail } = event; | ||
if (detail.Context === Context) { | ||
detail.value = this.value; | ||
detail.unsubscribe = () => { | ||
const index = this.listeners.indexOf(detail.callback); | ||
const virtual = makeVirtual(Container); | ||
if (index > -1) { | ||
this.listeners.splice(index, 1); | ||
} | ||
}; | ||
this.listeners.push(detail.callback); | ||
event.stopPropagation(); | ||
} | ||
}; | ||
this.addEventListener(contextEvent, this.eventHandler); | ||
} | ||
disconnectedCallback() { | ||
this.removeEventListener(contextEvent, this.eventHandler); | ||
} | ||
set value(value) { | ||
this._value = value; | ||
this.listeners.forEach(callback => callback(value)); | ||
} | ||
get value() { | ||
return this._value; | ||
} | ||
}; | ||
Context.Consumer = component(function ({ render: render$$1 }) { | ||
const context = useContext(Context); | ||
return render$$1(context); | ||
}); | ||
Context.defaultValue = defaultValue; | ||
return Context; | ||
}; | ||
export { component, useCallback, useEffect, useState, useReducer, useMemo, withHooks, withHooks as virtual, useContext, createContext, hook, Hook }; | ||
export default haunted; | ||
export { component, createContext, virtual, useCallback, useEffect, useState, useReducer, useMemo, useContext, useRef, hook, Hook }; |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
57139
25
1516
481
3