1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108import { derivedDisposable, IDisposable, IReader } from "@vscode/observables";
import React, { Context } from "react";
import { IPropertyTransformerFactory } from "./IPropertyTransformer";
import { obsView } from "./obsView";
import { mapObject } from "./utils";
import {
BaseViewModel,
getOrCreateViewModelContext,
PropsDesc,
PropsOut,
ViewModelContextSymbol,
} from "./viewModel";
/** Check if a transformer has _requiredContext defined (injected) */
type HasRequiredContext<T> = T extends { _requiredContext: Context<unknown> } ? true : false;
/** Required props: non-injected properties that must be provided */
type RequiredProps<T extends PropsDesc> = {
[K in keyof T as HasRequiredContext<T[K]> extends true ? never : K]: T[K] extends IPropertyTransformerFactory<infer U, any> ? U : never;
};
/** Optional props: injected properties that can be overridden */
type OptionalProps<T extends PropsDesc> = {
[K in keyof T as HasRequiredContext<T[K]> extends true ? K : never]?: T[K] extends IPropertyTransformerFactory<any, infer U> ? U : never;
};
/** Combined props type: required + optional injected */
type WithOptionalInjected<T extends PropsDesc> = RequiredProps<T> & OptionalProps<T>;
/** Collect unique _requiredContext from transformers */
function collectRequiredContexts(propsDesc: PropsDesc): Context<unknown>[] {
const contexts: Context<unknown>[] = [];
for (const t of Object.values(propsDesc)) {
const ctx = t._requiredContext;
if (ctx && !contexts.includes(ctx)) contexts.push(ctx);
}
return contexts;
}
// Overload 1: ViewModel-based classes with _props
export function viewWithModel<
TModelProps extends PropsDesc,
TProps extends PropsDesc,
TModel extends BaseViewModel<PropsOut<TModelProps>>
>(
viewModelCtor: (new (arg: PropsOut<TModelProps>) => TModel) & { _props: TModelProps; [ViewModelContextSymbol]?: Context<unknown> },
props: TProps,
render: (reader: IReader, model: TModel, props: PropsOut<TProps>) => React.ReactNode,
): React.ComponentType<WithOptionalInjected<TModelProps> & WithOptionalInjected<TProps>>;
// Overload 2: Simple classes without _props
export function viewWithModel<
TProps extends PropsDesc,
TModel extends IDisposable
>(
viewModelCtor: new () => TModel,
props: TProps,
render: (reader: IReader, model: TModel, props: PropsOut<TProps>) => React.ReactNode,
): React.ComponentType<WithOptionalInjected<TProps>>;
export function viewWithModel(
viewModelCtor: any,
props: any,
render: any,
): any {
const modelPropsDesc = '_props' in viewModelCtor ? viewModelCtor._props : {};
const requiredContexts = collectRequiredContexts({ ...modelPropsDesc, ...props });
// Always create the context so ProvideViewModel can work
const viewModelContext = getOrCreateViewModelContext(viewModelCtor);
const allContexts = [...requiredContexts, viewModelContext];
return obsView(
'viewWithModel',
(p, getContextValues) => {
const contextValues = getContextValues();
const providedModel = contextValues.get(viewModelContext);
const readableModelProps = '_props' in viewModelCtor
? mapObject(viewModelCtor._props, (v: any, k: string) => v.create((r: IReader) => p.read(r)[k], contextValues.get(v._requiredContext!)))
: {} as never;
const model = providedModel
? { read: () => providedModel, dispose: () => {} }
: derivedDisposable(reader => {
const modelProps = mapObject(
readableModelProps,
(v: any) => v.read(reader)
);
return new viewModelCtor(modelProps);
});
const readableProps = mapObject(props,
(v: any, k: string) => v.create(
(r: IReader) => p.read(r)[k],
contextValues.get(v._requiredContext!)
)
);
return (reader: IReader) => {
const m = model.read(reader);
const propValues = mapObject(readableProps, (v: any) => v.read(reader));
return render(reader, m, propValues);
};
},
allContexts.length > 0 ? allContexts : undefined
);
}