Skip to content

Commit

Permalink
Typed children for function-based widgets (#544)
Browse files Browse the repository at this point in the history
* initially typed children

* typed children

* more typed children

* Children cannot be defaulted with typed children

* Add test to demonstrate using typed children with tsx

* Change tsx property type from 'children' to '__children__' and only accept it being passed from .childre()

* remove unused import

* fix tests expecting defaulted children

* Fix interface

* address pull request feedback

* Further typed children changes

* Default children for widgets to empty array

* Only need to check length

* Change interface back

* default middleware result

* prettier
  • Loading branch information
agubler authored Oct 1, 2019
1 parent 99ac08c commit 47a39b4
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 60 deletions.
20 changes: 11 additions & 9 deletions src/core/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export interface RegistryInterface {
* @param widgetLabel The label of the widget to return
* @returns The RegistryItem for the widgetLabel, `null` if no entry exists
*/
get(label: RegistryLabel): WNodeFactory<any> | Callback<any, any, RenderResult> | Constructor<any> | null;
get(label: RegistryLabel): WNodeFactory<any> | Callback<any, any, any, RenderResult> | Constructor<any> | null;
get<T extends WNodeFactory<any>>(label: RegistryLabel): T | null;
get<T extends Callback<any, any, RenderResult>>(label: RegistryLabel): T | null;
get<T extends Callback<any, any, any, RenderResult>>(label: RegistryLabel): T | null;
get<T extends WidgetBaseInterface = WidgetBaseInterface>(label: RegistryLabel): Constructor<T> | null;

/**
Expand Down Expand Up @@ -99,7 +99,7 @@ export function isWidgetBaseConstructor<T extends WidgetBaseInterface = any>(ite
return Boolean(item && item._type === WIDGET_BASE_TYPE);
}

export function isWidgetFunction(item: any): item is Callback<any, any, RenderResult> {
export function isWidgetFunction(item: any): item is Callback<any, any, any, RenderResult> {
return Boolean(item && item.isWidget);
}

Expand All @@ -112,7 +112,7 @@ export function isWNodeFactory<W extends WidgetBaseTypes>(node: any): node is WN

export function isWidget<T extends WidgetBaseInterface = any>(
item: any
): item is Constructor<T> | Callback<any, any, RenderResult> {
): item is Constructor<T> | Callback<any, any, any, RenderResult> {
return isWidgetBaseConstructor(item) || isWidgetFunction(item);
}

Expand Down Expand Up @@ -199,21 +199,23 @@ export class Registry extends Evented<{}, RegistryLabel, RegistryEventObject> im
this.emitLoadedEvent(label, injectorItem);
}

public get(label: RegistryLabel): WNodeFactory<any> | Callback<any, any, RenderResult> | Constructor<any> | null;
public get(
label: RegistryLabel
): WNodeFactory<any> | Callback<any, any, any, RenderResult> | Constructor<any> | null;
public get<T extends WNodeFactory<any>>(label: RegistryLabel): T | null;
public get<T extends Callback<any, any, RenderResult>>(label: RegistryLabel): T | null;
public get<T extends Callback<any, any, any, RenderResult>>(label: RegistryLabel): T | null;
public get<T extends WidgetBaseInterface = WidgetBaseInterface>(label: RegistryLabel): Constructor<T> | null;
public get<T extends WidgetBaseInterface = WidgetBaseInterface>(
label: RegistryLabel
): WNodeFactory<T> | Callback<any, any, RenderResult> | Constructor<T> | null {
): WNodeFactory<T> | Callback<any, any, any, RenderResult> | Constructor<T> | null {
if (!this._widgetRegistry || !this.has(label)) {
return null;
}

const item = this._widgetRegistry.get(label);

if (isWidget<T>(item) || isWNodeFactory(item)) {
return item;
return item as any;
}

if (item instanceof Promise) {
Expand All @@ -224,7 +226,7 @@ export class Registry extends Evented<{}, RegistryLabel, RegistryEventObject> im
this._widgetRegistry.set(label, promise);

promise.then(
(widgetCtor) => {
(widgetCtor: any) => {
if (isWidgetConstructorDefaultExport<T>(widgetCtor)) {
widgetCtor = widgetCtor.default;
}
Expand Down
46 changes: 36 additions & 10 deletions src/core/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,30 +384,56 @@ export interface MiddlewareMap<

export type MiddlewareApiMap<U extends MiddlewareMap<any>> = { [P in keyof U]: ReturnType<U[P]>['api'] };

export interface Callback<Props, Middleware, ReturnValue> {
export interface Callback<Props, Children, Middleware, ReturnValue> {
(
options: {
id: string;
middleware: MiddlewareApiMap<Middleware>;
properties: () => Props;
children: () => DNode[];
children: () => Children extends any[] ? Children : [Children];
}
): ReturnValue;
}

export interface MiddlewareResult<Props, Middleware, ReturnValue> {
export interface MiddlewareResult<Props, Children, Middleware, ReturnValue> {
api: ReturnValue;
properties: Props;
callback: Callback<Props, Middleware, ReturnValue>;
callback: Callback<Props, Children, Middleware, ReturnValue>;
middlewares: Middleware;
}

export interface MiddlewareResultFactory<Props, Middleware, ReturnValue> {
(): MiddlewareResult<Props, Middleware, ReturnValue>;
export interface DefaultMiddlewareResult extends MiddlewareResult<any, any, any, any> {}

export interface MiddlewareResultFactory<Props, Children, Middleware, ReturnValue> {
(): MiddlewareResult<Props, Children, Middleware, ReturnValue>;
}

export interface WNodeFactory<W extends WidgetBaseTypes> {
(properties: W['properties'], children?: W['children']): WNode<W>;
export interface DefaultChildrenWNodeFactory<W extends WNodeFactoryTypes> {
(properties: W['properties'], children?: W['children'] extends any[] ? W['children'] : [W['children']]): WNode<W>;
new (): {
properties: W['properties'] & { __children__?: DNode | DNode[] };
};
properties: W['properties'];
children: W['children'];
}

export interface WNodeFactory<W extends WNodeFactoryTypes> {
(
properties: W['properties'],
children: W['children'] extends [any]
? W['children'][0][]
: W['children'] extends any[] ? W['children'] : [W['children']]
): WNode<W>;
new (): {
properties: W['properties'] & { __children__: W['children'] };
};
properties: W['properties'];
children: W['children'];
}

export interface WNodeFactoryTypes<P = any, C = any> {
readonly properties: P;
readonly children: C;
}

export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
Expand All @@ -421,7 +447,7 @@ export interface WNode<W extends WidgetBaseTypes = any> {
/**
* Constructor to create a widget or string constructor label
*/
widgetConstructor: Constructor<W> | RegistryLabel | LazyDefine<W> | Callback<any, any, RenderResult>;
widgetConstructor: Constructor<W> | RegistryLabel | LazyDefine<W> | Callback<any, any, any, RenderResult>;

/**
* Properties to set against a widget instance
Expand All @@ -431,7 +457,7 @@ export interface WNode<W extends WidgetBaseTypes = any> {
/**
* DNode children
*/
children: DNode[];
children: any[];

/**
* The type of node
Expand Down
121 changes: 107 additions & 14 deletions src/core/vdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
WidgetBaseTypes,
RegistryLabel,
DeferredVirtualProperties,
DomOptions
DomOptions,
DefaultChildrenWNodeFactory
} from './interfaces';
import { Registry, isWidget, isWidgetBaseConstructor, isWidgetFunction, isWNodeFactory } from './Registry';
import { auto } from './diff';
Expand All @@ -40,6 +41,9 @@ declare global {
interface IntrinsicElements {
[key: string]: VNodeProperties;
}
interface ElementChildrenAttribute {
__children__: {};
}
}
}

Expand All @@ -64,7 +68,7 @@ export interface WNodeWrapper extends BaseNodeWrapper {
instance?: any;
mergeNodes?: Node[];
nodeHandlerCalled?: boolean;
registryItem?: Callback<any, any, RenderResult> | Constructor<any> | null;
registryItem?: Callback<any, any, any, RenderResult> | Constructor<any> | null;
properties: any;
}

Expand Down Expand Up @@ -307,10 +311,20 @@ function updateAttributes(
export function w<W extends WidgetBaseTypes>(
node: WNode<W>,
properties: Partial<W['properties']>,
children?: W['children']
children?: W['properties'] extends { __children__: any } ? W['properties']['__children__'] : W['children']
): WNode<W>;
export function w<W extends WidgetBaseTypes>(
widgetConstructor: Constructor<W> | RegistryLabel | WNodeFactory<W> | LazyDefine<W>,
widgetConstructor: Constructor<W> | RegistryLabel | LazyDefine<W>,
properties: W['properties'],
children?: W['children']
): WNode<W>;
export function w<W extends WNodeFactory<any>>(
widgetConstructor: W,
properties: W['properties'],
children: W['children']
): WNode<W>;
export function w<W extends DefaultChildrenWNodeFactory<any>>(
widgetConstructor: W,
properties: W['properties'],
children?: W['children']
): WNode<W>;
Expand All @@ -321,10 +335,14 @@ export function w<W extends WidgetBaseTypes>(
| WNodeFactory<W>
| WNode<W>
| LazyDefine<W>
| Callback<any, any, RenderResult>,
| Callback<any, any, any, RenderResult>,
properties: W['properties'],
children?: W['children']
children?: any
): WNode<W> {
if ((properties as any).__children__) {
delete (properties as any).__children__;
}

if (isWNodeFactory<W>(widgetConstructorOrNode)) {
return widgetConstructorOrNode(properties, children);
}
Expand Down Expand Up @@ -641,30 +659,105 @@ function createFactory(callback: any, middlewares: any): any {
export function create<T extends MiddlewareMap, MiddlewareProps = ReturnType<T[keyof T]>['properties']>(
middlewares: T = {} as T
) {
function properties<Props extends {}>() {
function properties<Props>() {
function returns<ReturnValue>(
callback: Callback<WidgetProperties & Props & UnionToIntersection<MiddlewareProps>, T, ReturnValue>
callback: Callback<WidgetProperties & Props & UnionToIntersection<MiddlewareProps>, DNode[], T, ReturnValue>
): ReturnValue extends RenderResult
? WNodeFactory<{
? DefaultChildrenWNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: DNode[];
}>
: MiddlewareResultFactory<WidgetProperties & Props & UnionToIntersection<MiddlewareProps>, T, ReturnValue> {
: MiddlewareResultFactory<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
DNode[],
T,
ReturnValue
> {
return createFactory(callback, middlewares);
}

function children<Children>() {
function returns<ReturnValue>(
callback: Callback<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
>
): ReturnValue extends RenderResult
? WNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: Children;
}>
: MiddlewareResultFactory<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
> {
return createFactory(callback, middlewares);
}
return returns;
}
returns.children = children;
return returns;
}

function children<Children extends {}>() {
function properties<Props>() {
function returns<ReturnValue>(
callback: Callback<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
>
): ReturnValue extends RenderResult
? WNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: Children;
}>
: MiddlewareResultFactory<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
> {
return createFactory(callback, middlewares);
}
return returns;
}

function returns<ReturnValue>(
callback: Callback<WidgetProperties & UnionToIntersection<MiddlewareProps>, Children, T, ReturnValue>
): ReturnValue extends RenderResult
? WNodeFactory<{
properties: WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: Children;
}>
: MiddlewareResultFactory<
WidgetProperties & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
> {
return createFactory(callback, middlewares);
}
returns.properties = properties;
return returns;
}

function returns<ReturnValue>(
callback: Callback<WidgetProperties & UnionToIntersection<MiddlewareProps>, T, ReturnValue>
callback: Callback<WidgetProperties & UnionToIntersection<MiddlewareProps>, DNode[], T, ReturnValue>
): ReturnValue extends RenderResult
? WNodeFactory<{
? DefaultChildrenWNodeFactory<{
properties: WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: DNode[];
}>
: MiddlewareResultFactory<WidgetProperties & UnionToIntersection<MiddlewareProps>, T, ReturnValue> {
: MiddlewareResultFactory<WidgetProperties & UnionToIntersection<MiddlewareProps>, DNode[], T, ReturnValue> {
return createFactory(callback, middlewares);
}
returns.children = children;
returns.properties = properties;
return returns;
}
Expand Down Expand Up @@ -1269,7 +1362,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
function mount(mountOptions: Partial<MountOptions> = {}) {
_mountOptions = { ..._mountOptions, ...mountOptions };
const { domNode } = _mountOptions;
const renderResult = wrapNodes(renderer)({});
const renderResult = wrapNodes(renderer)({}, []);
const nextWrapper = {
id: `${wrapperId++}`,
node: renderResult,
Expand Down
9 changes: 6 additions & 3 deletions src/testing/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let middlewareId = 0;

interface HarnessOptions {
customComparator?: CustomComparator[];
middleware?: [MiddlewareResultFactory<any, any, any>, MiddlewareResultFactory<any, any, any>][];
middleware?: [MiddlewareResultFactory<any, any, any, any>, MiddlewareResultFactory<any, any, any, any>][];
}

const factory = create();
Expand All @@ -60,14 +60,17 @@ export function harness(renderFunc: () => WNode, options: HarnessOptions | Custo
let invalidated = true;
let wNode = renderFunc();
const renderStack: (DNode | DNode[])[] = [];
let widget: WidgetBase | Callback<any, any, RenderResult>;
let widget: WidgetBase | Callback<any, any, any, RenderResult>;
let middleware: any = {};
let properties: any = {};
let children: any = [];
let customDiffs: any[] = [];
let customDiffNames: string[] = [];
let customComparator: CustomComparator[] = [];
let mockMiddleware: [MiddlewareResultFactory<any, any, any>, MiddlewareResultFactory<any, any, any>][] = [];
let mockMiddleware: [
MiddlewareResultFactory<any, any, any, any>,
MiddlewareResultFactory<any, any, any, any>
][] = [];
if (Array.isArray(options)) {
customComparator = options;
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/testing/mocks/middleware/breakpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import resize from '../../../core/middleware/resize';
import icache from '../../../core/middleware/icache';
import breakpoint, { Breakpoints } from '../../../core/middleware/breakpoint';
import createResizeMock from './resize';
import { MiddlewareResult } from '../../../core/interfaces';
import { DefaultMiddlewareResult } from '../../../core/interfaces';

export function createBreakpointMock(breakpoints: Breakpoints = { SM: 0, MD: 576, LG: 768, XL: 960 }) {
const mockBreakpoints: any = {};
Expand Down Expand Up @@ -35,7 +35,7 @@ export function createBreakpointMock(breakpoints: Breakpoints = { SM: 0, MD: 576
};
});

function mockBreakpoint(): MiddlewareResult<any, any, any>;
function mockBreakpoint(): DefaultMiddlewareResult;
function mockBreakpoint(
key: string,
breakpointResult: {
Expand All @@ -49,7 +49,7 @@ export function createBreakpointMock(breakpoints: Breakpoints = { SM: 0, MD: 576
breakpoint: string;
contentRect: Partial<DOMRectReadOnly>;
}
): void | MiddlewareResult<any, any, any> {
): void | DefaultMiddlewareResult {
if (key && breakpointResult) {
if (!mockBreakpoints[key]) {
mockBreakpoints[key] = breakpointResult.breakpoint;
Expand Down
Loading

0 comments on commit 47a39b4

Please sign in to comment.