Please enable Javascript to view the contents

一个小问题:Formily 2.0 预览组件支持自定义样式

 ·  ☕ 4 分钟  ·  ✍️ 东 · 👀... 阅读

问题背景

公司的UI库集成formily 状态管理后,迅速投入了开发和应用,虽然我们的表单组件都配置了预览态,但仍不能满足需求,有些时候还是需要给默认的预览态再加一些样式;

举例说明,比如我们的input组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React from 'react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import Input from '../../Input';
import { InputProps } from '../../Input/input';
import PreviewText from '../PreviewText/index';

const FormInput = connect(
  Input,
  mapProps((props: InputProps) => {
    return {
      ...props,
    };
  }),
  mapReadPretty(PreviewText.Input) // 阅读态样式对应了PreviewText.Input中的样式
);

export default FormInput;

在previewText组件中的预览:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const PlaceholderContext = createContext<React.ReactNode>('--');
const Placeholder = PlaceholderContext.Provider;

const usePlaceholder = (value?: any) => {
  const placeholder = useContext(PlaceholderContext) || '--';
  return isValid(value) && value !== '' ? value : placeholder;
};

// Input 对应的阅读态 这里的样式取决于代码里className中的样式
const Input: React.FC<InputProps> = (props) => { // props Input 组件属性
  return (
    <div className={classNames(styles.previewContainer, sdf.bodyPrimary)}>
      {usePlaceholder(props.value)}
    </div>
  );
};

PreviewText.Input = Input;
export default PreviewText;

在项目中的应用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { FormInput, FormItem} from 'sugar-design' // sugar-design 是我司自研发的UI库,类似于antd
import { createForm } from '@formily/core'
import { FormProvider, createSchemaField } from '@formily/react'

class Demo extends React.PureComponent {
  render() {
    const SchemaField = createSchemaField({
      components: {
        FormInput,
        FormItem,
      },
    });

    const form = createForm();
    return (
      <FormProvider form={form}>
        <SchemaField>
          <SchemaField.String
            name="input"
            title="输入框"
            default={'input'}
            required
            x-decorator="FormItem"
            x-decorator-props = {{
              spacing: "md",
            }}
            x-component="FormInput"
          />
        </SchemaField>
        <br/>

         <Button
            onClick={() => {
              form.setState((state) => {
                state.readPretty = !state.readPretty
              })
            }}
          >
            切换阅读态
          </Button>
      </FormProvider>
    )
  }
}

方案探讨

在官方的文档中,mapReadPretty 的签名是:

1
2
3
interface mapReadPretty<Target extends React.FC> {
  (component: Target, readPrettyProps?: React.ComponentProps<Target>): React.FC
}

可以看到,connect时候调用mapReadPretty 方法时,第一个参数是自定义的预览组件,第二个是该组件的参数,在我们使用connect 之后的formInput 的时候,自定义预览组件在内部自动被使用了,没有一个入口可以传入我们想要的样式,但是在自定义预览组件被调用的时候,我们可以看到formily将组件属性都传入了,因此我想到了第一个方案: 通过给Input组件添加自定义样式参数previewStyle

准备添加的时候发现,这样会给input 组件添加属性,而这个属性实际在input组件中并没有用到,而是在预览状态下用到,预览态其实是formily中给到的概念,切换某个表单项状态,input组件只是其编辑态需要调用的一个组件,况且其他组件如果要添加自定义预览样式,也要新增属性的话,对我们的组件库侵入比较大,这样一想,给input增加组件并不妥;

也有小伙伴提出,可以像提供默认placeholder一样来提供一个自定义样式的provider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// PreviewText 中增加默认placeholder
const PlaceholderContext = createContext<React.ReactNode>('--');
const Placeholder = PlaceholderContext.Provider;
Text.Placeholder = Placeholder;

const PreviewText = Text;

// 使用:
<PreviewText.Placeholder value="暂无数据">
  <FormProvider form={form}>
      <SchemaField>
      ...

如果要加一个类似

1
<PreviewText.previewStyle value={{color: 'red'}}>

这样的功能也可以,但是这样做就会给代码加一层组件包裹,而且这样添加的样式无法实现个别组件定制,也就是provider包裹的只能提供一种样式给到所有组件的预览态,不够灵活

于是只有利用Schema中的自定义属性 x-data 了,缺点是增加自定义属性一定要加好文档,否则在官方文档里看不到的开发者也不能很好的理解和应用;

优点是x-data中的属性是被formily 监控到的,可以实现异步更新,属性变化,页面更新,当然也支持表单项维度的配置样式。

开发阶段

只增加style也不够灵活,开发者也有用类名传递的需求,于是一起增加了两个属性:previewStylepreviewClassName,previewText 中的部分实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 阅读态 支持schema中的data.previewStyle自定义阅读样式
const PreviewRender: React.FC<previewRenderProps> = observer((props: previewRenderProps) => {
  const field = useField<Field>();
  const previewStyle: CSSProperties = field.data?.previewStyle;
  const previewClassName: string = field.data?.previewClassName;
  return (
    <div
      className={classNames(styles.previewContainer, sdf.bodyPrimary, styles[useSize(props.size)], previewClassName)}
      style={previewStyle}
    >
      {usePlaceholder(props.previewText)}
    </div>
  );
});

// Input 对应的阅读态
const Input: React.FC<InputProps> = (props) => {
  return <PreviewRender previewText={usePlaceholder(props.value)} size={props.size} />;
};

这样就实现了配置表单schema中 x-data 属性的previewStylepreviewClassName,可以自定义预览态样式的问题了。

分享

目录