refactor(core-blocks): form context and type improvements

This commit is contained in:
Wroud
2020-09-06 23:52:34 +03:00
parent 19e51c3554
commit d37d386e5f
8 changed files with 208 additions and 82 deletions
@@ -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>
);
}
});
+1
View File
@@ -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';