Skip to content
Snippets Groups Projects
Unverified Commit a06a9e06 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Refactor FormField component (#22375)

* Extend types

* Refactor FormField

* Fix prop values

* Fix types

* Fix field type check
parent 4ae681cc
No related branches found
No related tags found
No related merge requests found
......@@ -3,14 +3,31 @@ export type DefaultFieldValue = unknown;
export type FieldValues = Record<FieldName, DefaultFieldValue>;
type FieldValidateResultOK = undefined;
type FieldValidateResultError = string;
export type BaseFieldDefinition = {
name: string;
type?: string;
title?: string;
description?: string;
initial?: unknown;
validate?: () => void;
normalize?: () => void;
placeholder?: string;
hidden?: boolean;
info?: string;
infoLabel?: string;
infoLabelTooltip?: string;
align?: "left" | "right";
horizontal?: boolean;
descriptionPosition?: "top" | "bottom";
visibleIf?: Record<FieldName, unknown>;
initial?: (value: unknown) => DefaultFieldValue;
validate?: (
value: DefaultFieldValue,
) => FieldValidateResultOK | FieldValidateResultError;
normalize?: (value: unknown) => DefaultFieldValue;
};
export type StandardFormFieldDefinition = BaseFieldDefinition & {
......@@ -28,6 +45,7 @@ export type FormFieldDefinition =
export type FormField<Value = DefaultFieldValue> = {
name: FieldName;
value: Value;
error?: string;
initialValue: Value;
active: boolean;
......
......@@ -10,7 +10,10 @@ export const FieldRow = styled.div`
margin-bottom: 0.5em;
`;
export const Label = styled.label`
export const Label = styled.label<{
horizontal?: boolean;
standAlone?: boolean;
}>`
margin-bottom: 0;
${props =>
props.horizontal &&
......@@ -42,7 +45,10 @@ export const InfoLabel = styled.span`
cursor: default;
`;
export const FieldContainer = styled.div`
export const FieldContainer = styled.div<{
horizontal?: boolean;
align?: "left" | "right";
}>`
width: 100%;
margin-right: ${props => (props.horizontal ? "1rem" : "")};
margin-left: ${props => (props.align === "left" ? "0.5rem" : "")};
......@@ -50,9 +56,4 @@ export const FieldContainer = styled.div`
export const InputContainer = styled.div`
flex-shrink: 0;
${props =>
props.horizontal &&
css`
margin-left: auto;
`}
`;
import React from "react";
import {
FieldName,
FieldValues,
FormField as FormFieldType,
BaseFieldDefinition,
FormFieldDefinition,
} from "metabase-types/forms";
import FormFieldView from "./FormFieldView";
type ReduxFormProps = Pick<FormFieldType, "name"> &
Partial<Pick<FormFieldType, "error" | "visited" | "active">>;
interface FormFieldProps extends BaseFieldDefinition, ReduxFormProps {
field: FormFieldType;
formField: FormFieldDefinition;
values: FieldValues;
className?: string;
children: React.ReactNode;
onChangeField: (fieldName: FieldName, value: unknown) => void;
}
const ALL_DOT_CHARS = /\./g;
function getFieldId(formFieldName: FieldName) {
return `formField-${formFieldName.replace(ALL_DOT_CHARS, "-")}`;
}
function getDescriptionPositionPropValue(
descriptionPosition?: "top" | "bottom",
formField?: FormFieldDefinition,
) {
return descriptionPosition ?? formField?.descriptionPosition ?? "top";
}
function getHiddenPropValue(hidden?: boolean, formField?: FormFieldDefinition) {
if (typeof hidden === "boolean") {
return hidden;
}
if (formField) {
return formField.hidden || formField.type === "hidden";
}
return false;
}
function getHorizontalPropValue(
horizontal?: boolean,
formField?: FormFieldDefinition,
) {
if (typeof horizontal === "boolean") {
return horizontal;
}
if (formField) {
return formField.horizontal || formField.type === "boolean";
}
return false;
}
function FormField({
className,
formField,
children,
...props
}: FormFieldProps) {
const title = props.title ?? formField?.title;
const type = props.type ?? formField.type;
const description = props.description ?? formField?.description;
const descriptionPosition = getDescriptionPositionPropValue(
props.descriptionPosition,
formField,
);
const info = props.info ?? formField?.info;
const infoLabel = props.infoLabel ?? formField?.infoLabel;
const infoLabelTooltip =
props.infoLabelTooltip ?? formField?.infoLabelTooltip;
const align = props.align ?? formField?.align ?? "right";
const hidden = getHiddenPropValue(props.hidden, formField);
const horizontal = getHorizontalPropValue(props.horizontal, formField);
const isToggle = type === "boolean";
const standAloneLabel = isToggle && align === "right" && !description;
if (hidden) {
return null;
}
const { name, error: errorProp, visited, active } = {
...(props.field || {}),
...props,
};
const shouldHideError = !visited || active;
const error = shouldHideError ? undefined : errorProp;
return (
<FormFieldView
fieldId={getFieldId(name)}
className={className}
name={name}
error={error}
title={title}
description={description}
descriptionPosition={descriptionPosition}
info={info}
infoLabel={infoLabel}
infoLabelTooltip={infoLabelTooltip}
align={align}
standAloneLabel={standAloneLabel}
horizontal={horizontal}
>
{children}
</FormFieldView>
);
}
export default FormField;
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "metabase/components/Tooltip";
import { BaseFieldDefinition } from "metabase-types/forms";
import { FormFieldDescription } from "./FormFieldDescription";
import {
FieldRow,
Label,
......@@ -12,84 +14,38 @@ import {
FieldContainer,
InfoLabel,
} from "./FormField.styled";
import { FormFieldDescription } from "./FormFieldDescription";
const formFieldCommon = {
title: PropTypes.string,
description: PropTypes.string,
descriptionPosition: PropTypes.oneOf(["top", "bottom"]),
info: PropTypes.string,
hidden: PropTypes.bool,
horizontal: PropTypes.bool,
};
const propTypes = {
...formFieldCommon,
field: PropTypes.object,
formField: PropTypes.shape({
...formFieldCommon,
type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
}),
// redux-form compatible:
name: PropTypes.string,
error: PropTypes.any,
visited: PropTypes.bool,
active: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
className: PropTypes.string,
};
const ALL_DOT_CHARS = /\./g;
function FormField(props) {
const {
className,
formField,
title = formField && formField.title,
description = formField && formField.description,
descriptionPosition = descriptionPosition ||
(formField && formField.descriptionPosition) ||
"top",
info = formField && formField.info,
infoLabel = formField && formField.infoLabel,
infoLabelTooltip = formField && formField.infoLabelTooltip,
hidden = formField && (formField.hidden || formField.type === "hidden"),
horizontal = formField &&
(formField.horizontal || formField.type === "boolean"),
align = formField?.align || "right",
children,
} = props;
if (hidden) {
return null;
}
let { name, error, visited, active } = {
...(props.field || {}),
...props,
};
const formFieldId = `formField-${name.replace(ALL_DOT_CHARS, "-")}`;
const isToggle = formField?.type === "boolean";
if (!visited || active) {
// if the field hasn't been visited or is currently active then don't show the error
error = null;
}
interface FormFieldViewProps extends BaseFieldDefinition {
fieldId: string;
error?: string;
className?: string;
standAloneLabel?: boolean;
children: React.ReactNode;
}
function FormFieldView({
fieldId,
className,
name,
error,
title,
description,
descriptionPosition,
info,
infoLabel,
infoLabelTooltip,
align,
horizontal,
standAloneLabel,
children,
}: FormFieldViewProps) {
const rootClassNames = cx("Form-field", className, {
"Form--fieldError": !!error,
flex: horizontal,
});
return (
<div id={formFieldId} className={rootClassNames}>
<div id={fieldId} className={rootClassNames}>
{align === "left" && <InputContainer>{children}</InputContainer>}
{(title || description) && (
<FieldContainer horizontal={horizontal} align={align}>
......@@ -99,7 +55,7 @@ function FormField(props) {
id={`${name}-label`}
htmlFor={name}
horizontal={horizontal}
standAlone={isToggle && align === "right" && !description}
standAlone={standAloneLabel}
>
{title}
{error && <span className="text-error">: {error}</span>}
......@@ -129,6 +85,4 @@ function FormField(props) {
);
}
FormField.propTypes = propTypes;
export default FormField;
export default FormFieldView;
export { default } from "./FormField";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment