Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ export interface Callback<Props, Children, Middleware, ReturnValue> {
children: () => Children extends any[] ? Children : [Children];
}
): ReturnValue;
middlewares?: any;
}

export interface MiddlewareResult<Props, Children, Middleware, ReturnValue> {
Expand Down
112 changes: 109 additions & 3 deletions src/core/vdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface BaseNodeWrapper {

export interface WNodeWrapper extends BaseNodeWrapper {
node: WNode<any>;
keys?: string[];
instance?: any;
mergeNodes?: Node[];
nodeHandlerCalled?: boolean;
Expand Down Expand Up @@ -615,13 +616,22 @@ function same(dnode1: DNodeWrapper, dnode2: DNodeWrapper): boolean {
} else if (isWNodeWrapper(dnode1) && isWNodeWrapper(dnode2)) {
const widgetConstructor1 = dnode1.registryItem || dnode1.node.widgetConstructor;
const widgetConstructor2 = dnode2.registryItem || dnode2.node.widgetConstructor;
const {
node: { properties: props1 }
} = dnode1;
const {
node: { properties: props2 }
} = dnode2;
if (dnode1.instance === undefined && typeof widgetConstructor2 === 'string') {
return false;
}
if (widgetConstructor1 !== widgetConstructor2) {
return false;
}
if (dnode1.node.properties.key !== dnode2.node.properties.key) {
if (props1.key !== props2.key) {
return false;
}
if (!((widgetConstructor1 as any).keys || []).every((key: string) => props1[key] === props2[key])) {
return false;
}
return true;
Expand Down Expand Up @@ -669,7 +679,7 @@ function arrayFrom(arr: any) {
return Array.prototype.slice.call(arr);
}

function createFactory(callback: any, middlewares: any): any {
function createFactory(callback: any, middlewares: any, key?: any): any {
const factory = (properties: any, children?: any) => {
if (properties) {
const result = w(callback, properties, children);
Expand All @@ -682,10 +692,22 @@ function createFactory(callback: any, middlewares: any): any {
callback
};
};
const keys = Object.keys(middlewares).reduce((keys: string[], middlewareName: any) => {
const middleware = middlewares[middlewareName];
if (middleware.keys) {
keys = [...keys, ...middleware.keys];
}
return keys;
}, key ? [key] : []);

callback.keys = keys;
factory.keys = keys;
factory.isFactory = true;
return factory;
}

type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];

export function create<T extends MiddlewareMap, MiddlewareProps = ReturnType<T[keyof T]>['properties']>(
middlewares: T = {} as T
) {
Expand All @@ -706,6 +728,30 @@ export function create<T extends MiddlewareMap, MiddlewareProps = ReturnType<T[k
return createFactory(callback, middlewares);
}

function key(key: KeysMatching<Props, string | number>) {
function returns<ReturnValue>(
callback: Callback<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
DNode[],
T,
ReturnValue
>
): ReturnValue extends RenderResult
? DefaultChildrenWNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: DNode[];
}>
: MiddlewareResultFactory<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
DNode[],
T,
ReturnValue
> {
return createFactory(callback, middlewares, key);
}
return returns;
}

function children<Children>() {
function returns<ReturnValue>(
callback: Callback<
Expand All @@ -732,9 +778,40 @@ export function create<T extends MiddlewareMap, MiddlewareProps = ReturnType<T[k
> {
return createFactory(callback, middlewares);
}

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

Expand Down Expand Up @@ -765,6 +842,35 @@ export function create<T extends MiddlewareMap, MiddlewareProps = ReturnType<T[k
> {
return createFactory(callback, middlewares);
}
function key(key: KeysMatching<Props, string | number>) {
function returns<ReturnValue>(
callback: Callback<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
>
): ReturnValue extends RenderResult
? UnionToIntersection<Children> extends undefined
? OptionalWNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: NonNullable<Children>;
}>
: WNodeFactory<{
properties: Props & WidgetProperties & UnionToIntersection<MiddlewareProps>;
children: Children;
}>
: MiddlewareResultFactory<
WidgetProperties & Props & UnionToIntersection<MiddlewareProps>,
Children,
T,
ReturnValue
> {
return createFactory(callback, middlewares, key);
}
return returns;
}
returns.key = key;
return returns;
}

Expand Down Expand Up @@ -1918,7 +2024,7 @@ export function renderer(renderer: () => RenderResult): Renderer {
};

widgetMetaMap.set(next.id, widgetMeta);
if ((Constructor as any).middlewares) {
if ((Constructor as any).middlewares && Object.keys((Constructor as any).middlewares).length) {
const { middlewares, ids } = resolveMiddleware((Constructor as any).middlewares, id);
widgetMeta.middleware = middlewares;
widgetMeta.middlewareIds = ids;
Expand Down
125 changes: 125 additions & 0 deletions tests/core/unit/vdom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3604,6 +3604,131 @@ jsdomDescribe('vdom', () => {
);
});

it('should use key as widget key', () => {
const middlewareFactory = create({ icache })
.properties<{ bar: string | number }>()
.key('bar');

const mid = middlewareFactory(({ middleware: { icache } }) => {
return () => {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return result;
};
});

const factory = create({ mid, icache })
.properties<{ foo: string | number }>()
.key('foo');

const AutomaticKey = factory(function AutomaticKey({ properties, middleware: { icache, mid } }) {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return (
<div>
{properties().key}
{properties().foo}
{properties().bar}
{`widget-state-${result}`}
{`middleware-state-${mid()}`}
</div>
);
});
const AutomaticCompositeKey = factory(function AutomaticKey({
properties,
middleware: { icache, mid }
}) {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return (
<div>
{properties().key}
{properties().foo}
{properties().bar}
{`widget-state-${result}`}
{`middleware-state-${mid()}`}
</div>
);
});
const AutomaticNumberKey = factory(function AutomaticKey({ properties, middleware: { icache, mid } }) {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return (
<div>
{properties().key}
{`${properties().foo}`}
{`${properties().bar}`}
{`widget-state-${result}`}
{`middleware-state-${mid()}`}
</div>
);
});
const AutomaticCompositeNumberKey = factory(function AutomaticKey({
properties,
middleware: { icache, mid }
}) {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return (
<div>
{`${properties().key}`}
{`${properties().foo}`}
{`${properties().bar}`}
{`widget-state-${result}`}
{`middleware-state-${mid()}`}
</div>
);
});
const AutomaticKeyMiddlewareOnly = create({ mid, icache })(function AutomaticKeyMiddlewareOnly({
properties,
middleware: { icache, mid }
}) {
let result = icache.getOrSet('num', 1);
icache.set('num', result + 1);
return (
<div>
{properties().key}
{`${properties().bar}`}
{`widget-state-${result}`}
{`middleware-state-${mid()}`}
</div>
);
});

const App = create({ icache })(function App({ middleware: { icache } }) {
const stringKey = icache.getOrSet('string-key', 'property-foo');
const numKey = icache.getOrSet('number-key', 4321);
return (
<div>
<AutomaticKey foo={stringKey} bar="property-bar" />
<AutomaticCompositeKey key="user-key" foo={stringKey} bar="property-bar" />
<AutomaticNumberKey foo={9999} bar={numKey} />
<AutomaticCompositeNumberKey foo={9999} key={1234} bar={numKey} />
<AutomaticKeyMiddlewareOnly bar={stringKey} />
<button
onclick={() => {
icache.set('string-key', 'property-new-foo');
icache.set('number-key', 43214321);
}}
/>
</div>
);
});
const root = document.createElement('root');
const r = renderer(() => <App />);
r.mount({ domNode: root });
assert.strictEqual(
root.outerHTML,
'<root><div><div>property-fooproperty-barwidget-state-1middleware-state-1</div><div>user-keyproperty-fooproperty-barwidget-state-1middleware-state-1</div><div>99994321widget-state-1middleware-state-1</div><div>123499994321widget-state-1middleware-state-1</div><div>property-foowidget-state-1middleware-state-1</div><button></button></div></root>'
);
(root.children[0].children[5] as HTMLButtonElement).click();
resolvers.resolve();
assert.strictEqual(
root.outerHTML,
'<root><div><div>property-new-fooproperty-barwidget-state-1middleware-state-1</div><div>user-keyproperty-new-fooproperty-barwidget-state-1middleware-state-1</div><div>999943214321widget-state-1middleware-state-1</div><div>1234999943214321widget-state-1middleware-state-1</div><div>property-new-foowidget-state-1middleware-state-1</div><button></button></div></root>'
);
});

describe('core middleware', () => {
describe('node', () => {
it('should invalidate widget once node is available', () => {
Expand Down