UNPKG

perlite

Version:

[![perlite logo](/docs/logo.svg)]()

168 lines (145 loc) 5.2 kB
import hr from 'hyperactiv'; import { render, nothing } from 'lit-html'; import { attrToVal, camelCase, kebabCase, noop, unrender } from './utils'; import type * as Type from './types'; const { observe, computed, dispose } = hr; export * from './utils'; export * from './types'; export * from './directives'; export * from 'lit-html'; export { observe, computed, dispose }; export const $ = ( { render: template = () => nothing, state: data = {}, target = document.body, ...options }: Type.Config, ...context ): Type.Widget => { const model: {} = (typeof data === 'function') ? data(...context) : data; Object.entries(target.dataset).forEach(([key, value]) => { if (key in model) model[key] = attrToVal(value); }); const state: ProxyConstructor = observe(model, { batch: true, deep: true, bind: true, ...options }); const emit = (type: string, detail: object, { bubbles = false, cancelable = true } = {}) => { target.dispatchEvent( new CustomEvent(type, { detail, bubbles, cancelable }) ); }; let mounted = false; const rerender = () => { render(template(state, emit, ...context), target); if (!mounted) { emit('mount', model); mounted = true; } emit('update', model); }; const renderer = computed(({ computeAsync }) => { if (mounted && !document.contains(target)) return destroy(); emit('state', model); return Promise.resolve() .then(() => computeAsync(rerender)) .catch(err => emit('error', err)); }); const events = new Set(); const on = (type: string, fn: (e: CustomEvent) => void, opts?: object | boolean) => { target.addEventListener(type, fn, opts); const off = () => { target.removeEventListener(type, fn, opts); return events.delete(off); }; events.add(off); return off; }; const effects = new Set(); const effect = (fn: () => void, opts?: object) => { const handle = computed(fn, opts); const cancel = () => { dispose(handle); return effects.delete(cancel); }; effects.add(cancel); return cancel; }; const observer = new MutationObserver((mutations: MutationRecord[]) => { mutations.forEach((mutation) => { if (mutation.type !== 'attributes') return; const el: Element = mutation.target as Element; const key = camelCase(mutation.attributeName.replace('data-', '')); if (!(key in state)) return; const value = el.getAttribute(mutation.attributeName); if (value !== mutation.oldValue) { const val = attrToVal(value); if (state[key] !== val) state[key] = val; } }); }); observer.observe(target, { attributeFilter: Object.entries(model).reduce((attrs, [key, val]) => { if (typeof val !== 'function') { attrs.push(`data-${kebabCase(key)}`); } return attrs; }, []), attributeOldValue: true, characterData: false, childList: false, subtree: false }); const destroy = (cb = noop) => { observer.disconnect(); dispose(renderer); effects.forEach((cancel: () => void) => cancel()); effects.clear(); emit('destroy', model); events.forEach((off: () => void) => off()); events.clear(); unrender(target); cb(model); }; const ctx = (fn: (...ctx: any[]) => any) => fn(...context); return { on, ctx, model, // plain state (object) state, // reactive state (proxy) effect, target, destroy, render: rerender, }; }; export const $$ = ({ target, ...config }: Type.Configs, ...context): Type.Widgets => { if (!(target as NodeList | Node[]).length) { target = [target] as Node[]; } const widgets = Array.prototype.map.call(target, (target: HTMLElement) => { return $({ ...config, target } as Type.Config, ...context); }); return { ...widgets, effect: (fn, opts): () => void => { const cancels = widgets.map((widget: Type.Widget) => widget.effect(fn(widget.state), opts)); return () => cancels.forEach(cancel => cancel()); }, on: (...args): () => void => { const offs = widgets.map((widget: Type.Widget) => widget.on(...args)); return () => offs.forEach(off => off()); }, destroy: (cb): void => widgets.forEach((widget: Type.Widget) => widget.destroy(cb)), render: (): void => widgets.forEach((widget: Type.Widget) => widget.render()), state: (fn: (state: ProxyConstructor) => void): void => { widgets.forEach((widget: Type.Widget) => fn(widget.state)) }, ctx: (fn: (...ctx: any[]) => any) => fn(...context), forEach: Array.prototype.forEach.bind(widgets), target, }; };