一个小功能,用户体验大优化:表单滚动定位到校验出错的元素

前言

大家好,我是阿林。

前段时间,产品在验收时给我们加了一个需求,用户提交表单时,要滚动定位到校验出错的地方,就像这样:

scroll.gif

我说:做这个应该会比较麻烦,主要是涉及到多个子表单组件通信,还要计算元素在页面的位置,还要写动画,大概 8 工时,要不不做了?

她柳眉皱起,考虑了很久,道:不行!用户体验至关重要,你尽力做嘛,多预估点开发时间就行。

我面露难色,很勉强地说:好吧,哎,那就上线时间往后面延一天吧,我尽量把这个功能做出来。

项目经理知道要加这个需求后,说:好,你加油!就延后一天上线吧,

我心道:小样儿,给我加需求,我一小时就做完的东西,骗你们说八小时你还真信,又多一天时间学(mo)习(yu)了,耶!

需求分析

这种需求主要是用在超级长的表单,长到超过一个页面。

要实现这个功能需要用到一个 Web API,Element.scrollIntoView

Element 接口的scrollIntoView()方法会滚动元素的父容器,使被调用scrollIntoView()的元素对用户可见。

很显然,我们只需要在表单提交校验不通过时,找到整个页面上第一个校验出错的元素,然后调这个元素的 scrollIntoView 方法,就能实现这个功能。

scrollIntoView 这个 api 帮我们把计算元素位置和动画的事情做了,但是怎么找到页面上第一个校验出错的元素呢?

element-uiel-form 组件为例,打开控制台发现,校验出错时元素会被加上 is-error class,很显然,我们可以调用元素的 getElementsByClassName方法,找到所有校验出错的元素,再取第一个就行。

image.png

现在就差最后一个问题了,多个子组件表单和父组件的通信如何实现?

vue 开发中,表单的校验一般是写到子组件的 validate 方法上,然后在父组件上给子组件定义 ref,然后通过 this.$refs 调用子组件的 validate 方法(element-ui就是这么做的)。

代码实现

以上面 gif 图 为例,假设这个页面有五个子表单组件。

父组件:

<ComponentA ref="componentAEle" />
<ComponentB ref="componentBEle" />
<ComponentC ref="componentCEle" />
<ComponentD ref="componentDEle" />
<ComponentE ref="componentEEle" />
<button @click="validate">

async validate() {
  const validRefs = [
    this.$refs.componentAEle,
    this.$refs.componentBEle,
    this.$refs.componentCEle,
    this.$refs.componentDEle,
    this.$refs.componentEEle
  ]

  for (const i in validRefs) {
    const res = await validRefs[i]?.validate()
    if (!res) {
      return false
    }
  }
  return true
}
复制代码
子组件,以 componentA 为例,其他几个子组件同理:

<el-form ref="componentAFormEle">
...
</el-form>

validate() {
  return new Promise(resolve => {
    this.$refs.componentAFormEle.validate(async valid => {
      if (!valid) {
        await this.$nextTick()
        this.$utils.scrollToError(this.$refs.componentAFormEle.$el)
      }
      resolve(valid)
    })
  })
}
复制代码
utils.js 封装的scrollToError方法

/**
 * 自动滚动到错误的校验框
 *
 * @param {*} el 包裹的元素
 * @param {string} [scrollOption={
 *     behavior: 'smooth',
 *     block: 'center'
 *   }] 滚动参数
 */
const scrollToError = (
  el,
  scrollOption = {
    behavior: 'smooth',
    block: 'center'
  }
) => {
  const isError = el.getElementsByClassName('is-error')
  isError[0].scrollIntoView(scrollOption)
}

复制代码

这里有一点要特别注意,调用子组件的 validate 方法时,一定要先调一次 this.$nextTick(),不然的话就会报错:

image.png

因为页面的is-error元素还没出现,程序就运行到 scrollIntoView 方法那里去了,所以才会出现 undefined 错误。所以要调一下 this.$nextTick(),等这一轮视图更新好了,再调后续的 scrollToError 方法。

总结

这种写法非常不vue,vue推荐的是,不到迫不得已,尽量不要写原生 DOM 事件,但我感觉我已经到迫不得已的情况了,如果你有更好的实现方法,一定要在评论区告诉我~

另外

前言除了产品验收完给我们加需求是真的,其他都是我编的~

现实是:

产品经理:这么简单的功能,哪来那么多理由?这个需求必须做。

项目经理:这么个小需求要开发8工时?不可能,晚上加个班,给我实现,临时加点小需求就不算工时了。

项目经理:上线时间延期一天?你在做梦,上线时间提前了算正常,延后了就扣绩效。

...

hhh,生活不易,阿林叹气,但是还是要保持乐观,努力学习呀!

希望我的文章能对前端路上的你有帮助。

Guess you like

Origin juejin.im/post/7054723301007949838