From d37d386e5f0b55295bf0d0bf8d04ac2dbecc5d47 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 6 Sep 2020 23:52:34 +0300 Subject: [PATCH] refactor(core-blocks): form context and type improvements --- .../core-blocks/src/FormControls/Checkbox.tsx | 39 +++++---- .../core-blocks/src/FormControls/Combobox.tsx | 83 ++++++++++++++----- .../src/FormControls/FormContext.ts | 15 ++++ .../src/FormControls/InputField.tsx | 37 +++++---- .../src/FormControls/SubmittingForm.tsx | 15 +++- .../core-blocks/src/FormControls/Switch.tsx | 41 +++++---- .../core-blocks/src/FormControls/Textarea.tsx | 59 +++++++++++-- webapp/packages/core-blocks/src/index.ts | 1 + 8 files changed, 208 insertions(+), 82 deletions(-) create mode 100644 webapp/packages/core-blocks/src/FormControls/FormContext.ts diff --git a/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx index 54b461e726..3204b15448 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkbox.tsx @@ -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 = { (props: ObjectProps): JSX.Element; } -export const Checkbox: CheckboxType = observer(function Checkbox( - props: ControlledProps | ObjectProps -) { - 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) { + const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { 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))( diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 33edb248be..3f874c7367 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -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 = Omit, 'onChange' | 'onSelect'> & { - value: string | undefined; +type BaseProps = Omit, '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({ - value, +type ControlledProps = BaseProps & { + name?: string; + value?: string; + onSelect?(value: TKey, name: string | undefined, prev: TKey): void; + onChange?(value: string, name: string | undefined): any; + + state?: never; +} + +type ObjectProps = BaseProps & { + 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 = { + (props: ControlledProps): JSX.Element; + (props: ObjectProps): JSX.Element; +} + +export const Combobox: ComboboxType = observer(function Combobox({ + value: controlledValue, + name, + state, propertyName, items, children, @@ -106,13 +130,14 @@ export function Combobox({ mod, long, readOnly, - keySelector = v => v as any, - valueSelector = v => v as any, + keySelector = v => v, + valueSelector = v => v, onChange = () => {}, onSelect, onSwitch, ...rest -}: Props) { +}: ControlledProps | ObjectProps) { + const context = useContext(FormContext); const ref = useRef(null); const menu = useMenuState({ placement: 'bottom-end', @@ -120,32 +145,49 @@ export function Combobox({ gutter: 4, }); const [searchValue, setSearchValue] = useState(''); + const value = state ? state[name] : controlledValue; const handleChange = useCallback( (event: React.ChangeEvent) => { 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) => { 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({ ({ ); -} +}); diff --git a/webapp/packages/core-blocks/src/FormControls/FormContext.ts b/webapp/packages/core-blocks/src/FormControls/FormContext.ts new file mode 100644 index 0000000000..26a80ce00b --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/FormContext.ts @@ -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(null); diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx index 03b84a81dc..767ebc2542 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -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, 'onChange' | 'name' | 'value'> & { mod?: 'surface'; @@ -40,20 +41,19 @@ type InputFieldType = { (props: ObjectProps): JSX.Element; } -export const InputField: InputFieldType = observer(function InputField( - props: ControlledProps | ObjectProps -) { - 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) { + const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { 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))( diff --git a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx index fd40b31400..0a26d69d43 100644 --- a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx +++ b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx @@ -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, HTMLFormElement> +import { FormContext } from './FormContext'; + +type FormDetailedProps = Omit, HTMLFormElement>, 'onChange'> & { + onChange?(value: string | number | boolean, name: string | undefined): void; +} export const SubmittingForm = forwardRef(function SubmittingForm( { children, onSubmit, + onChange = () => {}, ...rest }, ref @@ -25,9 +30,13 @@ export const SubmittingForm = forwardRef(fun } }, [onSubmit]); + const context = useMemo(() => ({ onChange }), [onChange]); + return (
- {children} + + {children} +