mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
refactor(core-blocks): form context and type improvements
This commit is contained in:
@@ -7,12 +7,13 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const styles = css`
|
||||
checkbox {
|
||||
@@ -57,21 +58,20 @@ type CheckboxType = {
|
||||
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): JSX.Element;
|
||||
}
|
||||
|
||||
export const Checkbox: CheckboxType = observer(function Checkbox(
|
||||
props: ControlledProps | ObjectProps<any, any>
|
||||
) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
state,
|
||||
checkboxLabel,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
long,
|
||||
onChange,
|
||||
...rest
|
||||
} = props;
|
||||
export const Checkbox: CheckboxType = observer(function Checkbox({
|
||||
name,
|
||||
value,
|
||||
state,
|
||||
checkboxLabel,
|
||||
checked: checkedControlled,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
long,
|
||||
onChange,
|
||||
...rest
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (state) {
|
||||
@@ -80,9 +80,12 @@ export const Checkbox: CheckboxType = observer(function Checkbox(
|
||||
if (onChange) {
|
||||
onChange(event.target.checked, name);
|
||||
}
|
||||
}, [state, name, onChange]);
|
||||
if (context) {
|
||||
context.onChange(event.target.checked, name);
|
||||
}
|
||||
}, [state, name, onChange, context]);
|
||||
|
||||
const checked = state ? state[name] : props.checked;
|
||||
const checked = state ? state[name] : checkedControlled;
|
||||
|
||||
return styled(useStyles(baseFormControlStyles, styles))(
|
||||
<field as="div" className={className} {...use({ long })}>
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import {
|
||||
useLayoutEffect, useCallback, useState, useRef
|
||||
useLayoutEffect, useCallback, useState, useRef, useContext
|
||||
} from 'react';
|
||||
import {
|
||||
useMenuState,
|
||||
@@ -22,6 +23,7 @@ import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { Icon } from '../Icons/Icon';
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
@@ -84,21 +86,43 @@ const styles = composes(
|
||||
`
|
||||
);
|
||||
|
||||
type Props<T> = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSelect'> & {
|
||||
value: string | undefined;
|
||||
type BaseProps<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSelect' | 'name' | 'value'> & {
|
||||
propertyName?: string;
|
||||
items: T[];
|
||||
items: TValue[];
|
||||
mod?: 'surface';
|
||||
long?: boolean;
|
||||
keySelector(item: T): string;
|
||||
valueSelector(item: T): string;
|
||||
onChange?(value: string): any;
|
||||
onSelect(value: T | null): void;
|
||||
keySelector(item: TValue): TKey;
|
||||
valueSelector(item: TValue): string;
|
||||
onSwitch?(state: boolean): void;
|
||||
}
|
||||
|
||||
export function Combobox<T>({
|
||||
value,
|
||||
type ControlledProps<TKey, TValue> = BaseProps<TKey, TValue> & {
|
||||
name?: string;
|
||||
value?: string;
|
||||
onSelect?(value: TKey, name: string | undefined, prev: TKey): void;
|
||||
onChange?(value: string, name: string | undefined): any;
|
||||
|
||||
state?: never;
|
||||
}
|
||||
|
||||
type ObjectProps<TValue, TKey extends keyof TState, TState> = BaseProps<TState[TKey], TValue> & {
|
||||
name: TKey;
|
||||
state: TState;
|
||||
onSelect?(value: TState[TKey], name: TKey | undefined, prev: TState[TKey]): void;
|
||||
onChange?(value: string, name: TKey | undefined): any;
|
||||
|
||||
value?: never;
|
||||
}
|
||||
|
||||
type ComboboxType = {
|
||||
<TKey, TValue>(props: ControlledProps<TKey, TValue>): JSX.Element;
|
||||
<TValue, TKey extends keyof TState, TState>(props: ObjectProps<TValue, TKey, TState>): JSX.Element;
|
||||
}
|
||||
|
||||
export const Combobox: ComboboxType = observer(function Combobox({
|
||||
value: controlledValue,
|
||||
name,
|
||||
state,
|
||||
propertyName,
|
||||
items,
|
||||
children,
|
||||
@@ -106,13 +130,14 @@ export function Combobox<T>({
|
||||
mod,
|
||||
long,
|
||||
readOnly,
|
||||
keySelector = v => v as any,
|
||||
valueSelector = v => v as any,
|
||||
keySelector = v => v,
|
||||
valueSelector = v => v,
|
||||
onChange = () => {},
|
||||
onSelect,
|
||||
onSwitch,
|
||||
...rest
|
||||
}: Props<T>) {
|
||||
}: ControlledProps<any, any> | ObjectProps<any, any, any>) {
|
||||
const context = useContext(FormContext);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const menu = useMenuState({
|
||||
placement: 'bottom-end',
|
||||
@@ -120,32 +145,49 @@ export function Combobox<T>({
|
||||
gutter: 4,
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const value = state ? state[name] : controlledValue;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onChange(value);
|
||||
onChange(value, name);
|
||||
setSearchValue(value);
|
||||
},
|
||||
[onChange]
|
||||
[name, onChange]
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
() => {
|
||||
menu.hide();
|
||||
onSelect(null);
|
||||
if (state) {
|
||||
state[name] = null;
|
||||
}
|
||||
if (onSelect) {
|
||||
onSelect(null, name, value);
|
||||
}
|
||||
if (context) {
|
||||
context.onChange(null, name);
|
||||
}
|
||||
setSearchValue('');
|
||||
},
|
||||
[menu, onSelect]
|
||||
[value, state, name, menu, context, onSelect]
|
||||
);
|
||||
|
||||
const handleMenuSelect = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
menu.hide();
|
||||
const id = event.currentTarget.id;
|
||||
onSelect(items.find(item => keySelector(item) === id)!);
|
||||
if (state) {
|
||||
state[name] = id;
|
||||
}
|
||||
if (onSelect) {
|
||||
onSelect(id, name, value);
|
||||
}
|
||||
if (context) {
|
||||
context.onChange(id, name);
|
||||
}
|
||||
},
|
||||
[menu, items, onSelect]
|
||||
[value, state, name, menu, context, onSelect]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => onSwitch && onSwitch(menu.visible), [onSwitch, menu.visible]);
|
||||
@@ -165,6 +207,7 @@ export function Combobox<T>({
|
||||
<input-box as="div">
|
||||
<input
|
||||
ref={ref}
|
||||
name={name}
|
||||
onChange={handleChange}
|
||||
value={selectedItem ? valueSelector(selectedItem) : searchValue}
|
||||
readOnly={!!selectedItem || readOnly}
|
||||
@@ -194,4 +237,4 @@ export function Combobox<T>({
|
||||
</input-box>
|
||||
</field>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface IFormContext {
|
||||
onChange(value: string | number | boolean | null | undefined, name: string | undefined): void;
|
||||
}
|
||||
|
||||
export const FormContext = createContext<IFormContext | null>(null);
|
||||
@@ -7,12 +7,13 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value'> & {
|
||||
mod?: 'surface';
|
||||
@@ -40,20 +41,19 @@ type InputFieldType = {
|
||||
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): JSX.Element;
|
||||
}
|
||||
|
||||
export const InputField: InputFieldType = observer(function InputField(
|
||||
props: ControlledProps | ObjectProps<any, any>
|
||||
) {
|
||||
const {
|
||||
name,
|
||||
required,
|
||||
state,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
long,
|
||||
onChange,
|
||||
...rest
|
||||
} = props;
|
||||
export const InputField: InputFieldType = observer(function InputField({
|
||||
name,
|
||||
value: valueControlled,
|
||||
required,
|
||||
state,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
long,
|
||||
onChange,
|
||||
...rest
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (state) {
|
||||
@@ -62,9 +62,12 @@ export const InputField: InputFieldType = observer(function InputField(
|
||||
if (onChange) {
|
||||
onChange(event.target.value, name);
|
||||
}
|
||||
}, [state, name, onChange]);
|
||||
if (context) {
|
||||
context.onChange(event.target.value, name);
|
||||
}
|
||||
}, [state, name, context, onChange]);
|
||||
|
||||
const value = state ? state[name] : props.value;
|
||||
const value = state ? state[name] : valueControlled;
|
||||
|
||||
return styled(useStyles(baseFormControlStyles))(
|
||||
<field as="div" className={className} {...use({ long })}>
|
||||
|
||||
@@ -6,14 +6,19 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useCallback, forwardRef } from 'react';
|
||||
import { useCallback, forwardRef, useMemo } from 'react';
|
||||
|
||||
type FormDetailedProps = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange'> & {
|
||||
onChange?(value: string | number | boolean, name: string | undefined): void;
|
||||
}
|
||||
|
||||
export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(function SubmittingForm(
|
||||
{
|
||||
children,
|
||||
onSubmit,
|
||||
onChange = () => {},
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
@@ -25,9 +30,13 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
|
||||
}
|
||||
}, [onSubmit]);
|
||||
|
||||
const context = useMemo(() => ({ onChange }), [onChange]);
|
||||
|
||||
return (
|
||||
<form {...rest} onSubmit={handleSubmit} ref={ref}>
|
||||
{children}
|
||||
<FormContext.Provider value={context}>
|
||||
{children}
|
||||
</FormContext.Provider>
|
||||
<button type="submit" hidden />
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const switchStyles = composes(
|
||||
css`
|
||||
@@ -96,21 +97,22 @@ type SwitchType = {
|
||||
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): JSX.Element;
|
||||
}
|
||||
|
||||
export const Switch: SwitchType = observer(function Switch(props: ControlledProps | ObjectProps<any, any>) {
|
||||
const {
|
||||
name,
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
state,
|
||||
className,
|
||||
children,
|
||||
onChange,
|
||||
mod = [],
|
||||
long,
|
||||
disabled,
|
||||
...rest
|
||||
} = props;
|
||||
export const Switch: SwitchType = observer(function Switch({
|
||||
name,
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
state,
|
||||
checked: checkedControlled,
|
||||
className,
|
||||
children,
|
||||
onChange,
|
||||
mod = [],
|
||||
long,
|
||||
disabled,
|
||||
...rest
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (state) {
|
||||
@@ -119,9 +121,12 @@ export const Switch: SwitchType = observer(function Switch(props: ControlledProp
|
||||
if (onChange) {
|
||||
onChange(event.target.checked, name);
|
||||
}
|
||||
}, [state, name, onChange]);
|
||||
if (context) {
|
||||
context.onChange(event.target.checked, name);
|
||||
}
|
||||
}, [state, name, context, onChange]);
|
||||
|
||||
const checked = state ? state[name] : props.checked;
|
||||
const checked = state ? state[name] : checkedControlled;
|
||||
|
||||
return styled(useStyles(
|
||||
baseFormControlStyles,
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { baseFormControlStyles } from './baseFormControlStyles';
|
||||
import { FormContext } from './FormContext';
|
||||
|
||||
const styles = css`
|
||||
textarea {
|
||||
@@ -18,25 +21,69 @@ const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> & {
|
||||
type BaseProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> & {
|
||||
mod?: 'surface';
|
||||
long?: boolean;
|
||||
onChange?(value: string): any;
|
||||
}
|
||||
|
||||
export function Textarea({
|
||||
type ControlledProps = BaseProps & {
|
||||
name?: string;
|
||||
value?: string;
|
||||
onChange?(value: string, name?: string): any;
|
||||
|
||||
state?: never;
|
||||
}
|
||||
|
||||
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
|
||||
name: TKey;
|
||||
state: TState;
|
||||
onChange?(value: string, name: TKey): any;
|
||||
|
||||
value?: never;
|
||||
}
|
||||
|
||||
type TextareaType = {
|
||||
(props: ControlledProps): JSX.Element;
|
||||
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): JSX.Element;
|
||||
}
|
||||
|
||||
export const Textarea: TextareaType = observer(function Textarea({
|
||||
name,
|
||||
value: controlledValue,
|
||||
state,
|
||||
children,
|
||||
className,
|
||||
mod,
|
||||
long,
|
||||
onChange = () => {},
|
||||
...rest
|
||||
}: Props) {
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (state) {
|
||||
state[name] = event.target.value;
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(event.target.value, name);
|
||||
}
|
||||
if (context) {
|
||||
context.onChange(event.target.value, name);
|
||||
}
|
||||
}, [state, name, onChange]);
|
||||
|
||||
const value = state ? state[name] : controlledValue;
|
||||
|
||||
return styled(useStyles(baseFormControlStyles, styles))(
|
||||
<field as="div" className={className} {...use({ long })}>
|
||||
<field-label as='label'>{children}</field-label>
|
||||
<textarea onChange={e => onChange(e.target.value)} {...rest} {...use({ mod })} />
|
||||
<textarea
|
||||
{...rest}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={handleChange}
|
||||
{...use({ mod })}
|
||||
/>
|
||||
</field>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from './Tabs';
|
||||
export * from './Button';
|
||||
export * from './FormControls/Checkbox';
|
||||
export * from './FormControls/Combobox';
|
||||
export * from './FormControls/FormContext';
|
||||
export * from './FormControls/InputField';
|
||||
export * from './FormControls/InputGroup';
|
||||
export * from './FormControls/Radio';
|
||||
|
||||
Reference in New Issue
Block a user