diff --git a/src/application.d.ts b/src/application.d.ts index 2b14c5ed..d2635907 100644 --- a/src/application.d.ts +++ b/src/application.d.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {default as Component, ComponentInstance} from './component' +import {default as Component} from './component' declare namespace Application { @@ -84,10 +84,6 @@ declare namespace Application { politeness?: 'off' | 'polite' | 'assertive' } - function Application( - config: Application.ApplicationConfig - ) : Application.ApplicationInstance - type RequireAtLeastOne = { [K in keyof T]-?: Required> & Partial>> }[keyof T] @@ -104,7 +100,7 @@ declare namespace Application { /** * Component to load when activating the route */ - component: typeof ComponentInstance, + component: Component, /** * Transition configuration for the route */ @@ -142,9 +138,9 @@ declare namespace Application { } } - export interface ApplicationInstance extends ComponentInstance {} + export interface ApplicationInstance extends Component.ComponentInstance {} - export interface ApplicationConfig extends Component.ComponentConfig { + export interface ApplicationConfig extends Component.ComponentConfig { /** * Routes definition * @@ -167,8 +163,12 @@ declare namespace Application { * * Root App component */ -declare function Application( - config: Application.ApplicationConfig +declare function Application< + Props extends string, + S extends Component.State, + M extends Component.Methods + >( + config: Application.ApplicationConfig ) : Application.ApplicationInstance export default Application; diff --git a/src/component.d.ts b/src/component.d.ts index d5e46f35..1701c1ae 100644 --- a/src/component.d.ts +++ b/src/component.d.ts @@ -22,6 +22,47 @@ declare namespace Component { */ type Name = string + + interface Announcer { + /** + * Announce a message to screen readers by specifying the politeness level + */ + speak: (message: string, politeness?: 'polite' | 'assertive') => void + + /** + * Announce a message to screen readers with a polite politeness level + */ + polite: (message: string) => void + + /** + * Announce a message to screen readers with an assertive politeness level + */ + assertive: (message: string) => void + + /** + * Stop all announcements + */ + stop: () => void + } + + interface Log { + /** + * Log an info message + */ + info(...args): typeof console.info + /** + * Log an error message + */ + error(...args): typeof console.error + /** + * Log a warning + */ + warn(...args): typeof console.warn + /** + * Log a debug message + */ + debug(...args): typeof console.debug + } interface AdvancedProp { /** * Name of the prop @@ -48,115 +89,175 @@ declare namespace Component { */ cast?: () => any } + type PropsArray = T[] + type AdvancedProps = AdvancedProp[] - type Props = string | AdvancedProp + // type ExtractPropNames = T extends (infer U)[] + // ? U extends string + // ? U + // : U extends AdvancedProp + // ? U['key'] + // : never + // : never; type NotFunction = T extends Function ? never : T; /** * Internal state of a Component instance */ - interface StateObject { + interface State { [key: string]: any } - interface Computed { - [key: string]: (this: ComponentInstance) => any + interface Computed { + [key: string]: (this: ComponentInstance & S & M & P) => any } - interface Watch { - [key: string]: (this: ComponentInstance, value: any, oldValue: any) => void + interface Watch { + [key: string]: (this: ComponentInstance & S & M & P, value: any, oldValue: any) => void } interface Methods { - [key: string]: (this: ComponentInstance) => any + [key: string]: (this: ComponentInstance & State & Methods, ...args: any) => any; } - interface Input { - [key: string]: (this: ComponentInstance, event: KeyboardEvent) => void, + // interface MethodsExtended extends Methods { + // [key: string]: (this: ComponentInstance & S & M & P) => any; + // } + + interface Input { + [key: string]: ((this: ComponentInstance & S & M & P, event: KeyboardEvent) => void) | undefined, + /** * Catch all input function * * Will be invoked when there is no dedicated function for a certain key - */ - 'any'?: (this: ComponentInstance, event: KeyboardEvent) => void, + */ + any?: (this: ComponentInstance & S & M & P, event: KeyboardEvent) => void, } interface Log { /** * Log an info message */ - info: typeof console.info + info(...args): typeof console.info /** * Log an error message */ - error: typeof console.error + error(...args): typeof console.error /** * Log a warning */ - warn: typeof console.warn + warn(...args): typeof console.warn /** * Log a debug message */ - debug: typeof console.debug + debug(...args): typeof console.debug } - interface Hooks { + interface Hooks { /** * Fires when the Component is being instantiated. * At this moment child elements will not be available yet */ - init?: (this: ComponentInstance) => void + init?: (this: ComponentInstance & S & M & P, ...args: any) => void /** * Fires when the Component is fully initialized and ready for interaction. */ - ready?: (this: ComponentInstance) => void + ready?: (this: ComponentInstance & S & M & P, ...args: any) => void /** * Triggers when the Component receives focus. * * This event can fire multiple times during the component's lifecycle */ - focus?: (this: ComponentInstance) => void + focus?: (this: ComponentInstance & S & M & P, ...args: any) => void /** * Triggers when the Component loses focus. * * This event can fire multiple times during the component's lifecycle */ - unfocus?: (this: ComponentInstance) => void + unfocus?: (this: ComponentInstance & S & M & P, ...args: any) => void /** * Fires when the Component is being destroyed and removed. */ - destroy?: (this: ComponentInstance) => void, + destroy?: (this: ComponentInstance & S & M & P, ...args: any) => void, /** * Fires upon each frame start (allowing you to tap directly into the renderloop) * * Note: This hook will fire continuously, multiple times per second! */ - frameTick?: (this: ComponentInstance) => void, + frameTick?: (this: ComponentInstance & S & M & P, ...args: any) => void, /** * Fires when the component enters the viewport _margin_ and is attached to the render tree * * This event can fire multiple times during the component's lifecycle */ - attach?: (this: ComponentInstance) => void, + attach?: (this: ComponentInstance & S & M & P, ...args: any) => void, /** * Fires when the component leaves the viewport _margin_ and is detached from the render tree * * This event can fire multiple times during the component's lifecycle */ - detach?: (this: ComponentInstance) => void, + detach?: (this: ComponentInstance & S & M & P, ...args: any) => void, /** * Fires when the component enters the visible viewport * * This event can fire multiple times during the component's lifecycle */ - enter?: (this: ComponentInstance) => void, + enter?: (this: ComponentInstance & S & M & P, ...args: any) => void, /** * Fires when the component leaves the visible viewport * * This event can fire multiple times during the component's lifecycle */ - exit?: (this: ComponentInstance) => void, + exit?: (this: ComponentInstance & S & M & P, ...args: any) => void, + } + + /** + * Route object + */ + interface Route { + path: string, + params: Record, + options?: { + inHistory?: boolean, + keepAlive?: boolean, + [key: string]: any, + }, + data?: Record, + component?: (args: { props: any }, holder: any, context: any) => Promise, + hooks?: { + before?: (route: Route, previousRoute: Route) => Promise, + } + } + + + interface Router { + /** + * Navigate to a different location + */ + to(location: string, data?: Record, options?: Record): void; + + /** + * Navigate to the previous location + */ + back(): boolean; + + /** + * Get the current route read-only + */ + readonly currentRoute: Route; + + /** + * Get the list of all routes + */ + readonly routes: Route[]; + + /** + * Get navigating state + */ + readonly navigating: boolean; + } export interface ComponentInstance { @@ -217,15 +318,29 @@ declare namespace Component { */ select: (ref: string) => ComponentInstance | ElementInstance - // tmp - [key: string]: any + /** + * Announcer methods for screen reader support + */ + $announcer: Announcer + + /** + * Triggers a forced update on state variables. + */ + $trigger: (key: string) => void + trigger: (key: string) => void + + /** + * Router instance + */ + $router: Router + } export interface ElementInstance { focus?: () => void } - export interface ComponentConfig { + export interface ComponentConfig { components?: any, /** * XML-based template string of the Component @@ -261,7 +376,7 @@ declare namespace Component { * } * ``` */ - state?: (this: ComponentInstance) => StateObject, + state?: (this: { [K in Props]: any}) => S, /** * Allowed props to be passed into the Component by the parent * @@ -279,27 +394,27 @@ declare namespace Component { * }] * ``` */ - props?: Props[], + props?: PropsArray | AdvancedProp[], /** * Computed properties */ - computed?: Computed, + computed?: Computed, /** * Watchers for changes to state variables, props or computed properties */ - watch?: Watch, + watch?: Watch, /** * Hooking into Lifecycle events */ - hooks?: Hooks, + hooks?: Hooks, /** * Methods for abstracting more complex business logic into separate function */ - methods?: Methods, + methods?: M //MethodsExtended, /** * Tapping into user input */ - input?: Input + input?: Input } } @@ -307,9 +422,13 @@ declare namespace Component { /** * Blits.Component() */ -declare function Component( +declare function Component< + Props extends string, + S extends Component.State, + M extends Component.Methods + >( name: Component.Name, - config: Component.ComponentConfig, + config: Component.ComponentConfig ) : Component.ComponentInstance export default Component; diff --git a/src/component.js b/src/component.js index 3168b33d..1d80840b 100644 --- a/src/component.js +++ b/src/component.js @@ -45,6 +45,13 @@ const required = (name) => { throw new Error(`Parameter ${name} is required`) } +/** + * Component factory function + * @param {string} name - The name of the component + * @param {object} config - The configuration object for the component + * @returns {function} - A factory function that creates a new component instance + * + */ const Component = (name = required('name'), config = required('config')) => { const setupComponent = (parentComponent) => { // code generation