@rws-framework/client
Version:
This package provides the core client-side framework for Realtime Web Suit (RWS), enabling modular, asynchronous web components, state management, and integration with backend services. It is located in `.dev/client`.
348 lines (282 loc) • 14 kB
text/typescript
import { ViewTemplate, ElementStyles, observable, html, Constructable, PartialFASTElementDefinition, attr, Observable } from '@microsoft/fast-element';
import { FoundationElement, FoundationElementDefinition, FoundationElementRegistry, OverrideFoundationElementDefinition } from '../../foundation/rws-foundation-element';
import ConfigService, { ConfigServiceInstance } from '../services/ConfigService';
import UtilsService, { UtilsServiceInstance } from '../services/UtilsService';
import DOMService, { DOMServiceInstance, DOMOutputType } from '../services/DOMService';
import ApiService, { ApiServiceInstance } from '../services/ApiService';
import NotifyService, { NotifyServiceInstance } from '../services/NotifyService';
import IndexedDBService, { IndexedDBServiceInstance } from '../services/IndexedDBService';
import { IRWSViewComponent, IAssetShowOptions } from '../types/IRWSViewComponent';
import RWSWindow, { RWSWindowComponentInterface, loadRWSRichWindow } from '../types/RWSWindow';
import { applyConstructor, RWSInject } from './_decorator';
import TheRWSService from '../services/_service';
import { handleExternalChange } from './_attrs/_external_handler';
import { IFastDefinition, isDefined, defineComponent, getDefinition } from './_definitions';
import { on, $emitDown, observe, sendEventToOutside } from './_event_handling';
import { domEvents } from '../events';
import CSSInjectionManager, { CSSInjectMode, ICSSInjectionOptions } from './_css_injection';
type ComposeMethodType<
T extends FoundationElementDefinition,
K extends Constructable<RWSViewComponent>
> = (this: K, elementDefinition: T) => (overrideDefinition?: OverrideFoundationElementDefinition<T>) => FoundationElementRegistry<FoundationElementDefinition, T>;
const _DEFAULT_INJECT_CSS_CACHE_LIMIT_DAYS = 1;
export interface IWithCompose<T extends RWSViewComponent> {
[key: string]: any
new(...args: any[]): T;
definition?: IFastDefinition
defineComponent: <T extends RWSViewComponent>(this: IWithCompose<T>) => void
isDefined<T extends RWSViewComponent>(this: IWithCompose<T>): boolean
compose: ComposeMethodType<FoundationElementDefinition, Constructable<T>>;
define<TType extends (...params: any[]) => any>(type: TType, nameOrDef?: string | PartialFASTElementDefinition | undefined): TType;
_verbose: boolean;
_toInject: { [key: string]: TheRWSService };
_depKeys: { [key: string]: string[] };
_externalAttrs: { [key: string]: string[] };
setExternalAttr: (componentName: string, key: string) => void
sendEventToOutside: <T>(eventName: string, data: T) => void
_EVENTS: {
component_define: string,
component_parted_load: string,
}
}
abstract class RWSViewComponent extends FoundationElement implements IRWSViewComponent {
__isLoading: boolean = true;
__exAttrLoaded: string[] = [];
private static instances: RWSViewComponent[] = [];
static fileList: string[] = [];
routeParams: Record<string, string> = {};
static autoLoadFastElement = true;
static _defined: { [key: string]: boolean } = {};
static _toInject: { [key: string]: TheRWSService } = {};
static _depKeys: { [key: string]: string[] } = { _all: [] };
static _externalAttrs: { [key: string]: string[] } = {};
static _verbose: boolean = false;
static FORCE_INJECT_STYLES?: string[] = [];
static FORCE_INJECT_MODE?: CSSInjectMode = 'adopted';
static FORCE_INJECT_MODE_PER_LINK?: Record<string, CSSInjectMode> = {};
static FORCE_PRELOADED_STYLES?: string[] = [];
static _EVENTS = {
component_define: 'rws:lifecycle:defineComponent',
component_parted_load: 'rws:lifecycle:loadPartedComponents',
}
(IndexedDBService, true) protected indexedDBService: IndexedDBServiceInstance;
(ConfigService, true) protected config: ConfigServiceInstance;
(DOMService, true) protected domService: DOMServiceInstance;
(UtilsService, true) protected utilsService: UtilsServiceInstance;
(ApiService, true) protected apiService: ApiServiceInstance;
(NotifyService, true) protected notifyService: NotifyServiceInstance;
trashIterator: number = 0;
fileAssets: {
[key: string]: ViewTemplate
} = {};
constructor() {
super();
applyConstructor(this);
}
connectedCallback() {
super.connectedCallback();
applyConstructor(this);
if (!(this.constructor as IWithCompose<this>).definition && (this.constructor as IWithCompose<this>).autoLoadFastElement) {
throw new Error('RWS component is not named. Add `static definition = {name, template};`');
}
this.applyFileList();
const rwsStyleLinks = (window as any).RWS?.styleLinks as Set<string> | undefined;
if (rwsStyleLinks) {
for (const link of rwsStyleLinks) {
if (!(RWSViewComponent.FORCE_INJECT_STYLES ?? []).includes(link)) {
RWSViewComponent.FORCE_INJECT_STYLES = [...(RWSViewComponent.FORCE_INJECT_STYLES ?? []), link];
}
}
}
const toInject = [
...(RWSViewComponent.FORCE_INJECT_STYLES ?? []),
...(RWSViewComponent.FORCE_PRELOADED_STYLES ?? [])
];
if (toInject.length > 0) {
this.injectStyles(toInject, RWSViewComponent.FORCE_INJECT_MODE);
}
RWSViewComponent.instances.push(this);
}
passRouteParams(routeParams: Record<string, string> = null) {
if (routeParams) {
this.routeParams = routeParams;
}
}
showAsset(assetName: string, options: IAssetShowOptions = {}): ViewTemplate<any, any> {
if (!this.fileAssets[assetName]) {
return html`<span></span>`;
throw new Error(`File asset "${assetName}" not declared in component "${(this.constructor as IWithCompose<this>).definition.name}"`);
}
return this.fileAssets[assetName];
}
on<T>(type: string, listener: (event: CustomEvent<T>) => any) {
return on.bind(this)(type, listener);
}
$emitDown<T>(eventName: string, payload: T) {
return $emitDown.bind(this)(eventName, payload);
}
observe(callback: (component: RWSViewComponent, node: Node, observer: MutationObserver) => Promise<void>, condition: (component: RWSViewComponent, node: Node) => boolean = null, observeRemoved: boolean = false) {
return observe.bind(this)(callback, condition, observeRemoved);
}
parse$<T extends Element>(input: NodeListOf<T>, directReturn: boolean = false): DOMOutputType<T> {
return this.domService.parse$<T>(input, directReturn);
}
$<T extends Element>(selectors: string, directReturn: boolean = false): DOMOutputType<T> {
return this.domService.$<T>(this.getShadowRoot(), selectors, directReturn);
}
async loadingString<T, C>(item: T, addContent: (cnt: C | { output: string }, paste?: boolean, error?: boolean) => void, shouldStop: (stopItem: T, addContent: (cnt: C | { output: string }, paste?: boolean, error?: boolean) => void) => Promise<boolean>) {
let dots = 1;
const maxDots = 3; // Maximum number of dots
const interval = setInterval(async () => {
const dotsString = '. '.repeat(dots);
const doesItStop = await shouldStop(item, addContent);
if (doesItStop) {
addContent({ output: '' }, true);
clearInterval(interval);
} else {
addContent({ output: `${dotsString}` }, true);
dots = (dots % (maxDots)) + 1;
}
}, 500);
}
async onDOMLoad(): Promise<void> {
return new Promise<void>((resolve) => {
if (this.getShadowRoot() !== null && this.getShadowRoot() !== undefined) {
resolve();
} else {
// If shadowRoot is not yet available, use MutationObserver to wait for it
const observer = new MutationObserver(() => {
if (this.getShadowRoot() !== null && this.getShadowRoot() !== undefined) {
observer.disconnect();
resolve();
}
});
observer.observe(this, { childList: true, subtree: true });
}
});
}
protected getShadowRoot(): ShadowRoot {
const shRoot: ShadowRoot | null = this.shadowRoot;
if (!shRoot) {
throw new Error(`Component ${(this.constructor as IWithCompose<this>).definition.name} lacks shadow root. If you wish to have component without shadow root extend your class with FASTElement`);
}
return shRoot;
}
forceReload() {
this.trashIterator += 1;
}
hotReplacedCallback() {
this.forceReload();
}
sendEventToOutside<T>(eventName: string, data: T) {
sendEventToOutside(eventName, data);
}
static sendEventToOutside<T>(eventName: string, data: T) {
sendEventToOutside(eventName, data);
}
static injectStyles(linkedStyles: string[], mode?: CSSInjectMode) {
if (mode) {
RWSViewComponent.FORCE_INJECT_MODE = mode;
}
const existing = RWSViewComponent.FORCE_INJECT_STYLES ?? [];
const toAdd = linkedStyles.filter(l => !existing.includes(l));
RWSViewComponent.FORCE_INJECT_STYLES = [...existing, ...toAdd];
}
private applyFileList(): void {
try {
(this.constructor as IWithCompose<this>).fileList.forEach((file: string) => {
if (this.fileAssets[file]) {
return;
}
this.apiService.pureGet(this.config.get('pubUrlFilePrefix') + file).then((response: string) => {
this.fileAssets = { ...this.fileAssets, [file]: html`${response}` };
});
});
} catch (e: Error | any) {
console.error('Error loading file content:', e.message);
console.error(e.stack);
}
}
static setExternalAttr(componentName: string, key: string) {
if (!Object.keys(RWSViewComponent._externalAttrs).includes(componentName)) {
RWSViewComponent._externalAttrs[componentName] = [];
}
RWSViewComponent._externalAttrs[componentName].push(key);
}
static hotReplacedCallback() {
this.getInstances().forEach(instance => instance.forceReload());
}
static isDefined<T extends RWSViewComponent>(this: IWithCompose<T>): boolean {
return isDefined<T>(this);
}
static defineComponent<T extends RWSViewComponent>(this: IWithCompose<T>): void {
return defineComponent<T>(this);
}
static getDefinition(tagName: string, htmlTemplate: ViewTemplate, styles: ElementStyles = null) {
return getDefinition(tagName, htmlTemplate, styles);
}
private static getInstances(): RWSViewComponent[] {
return RWSViewComponent.instances;
}
static getCachedStyles(styleLinks: string[]): CSSStyleSheet[] {
return CSSInjectionManager.getCachedStyles(styleLinks);
}
static hasCachedStyles(styleLinks: string[]): boolean {
return CSSInjectionManager.hasCachedStyles(styleLinks);
}
static getStylesOwnerComponent(): any {
return CSSInjectionManager.getStylesOwnerComponent();
}
static clearCachedStyles(): void {
CSSInjectionManager.clearCachedStyles();
}
protected async injectStyles(styleLinks: string[], mode: CSSInjectMode = 'adopted', maxDaysExp?: number) {
// Create a bridge object that exposes the necessary properties
const componentBridge = {
componentElement: this,
shadowRoot: this.shadowRoot,
indexedDBService: this.indexedDBService,
$emit: this.$emit.bind(this)
};
return CSSInjectionManager.injectStyles(componentBridge, styleLinks, { mode, maxDaysExp });
}
protected getInjectedStyles(styleLinks: string[]): CSSStyleSheet[] {
return CSSInjectionManager.getCachedStyles(styleLinks);
}
protected hasInjectedStyles(styleLinks: string[]): boolean {
return CSSInjectionManager.hasCachedStyles(styleLinks);
}
static injectAdoptedCSSText(cssText: string, styleName: string) {
return CSSInjectionManager.injectAdoptedCSSText(cssText, styleName);
}
static async preloadCSSText(cssText: string, styleName: string) {
RWSViewComponent.FORCE_PRELOADED_STYLES = [...(RWSViewComponent.FORCE_PRELOADED_STYLES ?? []), styleName];
return CSSInjectionManager.preloadCSSText(cssText, styleName);
}
/**
* Register link URL(s) to be injected into this component AND all future child components.
* Call from outside on the element instance: `element.addLinkedStyles([...])`
*/
async addLinkedStyles(styleLinks: string[], mode?: CSSInjectMode): Promise<void> {
RWSViewComponent.injectStyles([
...(RWSViewComponent.FORCE_INJECT_STYLES ?? []),
...styleLinks
], mode);
await this.injectStyles(styleLinks, mode ?? RWSViewComponent.FORCE_INJECT_MODE);
}
/**
* Preload a CSS text string, cache it under `styleName`, and inject into this component.
* All future child components will also receive it via FORCE_PRELOADED_STYLES.
* Call from outside on the element instance: `element.addPreloadedCSS(cssText, 'my-key')`
*/
async addPreloadedCSS(cssText: string, styleName: string): Promise<void> {
await RWSViewComponent.preloadCSSText(cssText, styleName);
await this.injectStyles([styleName], RWSViewComponent.FORCE_INJECT_MODE);
}
}
export default RWSViewComponent;
export { RWSViewComponent };
export type {
IAssetShowOptions,
IRWSViewComponent
} from '../types/IRWSViewComponent';