【Day06】数据输入元件 - FormControl

元件介绍

FormControl 让我们可以将 form input 所需要的共同前後文特性独立出来管理,使被 control 的子元件之间的样式能够保持一致性。例如在 form input 元件 TextField, Switch, Checkbox 当中,将 label, required, error ...等逻辑与样式独立出来藉由 FormControl 来管理。

参考设计 & 属性分析

这边指的 FormControl 灵感是取自 MUI 的元件,因为看到这个元件的概念很不错,所以想要借来改成自己适用的元件。

表单输入的时候有许多的状况需要处理,例如:

  • 栏位是否为必填
  • 栏位标题的名称、位置、间距
  • 限制输入格式(ex: 只能输入数字、email 格式、电话格式...)
  • 输入错误(ex: 不符合格式、必填没填)时的样式以及警告讯息

以标题名称为例,标题名称相对於输入元件的位置、距离,其实在各个元件都有雷同的地方,例如 Switch, Checkbox, Radio

这些部分真的看起来很像,所以如果在每个输入元件里面都把同样的逻辑一模一样再刻一次的话,想必这是违背了 Don't repeat yourself 原则。



另外有一些表单的处理,其实我觉得他没有必要一定要跟输入元件绑死在一起,以 TextField 来说,一个 TextField 最主要的功能就是让人可以输入 Text,如果他没有 label,其实他还是一个 TextField;如果他没有 error message,他仍然是一个 TextField。这些属性就算没有,也不会影响原本元件的功能,像这些部分我们就能够另外把它独立出来,让 TextField 就是一个纯粹的 TextField。

因此上述这些附加价值,我们就用 FormControl 来另外处理,一方面可以共用各种 form input 的共同样式和属性,另一方面也可以让 form input 的功能更保持单纯。

介面设计

属性 说明 类型 默认值
label 标题内容 string
placement 标题位置 top-left, top, top-right, left, right, bottom-left, bottom, bottom-right top-left
children 要管理的 form 内容 TextField, Switch, Radio, Checkbox
isRequired 是否必填(样式) boolean false
isError 是否错误(样式) boolean false
errorMessage 显示错误讯息 string
maxLength 限制最大输入长度 number
onChange 状态改变的 callback function function

元件实作

我们想像 FormControl 大概是像下面这样的结构:

<FormControlWrapper>
  <Label />
  {children}
  {(isError && errorMessage) && <ErrorMessage value={errorMessage} />}
</FormControlWrapper>

placement

首先我们来实作 placement,我们来偷看一下 MUI 是怎麽做的:

我们可以发现,无论 label 的位置是放在 form input 的哪个方位,他的 html 结构基本上都是一样的,示意结构如下:

<label>
  <span><Radio /></span>
  <span>{label}</span>
</label>

那既然都是同样的结构,要如何做到不同位置的摆放呢?关键就在於他的 css 样式,以 label 在上,Radio 在下的这个案例来说,他的 css 有一个值得注意的地方,就是使用 flex-direction: column-reverse;,看来这一切的谜团到这边就差不多解开了。

flex 布局的 flex-direction 属性能帮我们指定 flex 容器当中元素的主轴方向,其中有四个我们可以选用的值

flex-direction: row | row-reverse | column | column-reverse;

row 以及 row-reverse 都是横轴方向,布局的起点与终点为互相相反;另外 column 与 column-reverse 是纵轴方向,一样是起点与终点互为相反。

在 placement 传入元件之後,可以根据传进来的参数来决定要选用哪个 flex-direction 值,藉此在不改变 html 架构下能够做到 label 与 input control 不同方位的布局。

我们的招数一样用同一招,用物件的 key-value 结构来对应我们想要选用的 css 样式

const placementStyleMap = {
  'top-left': topLeftStyle,
  top: topStyle,
  'top-right': topRightStyle,
  left: leftStyle,
  right: rightStyle,
  'bottom-left': bottomLeftStyle,
  bottom: bottomStyle,
  'bottom-right': bottomRightStyle,
};

在各种不同方位的样式当中,flex-direction: column | column-reverse; 来决定 label 在 input control 的上方还是下方,而至於是左上、右上、左下、右下,则我们搭配另外一个 flex 布局的属性 align-items: flex-end | center | flex-start; 来决定,我们就能够做出不同方位的布局:

Required

isRequired 这个 boolean 让我们决定要不要在 label 上面显示必填的样式:

虽然这也是一个小功能,但是因为真的非常常被使用,因此我们也不想要每次用的时候都写一次。

实作如下,当 isRequired 为 true 的时候,就显示必填的星号 *,就是这麽单纯。

const RequiredSign = styled.span`
  color: ${(props) => props.theme.color.error};
`;

<div className="form-control__label">
  {label}
  {isRequired && <RequiredSign>*</RequiredSign>}
</div>

Error Message

按下表单送出按钮时,在表单送出前,同常会让前端先做一次检查,看看是否有不符合格式的栏位,是否有必填的栏位没有填到等等,若有错误的栏位,我们会显示如下的样式:

这个样式会需要 input border 变红,并且显示 Error Message。
Error Message 的处理,我们就是简单的 if...else... 判断,若 isError 为 true 并且也有传入 Error Message,我们就显示。

而 input border 变红色,其实我们在 TextField 里就有提供这样的 props 可传入,但是其实我们不太想要父层传一次 isError 进去,然後子层又传一次 isError,感觉有点重复,如下:

<FormControl isError={state.isError}>
  <TextField isError={state.isError} />
</FormControl>

因此这边我们希望借助 React.cloneElement 这个方法,把父层传入的 isError 直接往下传,这样就不用自己在外面手动把 isError 又传入子层,因此 FormControl 的做法会如下示意:

<FormControlWrapper>
  <Label />
  {React.cloneElement(children, {
    isError,
  })}
  {(isError && errorMessage) && <ErrorMessage value={errorMessage} />}
</FormControlWrapper>

输入字数长度限制

有时候我们 TextField 会需要限制输入字数的长度,类似像这样的概念我们在 LINE 的输入昵称当中也可以看得到:

其实我们并不是每一个 TextField 都会需要限制输入字数的长度,所以把这部分的功能拉出来用 FormControl 做我个人觉得也是很不错的,可以让 TextField 的功能保持单纯,下图是我们希望做到的样式:

我大概会想要这样操作元件,在 FormControl 传入一个最大长度限制,以及一个 onChange function:

<FormControl
  maxLength={12}
  onChange={...}
>
  <TextField />
</FormControl>

那要如何透过在 FormControl 这个父层传入 onChange ,就能够知道子层 TextField 的输入字数呢?还是那个千篇一律的招数 React.cloneElement

React.cloneElement(children, {
  ...otherProps,
  onChange: handleOnChange,
})

在 FormControl 元件内部,我们用 handleOnChange 这个 handle function 来监听 TextField 输入的变化,藉此来取得当前的输入值,这样我们就能够在 FormControl 这个元件内部的 state 来记录输入字数的长度啦!如下面程序码示意:

const [childrenValue, setChildrenValue] = useState('');

const handleOnChange = (event) => {
  const targetValue = event?.target?.value;
  if (maxLength && targetValue.length > maxLength) return;

  setChildrenValue(targetValue);
  if (typeof onChange === 'function') {
    onChange(event);
  }
};

我们得到当前 TextField 输入值 childrenValue 之後,在 UI 上面就能够刻画出我们预期的样式:

<LabelWrapper className="form-control__label-wrapper">
  <div className="form-control__label">
    {label}
    {isRequired && <RequiredSign>*</RequiredSign>}
  </div>
  {maxLength && <MaxLength>{`${childrenValue?.length} / ${maxLength}`}</MaxLength>}
</LabelWrapper>

到目前为止我们就能够完成一个简易的 FormControl 了!
详细程序码以及 Demo 附在下方连结


FormControl 元件原始码:
Source code

Storybook:
FormControl


<<:  DAY05随机森林演算法(续2)

>>:  DAY 04 实作环境配置 - 1

【没钱买ps,PyQt自己写】Day 26 - project / 替我们影片播放器增加一个显示进度的滑条 video player add slider (与昨日 bottleneck 处理细节)

看完这篇文章你会得到的成果图 多了一条滑条,我们可以直接控制,另外我们也可以直接透过滑条来操控进度 ...

[Day 07 - RWD] 跨平台生存之道 — RWD响应式网页设计

现在越来越多种类的装置出现,包括电脑、平板、手机,我们会在不同大小的萤幕上浏览网页,究竟网页要如何在...

DAY 11 Big Data 5Vs – Velocity(多样性)

另一个常见资料库分类是从「资料处理*」的应用角度来区分: 交易型Transaction: OLTP:...

[Day_16]回圈与生成式 - (2)

回圈结构 - 使用while while回圈结构与for回圈结构十分类似,while回圈结构常用於不...