Skip to content

Commit

Permalink
[duoyun-ui] Improve <dy-pat-form>
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Mar 6, 2024
1 parent c88c45a commit 3e5796b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 42 deletions.
2 changes: 1 addition & 1 deletion packages/duoyun-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "duoyun-ui",
"version": "1.1.18",
"version": "1.1.19",
"description": "A lightweight desktop UI component library, implemented using Gem",
"keywords": [
"frontend",
Expand Down
11 changes: 7 additions & 4 deletions packages/duoyun-ui/src/elements/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ export class DuoyunSelectElement extends GemElement<State> implements BasePicker

#onSearch = (evt: CustomEvent<string>) => {
this.setState({ search: evt.detail, open: true });
this.search(evt.detail);
evt.stopPropagation();
};

Expand Down Expand Up @@ -358,9 +357,9 @@ export class DuoyunSelectElement extends GemElement<State> implements BasePicker
);
this.effect(
() => {
if (this.state.open && !this.searchable && !this.inline) {
const restoreInert = setBodyInert(this.optionsRef.element!);
this.optionsRef.element?.focus();
if (this.state.open && !this.searchable && !this.inline && this.optionsRef.element) {
const restoreInert = setBodyInert(this.optionsRef.element);
this.optionsRef.element.focus();
return () => {
restoreInert();
this.focus();
Expand All @@ -369,6 +368,10 @@ export class DuoyunSelectElement extends GemElement<State> implements BasePicker
},
() => [this.state.open],
);
this.effect(
([search]) => this.search(search),
() => [this.state.search],
);
};

#getOptions = () => {
Expand Down
98 changes: 62 additions & 36 deletions packages/duoyun-ui/src/patterns/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type FormItemProps<T = unknown> = {
field: keyof T | string[];

options?: DuoyunFormItemElement['options'];
getOptions?: (input: string) => Promise<DuoyunFormItemElement['options']>;
multiple?: boolean;
placeholder?: string;
searchable?: boolean;
Expand All @@ -29,6 +30,8 @@ type FormItemProps<T = unknown> = {
rules?: DuoyunFormItemElement['rules'];

slot?: TemplateResult | HTMLElement | HTMLElement[];

isHidden?: (data: T) => boolean;
};

export type FormItem<T = unknown> =
Expand Down Expand Up @@ -65,24 +68,37 @@ const style = createCSSSheet(css`
}
`);

type OptionsRecord = {
loading: boolean;
options?: DuoyunFormItemElement['options'];
};

type State<T> = {
data: T;
optionsRecord: Partial<Record<string, OptionsRecord>>;
};

/**
* @customElement dy-pat-form
*/
@customElement('dy-pat-form')
@adoptedStyle(blockContainer)
@adoptedStyle(focusStyle)
@adoptedStyle(style)
export class DyPatFormElement<T = Record<string, unknown>> extends GemElement<T> {
export class DyPatFormElement<T = Record<string, unknown>> extends GemElement<State<T>> {
@refobject formRef: RefObject<DuoyunFormElement>;

@property data?: T;
@property formItems?: FormItem<T>[];

state: T = {} as T;
state: State<T> = {
data: {} as T,
optionsRecord: {},
};

#onChange = ({ detail }: CustomEvent<any>) => {
this.setState(
Object.keys(detail).reduce((p, c) => {
this.setState({
data: Object.keys(detail).reduce((p, c) => {
const keys = c.split(',');
if (keys.length === 1) {
p[c] = detail[c];
Expand All @@ -92,37 +108,47 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement<T>
a[lastKey] = detail[c];
}
return p;
}, this.state as any),
);
}, this.state.data as any),
});
};

#onOptionsChange = async <T>(props: FormItemProps<T>, input: string) => {
if (!props.getOptions) return;
const options = (this.state.optionsRecord[String(props.field)] ||= {} as OptionsRecord);
options.loading = true;
this.update();
try {
options.options = await props.getOptions(input);
} finally {
options.loading = false;
this.update();
}
};

#renderItem = <T>({
label,
field,
type,
clearable,
multiple,
placeholder,
required,
rules,
options,
searchable,
slot,
}: FormItemProps<T>) => {
#renderItem = <T>(props: FormItemProps<T>) => {
const { optionsRecord, data } = this.state;
const name = String(props.field);
const onChange = (evt: CustomEvent) => props.type === 'text' && this.#onOptionsChange(props, evt.detail);
const onSearch = (evt: CustomEvent) => props.type === 'select' && this.#onOptionsChange(props, evt.detail);
return html`
<dy-form-item
.label=${label}
.value=${readProp(this.state!, field)}
.name=${String(field)}
.type=${type}
.placeholder=${placeholder || ''}
.rules=${rules}
.options=${options}
?multiple=${multiple}
?clearable=${clearable}
?searchable=${searchable}
?required=${required}
>${slot}</dy-form-item
?hidden=${props.isHidden?.(data as unknown as T)}
.label=${props.label}
.value=${readProp(this.state.data!, props.field)}
.name=${name}
.type=${props.type}
.placeholder=${props.placeholder || ''}
.rules=${props.rules}
.loading=${!!optionsRecord[name]?.loading}
.options=${props.options || optionsRecord[name]?.options}
?multiple=${props.multiple}
?clearable=${props.clearable}
?searchable=${props.searchable || !!props.getOptions}
?required=${props.required}
@change=${onChange}
@clear=${onChange}
@search=${onSearch}
>${props.slot}</dy-form-item
>
`;
};
Expand All @@ -141,7 +167,7 @@ export class DyPatFormElement<T = Record<string, unknown>> extends GemElement<T>
this.memo(
() => {
if (this.data) {
this.state = structuredClone(this.data);
this.state.data = structuredClone(this.data);
}
},
() => [this.data],
Expand Down Expand Up @@ -194,17 +220,17 @@ export function createForm<T = Record<string, unknown>>(options: CreateFormOptio
.data=${options.data}
></dy-pat-form>
`,
prepareClose: (ele) => options.prepareClose?.(ele.state),
prepareClose: (ele) => options.prepareClose?.(ele.state.data),
prepareOk: async (ele) => {
const valid = await ele.valid();
if (!valid) throw null;
await waitLoading(options.prepareOk?.(ele.state));
await waitLoading(options.prepareOk?.(ele.state.data));
await DuoyunWaitElement.instance?.removed;
},
})
.then((ele) => ele.state)
.then((ele) => ele.state.data)
.catch((ele) => {
throw ele.state;
throw ele.state.data;
})
.finally(() => {
if (options.query) {
Expand Down
9 changes: 9 additions & 0 deletions packages/gem-examples/src/console/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,20 @@ export class ConsolePageItemElement extends GemElement {
field: 'username',
label: 'Username',
required: true,
async getOptions(input) {
await sleep(300);
return Array(4)
.fill(null)
.map((_, index) => ({ label: `${input}-${index}` }));
},
},
{
type: 'text',
field: 'name',
label: 'Name',
isHidden(data) {
return !data.username;
},
},
],
{
Expand Down
2 changes: 1 addition & 1 deletion packages/gem-port/src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function createReactSourceFile(elementFilePath: string, outDir: string) {
.join(';\n')}
}
const ${componentName}: ForwardRefExoticComponent<Omit<${componentPropsName}, "ref"> & RefAttributes<${componentMethodsName}>> = forwardRef<${componentMethodsName}, ${componentPropsName}>(function (props, ref): JSX.Element {
export const ${componentName}: ForwardRefExoticComponent<Omit<${componentPropsName}, "ref"> & RefAttributes<${componentMethodsName}>> = forwardRef<${componentMethodsName}, ${componentPropsName}>(function (props, ref): JSX.Element {
const elementRef = useRef<${constructorName}>(null);
useImperativeHandle(ref, () => {
return {
Expand Down

0 comments on commit 3e5796b

Please sign in to comment.