refactor chakra-ui-lib

This commit is contained in:
Bowen Tan 2021-12-30 17:38:19 +08:00
parent 70dad17dee
commit 93fa2244d4
31 changed files with 1718 additions and 1913 deletions

View File

@ -1,7 +1,6 @@
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Box as BaseBox } from '@chakra-ui/react';
import { ComponentImplementation, Slot, GRID_HEIGHT } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot, GRID_HEIGHT } from '@sunmao-ui/runtime';
import { pick } from 'lodash-es';
import { css } from '@emotion/css';
@ -270,11 +269,30 @@ const StyleSchema = Type.Partial(
);
const StyleProps = Object.keys(StyleSchema.properties);
const Box: ComponentImplementation<Static<typeof StyleSchema>> = ({
slotsMap,
customStyle,
...restProps
}) => {
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'box',
displayName: 'Box',
isDraggable: true,
isResizable: true,
description: 'chakra-ui box',
exampleProperties: {
w: GRID_HEIGHT,
h: GRID_HEIGHT,
border: '1px solid black',
},
exampleSize: [6, 6],
},
spec: {
properties: StyleSchema,
state: Type.Object({}),
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
})(({ slotsMap, customStyle, ...restProps }) => {
const styleProps = pick(restProps, StyleProps);
return (
<BaseBox
@ -292,32 +310,4 @@ const Box: ComponentImplementation<Static<typeof StyleSchema>> = ({
<Slot slotsMap={slotsMap} slot="content" />
</BaseBox>
);
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'box',
displayName: 'Box',
isDraggable: true,
isResizable: true,
description: 'chakra-ui box',
exampleProperties: {
w: GRID_HEIGHT,
h: GRID_HEIGHT,
border: '1px solid black',
},
exampleSize: [6, 6],
},
spec: {
properties: StyleSchema,
state: {},
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
}),
impl: Box,
};
});

View File

@ -1,47 +1,10 @@
import { useEffect, useRef } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { css } from '@emotion/css';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Button as BaseButton } from '@chakra-ui/react';
import { ComponentImplementation, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { Text, TextPropertySchema, implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
const Button: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
mergeState,
subscribeMethods,
callbackMap: callbacks,
colorScheme,
isLoading,
customStyle,
}) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [text.raw]);
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
subscribeMethods({
click() {
ref.current?.click();
},
});
}, []);
return (
<BaseButton
className={css`
${customStyle?.content}
`}
{...{ colorScheme, isLoading }}
ref={ref}
onClick={callbacks?.onClick}
>
<Text value={text} />
</BaseButton>
);
};
const StateSchema = Type.Object({
value: Type.String(),
});
@ -52,37 +15,68 @@ const PropsSchema = Type.Object({
isLoading: Type.Optional(Type.Boolean()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'button',
displayName: 'Button',
description: 'chakra-ui button',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'text',
format: 'plain',
},
colorScheme: 'blue',
isLoading: false,
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'button',
displayName: 'Button',
description: 'chakra-ui button',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'text',
format: 'plain',
},
exampleSize: [2, 1],
colorScheme: 'blue',
isLoading: false,
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: [
{
name: 'click',
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
click: void 0,
},
slots: [],
styleSlots: ['content'],
events: ['onClick'],
},
})(
({
text,
mergeState,
subscribeMethods,
callbackMap: callbacks,
colorScheme,
isLoading,
customStyle,
}) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [text.raw]);
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
subscribeMethods({
click() {
ref.current?.click();
},
],
slots: [],
styleSlots: ['content'],
events: ['onClick'],
},
}),
impl: Button,
};
});
}, []);
return (
<BaseButton
className={css`
${customStyle?.content}
`}
{...{ colorScheme, isLoading }}
ref={ref}
onClick={callbacks?.onClick}
>
<Text value={text} />
</BaseButton>
);
}
);

View File

@ -1,8 +1,7 @@
import { useState, useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Checkbox as BaseCheckbox, useCheckboxGroupContext } from '@chakra-ui/react';
import { ComponentImplementation, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
import { css } from '@emotion/css';
@ -16,84 +15,11 @@ export const SizePropertySchema = Type.KeyOf(
);
export const CheckboxStateSchema = Type.Object({
value: Type.String(),
Text: Type.String(),
value: Type.Union([Type.String(), Type.Number()]),
text: Type.String(),
checked: Type.Boolean(),
});
const Checkbox: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
value,
defaultIsChecked,
isDisabled,
isFocusable,
isInValid,
isReadOnly,
isRequired,
size,
spacing,
colorScheme,
mergeState,
customStyle,
}) => {
const groupContext = useCheckboxGroupContext();
let _defaultIsChecked = false;
if (typeof defaultIsChecked === 'boolean') {
_defaultIsChecked = defaultIsChecked;
} else if (groupContext) {
_defaultIsChecked = groupContext.value.some(val => val === value);
}
const [checked, setChecked] = useState(_defaultIsChecked);
useEffect(() => {
mergeState({ text: text.raw });
}, [text.raw]);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
mergeState({ checked });
}, [checked]);
useEffect(() => {
setChecked(!!defaultIsChecked);
}, [setChecked, defaultIsChecked]);
const args: {
colorScheme?: Static<typeof ColorSchemePropertySchema>;
size?: Static<typeof SizePropertySchema>;
} = {};
if (colorScheme) args.colorScheme = colorScheme;
if (size) args.size = size;
return (
<BaseCheckbox
height="10"
value={value}
isChecked={checked}
isDisabled={isDisabled}
isFocusable={isFocusable}
isInvalid={isInValid}
isReadOnly={isReadOnly}
isRequired={isRequired}
size={size}
spacing={spacing}
colorScheme={colorScheme}
onChange={e => {
setChecked(e.target.checked);
}}
className={css`
${customStyle?.content}
`}
>
<Text value={text} />
</BaseCheckbox>
);
};
const PropsSchema = Type.Object({
text: TextPropertySchema,
value: Type.Union([Type.String(), Type.Number()]),
@ -108,35 +34,105 @@ const PropsSchema = Type.Object({
colorScheme: ColorSchemePropertySchema,
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'checkbox',
description: 'chakra-ui checkbox',
displayName: 'Checkbox',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'Checkbox',
format: 'plain',
},
value: 'checkbox 1',
defaultIsChecked: true,
isDisabled: false,
size: 'md',
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'checkbox',
description: 'chakra-ui checkbox',
displayName: 'Checkbox',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'Checkbox',
format: 'plain',
},
exampleSize: [3, 1],
value: 'checkbox 1',
defaultIsChecked: true,
isDisabled: false,
size: 'md',
},
spec: {
properties: PropsSchema,
state: CheckboxStateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: Checkbox,
};
exampleSize: [3, 1],
},
spec: {
properties: PropsSchema,
state: CheckboxStateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
text,
value,
defaultIsChecked,
isDisabled,
isFocusable,
isInValid,
isReadOnly,
isRequired,
size,
spacing,
colorScheme,
mergeState,
customStyle,
}) => {
const groupContext = useCheckboxGroupContext();
let _defaultIsChecked = false;
if (typeof defaultIsChecked === 'boolean') {
_defaultIsChecked = defaultIsChecked;
} else if (groupContext) {
_defaultIsChecked = groupContext.value.some(val => val === value);
}
const [checked, setChecked] = useState(_defaultIsChecked);
useEffect(() => {
mergeState({ text: text.raw });
}, [text.raw]);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
mergeState({ checked });
}, [checked]);
useEffect(() => {
setChecked(!!defaultIsChecked);
}, [setChecked, defaultIsChecked]);
const args: {
colorScheme?: Static<typeof ColorSchemePropertySchema>;
size?: Static<typeof SizePropertySchema>;
} = {};
if (colorScheme) args.colorScheme = colorScheme;
if (size) args.size = size;
return (
<BaseCheckbox
height="10"
value={value}
isChecked={checked}
isDisabled={isDisabled}
isFocusable={isFocusable}
isInvalid={isInValid}
isReadOnly={isReadOnly}
isRequired={isRequired}
size={size}
spacing={spacing}
colorScheme={colorScheme}
onChange={e => {
setChecked(e.target.checked);
}}
className={css`
${customStyle?.content}
`}
>
<Text value={text} />
</BaseCheckbox>
);
}
);

View File

@ -1,25 +1,43 @@
import { useState, useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { CheckboxGroup as BaseCheckboxGroup } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
import { SizePropertySchema, IsDisabledSchema } from './Checkbox';
const DefaultValueSchema = Type.Optional(
Type.Array(Type.Union([Type.String(), Type.Number()]))
);
const DefaultValueSchema = Type.Array(Type.Union([Type.String(), Type.Number()]));
const StateSchema = Type.Object({
value: Type.String(),
value: DefaultValueSchema,
});
const CheckboxGroup: ComponentImplementation<Static<typeof PropsSchema>> = ({
size,
defaultValue,
isDisabled,
slotsMap,
mergeState,
}) => {
const PropsSchema = Type.Object({
size: SizePropertySchema,
isDisabled: IsDisabledSchema,
defaultValue: Type.Optional(DefaultValueSchema),
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'checkbox_group',
displayName: 'Checkbox Group',
description: 'chakra-ui checkbox group',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: [],
},
exampleSize: [3, 3],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
})(({ size, defaultValue, isDisabled, slotsMap, mergeState }) => {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
mergeState({ value });
@ -35,36 +53,4 @@ const CheckboxGroup: ComponentImplementation<Static<typeof PropsSchema>> = ({
<Slot slotsMap={slotsMap} slot="content" />
</BaseCheckboxGroup>
);
};
const PropsSchema = Type.Object({
size: SizePropertySchema,
isDisabled: IsDisabledSchema,
defaultValue: DefaultValueSchema,
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'checkbox_group',
displayName: 'Checkbox Group',
description: 'chakra-ui checkbox group',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: [],
},
exampleSize: [3, 3],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
}),
impl: CheckboxGroup,
};

View File

@ -1,6 +1,9 @@
import { useEffect, useState, useRef } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { ComponentImplementation, Slot, DIALOG_CONTAINER_ID } from '@sunmao-ui/runtime';
import {
implementRuntimeComponent2,
Slot,
DIALOG_CONTAINER_ID,
} from '@sunmao-ui/runtime';
import {
AlertDialog,
AlertDialogBody,
@ -12,7 +15,7 @@ import {
ModalContentProps,
ModalOverlayProps,
} from '@chakra-ui/react';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
const HandleButtonPropertySchema = Type.Object({
@ -20,104 +23,6 @@ const HandleButtonPropertySchema = Type.Object({
colorScheme: ColorSchemePropertySchema,
});
const Dialog: ComponentImplementation<Static<typeof PropsSchema>> = ({
slotsMap,
subscribeMethods,
callbackMap: callbacks,
title: customerTitle,
disableConfirm,
confirmButton = {
text: 'confirm',
colorScheme: 'red',
},
cancelButton = {
text: 'cancel',
colorScheme: 'blue',
},
customStyle,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState(customerTitle || '');
const cancelRef = useRef(null);
const containerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
containerRef.current = document.getElementById(DIALOG_CONTAINER_ID);
}, [containerRef]);
useEffect(() => {
subscribeMethods({
openDialog({ title }) {
setIsOpen(true);
setTitle(title);
},
confirmDialog() {
setIsOpen(false);
},
cancelDialog() {
setIsOpen(false);
},
});
}, []);
const dialogContentProps: ModalContentProps = {
position: 'absolute',
width: 'full',
containerProps: { position: 'absolute', width: 'full', height: 'full' },
};
const dialogOverlayProps: ModalOverlayProps = {
position: 'absolute',
width: 'full',
height: 'full',
};
const portalProps = {
appendToParentPortal: true,
containerRef,
};
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={() => setIsOpen(false)}
trapFocus={false}
portalProps={containerRef.current ? portalProps : undefined}
>
<AlertDialogOverlay {...(containerRef.current ? dialogOverlayProps : {})}>
<AlertDialogContent
className={`${customStyle?.content}`}
{...(containerRef.current ? dialogContentProps : {})}
>
<AlertDialogHeader>{title}</AlertDialogHeader>
<AlertDialogBody>
<Slot slotsMap={slotsMap} slot="content" />
</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
colorScheme={cancelButton.colorScheme}
onClick={callbacks?.cancelDialog}
>
{cancelButton.text}
</Button>
<Button
disabled={disableConfirm}
colorScheme={confirmButton.colorScheme}
onClick={callbacks?.confirmDialog}
ml={3}
>
{confirmButton.text}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
const PropsSchema = Type.Object({
title: Type.Optional(Type.String()),
confirmButton: HandleButtonPropertySchema,
@ -125,44 +30,132 @@ const PropsSchema = Type.Object({
disableConfirm: Type.Optional(Type.Boolean()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'dialog',
displayName: 'Dialog',
description: 'chakra_ui dialog',
isDraggable: false,
isResizable: false,
exampleProperties: {
title: 'Dialog',
confirmButton: 'Confirm',
cancelButton: 'Cancel',
disableConfirm: false,
},
exampleSize: [6, 6],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'dialog',
displayName: 'Dialog',
description: 'chakra_ui dialog',
isDraggable: false,
isResizable: false,
exampleProperties: {
title: 'Dialog',
confirmButton: 'Confirm',
cancelButton: 'Cancel',
disableConfirm: false,
},
spec: {
properties: PropsSchema,
state: {},
methods: [
{
name: 'openDialog',
parameters: Type.Object({
title: Type.String(),
}),
},
{
name: 'confirmDialog',
},
{
name: 'cancelDialog',
},
],
slots: ['content'],
styleSlots: ['content'],
events: ['cancelDialog', 'confirmDialog'],
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
methods: {
openDialog: Type.Object({
title: Type.String(),
}),
confirmDialog: void 0,
cancelDialog: void 0,
},
}),
impl: Dialog,
};
slots: ['content'],
styleSlots: ['content'],
events: ['cancelDialog', 'confirmDialog'],
},
})(
({
slotsMap,
subscribeMethods,
callbackMap: callbacks,
title: customerTitle,
disableConfirm,
confirmButton = {
text: 'confirm',
colorScheme: 'red',
},
cancelButton = {
text: 'cancel',
colorScheme: 'blue',
},
customStyle,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState(customerTitle || '');
const cancelRef = useRef(null);
const containerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
containerRef.current = document.getElementById(DIALOG_CONTAINER_ID);
}, [containerRef]);
useEffect(() => {
subscribeMethods({
openDialog({ title }) {
setIsOpen(true);
setTitle(title);
},
confirmDialog() {
setIsOpen(false);
},
cancelDialog() {
setIsOpen(false);
},
});
}, []);
const dialogContentProps: ModalContentProps = {
position: 'absolute',
width: 'full',
containerProps: { position: 'absolute', width: 'full', height: 'full' },
};
const dialogOverlayProps: ModalOverlayProps = {
position: 'absolute',
width: 'full',
height: 'full',
};
const portalProps = {
appendToParentPortal: true,
containerRef,
};
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={() => setIsOpen(false)}
trapFocus={false}
portalProps={containerRef.current ? portalProps : undefined}
>
<AlertDialogOverlay {...(containerRef.current ? dialogOverlayProps : {})}>
<AlertDialogContent
className={`${customStyle?.content}`}
{...(containerRef.current ? dialogContentProps : {})}
>
<AlertDialogHeader>{title}</AlertDialogHeader>
<AlertDialogBody>
<Slot slotsMap={slotsMap} slot="content" />
</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
colorScheme={cancelButton.colorScheme}
onClick={callbacks?.cancelDialog}
>
{cancelButton.text}
</Button>
<Button
disabled={disableConfirm}
colorScheme={confirmButton.colorScheme}
onClick={callbacks?.confirmDialog}
ml={3}
>
{confirmButton.text}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
}
);

View File

@ -1,9 +1,28 @@
import { Divider } from '@chakra-ui/react';
import { css } from '@emotion/css';
import { createComponent } from '@sunmao-ui/core';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
const DividerImpl: ComponentImplementation = ({ customStyle }) => {
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'divider',
displayName: 'Divider',
description: 'chakra-ui divider',
isDraggable: true,
isResizable: true,
exampleProperties: {},
exampleSize: [4, 1],
},
spec: {
properties: Type.Object({}),
state: Type.Object({}),
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(({ customStyle }) => {
return (
<Divider
className={css`
@ -11,28 +30,4 @@ const DividerImpl: ComponentImplementation = ({ customStyle }) => {
`}
/>
);
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'divider',
displayName: 'Divider',
description: 'chakra-ui divider',
isDraggable: true,
isResizable: true,
exampleProperties: {},
exampleSize: [4, 1],
},
spec: {
properties: {},
state: {},
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: DividerImpl,
};
});

View File

@ -1,164 +1,158 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { css } from '@emotion/css';
import { Type, Static } from '@sinclair/typebox';
import { createComponent } from '@sunmao-ui/core';
import { Type } from '@sinclair/typebox';
import { Button, VStack } from '@chakra-ui/react';
import { ComponentImplementation, Slot, watch } from '@sunmao-ui/runtime';
const FormImpl: ComponentImplementation<Static<typeof PropsSchema>> = ({
mergeState,
subscribeMethods,
hideSubmit,
slotsMap,
callbackMap,
services,
customStyle,
}) => {
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
const [isFormInvalid, setIsFormInvalid] = useState<boolean>(false);
const formDataRef = useRef<Record<string, any>>({});
const formControlIds = useMemo<string[]>(() => {
return (
slotsMap?.get('content')?.map(slot => {
return slot.id;
}) || []
);
}, [slotsMap]);
useEffect(() => {
setInvalidArray(
formControlIds.map(fcid => {
return services.stateManager.store[fcid].isInvalid;
})
);
}, []);
useEffect(() => {
const disable = invalidArray.some(v => v);
setIsFormInvalid(disable);
mergeState({
disableSubmit: disable,
});
}, [invalidArray]);
useEffect(() => {
subscribeMethods({
resetForm() {
formControlIds.forEach(fcId => {
const inputId = services.stateManager.store[fcId].inputId;
services.apiService.send('uiMethod', {
componentId: inputId,
name: 'resetInputValue',
});
});
},
});
}, [formControlIds]);
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
formControlIds.forEach((fcId, i) => {
// watch isInvalid
let stop = watch(
() => {
return services.stateManager.store[fcId].isInvalid;
},
newV => {
setInvalidArray(oldValidArray => {
const newValidArray = [...oldValidArray];
newValidArray[i] = newV;
return newValidArray;
});
}
);
stops.push(stop);
// watch value
stop = watch(
() => {
return services.stateManager.store[fcId].value;
},
newV => {
const fcState = services.stateManager.store[fcId];
formDataRef.current[fcState.fieldName] = newV;
mergeState({ data: { ...formDataRef.current } });
}
);
stops.push(stop);
});
return () => {
stops.forEach(s => {
s();
});
};
}, [formControlIds]);
const onSubmit = () => {
callbackMap?.onSubmit();
};
return (
<VStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
spacing="5"
className={css`
${customStyle?.content}
`}
>
<Slot slotsMap={slotsMap} slot="content" />
{hideSubmit ? undefined : (
<Button
marginInlineStart="auto !important"
disabled={isFormInvalid}
onClick={onSubmit}
>
</Button>
)}
</VStack>
);
};
import { implementRuntimeComponent2, Slot, watch } from '@sunmao-ui/runtime';
const PropsSchema = Type.Object({
hideSubmit: Type.Boolean(),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'form',
displayName: 'Form',
description: 'chakra-ui form',
isDraggable: true,
isResizable: true,
exampleProperties: {
hideSubmit: false,
},
exampleSize: [4, 6],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'form',
displayName: 'Form',
description: 'chakra-ui form',
isDraggable: true,
isResizable: true,
exampleProperties: {
hideSubmit: false,
},
spec: {
properties: PropsSchema,
state: Type.Object({
data: Type.Any(),
disableSubmit: Type.Boolean(),
}),
methods: [
{
name: 'resetForm',
exampleSize: [4, 6],
},
spec: {
properties: PropsSchema,
state: Type.Object({
data: Type.Any(),
disableSubmit: Type.Boolean(),
}),
methods: {
resetForm: void 0,
},
slots: ['content'],
styleSlots: ['content'],
events: ['onSubmit'],
},
})(
({
mergeState,
subscribeMethods,
hideSubmit,
slotsMap,
callbackMap,
services,
customStyle,
}) => {
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
const [isFormInvalid, setIsFormInvalid] = useState<boolean>(false);
const formDataRef = useRef<Record<string, any>>({});
const formControlIds = useMemo<string[]>(() => {
return (
slotsMap?.get('content')?.map(slot => {
return slot.id;
}) || []
);
}, [slotsMap]);
useEffect(() => {
setInvalidArray(
formControlIds.map(fcid => {
return services.stateManager.store[fcid].isInvalid;
})
);
}, []);
useEffect(() => {
const disable = invalidArray.some(v => v);
setIsFormInvalid(disable);
mergeState({
disableSubmit: disable,
});
}, [invalidArray]);
useEffect(() => {
subscribeMethods({
resetForm() {
formControlIds.forEach(fcId => {
const inputId = services.stateManager.store[fcId].inputId;
services.apiService.send('uiMethod', {
componentId: inputId,
name: 'resetInputValue',
});
});
},
],
slots: ['content'],
styleSlots: ['content'],
events: ['onSubmit'],
},
}),
impl: FormImpl,
};
});
}, [formControlIds]);
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
formControlIds.forEach((fcId, i) => {
// watch isInvalid
let stop = watch(
() => {
return services.stateManager.store[fcId].isInvalid;
},
newV => {
setInvalidArray(oldValidArray => {
const newValidArray = [...oldValidArray];
newValidArray[i] = newV;
return newValidArray;
});
}
);
stops.push(stop);
// watch value
stop = watch(
() => {
return services.stateManager.store[fcId].value;
},
newV => {
const fcState = services.stateManager.store[fcId];
formDataRef.current[fcState.fieldName] = newV;
mergeState({ data: { ...formDataRef.current } });
}
);
stops.push(stop);
});
return () => {
stops.forEach(s => {
s();
});
};
}, [formControlIds]);
const onSubmit = () => {
callbackMap?.onSubmit();
};
return (
<VStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
spacing="5"
className={css`
${customStyle?.content}
`}
>
<Slot slotsMap={slotsMap} slot="content" />
{hideSubmit ? undefined : (
<Button
marginInlineStart="auto !important"
disabled={isFormInvalid}
onClick={onSubmit}
>
</Button>
)}
</VStack>
);
}
);

View File

@ -1,6 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { first } from 'lodash-es';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import {
FormControl,
@ -11,7 +10,7 @@ import {
Text,
} from '@chakra-ui/react';
import { css } from '@emotion/css';
import { ComponentImplementation, Slot, watch } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot, watch } from '@sunmao-ui/runtime';
import { CheckboxStateSchema } from '../Checkbox';
const FormItemCSS = {
@ -19,112 +18,6 @@ const FormItemCSS = {
width: '66%',
};
const FormControlImpl: ComponentImplementation<{
label: string;
fieldName: string;
isRequired: boolean;
helperText: string;
}> = ({
label,
fieldName,
isRequired,
helperText,
slotsMap,
mergeState,
services,
customStyle,
}) => {
const [inputValue, setInputValue] = useState('');
// don't show Invalid state on component mount
const [hideInvalid, setHideInvalid] = useState(true);
const inputId = useMemo(() => first(slotsMap?.get('content'))?.id || '', []);
const [validResult, setValidResult] = useState({
isInvalid: false,
errorMsg: '',
});
const { isInvalid, errorMsg } = validResult;
useEffect(() => {
if (!inputId) return;
const stop = watch(
() => {
const inputState = services.stateManager.store[inputId];
if (!inputState) return '';
if (inputState.checked !== undefined) {
// special treatment for checkbox
return (inputState as Static<typeof CheckboxStateSchema>).checked;
} else {
return inputState.value;
}
},
newV => {
setInputValue(newV);
}
);
setInputValue(services.stateManager.store[inputId].value);
return stop;
}, [inputId, setInputValue]);
useEffect(() => {
if (!inputId) return;
const stop = watch(
() => {
return services.stateManager.store[inputId]?.validResult;
},
newV => {
setValidResult(newV);
}
);
if (services.stateManager.store[inputId]?.validResult) {
setValidResult(services.stateManager.store[inputId].validResult);
}
return stop;
}, [inputId, setValidResult]);
useEffect(() => {
if (!inputId) return;
if (inputValue) {
// After inputValue first change, begin to show Invalid state
setHideInvalid(false);
}
mergeState({
inputId: inputId,
fieldName,
isInvalid: !!(isInvalid || (!inputValue && isRequired)),
value: inputValue,
});
}, [inputId, inputId, fieldName, isInvalid, isRequired, inputValue]);
const placeholder = <Text color="gray.200">Please Add Input Here</Text>;
const slotView = <Slot {...FormItemCSS} slotsMap={slotsMap} slot="content" />;
return (
<FormControl
isRequired={isRequired}
isInvalid={!hideInvalid && (isInvalid || (!inputValue && isRequired))}
display="flex"
flexDirection="column"
alignItems="end"
className={css`
${customStyle?.content}
`}
>
<HStack width="full">
<FormLabel flex="0 0 auto" width="33%" margin="auto 0">
{label}
</FormLabel>
{inputId ? slotView : placeholder}
</HStack>
{errorMsg ? (
<FormErrorMessage {...FormItemCSS}>{errorMsg}</FormErrorMessage>
) : undefined}
{helperText ? (
<FormHelperText {...FormItemCSS}>{helperText}</FormHelperText>
) : undefined}
</FormControl>
);
};
const PropsSchema = Type.Object({
label: Type.String(),
fieldName: Type.String(),
@ -132,36 +25,134 @@ const PropsSchema = Type.Object({
helperText: Type.String(),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'formControl',
isResizable: false,
isDraggable: true,
displayName: 'Form Control',
description: 'chakra-ui formControl',
exampleProperties: {
label: 'name',
fieldName: 'name',
isRequired: false,
helperText: '',
},
exampleSize: [4, 2],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'formControl',
isResizable: false,
isDraggable: true,
displayName: 'Form Control',
description: 'chakra-ui formControl',
exampleProperties: {
label: 'name',
fieldName: 'name',
isRequired: false,
helperText: '',
},
spec: {
properties: PropsSchema,
state: Type.Object({
inputId: Type.String(),
fieldName: Type.String(),
isInvalid: Type.Boolean(),
value: Type.Any(),
}),
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
}),
impl: FormControlImpl,
};
exampleSize: [4, 2],
},
spec: {
properties: PropsSchema,
state: Type.Object({
inputId: Type.String(),
fieldName: Type.String(),
isInvalid: Type.Boolean(),
value: Type.Any(),
}),
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
})(
({
label,
fieldName,
isRequired,
helperText,
slotsMap,
mergeState,
services,
customStyle,
}) => {
const [inputValue, setInputValue] = useState('');
// don't show Invalid state on component mount
const [hideInvalid, setHideInvalid] = useState(true);
const inputId = useMemo(() => first(slotsMap?.get('content'))?.id || '', []);
const [validResult, setValidResult] = useState({
isInvalid: false,
errorMsg: '',
});
const { isInvalid, errorMsg } = validResult;
useEffect(() => {
if (!inputId) return;
const stop = watch(
() => {
const inputState = services.stateManager.store[inputId];
if (!inputState) return '';
if (inputState.checked !== undefined) {
// special treatment for checkbox
return (inputState as Static<typeof CheckboxStateSchema>).checked;
} else {
return inputState.value;
}
},
newV => {
setInputValue(newV);
}
);
setInputValue(services.stateManager.store[inputId].value);
return stop;
}, [inputId, setInputValue]);
useEffect(() => {
if (!inputId) return;
const stop = watch(
() => {
return services.stateManager.store[inputId]?.validResult;
},
newV => {
setValidResult(newV);
}
);
if (services.stateManager.store[inputId]?.validResult) {
setValidResult(services.stateManager.store[inputId].validResult);
}
return stop;
}, [inputId, setValidResult]);
useEffect(() => {
if (!inputId) return;
if (inputValue) {
// After inputValue first change, begin to show Invalid state
setHideInvalid(false);
}
mergeState({
inputId: inputId,
fieldName,
isInvalid: !!(isInvalid || (!inputValue && isRequired)),
value: inputValue,
});
}, [inputId, inputId, fieldName, isInvalid, isRequired, inputValue]);
const placeholder = <Text color="gray.200">Please Add Input Here</Text>;
const slotView = <Slot {...FormItemCSS} slotsMap={slotsMap} slot="content" />;
return (
<FormControl
isRequired={isRequired}
isInvalid={!hideInvalid && (isInvalid || (!inputValue && isRequired))}
display="flex"
flexDirection="column"
alignItems="end"
className={css`
${customStyle?.content}
`}
>
<HStack width="full">
<FormLabel flex="0 0 auto" width="33%" margin="auto 0">
{label}
</FormLabel>
{inputId ? slotView : placeholder}
</HStack>
{errorMsg ? (
<FormErrorMessage {...FormItemCSS}>{errorMsg}</FormErrorMessage>
) : undefined}
{helperText ? (
<FormHelperText {...FormItemCSS}>{helperText}</FormHelperText>
) : undefined}
</FormControl>
);
}
);

View File

@ -1,8 +1,7 @@
import { createComponent } from '@sunmao-ui/core';
import { css } from '@emotion/css';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { HStack as BaseHStack } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
import {
DirectionSchema,
FlexWrapSchema,
@ -11,15 +10,36 @@ import {
SpacingSchema,
} from './Stack';
const HStack: ComponentImplementation<Static<typeof PropsSchema>> = ({
direction,
wrap,
align,
justify,
spacing,
slotsMap,
customStyle,
}) => {
const PropsSchema = Type.Object({
direction: DirectionSchema,
wrap: FlexWrapSchema,
align: AlignItemsSchema,
justify: JustifyContentSchema,
spacing: SpacingSchema,
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'hstack',
description: 'chakra-ui hstack',
displayName: 'HStack',
exampleProperties: {
spacing: '24px',
},
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
slots: ['content'],
styleSlots: ['content'],
methods: {},
events: [],
},
})(({ direction, wrap, align, justify, spacing, slotsMap, customStyle }) => {
return (
<BaseHStack
height="full"
@ -37,33 +57,4 @@ const HStack: ComponentImplementation<Static<typeof PropsSchema>> = ({
<Slot slotsMap={slotsMap} slot="content" />
</BaseHStack>
);
};
const PropsSchema = Type.Object({
direction: DirectionSchema,
wrap: FlexWrapSchema,
align: AlignItemsSchema,
justify: JustifyContentSchema,
spacing: SpacingSchema,
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'hstack',
description: 'chakra-ui hstack',
displayName: 'HStack',
exampleProperties: {
spacing: '24px',
},
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
slots: ['content'],
styleSlots: ['content'],
},
}),
impl: HStack,
};

View File

@ -1,8 +1,7 @@
import { Image as BaseImage } from '@chakra-ui/react';
import { css } from '@emotion/css';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
const BoxSizePropertySchema = Type.Optional(
Type.Union([
@ -64,48 +63,6 @@ const BorderRadiusSchema = Type.Optional(
])
);
const Image: ComponentImplementation<Static<typeof PropsSchema>> = ({
boxSize,
src,
alt,
objectFit,
borderRadius,
fallbackSrc,
ignoreFallback,
htmlWidth,
htmlHeight,
crossOrigin,
callbackMap,
customStyle,
}) => {
const style = boxSize
? css`
${customStyle?.content}
`
: css`
height: 100%;
width: 100%;
${customStyle?.content}
`;
return (
<BaseImage
className={style}
src={src}
alt={alt}
objectFit={objectFit}
boxSize={boxSize}
onLoad={callbackMap?.onLoad}
htmlHeight={htmlHeight}
htmlWidth={htmlWidth}
crossOrigin={crossOrigin}
onError={callbackMap?.onError}
ignoreFallback={ignoreFallback}
borderRadius={borderRadius}
fallbackSrc={fallbackSrc}
></BaseImage>
);
};
const StateSchema = Type.Object({
value: Type.String(),
});
@ -130,32 +87,71 @@ const PropsSchema = Type.Object({
),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'image',
displayName: 'Image',
description: 'chakra_ui image',
isDraggable: true,
isResizable: true,
exampleProperties: {
src: 'https://bit.ly/dan-abramov',
alt: 'dan-abramov',
objectFit: 'cover',
borderRadius: 5,
fallbackSrc: 'https://via.placeholder.com/150',
},
exampleSize: [6, 6],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'image',
displayName: 'Image',
description: 'chakra_ui image',
isDraggable: true,
isResizable: true,
exampleProperties: {
src: 'https://bit.ly/dan-abramov',
alt: 'dan-abramov',
objectFit: 'cover',
borderRadius: 5,
fallbackSrc: 'https://via.placeholder.com/150',
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: ['onLoad', 'onError'],
},
}),
impl: Image,
};
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: ['onLoad', 'onError'],
},
})(
({
boxSize,
src,
alt,
objectFit,
borderRadius,
fallbackSrc,
ignoreFallback,
htmlWidth,
htmlHeight,
crossOrigin,
callbackMap,
customStyle,
}) => {
const style = boxSize
? css`
${customStyle?.content}
`
: css`
height: 100%;
width: 100%;
${customStyle?.content}
`;
return (
<BaseImage
className={style}
src={src}
alt={alt}
objectFit={objectFit}
boxSize={boxSize}
onLoad={callbackMap?.onLoad}
htmlHeight={htmlHeight}
htmlWidth={htmlWidth}
crossOrigin={crossOrigin}
onError={callbackMap?.onError}
ignoreFallback={ignoreFallback}
borderRadius={borderRadius}
fallbackSrc={fallbackSrc}
></BaseImage>
);
}
);

View File

@ -7,9 +7,8 @@ import {
InputRightAddon,
InputRightElement,
} from '@chakra-ui/react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const AppendElementPropertySchema = Type.Union([
@ -25,83 +24,6 @@ const AppendElementPropertySchema = Type.Union([
}),
]);
const Input: ComponentImplementation<Static<typeof PropsSchema>> = ({
variant,
placeholder,
size,
focusBorderColor,
isDisabled,
isRequired,
left,
right,
mergeState,
subscribeMethods,
defaultValue,
customStyle,
}) => {
const [value, setValue] = React.useState(defaultValue || ''); // TODO: pin input
const onChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setValue(event.target.value);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
setValue(defaultValue || '');
}, [defaultValue]);
useEffect(() => {
subscribeMethods({
setInputValue({ value }) {
setValue(value);
},
resetInputValue() {
setValue(defaultValue || '');
},
});
}, []);
return (
<InputGroup size={size} background="white">
{left ? (
left.type === 'addon' ? (
<InputLeftAddon>{left.children}</InputLeftAddon>
) : (
<InputLeftElement fontSize={left.fontSize} color={left.color}>
{left.children}
</InputLeftElement>
)
) : (
<></>
)}
<BaseInput
value={value}
variant={variant}
placeholder={placeholder}
focusBorderColor={focusBorderColor}
isDisabled={isDisabled}
isRequired={isRequired}
onChange={onChange}
className={css`
${customStyle?.content}
`}
/>
{right ? (
right.type === 'addon' ? (
<InputRightAddon>{right.children}</InputRightAddon>
) : (
<InputRightElement fontSize={right.fontSize} color={right.color}>
{right.children}
</InputRightElement>
)
) : (
<></>
)}
</InputGroup>
);
};
const StateSchema = Type.Object({
value: Type.String(),
});
@ -132,43 +54,112 @@ const PropsSchema = Type.Object({
defaultValue: Type.Optional(Type.String()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'input',
displayName: 'Input',
description: 'chakra_ui input',
isDraggable: true,
isResizable: true,
exampleProperties: {
variant: 'outline',
placeholder: 'Please input value',
size: 'md',
isDisabled: false,
isRequired: false,
defaultValue: '',
},
exampleSize: [4, 1],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'input',
displayName: 'Input',
description: 'chakra_ui input',
isDraggable: true,
isResizable: true,
exampleProperties: {
variant: 'outline',
placeholder: 'Please input value',
size: 'md',
isDisabled: false,
isRequired: false,
defaultValue: '',
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: [
{
name: 'setInputValue',
parameters: Type.Object({
value: Type.String(),
}),
},
{
name: 'resetInputValue',
},
],
slots: [],
styleSlots: ['content'],
events: [],
exampleSize: [4, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
setInputValue: Type.Object({
value: Type.String(),
}),
resetInputValue: void 0,
},
}),
impl: Input,
};
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
variant,
placeholder,
size,
focusBorderColor,
isDisabled,
isRequired,
left,
right,
mergeState,
subscribeMethods,
defaultValue,
customStyle,
}) => {
const [value, setValue] = React.useState(defaultValue || ''); // TODO: pin input
const onChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setValue(event.target.value);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
setValue(defaultValue || '');
}, [defaultValue]);
useEffect(() => {
subscribeMethods({
setInputValue({ value }) {
setValue(value);
},
resetInputValue() {
setValue(defaultValue || '');
},
});
}, []);
return (
<InputGroup size={size} background="white">
{left ? (
left.type === 'addon' ? (
<InputLeftAddon>{left.children}</InputLeftAddon>
) : (
<InputLeftElement fontSize={left.fontSize} color={left.color}>
{left.children}
</InputLeftElement>
)
) : (
<></>
)}
<BaseInput
value={value}
variant={variant}
placeholder={placeholder}
focusBorderColor={focusBorderColor}
isDisabled={isDisabled}
isRequired={isRequired}
onChange={onChange}
className={css`
${customStyle?.content}
`}
/>
{right ? (
right.type === 'addon' ? (
<InputRightAddon>{right.children}</InputRightAddon>
) : (
<InputRightElement fontSize={right.fontSize} color={right.color}>
{right.children}
</InputRightElement>
)
) : (
<></>
)}
</InputGroup>
);
}
);

View File

@ -1,15 +1,42 @@
import { useEffect } from 'react';
import { Kbd as BaseKbd } from '@chakra-ui/react';
import { Static, Type } from '@sinclair/typebox';
import { createComponent } from '@sunmao-ui/core';
import { ComponentImplementation, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const Kbd: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
mergeState,
customStyle,
}) => {
const StateSchema = Type.Object({
value: Type.String(),
});
const PropsSchema = Type.Object({
text: TextPropertySchema,
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'kbd',
displayName: 'Kbd',
description: 'chakra-ui keyboard',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'enter',
format: 'plain',
},
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(({ text, mergeState, customStyle }) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [text.raw]);
@ -23,41 +50,4 @@ const Kbd: ComponentImplementation<Static<typeof PropsSchema>> = ({
<Text value={text} />
</BaseKbd>
);
};
const StateSchema = Type.Object({
value: Type.String(),
});
const PropsSchema = Type.Object({
text: TextPropertySchema,
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'kbd',
displayName: 'Kbd',
description: 'chakra-ui keyboard',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'enter',
format: 'plain',
},
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: Kbd,
};

View File

@ -1,15 +1,40 @@
import { Link } from '@chakra-ui/react';
import { css } from '@emotion/css';
import { Static, Type } from '@sinclair/typebox';
import { createComponent } from '@sunmao-ui/core';
import { ComponentImplementation, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2, Text, TextPropertySchema } from '@sunmao-ui/runtime';
const LinkImpl: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
href,
isExternal,
customStyle,
}) => {
const PropsSchema = Type.Object({
text: TextPropertySchema,
href: Type.String(),
isExternal: Type.Optional(Type.Boolean()),
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'link',
displayName: 'Link',
description: 'chakra-ui link',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'link',
format: 'plain',
},
href: 'https://www.google.com',
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(({ text, href, isExternal, customStyle }) => {
return (
<Link
href={href}
@ -22,40 +47,4 @@ const LinkImpl: ComponentImplementation<Static<typeof PropsSchema>> = ({
<Text value={text} />
</Link>
);
};
const PropsSchema = Type.Object({
text: TextPropertySchema,
href: Type.String(),
isExternal: Type.Optional(Type.Boolean()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'link',
displayName: 'Link',
description: 'chakra-ui link',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'link',
format: 'plain',
},
href: 'https://www.google.com',
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: {},
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: LinkImpl,
};

View File

@ -1,8 +1,7 @@
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { List as BaseList, ListItem as BaseListItem } from '@chakra-ui/react';
import {
ComponentImplementation,
implementRuntimeComponent2,
LIST_ITEM_EXP,
LIST_ITEM_INDEX_EXP,
RuntimeModuleSchema,
@ -10,13 +9,51 @@ import {
} from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const List: ComponentImplementation<Static<typeof PropsSchema>> = ({
listData,
template,
app,
services,
customStyle,
}) => {
const PropsSchema = Type.Object({
listData: Type.Array(Type.Record(Type.String(), Type.String())),
template: RuntimeModuleSchema,
});
const exampleProperties = {
listData: [
{
id: '1',
name: 'Bowen Tan',
},
],
template: {
id: 'listItemName-{{$listItem.id}}',
type: 'core/v1/text',
properties: {
value: {
raw: 'Name{{$listItem.name}}',
format: 'plain',
},
},
traits: [],
},
};
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'list',
description: 'chakra-ui list',
displayName: 'List',
isDraggable: true,
isResizable: true,
exampleProperties,
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
methods: {},
state: Type.Object({}),
slots: [],
styleSlots: ['content'],
events: [],
},
})(({ listData, template, app, services, customStyle }) => {
if (!listData) {
return null;
}
@ -52,53 +89,4 @@ const List: ComponentImplementation<Static<typeof PropsSchema>> = ({
{listItems}
</BaseList>
);
};
const PropsSchema = Type.Object({
listData: Type.Array(Type.Record(Type.String(), Type.String())),
template: RuntimeModuleSchema,
});
const exampleProperties = {
listData: [
{
id: '1',
name: 'Bowen Tan',
},
],
template: {
id: 'listItemName-{{$listItem.id}}',
type: 'core/v1/text',
properties: {
value: {
raw: 'Name{{$listItem.name}}',
format: 'plain',
},
},
traits: [],
},
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'list',
description: 'chakra-ui list',
displayName: 'List',
isDraggable: true,
isResizable: true,
exampleProperties,
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
methods: {},
state: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: List,
};

View File

@ -1,58 +1,14 @@
import { useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Select as BaseMultiSelect } from 'chakra-react-select';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import { Box } from '@chakra-ui/react';
import { css } from '@emotion/css';
const StateSchema = Type.Object({
value: Type.String(),
value: Type.Array(Type.String()),
});
const MultiSelect: ComponentImplementation<Static<typeof PropsSchema>> = ({
options,
placeholder,
defaultValue,
isRequired,
isDisabled,
size,
variant,
mergeState,
customStyle,
}) => {
useEffect(() => {
const newValue = (defaultValue || []).map(o => o.value);
mergeState({ value: newValue });
}, []);
const onChange = (options: Static<typeof OptionsSchema>) => {
const newValue = options.map(o => o.value);
mergeState({ value: newValue });
};
return (
<Box
width="full"
className={css`
${customStyle?.content}
`}
>
<BaseMultiSelect
isMulti
options={options}
placeholder={placeholder}
isRequired={isRequired}
isDisabled={isDisabled}
size={size}
variant={variant}
onChange={onChange}
defaultValue={defaultValue}
/>
</Box>
);
};
const OptionsSchema = Type.Array(
Type.Object({
label: Type.String(),
@ -107,8 +63,7 @@ const exampleProperties = {
],
};
export default {
...createComponent({
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'multiSelect',
@ -124,9 +79,48 @@ export default {
state: StateSchema,
methods: {},
slots: [],
styleSlots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: MultiSelect,
};
})(({
options,
placeholder,
defaultValue,
isRequired,
isDisabled,
size,
variant,
mergeState,
customStyle,
}) => {
useEffect(() => {
const newValue = (defaultValue || []).map(o => o.value);
mergeState({ value: newValue });
}, []);
const onChange = (options: Static<typeof OptionsSchema>) => {
const newValue = options.map(o => o.value);
mergeState({ value: newValue });
};
return (
<Box
width="full"
className={css`
${customStyle?.content}
`}
>
<BaseMultiSelect
isMulti
options={options}
placeholder={placeholder}
isRequired={isRequired}
isDisabled={isDisabled}
size={size}
variant={variant}
onChange={onChange}
defaultValue={defaultValue}
/>
</Box>
);
})

View File

@ -6,74 +6,10 @@ import {
NumberIncrementStepper,
NumberDecrementStepper,
} from '@chakra-ui/react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const NumberInput: ComponentImplementation<Static<typeof PropsSchema>> = ({
defaultValue = 0,
min,
max,
step,
precision,
clampValueOnBlur = true,
allowMouseWheel = false,
size,
customerIncrement,
customerDecrement,
mergeState,
subscribeMethods,
customStyle,
}) => {
const [value, setValue] = useState(defaultValue);
const onChange = (_: string, valueAsNumber: number) => setValue(valueAsNumber || 0);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
setValue(defaultValue || 0);
}, [defaultValue]);
useEffect(() => {
subscribeMethods({
setInputValue({ value }) {
setValue(value);
},
resetInputValue() {
setValue(defaultValue);
},
});
}, []);
return (
<BaseNumberInput
background="white"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
step={step}
precision={precision}
clampValueOnBlur={clampValueOnBlur}
allowMouseWheel={allowMouseWheel}
size={size}
onChange={onChange}
className={css`
${customStyle?.content}
`}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper {...customerIncrement} />
<NumberDecrementStepper {...customerDecrement} />
</NumberInputStepper>
</BaseNumberInput>
);
};
const PropsSchema = Type.Object({
defaultValue: Type.Optional(Type.Number()),
min: Type.Optional(Type.Number()),
@ -106,38 +42,93 @@ const StateSchema = Type.Object({
value: Type.Number(),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'number_input',
description: 'chakra_ui number input',
displayName: 'Number Input',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: 0,
},
exampleSize: [4, 1],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'number_input',
description: 'chakra_ui number input',
displayName: 'Number Input',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: 0,
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: [
{
name: 'setInputValue',
parameters: Type.Object({
value: Type.Number(),
}),
},
{
name: 'resetInputValue',
},
],
slots: [],
styleSlots: ['content'],
events: [],
exampleSize: [4, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
setInputValue: Type.Object({
value: Type.Number(),
}),
resetInputValue: void 0,
},
}),
impl: NumberInput,
};
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
defaultValue = 0,
min,
max,
step,
precision,
clampValueOnBlur = true,
allowMouseWheel = false,
size,
customerIncrement,
customerDecrement,
mergeState,
subscribeMethods,
customStyle,
}) => {
const [value, setValue] = useState(defaultValue);
const onChange = (_: string, valueAsNumber: number) => setValue(valueAsNumber || 0);
useEffect(() => {
mergeState({ value });
}, [value]);
useEffect(() => {
setValue(defaultValue || 0);
}, [defaultValue]);
useEffect(() => {
subscribeMethods({
setInputValue({ value }) {
setValue(value);
},
resetInputValue() {
setValue(defaultValue);
},
});
}, []);
return (
<BaseNumberInput
background="white"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
step={step}
precision={precision}
clampValueOnBlur={clampValueOnBlur}
allowMouseWheel={allowMouseWheel}
size={size}
onChange={onChange}
className={css`
${customStyle?.content}
`}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper {...customerIncrement} />
<NumberDecrementStepper {...customerDecrement} />
</NumberInputStepper>
</BaseNumberInput>
);
}
);

View File

@ -1,60 +1,14 @@
import { useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Radio as BaseRadio } from '@chakra-ui/react';
import { ComponentImplementation, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Text, TextPropertySchema } from '@sunmao-ui/runtime';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
import { css } from '@emotion/css';
const StateSchema = Type.Object({
value: Type.String(),
value: Type.Union([Type.String(), Type.Number()]),
});
const Radio: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
value,
isDisabled,
isFocusable,
isInValid,
isReadOnly,
isRequired,
name,
size,
spacing,
colorScheme,
mergeState,
customStyle,
}) => {
useEffect(() => {
mergeState({ text: text.raw });
}, [text.raw]);
useEffect(() => {
mergeState({ value });
}, [value]);
return (
<BaseRadio
height="10"
value={value}
isDisabled={isDisabled}
isFocusable={isFocusable}
isInvalid={isInValid}
isReadOnly={isReadOnly}
isRequired={isRequired}
name={name}
size={size}
spacing={spacing}
colorScheme={colorScheme}
className={css`
${customStyle?.content}
`}
>
<Text value={text} />
</BaseRadio>
);
};
const PropsSchema = Type.Object({
text: TextPropertySchema,
value: Type.Union([Type.String(), Type.Number()]),
@ -75,8 +29,7 @@ const PropsSchema = Type.Object({
colorScheme: ColorSchemePropertySchema,
});
export default {
...createComponent({
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'radio',
@ -103,6 +56,47 @@ export default {
styleSlots: ['content'],
events: [],
},
}),
impl: Radio,
};
})(({
text,
value,
isDisabled,
isFocusable,
isInValid,
isReadOnly,
isRequired,
name,
size,
spacing,
colorScheme,
mergeState,
customStyle,
}) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [text.raw]);
useEffect(() => {
mergeState({ value });
}, [value]);
return (
<BaseRadio
height="10"
value={value}
isDisabled={isDisabled}
isFocusable={isFocusable}
isInvalid={isInValid}
isReadOnly={isReadOnly}
isRequired={isRequired}
name={name}
size={size}
spacing={spacing}
colorScheme={colorScheme}
className={css`
${customStyle?.content}
`}
>
<Text value={text} />
</BaseRadio>
);
})

View File

@ -1,21 +1,41 @@
import { useState, useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { RadioGroup as BaseRadioGroup } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const StateSchema = Type.Object({
value: Type.String(),
value: Type.Union([Type.String(), Type.Number()]),
});
const RadioGroup: ComponentImplementation<Static<typeof PropsSchema>> = ({
defaultValue,
isNumerical,
slotsMap,
mergeState,
customStyle,
}) => {
const PropsSchema = Type.Object({
defaultValue: Type.Union([Type.String(), Type.Number()]),
isNumerical: Type.Optional(Type.Boolean()),
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'radio_group',
displayName: 'RadioGroup',
description: 'chakra-ui radio group',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: 0,
isNumerical: true,
},
exampleSize: [3, 3],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
})(({ defaultValue, isNumerical, slotsMap, mergeState, customStyle }) => {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
@ -37,36 +57,4 @@ const RadioGroup: ComponentImplementation<Static<typeof PropsSchema>> = ({
<Slot slotsMap={slotsMap} slot="content" />
</BaseRadioGroup>
);
};
const PropsSchema = Type.Object({
defaultValue: Type.Union([Type.String(), Type.Number()]),
isNumerical: Type.Optional(Type.Boolean()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'radio_group',
displayName: 'RadioGroup',
description: 'chakra-ui radio group',
isDraggable: true,
isResizable: true,
exampleProperties: {
defaultValue: 0,
isNumerical: true,
},
exampleSize: [3, 3],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
}),
impl: RadioGroup,
};

View File

@ -1,8 +1,27 @@
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { createComponent } from '@sunmao-ui/core';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
const Root: ComponentImplementation<Record<string, unknown>> = ({ slotsMap }) => {
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'root',
displayName: 'Root',
description: 'chakra-ui provider',
isDraggable: false,
isResizable: true,
exampleProperties: {},
exampleSize: [6, 6],
},
spec: {
properties: Type.Object({}),
state: Type.Object({}),
methods: {},
slots: ['root'],
styleSlots: [],
events: [],
},
})(({ slotsMap }) => {
return (
<ChakraProvider
theme={extendTheme({
@ -13,28 +32,4 @@ const Root: ComponentImplementation<Record<string, unknown>> = ({ slotsMap }) =>
<Slot slotsMap={slotsMap} slot="root" />
</ChakraProvider>
);
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'root',
displayName: 'Root',
description: 'chakra-ui provider',
isDraggable: false,
isResizable: true,
exampleProperties: {},
exampleSize: [6, 6],
},
spec: {
properties: {},
state: {},
methods: {},
slots: ['root'],
styleSlots: [],
events: [],
},
}),
impl: Root,
};
});

View File

@ -1,66 +1,13 @@
import { useState, useEffect } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Select as BaseSelect } from '@chakra-ui/react';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import { css } from '@emotion/css';
const StateSchema = Type.Object({
value: Type.String(),
});
const Select: ComponentImplementation<Static<typeof PropsSchema>> = ({
options,
placeholder,
defaultValue,
errorBorderColor,
focusBorderColor,
isDisabled,
isInvalid,
isReadOnly,
isRequired,
size,
variant,
mergeState,
customStyle,
}) => {
const [value, setValue] = useState<string | undefined>(defaultValue);
useEffect(() => {
setValue(defaultValue);
}, [defaultValue]);
useEffect(() => {
mergeState({ value: value });
}, [value]);
return (
<BaseSelect
background="white"
placeholder={placeholder}
value={value}
errorBorderColor={errorBorderColor}
focusBorderColor={focusBorderColor}
isDisabled={isDisabled}
isInvalid={isInvalid}
isReadOnly={isReadOnly}
isRequired={isRequired}
size={size}
variant={variant}
onChange={e => setValue(e.target.value)}
className={css`
${customStyle?.content}
`}
>
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</BaseSelect>
);
};
const PropsSchema = Type.Object({
options: Type.Array(
Type.Object({
@ -111,26 +58,75 @@ const exampleProperties = {
],
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'select',
displayName: 'Select',
description: 'chakra-ui select',
isResizable: true,
isDraggable: true,
exampleProperties,
exampleSize: [4, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
}),
impl: Select,
};
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'select',
displayName: 'Select',
description: 'chakra-ui select',
isResizable: true,
isDraggable: true,
exampleProperties,
exampleSize: [4, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
slots: [],
styleSlots: ['content'],
events: [],
},
})(
({
options,
placeholder,
defaultValue,
errorBorderColor,
focusBorderColor,
isDisabled,
isInvalid,
isReadOnly,
isRequired,
size,
variant,
mergeState,
customStyle,
}) => {
const [value, setValue] = useState<string | undefined>(defaultValue);
useEffect(() => {
setValue(defaultValue);
}, [defaultValue]);
useEffect(() => {
mergeState({ value: value });
}, [value]);
return (
<BaseSelect
background="white"
placeholder={placeholder}
value={value}
errorBorderColor={errorBorderColor}
focusBorderColor={focusBorderColor}
isDisabled={isDisabled}
isInvalid={isInvalid}
isReadOnly={isReadOnly}
isRequired={isRequired}
size={size}
variant={variant}
onChange={e => setValue(e.target.value)}
className={css`
${customStyle?.content}
`}
>
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</BaseSelect>
);
}
);

View File

@ -1,7 +1,6 @@
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Stack as BaseStack } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
export const DirectionSchema = Type.Optional(
Type.Union([
@ -25,32 +24,19 @@ export const DirectionSchema = Type.Optional(
),
])
);
export const FlexWrapSchema = Type.Optional(Type.KeyOf(
Type.Object({
nowrap: Type.String(),
wrap: Type.String(),
'wrap-reverse': Type.String(),
})
));
export const FlexWrapSchema = Type.Optional(
Type.KeyOf(
Type.Object({
nowrap: Type.String(),
wrap: Type.String(),
'wrap-reverse': Type.String(),
})
)
);
export const AlignItemsSchema = Type.Optional(Type.String());
export const JustifyContentSchema = Type.Optional(Type.String());
export const SpacingSchema = Type.Optional(Type.Union([Type.String(), Type.Number()]));
const Stack: ComponentImplementation<Static<typeof PropsSchema>> = ({
direction,
wrap,
align,
justify,
spacing,
slotsMap,
}) => {
return (
<BaseStack {...{ direction, wrap, align, justify, spacing }}>
<Slot slotsMap={slotsMap} slot="content" />
</BaseStack>
);
};
const PropsSchema = Type.Object({
direction: DirectionSchema,
wrap: FlexWrapSchema,
@ -59,29 +45,32 @@ const PropsSchema = Type.Object({
spacing: SpacingSchema,
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'stack',
displayName: 'Stack',
description: 'chakra-ui stack',
isResizable: true,
isDraggable: true,
exampleProperties: {
direction: 'column',
spacing: 10,
},
exampleSize: [6, 6],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'stack',
displayName: 'Stack',
description: 'chakra-ui stack',
isResizable: true,
isDraggable: true,
exampleProperties: {
direction: 'column',
spacing: 10,
},
spec: {
properties: PropsSchema,
state: {},
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
}),
impl: Stack,
};
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
})(({ direction, wrap, align, justify, spacing, slotsMap }) => {
return (
<BaseStack {...{ direction, wrap, align, justify, spacing }}>
<Slot slotsMap={slotsMap} slot="content" />
</BaseStack>
);
});

View File

@ -11,227 +11,214 @@ import {
Box,
Spinner,
} from '@chakra-ui/react';
import { Static } from '@sinclair/typebox';
import { TablePagination } from './Pagination';
import { ComponentImplementation } from '@sunmao-ui/runtime';
import {
ColumnsPropertySchema,
DataPropertySchema,
IsMultiSelectPropertySchema,
MajorKeyPropertySchema,
RowsPerPagePropertySchema,
TableSizePropertySchema,
} from './TableTypes';
import { TableTd } from './TableTd';
import { implementTable } from './spec';
type SortRule = {
key: string;
desc: boolean;
};
export const TableImpl: ComponentImplementation<{
data?: Static<typeof DataPropertySchema>;
majorKey: Static<typeof MajorKeyPropertySchema>;
rowsPerPage: Static<typeof RowsPerPagePropertySchema>;
size: Static<typeof TableSizePropertySchema>;
columns: Static<typeof ColumnsPropertySchema>;
isMultiSelect: Static<typeof IsMultiSelectPropertySchema>;
}> = ({
data,
majorKey,
rowsPerPage,
size,
columns,
isMultiSelect,
mergeState,
services,
app,
}) => {
const [selectedItem, setSelectedItem] = useState<Record<string, any> | undefined>();
const [selectedItems, setSelectedItems] = useState<Array<Record<string, any>>>([]);
const [currentPage, setCurrentPage] = useState<number>(0);
const [sortRule, setSortRule] = useState<SortRule | undefined>();
const pageNumber = Math.ceil((data?.length || 0) / rowsPerPage);
export const TableImpl = implementTable(
({
data,
majorKey,
rowsPerPage,
size,
columns,
isMultiSelect,
mergeState,
services,
app,
}) => {
const [selectedItem, setSelectedItem] = useState<Record<string, any> | undefined>();
const [selectedItems, setSelectedItems] = useState<Array<Record<string, any>>>([]);
const [currentPage, setCurrentPage] = useState<number>(0);
const [sortRule, setSortRule] = useState<SortRule | undefined>();
const pageNumber = Math.ceil((data?.length || 0) / rowsPerPage);
useEffect(() => {
// reset table state when data source changes
updateSelectedItems([]);
updateSelectedItem(undefined);
setCurrentPage(0);
setSortRule(undefined);
}, [data]);
useEffect(() => {
// reset table state when data source changes
updateSelectedItems([]);
updateSelectedItem(undefined);
setCurrentPage(0);
setSortRule(undefined);
}, [data]);
const updateSelectedItems = (items: Array<Record<string, any>>) => {
setSelectedItems(items);
mergeState({ selectedItems: items });
};
const updateSelectedItem = (item?: Record<string, any>) => {
setSelectedItem(item);
mergeState({ selectedItem: item });
};
const sortedData = useMemo(() => {
if (!sortRule) return data;
const sorted = sortBy(data, sortRule.key);
return sortRule.desc ? sorted.reverse() : sorted;
}, [sortRule, data]);
const currentPageData = sortedData?.slice(
currentPage * rowsPerPage,
currentPage * rowsPerPage + rowsPerPage
);
function isItemSelected(target: any) {
if (isMultiSelect) {
return selectedItems.findIndex(item => item[majorKey] === target[majorKey]) > -1;
}
return selectedItem && selectedItem[majorKey] === target[majorKey];
}
function selectItem(item: any) {
if (isMultiSelect) {
let newSelectedItems;
if (isItemSelected(item)) {
newSelectedItems = selectedItems.filter(
selectedItem => selectedItem[majorKey] != item[majorKey]
);
} else {
newSelectedItems = selectedItems.concat(item);
}
updateSelectedItems(newSelectedItems);
}
updateSelectedItem(item);
}
const allCheckbox = useMemo(() => {
if (!data) return null;
const isAllChecked = isMultiSelect && selectedItems.length === data.length;
const isIndeterminate =
selectedItems.length > 0 && selectedItems.length < data.length;
const onChange = (e: any) => {
if (e.target.checked) {
updateSelectedItems(data);
} else {
updateSelectedItems([]);
}
const updateSelectedItems = (items: Array<Record<string, any>>) => {
setSelectedItems(items);
mergeState({ selectedItems: items });
};
return (
<Th paddingX="4" paddingY="2" width="10" key="allCheckbox">
<Checkbox
size="lg"
isIndeterminate={isIndeterminate}
checked={isAllChecked}
onChange={onChange}
></Checkbox>
</Th>
const updateSelectedItem = (item?: Record<string, any>) => {
setSelectedItem(item);
mergeState({ selectedItem: item });
};
const sortedData = useMemo(() => {
if (!sortRule) return data;
const sorted = sortBy(data, sortRule.key);
return sortRule.desc ? sorted.reverse() : sorted;
}, [sortRule, data]);
const currentPageData = sortedData?.slice(
currentPage * rowsPerPage,
currentPage * rowsPerPage + rowsPerPage
);
}, [selectedItems.length, data]);
const tableContent = (
<>
<BaseTable size={size || 'md'}>
<Thead>
<Tr height="10">
{isMultiSelect ? allCheckbox : undefined}
{columns.map(({ title, key }) => {
let sortArrow;
if (sortRule && sortRule.key === key) {
sortArrow = sortRule.desc ? '⬇️' : '⬆️';
}
function isItemSelected(target: any) {
if (isMultiSelect) {
return selectedItems.findIndex(item => item[majorKey] === target[majorKey]) > -1;
}
return selectedItem && selectedItem[majorKey] === target[majorKey];
}
const onClick = () => {
function selectItem(item: any) {
if (isMultiSelect) {
let newSelectedItems;
if (isItemSelected(item)) {
newSelectedItems = selectedItems.filter(
selectedItem => selectedItem[majorKey] != item[majorKey]
);
} else {
newSelectedItems = selectedItems.concat(item);
}
updateSelectedItems(newSelectedItems);
}
updateSelectedItem(item);
}
const allCheckbox = useMemo(() => {
if (!data) return null;
const isAllChecked = isMultiSelect && selectedItems.length === data.length;
const isIndeterminate =
selectedItems.length > 0 && selectedItems.length < data.length;
const onChange = (e: any) => {
if (e.target.checked) {
updateSelectedItems(data);
} else {
updateSelectedItems([]);
}
};
return (
<Th paddingX="4" paddingY="2" width="10" key="allCheckbox">
<Checkbox
size="lg"
isIndeterminate={isIndeterminate}
checked={isAllChecked}
onChange={onChange}
></Checkbox>
</Th>
);
}, [selectedItems.length, data]);
const tableContent = (
<>
<BaseTable size={size || 'md'}>
<Thead>
<Tr height="10">
{isMultiSelect ? allCheckbox : undefined}
{columns.map(({ title, key }) => {
let sortArrow;
if (sortRule && sortRule.key === key) {
setSortRule({ key, desc: !sortRule.desc });
} else {
setSortRule({ key, desc: true });
sortArrow = sortRule.desc ? '⬇️' : '⬆️';
}
const onClick = () => {
if (sortRule && sortRule.key === key) {
setSortRule({ key, desc: !sortRule.desc });
} else {
setSortRule({ key, desc: true });
}
};
return (
<Th paddingX="4" paddingY="2" key={key} onClick={onClick}>
{title}
{sortArrow}
</Th>
);
})}
</Tr>
</Thead>
<Tbody>
{currentPageData?.map((item, i) => {
const isSelected = isItemSelected(item);
let onClickToggle = true;
const onClickCheckbox = (e: React.MouseEvent<HTMLElement>) => {
// chakra-ui checkbox has a bug which will trigger onClick twice
// so here I stopPropagation one of every two events
// https://github.com/chakra-ui/chakra-ui/issues/2854
if (onClickToggle) {
e.stopPropagation();
}
onClickToggle = !onClickToggle;
};
const checkbox = (
<Td
paddingX="4"
paddingY="2"
width="10"
key="$checkbox"
onClick={onClickCheckbox}
>
<Checkbox size="lg" isChecked={isSelected}></Checkbox>
</Td>
);
return (
<Th paddingX="4" paddingY="2" key={key} onClick={onClick}>
{title}
{sortArrow}
</Th>
<Tr
key={item[majorKey]}
height="10"
bgColor={isSelected ? 'yellow.100' : undefined}
onClick={() => {
selectItem(item);
}}
>
{isMultiSelect ? checkbox : undefined}
{columns.map(column => (
<TableTd
index={i}
key={column.key}
item={item}
column={column}
onClickItem={() => selectItem(item)}
services={services}
app={app}
/>
))}
</Tr>
);
})}
</Tr>
</Thead>
<Tbody>
{currentPageData?.map((item, i) => {
const isSelected = isItemSelected(item);
let onClickToggle = true;
const onClickCheckbox = (e: React.MouseEvent<HTMLElement>) => {
// chakra-ui checkbox has a bug which will trigger onClick twice
// so here I stopPropagation one of every two events
// https://github.com/chakra-ui/chakra-ui/issues/2854
if (onClickToggle) {
e.stopPropagation();
}
onClickToggle = !onClickToggle;
};
const checkbox = (
<Td
paddingX="4"
paddingY="2"
width="10"
key="$checkbox"
onClick={onClickCheckbox}
>
<Checkbox size="lg" isChecked={isSelected}></Checkbox>
</Td>
);
</Tbody>
</BaseTable>
<TablePagination
pageNumber={pageNumber}
currentPage={currentPage}
onChange={v => setCurrentPage(v)}
/>
</>
);
return (
<Tr
key={item[majorKey]}
height="10"
bgColor={isSelected ? 'yellow.100' : undefined}
onClick={() => {
selectItem(item);
}}
>
{isMultiSelect ? checkbox : undefined}
{columns.map(column => (
<TableTd
index={i}
key={column.key}
item={item}
column={column}
onClickItem={() => selectItem(item)}
services={services}
app={app}
/>
))}
</Tr>
);
})}
</Tbody>
</BaseTable>
<TablePagination
pageNumber={pageNumber}
currentPage={currentPage}
onChange={v => setCurrentPage(v)}
/>
</>
);
const loadingSpinner = (
<Box display="flex" height="full">
<Spinner size="xl" margin="auto" />
</Box>
);
const loadingSpinner = (
<Box display="flex" height="full">
<Spinner size="xl" margin="auto" />
</Box>
);
return (
<Box
width="full"
height="full"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="base"
overflow="auto"
>
{!data ? loadingSpinner : tableContent}
</Box>
);
};
return (
<Box
width="full"
height="full"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="base"
overflow="auto"
>
{!data ? loadingSpinner : tableContent}
</Box>
);
}
);

View File

@ -1,64 +1,3 @@
import { createComponent } from '@sunmao-ui/core';
import { Type } from '@sinclair/typebox';
import { TableImpl } from './Table';
import {
ColumnsPropertySchema,
DataPropertySchema,
MajorKeyPropertySchema,
RowsPerPagePropertySchema,
TableStateSchema,
TableSizePropertySchema,
IsMultiSelectPropertySchema,
} from './TableTypes';
const PropsSchema = Type.Object({
data: DataPropertySchema,
majorKey: MajorKeyPropertySchema,
rowsPerPage: RowsPerPagePropertySchema,
size: TableSizePropertySchema,
columns: ColumnsPropertySchema,
isMultiSelect: IsMultiSelectPropertySchema,
});
const exampleProperties = {
data: [
{
id: '1',
name: 'Bowen Tan',
},
],
columns: [
{
key: 'name',
title: 'Name',
type: 'text',
},
],
majorKey: 'id',
rowsPerPage: 5,
isMultiSelect: false,
};
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'table',
displayName: 'Table',
description: 'chakra-ui table',
isDraggable: true,
isResizable: true,
exampleProperties,
exampleSize: [8, 6],
},
spec: {
properties: PropsSchema,
state: TableStateSchema,
methods: {},
slots: [],
styleSlots: [],
events: [],
},
}),
impl: TableImpl,
};
export default TableImpl

View File

@ -0,0 +1,61 @@
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2 } from '@sunmao-ui/runtime';
import {
ColumnsPropertySchema,
DataPropertySchema,
MajorKeyPropertySchema,
RowsPerPagePropertySchema,
TableStateSchema,
TableSizePropertySchema,
IsMultiSelectPropertySchema,
} from './TableTypes';
const PropsSchema = Type.Object({
data: DataPropertySchema,
majorKey: MajorKeyPropertySchema,
rowsPerPage: RowsPerPagePropertySchema,
size: TableSizePropertySchema,
columns: ColumnsPropertySchema,
isMultiSelect: IsMultiSelectPropertySchema,
});
const exampleProperties = {
data: [
{
id: '1',
name: 'Bowen Tan',
},
],
columns: [
{
key: 'name',
title: 'Name',
type: 'text',
},
],
majorKey: 'id',
rowsPerPage: 5,
isMultiSelect: false,
};
export const implementTable = implementRuntimeComponent2({
kind: 'Component',
version: 'chakra_ui/v1',
metadata: {
name: 'table',
displayName: 'Table',
description: 'chakra-ui table',
isDraggable: true,
isResizable: true,
exampleProperties,
exampleSize: [8, 6],
},
spec: {
properties: PropsSchema,
state: TableStateSchema,
methods: {},
slots: [],
styleSlots: [],
events: [],
},
})

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import { css } from '@emotion/css';
import { createComponent } from '@sunmao-ui/core';
import {
Tabs as BaseTabs,
TabList,
@ -9,16 +8,42 @@ import {
TabPanel,
Text,
} from '@chakra-ui/react';
import { Type, Static } from '@sinclair/typebox';
import { ComponentImplementation, getSlots } from '@sunmao-ui/runtime';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent2, getSlots } from '@sunmao-ui/runtime';
const Tabs: ComponentImplementation<Static<typeof PropsSchema>> = ({
tabNames,
mergeState,
initialSelectedTabIndex,
slotsMap,
customStyle,
}) => {
const StateSchema = Type.Object({
selectedTabIndex: Type.Number(),
});
const PropsSchema = Type.Object({
tabNames: Type.Array(Type.String()),
initialSelectedTabIndex: Type.Optional(Type.Number()),
});
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'tabs',
displayName: 'Tabs',
description: 'chakra-ui tabs',
isDraggable: true,
isResizable: true,
exampleProperties: {
tabNames: [],
initialSelectedTabIndex: 0,
},
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
// tab slot is dynamic
slots: ['content'],
styleSlots: ['tabItem', 'tabContent'],
events: [],
},
})(({ tabNames, mergeState, initialSelectedTabIndex, slotsMap, customStyle }) => {
const [selectedTabIndex, setSelectedTabIndex] = useState(initialSelectedTabIndex ?? 0);
useEffect(() => {
@ -62,41 +87,4 @@ const Tabs: ComponentImplementation<Static<typeof PropsSchema>> = ({
</TabPanels>
</BaseTabs>
);
};
const StateSchema = Type.Object({
selectedTabIndex: Type.Number(),
});
const PropsSchema = Type.Object({
tabNames: Type.Array(Type.String()),
initialSelectedTabIndex: Type.Optional(Type.Number()),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'tabs',
displayName: 'Tabs',
description: 'chakra-ui tabs',
isDraggable: true,
isResizable: true,
exampleProperties: {
tabNames: [],
initialSelectedTabIndex: 0,
},
exampleSize: [6, 6],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {},
// tab slot is dynamic
slots: ['content'],
styleSlots: ['tabItem', 'tabContent'],
events: [],
},
}),
impl: Tabs,
};

View File

@ -1,38 +1,8 @@
import { createComponent } from '@sunmao-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { Tooltip } from '@chakra-ui/react';
import { ComponentImplementation, Slot, TextPropertySchema } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot, TextPropertySchema } from '@sunmao-ui/runtime';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
const TooltipImpl: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
shouldWrapChildren,
placement = 'auto',
isOpen,
hasArrow,
isDisabled,
defaultIsOpen,
slotsMap,
}) => {
return (
/*
Chakra tooltip requires children to be created by forwardRef.
If not, should add shouldWrapChildren.
*/
<Tooltip
label={text}
placement={placement}
isOpen={isOpen}
hasArrow={hasArrow}
isDisabled={isDisabled}
defaultIsOpen={defaultIsOpen}
shouldWrapChildren={shouldWrapChildren}
>
<Slot slotsMap={slotsMap} slot="content" />
</Tooltip>
);
};
const PropsSchema = Type.Object({
text: TextPropertySchema,
colorScheme: ColorSchemePropertySchema,
@ -64,28 +34,54 @@ const PropsSchema = Type.Object({
),
});
export default {
...createComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'tooltip',
description: 'chakra-ui tooltip',
displayName: 'Tooltip',
isDraggable: false,
isResizable: false,
exampleProperties: {
text: 'tooltip',
},
exampleSize: [2, 1],
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'tooltip',
description: 'chakra-ui tooltip',
displayName: 'Tooltip',
isDraggable: false,
isResizable: false,
exampleProperties: {
text: 'tooltip',
},
spec: {
properties: PropsSchema,
state: {},
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
}),
impl: TooltipImpl,
};
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
methods: {},
slots: ['content'],
styleSlots: [],
events: [],
},
})(
({
text,
shouldWrapChildren,
placement = 'auto',
isOpen,
hasArrow,
isDisabled,
defaultIsOpen,
slotsMap,
}) => {
return (
/*
Chakra tooltip requires children to be created by forwardRef.
If not, should add shouldWrapChildren.
*/
<Tooltip
label={text}
placement={placement}
isOpen={isOpen}
hasArrow={hasArrow}
isDisabled={isDisabled}
defaultIsOpen={defaultIsOpen}
shouldWrapChildren={shouldWrapChildren}
>
<Slot slotsMap={slotsMap} slot="content" />
</Tooltip>
);
}
);

View File

@ -1,8 +1,7 @@
import { createComponent } from '@sunmao-ui/core';
import { css } from '@emotion/css';
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import { VStack as BaseVStack } from '@chakra-ui/react';
import { ComponentImplementation, Slot } from '@sunmao-ui/runtime';
import { implementRuntimeComponent2, Slot } from '@sunmao-ui/runtime';
import {
DirectionSchema,
FlexWrapSchema,
@ -11,34 +10,6 @@ import {
SpacingSchema,
} from './Stack';
const VStack: ComponentImplementation<Static<typeof PropsSchema>> = ({
direction,
wrap,
align,
justify,
spacing,
slotsMap,
customStyle,
}) => {
return (
<BaseVStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
className={css`
${customStyle?.content}
`}
{...{ direction, wrap, align, justify, spacing }}
>
<Slot slotsMap={slotsMap} slot="content" />
</BaseVStack>
);
};
const PropsSchema = Type.Object({
direction: DirectionSchema,
wrap: FlexWrapSchema,
@ -47,20 +18,51 @@ const PropsSchema = Type.Object({
spacing: SpacingSchema,
});
export default {
...createComponent({
export default implementRuntimeComponent2({
version: 'chakra_ui/v1',
metadata: {
name: 'vstack',
displayName: 'VStack',
description: 'chakra-ui vstack',
exampleProperties: {
spacing: '24px',
},
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
slots: ['content'],
styleSlots: ['content'],
methods: {},
events: [],
},
}),
impl: VStack,
};
})(({
direction,
wrap,
align,
justify,
spacing,
slotsMap,
customStyle,
}) => {
return (
<BaseVStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
className={css`
${customStyle?.content}
`}
{...{ direction, wrap, align, justify, spacing }}
>
<Slot slotsMap={slotsMap} slot="content" />
</BaseVStack>
);
})

View File

@ -23,6 +23,7 @@ export function initSunmaoUI(dependencies = {}) {
export * from './utils/parseType';
export * from './utils/parseTypeBox';
export * from './utils/buildKit';
export * from './utils/encodeDragDataTransfer';
export * from './types/RuntimeSchema';
export * from './types/TraitPropertiesSchema';

View File

@ -55,7 +55,7 @@ export type ImplementedRuntimeModule = RuntimeModuleSpec & {
};
export type SunmaoLib = {
components?: ImplementedRuntimeComponent[];
components?: ImplementedRuntimeComponent2<string, string, string, string>[];
traits?: ImplementedRuntimeTrait[];
modules?: ImplementedRuntimeModule[];
};

View File

@ -66,7 +66,7 @@ export type CallbackMap<K extends string> = Record<K, () => void>;
export type SubscribeMethods<U> = (map: {
[K in keyof U]: (parameters: U[K]) => void;
}) => void;
export type MergeState<T> = (partialState: T) => void;
export type MergeState<T> = (partialState: Partial<T>) => void;
type RuntimeFunctions<TState, TMethods> = {
mergeState: MergeState<TState>;

View File

@ -4,7 +4,7 @@ import {
CreateComponentOptions2,
RuntimeComponentSpec2,
} from '@sunmao-ui/core';
import { ComponentImplementation } from 'src/services/registry';
import { ComponentImplementation } from '../services/registry';
export type ImplementedRuntimeComponent2<
KMethodName extends string,