๐Ÿ“ฆ hediet / vscode-observables

๐Ÿ“„ viewWithModel.ts ยท 108 lines
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
    );
}