impl #388, support slot props and apply to tabs component

This commit is contained in:
Yanzhen Yu 2022-05-05 11:32:51 +08:00
parent 24e7b87c89
commit 9b4cf9be0c
15 changed files with 120 additions and 47 deletions

View File

@ -37,7 +37,7 @@
"type": "chakra_ui/v1/button",
"properties": {
"text": {
"raw": "in tab1",
"raw": "only in tab {{ $slot.tabIndex + 1 }}",
"format": "plain"
}
},
@ -48,7 +48,8 @@
"container": {
"id": "tabs",
"slot": "content"
}
},
"ifCondition": "{{ $slot.tabIndex === 0 }}"
}
}
]

View File

@ -20,7 +20,11 @@ export default implementRuntimeComponent({
properties: Type.Object({}),
state: Type.Object({}),
methods: {},
slots: ['root'],
slots: {
root: {
slotProps: Type.Object({}),
},
},
styleSlots: [],
events: [],
},
@ -32,7 +36,7 @@ export default implementRuntimeComponent({
useSystemColorMode: false,
})}
>
<div ref={elementRef}>{slotsElements.root}</div>
<div ref={elementRef}>{slotsElements.root ? <slotsElements.root /> : null}</div>
</ChakraProvider>
);
});

View File

@ -49,7 +49,13 @@ export default implementRuntimeComponent({
state: StateSpec,
methods: {},
// tab slot is dynamic
slots: ['content'],
slots: {
content: {
slotProps: Type.Object({
tabIndex: Type.Number(),
}),
},
},
styleSlots: ['tabItem', 'tabContent'],
events: [],
},
@ -91,9 +97,6 @@ export default implementRuntimeComponent({
</TabList>
<TabPanels>
{tabNames.map((_, idx) => {
const ele = slotsElements.content
? ([] as React.ReactElement[]).concat(slotsElements.content)[idx]
: placeholder;
return (
<TabPanel
key={idx}
@ -101,7 +104,11 @@ export default implementRuntimeComponent({
${customStyle?.tabContent}
`}
>
{ele}
{slotsElements?.content ? (
<slotsElements.content tabIndex={idx} />
) : (
placeholder
)}
</TabPanel>
);
})}

View File

@ -2,6 +2,7 @@ import { JSONSchema7 } from 'json-schema';
import { parseVersion, Version } from './version';
import { ComponentMetadata } from './metadata';
import { MethodSchema } from './method';
import { SlotSchema } from './slot';
type ComponentSpec<
KMethodName extends string,
@ -13,7 +14,7 @@ type ComponentSpec<
state: JSONSchema7;
methods: Record<KMethodName, MethodSchema['parameters']>;
styleSlots: ReadonlyArray<KStyleSlot>;
slots: ReadonlyArray<KSlot>;
slots: Record<KSlot, SlotSchema>;
events: ReadonlyArray<KEvent>;
};

View File

@ -5,3 +5,4 @@ export * from './application';
export * from './method';
export * from './module';
export * from './version';
export * from './slot';

View File

@ -0,0 +1,5 @@
import { JSONSchema7 } from 'json-schema';
export type SlotSchema = {
slotProps?: JSONSchema7;
};

View File

@ -24,7 +24,14 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(
() => {
return c.traits.map(t => executeTrait(t, stateManager.deepEval(t.properties)));
return c.traits.map(t =>
executeTrait(
t,
stateManager.deepEval(t.properties, {
scopeObject: { $slot: props.slotProps },
})
)
);
}
);
@ -49,6 +56,9 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
newResults[i] = traitResult;
return newResults;
});
},
{
scopeObject: { $slot: props.slotProps },
}
);
stops.push(stop);
@ -58,7 +68,7 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
// because mergeState will be called during the first render of component, and state will change
setTraitResults(c.traits.map((trait, i) => executeTrait(trait, properties[i])));
return () => stops.forEach(s => s());
}, [c.id, c.traits, executeTrait, stateManager]);
}, [c.id, c.traits, executeTrait, stateManager, props.slotProps]);
// reduce traitResults
const propsFromTraits: TraitResult<string, string>['props'] = useMemo(() => {
@ -81,7 +91,10 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
// component properties
const [evaledComponentProperties, setEvaledComponentProperties] = useState(() => {
return merge(
stateManager.deepEval(c.properties, { fallbackWhenError: () => undefined }),
stateManager.deepEval(c.properties, {
fallbackWhenError: () => undefined,
scopeObject: { $slot: props.slotProps },
}),
propsFromTraits
);
});
@ -92,13 +105,13 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
({ result: newResult }: any) => {
setEvaledComponentProperties({ ...newResult });
},
{ fallbackWhenError: () => undefined }
{ fallbackWhenError: () => undefined, scopeObject: { $slot: props.slotProps } }
);
// must keep this line, reason is the same as above
setEvaledComponentProperties({ ...result });
return stop;
}, [c.properties, stateManager]);
}, [c.properties, stateManager, props.slotProps]);
useEffect(() => {
const clearFunctions = propsFromTraits?.componentDidMount?.map(e => e());

View File

@ -13,13 +13,18 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
const { executeTrait } = useRuntimeFunctions(props);
const unmountTraits = useMemo(
() => c.traits.filter(t => registry.getTraitByType(t.type).metadata.annotations?.beforeRender),
() =>
c.traits.filter(
t => registry.getTraitByType(t.type).metadata.annotations?.beforeRender
),
[c.traits, registry]
);
const [isHidden, setIsHidden] = useState(() => {
const results: TraitResult<string, string>[] = unmountTraits.map(t => {
const properties = stateManager.deepEval(t.properties);
const properties = stateManager.deepEval(t.properties, {
scopeObject: { $slot: props.slotProps },
});
return executeTrait(t, properties);
});
return results.some(result => result.unmount);
@ -42,8 +47,12 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
const stops: ReturnType<typeof watch>[] = [];
if (unmountTraits.length > 0) {
unmountTraits.forEach(t => {
const { result, stop } = stateManager.deepEvalAndWatch(t.properties, newValue =>
traitChangeCallback(t, newValue.result)
const { result, stop } = stateManager.deepEvalAndWatch(
t.properties,
newValue => traitChangeCallback(t, newValue.result),
{
scopeObject: { $slot: props.slotProps },
}
);
traitChangeCallback(t, result);
stops.push(stop);
@ -52,7 +61,14 @@ export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperPr
return () => {
stops.forEach(stop => stop());
};
}, [c, executeTrait, unmountTraits, stateManager, traitChangeCallback]);
}, [
c,
executeTrait,
unmountTraits,
stateManager,
traitChangeCallback,
props.slotProps,
]);
// If a component is unmount, its state would be removed.
// So if it mount again, we should init its state again.

View File

@ -1,16 +1,18 @@
import React from 'react';
import { RuntimeComponentSchema } from '@sunmao-ui/core';
import { ImplWrapperProps } from '../../../../types';
import { RuntimeComponentSchema, SlotSchema } from '@sunmao-ui/core';
import { ImplWrapperProps, SlotsElements } from '../../../../types';
import { ImplWrapper } from '../ImplWrapper';
export function useSlotElements(props: ImplWrapperProps) {
export function useSlotElements(
props: ImplWrapperProps
): SlotsElements<Record<string, SlotSchema>> {
const { component: c, childrenMap } = props;
const childrenCache = new Map<RuntimeComponentSchema, React.ReactElement>();
if (!childrenMap[c.id]) {
return {};
}
const slotElements: Record<string, React.ReactElement[] | React.ReactElement> = {};
const slotElements: SlotsElements<Record<string, SlotSchema>> = {};
for (const slot in childrenMap[c.id]) {
const slotChildren = childrenMap[c.id][slot].map(child => {
if (!childrenCache.get(child)) {
@ -20,7 +22,9 @@ export function useSlotElements(props: ImplWrapperProps) {
return childrenCache.get(child)!;
});
slotElements[slot] = slotChildren.length === 1 ? slotChildren[0] : slotChildren;
slotElements[slot] = slotProps => {
return <>{slotChildren.map(child => React.cloneElement(child, { slotProps }))}</>;
};
}
return slotElements;
}

View File

@ -19,7 +19,7 @@ type EvalOptions = {
scopeObject?: Record<string, any>;
overrideScope?: boolean;
fallbackWhenError?: (exp: string) => any;
noConsoleError?: boolean
noConsoleError?: boolean;
};
// TODO: use web worker

View File

@ -1,14 +1,15 @@
import { Type } from '@sinclair/typebox';
import { implementRuntimeTrait } from '../../utils/buildKit';
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
import { implementRuntimeTrait } from 'src/utils/buildKit';
const ContainerSpec = Type.Object({
const ContainerPropertySpec = Type.Object({
id: Type.String(),
slot: Type.String(),
});
export const SlotTraitPropertiesSpec = Type.Object({
container: ContainerSpec,
export const PropsSpec = Type.Object({
container: ContainerPropertySpec,
ifCondition: Type.Optional(Type.Boolean()),
});
export default implementRuntimeTrait({
@ -16,12 +17,18 @@ export default implementRuntimeTrait({
metadata: {
name: CoreTraitName.Slot,
description: 'nested components by slots',
annotations: {
beforeRender: true,
},
},
spec: {
properties: SlotTraitPropertiesSpec,
state: {},
properties: PropsSpec,
state: Type.Object({}),
methods: [],
},
})(() => () => ({
props: null,
}));
})(() => {
return ({ ifCondition }) => ({
props: null,
unmount: ifCondition === false,
});
});

View File

@ -1,7 +1,9 @@
import { Static } from '@sinclair/typebox';
import {
RuntimeApplication,
RuntimeComponentSchema,
RuntimeComponent,
SlotSchema,
} from '@sunmao-ui/core';
import React from 'react';
import { UIServices, ComponentParamsFromApp } from './application';
@ -14,18 +16,18 @@ export type ImplWrapperProps<KSlot extends string = string> = {
services: UIServices;
isInModule: boolean;
app?: RuntimeApplication;
slotProps?: unknown;
} & ComponentParamsFromApp;
export type ComponentImplProps<
TState,
TMethods,
KSlot extends string,
TSlots extends Record<string, SlotSchema>,
KStyleSlot extends string,
KEvent extends string
> = ImplWrapperProps<KSlot> &
> = ImplWrapperProps &
TraitResult<KStyleSlot, KEvent>['props'] &
RuntimeFunctions<TState, TMethods> & {
slotsElements: Record<KSlot, React.ReactElement[] | React.ReactElement>;
RuntimeFunctions<TState, TMethods, TSlots> & {
elementRef?: React.Ref<any>;
getElement?: (ele: HTMLElement) => void;
};
@ -34,10 +36,10 @@ export type ComponentImpl<
TProps = any,
TState = any,
TMethods = Record<string, any>,
KSlot extends string = string,
TSlots extends Record<string, SlotSchema> = Record<string, any>,
KStyleSlot extends string = string,
KEvent extends string = string
> = React.FC<TProps & ComponentImplProps<TState, TMethods, KSlot, KStyleSlot, KEvent>>;
> = React.FC<TProps & ComponentImplProps<TState, TMethods, TSlots, KStyleSlot, KEvent>>;
export type ImplementedRuntimeComponent<
KMethodName extends string,
@ -60,8 +62,16 @@ type SubscribeMethods<U> = (map: {
[K in keyof U]: (parameters: U[K]) => void;
}) => void;
type MergeState<T> = (partialState: Partial<T>) => void;
export type SlotsElements<U extends Record<string, SlotSchema>> = {
[K in keyof U]?: React.FC<Static<U[K]['slotProps']>>;
};
export type RuntimeFunctions<TState, TMethods> = {
export type RuntimeFunctions<
TState,
TMethods,
TSlots extends Record<string, SlotSchema>
> = {
mergeState: MergeState<TState>;
subscribeMethods: SubscribeMethods<TMethods>;
slotsElements: SlotsElements<TSlots>;
};

View File

@ -8,15 +8,15 @@ export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
customStyle?: Record<KStyleSlot, string>;
callbackMap?: CallbackMap<KEvent>;
componentDidUnmount?: Array<() => void>;
componentDidMount?: Array<() => Function | void>
componentDidUpdate?: Array<() => Function | void>
componentDidMount?: Array<() => Function | void>;
componentDidUpdate?: Array<() => Function | void>;
} | null;
unmount?: boolean;
};
export type TraitImpl<T = any> = (
props: T &
RuntimeFunctions<unknown, unknown> & {
RuntimeFunctions<unknown, unknown, any> & {
trait: RuntimeTraitSchema;
componentId: string;
services: UIServices;

View File

@ -32,7 +32,7 @@ export function implementRuntimeComponent<
Static<T['spec']['properties']>,
Static<T['spec']['state']>,
ToMap<T['spec']['methods']>,
ToStringUnion<T['spec']['slots']>,
T['spec']['slots'],
ToStringUnion<T['spec']['styleSlots']>,
ToStringUnion<T['spec']['events']>
>

View File

@ -1,5 +1,7 @@
import { RuntimeComponentSchema } from '@sunmao-ui/core';
import { ChildrenMap } from '../types';
import { PropsSpec as SlotPropsSpec } from '../traits/core/Slot';
import { Static } from '@sinclair/typebox';
export function resolveChildrenMap(components: RuntimeComponentSchema[]): {
childrenMap: ChildrenMap<string>;
@ -14,7 +16,9 @@ export function resolveChildrenMap(components: RuntimeComponentSchema[]): {
topLevelComponents.push(c);
continue;
}
const { id, slot } = slotTrait.properties.container as any;
const {
container: { id, slot },
} = slotTrait.properties as Static<typeof SlotPropsSpec>;
if (!childrenMap[id]) {
childrenMap[id] = {
_allChildren: [],