[Vue]自定义指令禁用所有表单元素

6,842 阅读3分钟

背景

最近几个项目都有流程式的表单输入。产品提出,在上一步的表单提交完毕之后,不能够修改但是依然要展示给用户(用户在填写下一步的表单时可能需要参照上一步表单中的内容)。
最初因为项目工期紧张,直接采用了对表单容器添加pointer-events: none;的作法。这样导致了很多问题,表单元素的样式并没有变为disabled状态,看上去还是可以点击的;使用Tab键索引到元素之后,还是可以触发表单行为;容器内的元素的滚动条失效。

方案

解决这个问题有一个很直接的思路,就是遍历所有的表单元素,添加disabled属性。但是项目中的表单元素很多,一个个添加,非常麻烦。有没有简单一点的办法呢?

利用fieldset元素

无意中看到张鑫旭老师个人空间中的一篇文章如何disabled禁用所有表单input输入框元素。阅读之后发现可以给form表单下添加一个fieldset元素,通过给这个fieldset元素设置disabled属性来统一禁用所有的表单元素。

<form>
    <fieldset disabled>
        <legend>表单标题</legend>
        <...>
    </fieldset>
</form>

自定义指令

因为项目使用的是vue框架,对于添加fieldset元素这种dom操作,我第一时间想到了自定义指令。

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令

自定义指令在平时也用过一些,例如常见的transfer-dom,clickoutside。借此机会也来自己实践一下。

因为我们的需求是要在form元素下插入fieldset,因此只要使用inserted钩子即可(保证form已经插入到dom当中),类似官网文档给出的autofocus的例子。

export default {
  inserted(el, {
    value
  }) {
    if (el.tagName.toLocaleLowerCase() === "form") {
      wrapInner(el, value)
    }
    Array.from(el.querySelectorAll("form")).forEach(child => {
      wrapInner(child, value)
    })
  }
};

inserted钩子的第一个参数代表被绑定的元素。遍历这个元素的所有子代元素,找到form元素,执行包裹逻辑。需要注意的是,有可能这个元素本身就是form,所以增加一个判断。从第二个参数中解构出value属性(指令的绑定值,例如:v-disabled="true" 中,绑定值为 true)。
相信大家也比较少用原生api操作dom,这里详细说下如何完成对form内元素包裹fieldset标签。
jquery当中恰好有一个比较冷门的api:wrapInner可以满足我们的需求,来看下jquery的实现,核心逻辑如下

// The elements to wrap the target around
// 创建一个包裹元素
wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
// 插入到目标元素的第一个子元素之前
if ( this[ 0 ].parentNode ) {
	wrap.insertBefore( this[ 0 ] );
}
wrap.map( function() {
	var elem = this;
	while ( elem.firstElementChild ) {
		elem = elem.firstElementChild;
	}
	return elem;
} ).append( this );

参考jquery的实现思路,代码如下

function wrapInner(el, disabled) {
  const wrapper = document.createElement('fieldset');
  wrapper.disabled = disabled

  //先插入一个空的fieldset标签到第一个元素之前
  el.insertBefore(wrapper, el.firstElementChild);
  const children = el.children,
    len = children.length;
    //把原来的每个子元素移入到fieldset内
    //不是children[i],insertBefore是同步操作
    //每次插入的都是第一个元素
  for (let i = 1; i < len; i++) {
    wrapper.insertBefore(children[1], null);
  }
}

总结

这样的方案要求表单元素被嵌套在form内。这里也可以看出HTML语义化的好处,除了代码可读性好,SEO有帮助,无障碍优化外,还能充分发挥出标签本身的特性。 如果项目使用一些第三方框架,例如element-ui,使用封装过的form组件(自带disabled属性)会更好一些。
代码传送门