@furystack/shades
Version:
Google Authentication Provider for FuryStack
197 lines • 9.05 kB
JavaScript
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 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);
}
}
/**
* Finialize 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