mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-02-17 17:40:31 +08:00
impl #388, support slot props and apply to tabs component
This commit is contained in:
parent
24e7b87c89
commit
9b4cf9be0c
@ -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 }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -5,3 +5,4 @@ export * from './application';
|
||||
export * from './method';
|
||||
export * from './module';
|
||||
export * from './version';
|
||||
export * from './slot';
|
||||
|
5
packages/core/src/slot.ts
Normal file
5
packages/core/src/slot.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
|
||||
export type SlotSchema = {
|
||||
slotProps?: JSONSchema7;
|
||||
};
|
@ -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());
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ type EvalOptions = {
|
||||
scopeObject?: Record<string, any>;
|
||||
overrideScope?: boolean;
|
||||
fallbackWhenError?: (exp: string) => any;
|
||||
noConsoleError?: boolean
|
||||
noConsoleError?: boolean;
|
||||
};
|
||||
|
||||
// TODO: use web worker
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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']>
|
||||
>
|
||||
|
@ -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: [],
|
||||
|
Loading…
Reference in New Issue
Block a user