9

给 Antd 的 DatePicker 组件实现带有至今的功能

 9 months ago
source link: https://www.xiabingbao.com/post/react/abtd-datepicker-tillnow-s3pm8f.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
如何给 Antd 中的 DatePicker 添加「至今」功能

在一些比如选填教育经历、工作经历等场景的表单时,经常会遇到当前阶段是在职的情况,那这时应该选择「至今」。但目前 <DatePicker /> 组件并不支持这一功能,需要我们自己来实现。

<DatePicker />组件不支持直接设置中文,否则会显示Invalid Date。因此这里我们用 <Input /> 标签来日期和「至今」的文案。

选择「至今」的场景,一般是在日期区间中,在第 2 个日期选择时使用。我们先从单个的日期选择,一步步深入。

我们先看下最终的效果,然后再看实现过程。

DatePicker中的至今功能-蚊子的前端博客

1. 单个日期选择中的页脚

我们可以使用 <DatePicker />组件中的 renderExtraFooter 属性来设置「至今」按钮,然后添加点击事件。

const tillNowConfig = {
  text: "至今",
};

const DatePickerTillNow = (props) => {
  return (
    <div className="datepicker-till-now">
      <Input placeholder="结束日期" value={value} />
      <DatePicker
        showToday={false}
        {...props}
        onChange={handleChange}
        ref={ref}
        renderExtraFooter={() => (
          <div className="tillnow-btn" style={{ textAlign: "center" }}>
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

我们这里把 DatePicker 和 Input 重叠排布,并且让 DatePicker 在前面,然后把里面的显示标签置为透明。这样做的好处是:

  1. 能正常使用日期组件的功能,比如呼起日期面板、清除输入等;

  2. 让后置的 Input 标签进行展示,日期和中文都可以正常展示;

CSS 样式:

.datepicker-till-now {
  position: relative;

  .ant-picker {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

    // 只隐藏最内层的显示区域
    .ant-picker-input input {
      opacity: 0;
    }
  }
}

上面还有两个变量还需要我们来实现,value 和 onChange。

1.1 日期的 value

我们接收到 props.value 后,需要判断下是否有「至今」的标识。我们在这里约定显示「至今」的标识是:

value 对象中的 tillNow 属性为 true。

关于这个标识的问题,在最开始实现时,后端接口只能接收 datetime 的字符串格式(数据库中的格式已固定)。然后我们前后端约定是2099年表示是至今的功能。这个年份导致我在实现时,陷入了误区。当时就想着封装的组件,在选择至今时能直接得到 2099 年,但存在的一个问题是:打开日期面板重新选择时,年份会自动跳转到 2099 年,而不是今日。为了解决这个问题,又用了很多临时变量来进行存储,才实现这样的功能。

后来在写这篇文章时才想到,value 本身就是 dayjs 或者 moment 的对象,我们可以给该对象添加一个自定义属性 tillNow;这样既不影响 antd 原来组件的使用,也能标识出「至今」来。只是在调用该组件或者通过该组件得到数据提交接口时,需要把 2099 年和 tillNow 属性 互相转换一下。

先看下 tillNow 属性如何转成「至今」:

/**
 * 若value有值,则判断是否是否有 tillNow 属性;
 * 若value无值,则返回undefined;
 */
const value = useMemo(() => {
  if (props?.value) {
    if (!dayjs.isDayjs(props.value)) {
      throw new Error("DatepickerTillNow's value is not dayjs");
    }
    if (props.value.tillNow) {
      return tillNowConfig.text;
    }
    const format = props.format || "YYYY-MM-DD";
    return dayjs(props.value).format(format);
  }
  return undefined;
}, [props?.value]);

然后将该 value 给到 Input 标签即可:

<Input placeholder="结束日期" value={value} />

组件 DatePicker 中的 value 无需转换,直接使用 props.value 即可。tillNow属性对该组件也没任何影响。

1.2 日期变化 onChange

日期切换,主要考虑两种情况:

  • DatePicker 组件本身的切换,如点击某个具体日期,或者清除日期等;

  • 点击「至今」按钮;至今并不等同于选择的今日的日期;

首先来看下 DatePicker 组件自己的 onChange 事件:

const handleChange = (value: dayjs.Dayjs) => {
  if (value) {
    // 点击某个具体日期时,清除 tillNow 属性
    value.tillNow = undefined;
  }
  props?.onChange(value);
};

当点击至今的按钮时:

// 点击至今按钮
const handleClickSoFar = () => {
  const day = dayjs();
  day.tillNow = true; // 使用 tillNow 属性标记为「至今」

  // 让 DatePicker 失去焦点,即关闭日期选择下拉框
  ref.current?.blur();
  props?.onChange(day);
};

对日期下拉面板的选择和至今按钮的点击进行处理后,告知外层组件 value 有发生变化。外层的 Form 组件会将变化后的 value 再重新告知到当前组件。

1.3 外层 Form 表单

外层组件在提交表单时,需要根据当前日期是否有 tillNow 属性,再转换为接口需要的格式。

const App = () => {
  const [form] = Form.useForm();

  const handleClick = () => {
    const values = form.getFieldsValue();
    if (values?.date) {
      // 若有 tillNow 属性,则将其设置为接口需要的格式
      values.date = dayjs(values.date.tillNow ? "2099-12-31" : values.date).format("YYYY-MM-DD");
    }
    console.log(values);
  };

  return (
    <Form form={form} labelCol={{ span: 5 }}>
      <Form.Item name="date" label="日期选择">
        <DatepickerTillNow />
      </Form.Item>
      <Button onClick={handleClick}>提交</Button>
    </Form>
  );
};

2. 区间日期中的「至今」页脚

区间日期中对「至今」的处理,与单个日期中的处理很像。只不过在区间日期中,value 是一个有两个日期的数组。我们需要对后一个日期进行处理。

用于显示中文的 Input 标签,也需要定位到结束日期的位置。

const RangePicker = () => {
  return (
    <div className="datepicker-till-now datepicker-till-now-range">
      <Input placeholder="结束日期" value={value?.[1]} />
      <DatePicker.RangePicker
        showToday={false}
        {...props}
        onCalendarChange={console.log}
        ref={ref}
        renderExtraFooter={() => (
          <div
            className="sofar-btn"
            style={{
              textAlign: "center",
              width: "50%",
              transform: "translateX(100%)",
            }}
          >
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

// 把有至今选项的 RangePicker 挂载到 DatepickerTillNow 上
DatepickerTillNow.RangePicker = RangePicker;

对应的 CSS 样式:

.datepicker-till-now {
  position: relative;

  .ant-picker {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0.5;

    .ant-picker-input input {
      opacity: 0;
    }
  }

  &.datepicker-till-now-range {
    .ant-input {
      text-indent: 50%;
      padding-left: 24px;
    }
    .ant-picker-range {
      .ant-picker-input input {
        opacity: 1;
      }
      & > div:nth-child(3) {
        input {
          opacity: 0;
        }
      }
    }
  }
}

2.1 value 的处理

区间日期的 value 是一个有两个日期的数组,我们需要对有 tillNow 属性的日期对象,进行特殊处理。

const value = useMemo(() => {
  if (Array.isArray(props.value)) {
    return props.value.map((item: dayjs.Dayjs) => {
      if (item) {
        if (item.tillNow) {
          // 有 tillNow 属性,则显示至今的文案
          return tillNowConfig.text;
        }
        // 格式化
        return dayjs(item).format(props?.format || "YYYY-MM-DD");
      }
      return item;
    });
  }
  return [];
}, [props?.value, props?.format]);

然后 Input 标签只展示 value?.[1] 的值。

2.2 日期的变化 onCalendarChange

我们在这里使用了 onCalendarChange 属性,而不是 onChange,是因为需要用点击的第 1 个日期和「至今」按钮的日期进行拼接。而在 onChange 事件里,是拿不到刚才第 1 次点击的那个日期的。

const RangePicker = () => {
  const ref = useRef(null);
  const firstDateRef = useRef(null);

  // 点击至今按钮
  const handleClickSoFar = () => {
    const dd = dayjs();
    dd.tillNow = true;
    props?.onChange([firstDateRef.current, dd]);
    ref.current?.blur();
  };

  return (
    <div className="datepicker-till-now datepicker-till-now-range">
      <Input placeholder="结束日期" value={value?.[1]} />
      <DatePicker.RangePicker
        showToday={false}
        {...props}
        onCalendarChange={(value) => {
          if (Array.isArray(value)) {
            // 将刚才点击的第1个日期存储起来
            firstDateRef.current = value[0];
          }
        }}
        ref={ref}
        renderExtraFooter={() => (
          <div
            className="sofar-btn"
            style={{
              textAlign: "center",
              width: "50%",
              transform: "translateX(100%)",
            }}
          >
            <Button type="link" onClick={handleClickSoFar}>
              {tillNowConfig.text}
            </Button>
          </div>
        )}
      />
    </div>
  );
};

至此,两种日期中,添加「至今」按钮的功能已经实现了。

我们在上面的实现中,有很多,其实并未完全处理 props 中的参数,只是为了更快地演示下至今功能的实现。

我把代码放到 GitHub 上了:antd-datepicker-tillnow


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK