UNPKG

@furystack/shades

Version:

Google Authentication Provider for FuryStack

197 lines 9.07 kB
import { hasInjectorReference, Injector } from '@furystack/inject'; import { ObservableValue } from '@furystack/utils'; import { LocationService } from './services/location-service.js'; import { ResourceManager } from './services/resource-manager.js'; import { attachProps, attachStyles } from './shade-component.js'; /** * Factory method for creating Shade components * @param o The options object for component creation * @returns the JSX element */ export const Shade = (o) => { // register shadow-dom element const customElementName = o.shadowDomName; const existing = customElements.get(customElementName); if (!existing) { const ElementBase = o.elementBase || HTMLElement; customElements.define(customElementName, class extends ElementBase { _renderCount = 0; /** * @returns the current render count */ getRenderCount() { return this._renderCount; } resourceManager = new ResourceManager(); connectedCallback() { o.onAttach?.(this.getRenderOptions()); this.callConstructed(); } async disconnectedCallback() { o.onDetach?.(this.getRenderOptions()); await this.resourceManager[Symbol.asyncDispose](); this.cleanup?.(); } /** * Will be triggered when updating the external props object */ props; /** * Will be updated when on children change */ shadeChildren; /** * @param options Options for rendering the component * @returns the JSX element */ render = (options) => { this._renderCount++; return o.render(options); }; /** * @returns values for the current render options */ getRenderOptions = () => { const renderOptions = { props: this.props, injector: this.injector, children: this.shadeChildren, element: this, renderCount: this._renderCount, useObservable: (key, obesrvable, options) => { const onChange = options?.onChange || (() => this.updateComponent()); return this.resourceManager.useObservable(key, obesrvable, onChange, options); }, useState: (key, initialValue) => this.resourceManager.useState(key, initialValue, this.updateComponent.bind(this)), useSearchState: (key, initialValue) => this.resourceManager.useObservable(`useSearchState-${key}`, this.injector.getInstance(LocationService).useSearchParam(key, initialValue), () => this.updateComponent()), useStoredState: (key, initialValue, storageArea = localStorage) => { const getFromStorage = () => { const value = storageArea?.getItem(key); return value ? JSON.parse(value) : initialValue; }; const setToStorage = (value) => { if (JSON.stringify(value) !== storageArea?.getItem(key)) { const newValue = JSON.stringify(value); storageArea?.setItem(key, newValue); } if (JSON.stringify(observable.getValue()) !== JSON.stringify(value)) { observable.setValue(value); } }; const observable = this.resourceManager.useDisposable(`useStoredState-${key}`, () => new ObservableValue(getFromStorage())); const updateFromStorageEvent = (e) => { if (e.key === key && e.storageArea === storageArea) { setToStorage((e.newValue && JSON.parse(e.newValue)) || initialValue); } }; this.resourceManager.useDisposable(`useStoredState-${key}-storage-event`, () => { window.addEventListener('storage', updateFromStorageEvent); const channelName = `useStoredState-broadcast-channel`; const messageChannel = new BroadcastChannel(channelName); messageChannel.onmessage = (e) => { if (e.data.key === key) { setToStorage(e.data.value); } }; const subscription = observable.subscribe((value) => { messageChannel.postMessage({ key, value }); }); return { [Symbol.dispose]: () => { window.removeEventListener('storage', updateFromStorageEvent); subscription[Symbol.dispose](); messageChannel.close(); }, }; }); observable.subscribe(setToStorage); return this.resourceManager.useObservable(`useStoredState-${key}`, observable, () => this.updateComponent()); }, useDisposable: this.resourceManager.useDisposable.bind(this.resourceManager), }; return renderOptions; }; /** * Updates the component in the DOM. */ updateComponent() { const renderResult = this.render(this.getRenderOptions()); if (renderResult === null || renderResult === undefined) { this.innerHTML = ''; } if (typeof renderResult === 'string' || typeof renderResult === 'number') { this.innerHTML = renderResult; } if (renderResult instanceof HTMLElement) { this.replaceChildren(renderResult); } if (renderResult instanceof DocumentFragment) { this.replaceChildren(renderResult); } } /** * Finalize the component initialization after it gets the Props. Called by the framework internally */ callConstructed() { this.updateComponent(); const cleanupResult = o.constructed && o.constructed(this.getRenderOptions()); if (cleanupResult instanceof Promise) { cleanupResult .then((cleanup) => (this.cleanup = cleanup)) .catch(() => { /** */ }); } else { // construct is not async this.cleanup = cleanupResult; } } cleanup = undefined; _injector; getInjectorFromParent() { let parent = this.parentElement; while (parent) { if (parent.injector) { return parent.injector; } parent = parent.parentElement; } } get injector() { if (this._injector) { return this._injector; } const fromProps = hasInjectorReference(this.props) && this.props.injector; if (fromProps && fromProps instanceof Injector) { return fromProps; } const fromParent = this.getInjectorFromParent(); if (fromParent) { this._injector = fromParent; return fromParent; } // Injector not set explicitly and not found on parents! return new Injector(); } set injector(i) { this._injector = i; } }, o.elementBaseName ? { extends: o.elementBaseName } : undefined); } else { throw Error(`A custom shade with shadow DOM name '${o.shadowDomName}' has already been registered!`); } return (props, children) => { const ElementType = customElements.get(customElementName); const el = new ElementType({ ...props, }); el.props = props || {}; el.shadeChildren = children; attachStyles(el, { style: o.style }); attachProps(el, props); return el; }; }; //# sourceMappingURL=shade.js.map