panel
Version:
Web Components with Virtual DOM: lightweight composable web apps
392 lines (318 loc) • 13.7 kB
TypeScript
// Type definitions for panel
// Project: panel
// Definitions by: Mixpanel (https://mixpanel.com)
import {VNode} from 'snabbdom';
import WebComponent from 'webcomponent';
export {h} from 'snabbdom';
export {jsx} from 'snabbdom-jsx-lite';
import {JsxVNode, JsxVNodeProps} from 'snabbdom-jsx-lite';
export class StateStore<State> {
constructor(options: {store?: StateStore<State>});
/** A readonly version of controller's state */
get state(): State;
/** Update the state by passing in a property bag */
update(props?: Partial<State>): void;
/**
* @internal Subscribe to state updates via a listener callback.
* Only use for rendering and debugging purposes
*/
subscribeUpdates(listener: (props: Partial<State>) => void): void;
/** @internal Unsubscribe the listener callback that was passed to subscribeUpdates */
unsubscribeUpdates(listener: (props: Partial<State>) => void): void;
}
export class StateController<State> {
constructor(options: {store?: StateStore<State>});
/** A readonly version of controller's state */
get state(): State;
/** An initial default property bag for the controller's state */
get defaultState(): State;
/** Update the state by passing in a property bag */
_update(props?: Partial<State>): void;
/**
* @internal Subscribe to state updates via a listener callback.
* panel component uses this to trigger dom update pipeline
* Only use for rendering and debugging purposes
*/
subscribeUpdates(listener: (props: Partial<State>) => void): void;
/** @internal Unsubscribe the listener callback that was passed to subscribeUpdates */
unsubscribeUpdates(listener: (props: Partial<State>) => void): void;
}
export interface PanelHelpers {
[]: any;
}
export interface PanelHooks<State, Params> {
/** Function called before an update is applied */
preUpdate?: (stateUpdate: Partial<State>, params?: Partial<Params>) => void;
/** Function called after an update is applied */
postUpdate?: (stateUpdate: Partial<State>, params?: Partial<Params>) => void;
[]: (params: any) => void;
}
// this type is not checked in the Component ContextRegistryT, the Component JS manually checks for these properties instead
export interface PanelLifecycleContext {
// optional callback that executes each time a component using this context is connected to the DOM
bindToComponent?(component: Component<any>): void;
// optional callback that executes each time a component using this context is disconnected from the DOM
unbindFromComponent?(component: Component<any>): void;
}
export interface ConfigOptions<StateT, AppStateT = unknown, ContextRegistryT = unknown, ParamT = unknown> {
/** Function transforming state object to virtual dom tree */
template(scope?: StateT): VNode;
params?: {[param in keyof ParamT]: InferType<ParamT[param]> | ParamType<ParamT[param]>};
/** Component-specific Shadow DOM stylesheet */
css?: string;
/** object to provide default value for params */
defaultParams?: Partial<ParamT>;
/** An initial default value for the component's state property */
defaultState?: StateT;
/** Default contexts for the component and its descendants to use if no context parent provides them */
defaultContexts?: Partial<ContextRegistryT>;
/** Names of contexts for the component to attach and depend upon */
contexts?: Array<keyof ContextRegistryT>;
/**
* A state object to share with nested descendant components. If not set, root component
* shares entire state object with all descendants. Only applicable to app root components.
*/
appState?: AppStateT;
/** Properties and functions injected automatically into template state object */
helpers?: PanelHelpers;
/** Extra rendering/lifecycle callbacks */
hooks?: PanelHooks<StateT, ParamT>;
/** Object mapping string route expressions to handler functions */
routes?: {[route: string]: Function};
/** Whether to apply updates to DOM immediately, instead of batching to one update per frame */
updateSync?: boolean;
/** Whether to use Shadow DOM */
useShadowDom?: boolean;
/** Defines the threshold at which 'slowRender' events will be dispatched, defaults to 20ms */
slowThreshold?: number;
}
export interface AttrSchema {
/** Type of the attribute. One of 'string' | 'number' | 'boolean' | 'json' */
type: string;
/** Default value if the attr is not defined */
default?: any;
/** Description of attribute, what it does e.t.c */
description?: string;
/** Possible values of an attribute. e.g ['primary', 'secondary'] */
enum?: string[];
/** When setAttribute is invoked, console.warn that attr is deprecated e.g 'use xyz instead' */
deprecatedMsg?: string;
/**
* For a type: `json` attr, the typescript type that corresponds to it.
* Can be used to auto-generate Attrs interface
*/
tsType?: string;
/**
* Explicitly require an attribute to be passed, useful when no default value can be inferred.
*/
required?: boolean;
}
export type AttrsSchema<T> = {
[]: string | AttrSchema;
};
export interface AnyAttrs {
[]: any;
}
type InferType<T> = T extends string
? StringConstructor
: T extends number
? NumberConstructor
: T extends boolean
? BooleanConstructor
: T extends unknown[]
? ArrayConstructor
: T extends Map<unknown, unknown>
? MapConstructor
: T extends Set<unknown>
? SetConstructor
: T extends (...args: any[]) => any
? FunctionConstructor
: ObjectConstructor;
interface ParamType<T> {
type: InferType<T>;
required?: boolean;
}
export class Component<
StateT,
AttrsT = AnyAttrs,
AppStateT = unknown,
AppT = unknown,
ContextRegistryT = unknown,
ParamT extends Record<string, any> = unknown
> extends WebComponent {
/** The first Panel Component ancestor in the DOM tree; null if this component is the root */
$panelParent: Component<unknown>;
/**
* Attributes schema that defines the component's html attributes and their types
* Panel auto parses attribute changes into this.attrs object and $attrs template helper
*/
static get attrsSchema(): {[attr: string]: string | AttrSchema};
/** New panel params */
params: Readonly<ParamT>;
/** A reference to the top-level component */
app: AppT;
/** State object to share with nested descendant components */
appState: AppStateT;
/** Refers to the outer-most element in the template file for shadow DOM components. Otherwise, el refers to the component itself. */
el: HTMLElement;
/** A flag that represents whether the component is currently connected and initialized */
initialized: boolean;
/** Defines the state of the component, including all the properties required for rendering */
state: StateT;
readonly timings: Readonly<{
/** The time in ms that the component constructor ran */
createdAt: number;
/** The time in ms that component initialization started (also see 'initializingCompletedAt') */
initializingStartedAt: number;
/** The time in ms that component initialization completed (also see 'initializingStartedAt') */
initializingCompletedAt: number;
/** The time in ms that the last #attributeChangedCallback ran */
lastAttributeChangedAt: number;
/** The time in ms that the last #update ran */
lastUpdateAt: number;
/** The time in ms that the last render ran */
lastRenderAt: number;
}>;
/** Applies the static stylesheet for this component class */
applyStaticStyle(styleSheetText: null | string, options?: {ignoreCache: boolean}): void;
/** Defines standard component configuration */
get config(): ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>;
/**
* Template helper functions defined in config object, and exposed to template code as $helpers.
* This getter uses the component's internal config cache.
*/
get helpers(): this['config']['helpers'];
/** Gets the attribute value. Throws an error if attr not defined in attrsSchema */
attr<A extends keyof AttrsT>(attr: A): AttrsT[A];
/** Attributes parsed from component's html attributes using attrsSchema */
attrs(): AttrsT;
/**
* For use inside view templates, to create a child Panel component nested under this
* component, which will share its state object and update cycle.
*/
child<T = object>(tagName: string, config?: T): VNode;
/**
* Searches the component's Panel ancestors for the first component of the
* given type (HTML tag name).
*/
findPanelParentByTagName(tagName: string): Component<any>;
/**
* Fetches a value from the component's configuration map (a combination of
* values supplied in the config() getter and defaults applied automatically).
*/
getConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>(key: K): this['config'][K];
/** Sets a value in the component's configuration map after element initialization */
setConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>(
key: K,
val: ConfigOptions<StateT, AppStateT, ContextRegistryT>[K],
): void;
/**
* set the params for the this component
* triggers a component update
* if shouldComponentUpdate callback returns true
*/
setParams(params: Partial<ParamT>): void;
/**
* Same API as react's `shouldComponentUpdate` usage
* if child component implements this method, parent implmentation wil be discarded
* only difference is the `params` or `state` could sometimes be null indicating that
* the update is not related to `params` or `state`
*
* To be overridden by subclasses, defining conditional logic for whether
* a component should rerender its template given the state and params to be applied.
* In most cases this method can be left untouched, but can provide improved
* performance when dealing with very many DOM elements.
*
* @example
* shouldComponentUpdate(params, state) {
* // don't need to rerender if result set ID hasn't changed
* if (state && state.largeResultSetID === this._cachedResultID) {
* return false
* }
* if (params && params.bookmark.id === this.params.bookmark.id) {
* return false;
* }
* return !shallowEqual(params, this.params);
* }
*/
shouldComponentUpdate(params: ParamT | null, state: StateT | null): boolean;
/**
* To be overridden by subclasses, defining conditional logic for whether
* a component should rerender its template given the state and params to be applied.
* In most cases this method can be left untouched, but can provide improved
* performance when dealing with very many DOM elements.
*
* @deprecated use shouldComponentUpdate instead
*/
shouldUpdate(state: StateT): boolean;
/**
* Executes the route handler matching the given URL fragment, and updates
* the URL, as though the user had navigated explicitly to that address.
*/
navigate(fragment: string, stateUpdate?: Partial<StateT>): void;
/** Run a user-defined hook with the given parameters */
runHook: (
hookName: keyof ConfigOptions<StateT, AppStateT, ContextRegistryT>['hooks'],
options: {cascade: boolean; exclude: Component<any, any>},
params: any,
) => void;
/**
* Applies a state update specifically to app state shared across components.
* In apps which don't specify `appState` in the root component config, all
* state is shared across all parent and child components and the standard
* update() method should be used instead.
*/
updateApp(stateUpdate?: Partial<AppStateT>): void;
/**
* Applies a state update, triggering a re-render check of the component as
* well as any other components sharing the same state. This is the primary
* means of updating the DOM in a Panel application.
*/
update(stateUpdate?: Partial<StateT> | ((state: StateT) => Partial<StateT>)): void;
/**
* Helper function which will queue a function to be run once the component has been
* initialized and added to the DOM. If the component has already had its connectedCallback
* run, the function will run immediately.
*
* It can optionally return a function to be enqueued to be run just before the component is
* removed from the DOM. This occurs during the disconnectedCallback lifecycle.
*/
onConnected(callback: () => void | (() => void)): void;
/**
* Helper function which will queue a function to be run just before the component is
* removed from the DOM. This occurs during the disconnectedCallback lifecycle.
*/
onDisconnected(callback: () => void): void;
/**
* Returns the default context of the highest (ie. closest to the document root) ancestor component
* that has configured a default context for the context name,
* or it will return the component's own default context if no ancestor context was found.
*/
getContext<ContextKey extends keyof ContextRegistryT>(contextName: ContextKey): ContextRegistryT[ContextKey];
}
/**
* Panel component that only accepts 3 generic types
*/
export class ParamComponent<ParamT = unknown, StateT = unknown, ContextRegistryT = unknown> extends Component<
StateT,
unknown,
unknown,
unknown,
ContextRegistryT,
ParamT
> {}
// define jsx IntrinsicElement inside namespace jsx to play well with react
declare global {
/**
* opt-in jsx intrinsic global interfaces
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
*/
namespace jsx {
namespace JSX {
type Element = JsxVNode;
interface IntrinsicElements {
[]: JsxVNodeProps;
}
}
}
}