文章目录

  • 问题
  • 演示案例
  • 解决方案1:修改源码(间接修改)
  • 解决方案2:使用js动态获取对应DOM节点,并拼接DOM(不推荐)
  • 鸡汤


问题

elementUI框架虽然没有做的像antd一样好,但是在vue生态中的位置和用户量都是不低的。

在elementUI的使用中,有许多参数,有的参数要求只能是一个String(例如:表单的验证提示信息、警告、提示信息…)

如果你直接在其中包含html标签,那么渲染出来的结果会将html转义(不会解析html标签)。

然后编程问题的解决方案都是有无限种的

接下来我讲一下我自己常用的几种方案

演示案例

接下来我就以其表单组件的提示功能做演示,例子是一个简单的表单验证功能,期望在提示的信息中有一个重置表单的按钮。
代码

<template>
    <div id="box">
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
            <el-form-item label="活动名称" prop="name">
                <el-input v-model="ruleForm.name"></el-input>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
    export default {
        name: "TestM",
        data() {
            return {
                ruleForm: {
                    name: ''
                },
                rules: {
                    name: [
                        { required: true, message: '请输入活动名称', trigger: 'blur' },
                        { min: 3, max: 5, message: '长度在 3 到 5 个字符 <span style="color:#0f0" @Click="spanClick">重置表单</span>', trigger: 'blur' }
                    ],
                }
            };
        },
        methods:{
            spanClick(){
                this.ruleForm.name = ''
            }
            
        }
    }
</script>

目前效果

类型Element缺少类型HTMLElement_表单


很明显,这不是我们想要的效果。出现这种效果的原因当然也是只有一个,就是html标签被转义了。

解决方案1:修改源码(间接修改)

缺点:代码两会有一点点多(多不了多少)
优点:对于使用需求较多的情况,可复用

我这里说的修改源码不是直接修改依赖包中的源码,而生将对应组件的源码复制出来构成一个单独的组件,这样就不会对依赖包产生影响。

想要修改源码,知道需要修改哪里是必须的,首先我们打开elementUI的el-form-item组件源码。

为啥是el-form-item的源码,不是el-form或者el-input的源码?

这个可以打开浏览器的开发面板

类型Element缺少类型HTMLElement_vue.js_02


不难发现这个错误提示信息是在el-form-item里面,而且和el-input组件是兄弟关系,由此得出结论,这个错误提示信息是在el-form-item组件中(查找该元素所属组件:知道该元素的第一父级组件

接下来我们记住这个类名

类型Element缺少类型HTMLElement_vue.js_03


然后再el-form-item组件中搜索,不难找到他的位置。

以下是定位到的代码部分

<transition name="el-zoom-in-top">
        <slot
          v-if="validateState === 'error' && showMessage && form.showMessage"
          name="error"
          :error="validateMessage">
          <div
            class="el-form-item__error"
            :class="{
              'el-form-item__error--inline': typeof inlineMessage === 'boolean'
                ? inlineMessage
                : (elForm && elForm.inlineMessage || false)
            }"
          >
            {{validateMessage}}
          </div>
        </slot>
      </transition>

类型Element缺少类型HTMLElement_javascript_04


不难发现,elementUI使用了{{}}的方式将提示信息渲染,大家都知道v-text、{{ }}这类方式都是会将其要渲染的信息进行转义的。

找到了问题的根源,接下来就是修改了
我们不能直接修改改文件,至于为什么,请百度为什么不建议修改node_modules下的代码?。
1、新建一个form-item(组件名字尽量与源码组件名字一样)组件

<template>
    
</template>

<script>
    export default {
        name: "form-item"
    }
</script>

<style scoped>

</style>

2、将源码复制过来

<template>
  <div class="el-form-item" :class="[{
      'el-form-item--feedback': elForm && elForm.statusIcon,
      'is-error': validateState === 'error',
      'is-validating': validateState === 'validating',
      'is-success': validateState === 'success',
      'is-required': isRequired || required,
      'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
    },
    sizeClass ? 'el-form-item--' + sizeClass : ''
  ]">
    <label-wrap
      :is-auto-width="labelStyle && labelStyle.width === 'auto'"
      :update-all="form.labelWidth === 'auto'">
      <label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
        <slot name="label">{{label + form.labelSuffix}}</slot>
      </label>
    </label-wrap>
    <div class="el-form-item__content" :style="contentStyle">
      <slot></slot>
      <transition name="el-zoom-in-top">
        <slot
          v-if="validateState === 'error' && showMessage && form.showMessage"
          name="error"
          :error="validateMessage">
          <div
            class="el-form-item__error"
            :class="{
              'el-form-item__error--inline': typeof inlineMessage === 'boolean'
                ? inlineMessage
                : (elForm && elForm.inlineMessage || false)
            }"
          >
            {{validateMessage}}
          </div>
        </slot>
      </transition>
    </div>
  </div>
</template>
<script>
  import AsyncValidator from 'async-validator';
  import emitter from 'element-ui/src/mixins/emitter';
  import objectAssign from 'element-ui/src/utils/merge';
  import { noop, getPropByPath } from 'element-ui/src/utils/util';
  import LabelWrap from './label-wrap';
  export default {
    name: 'ElFormItem',

    componentName: 'ElFormItem',

    mixins: [emitter],

    provide() {
      return {
        elFormItem: this
      };
    },

    inject: ['elForm'],

    props: {
      label: String,
      labelWidth: String,
      prop: String,
      required: {
        type: Boolean,
        default: undefined
      },
      rules: [Object, Array],
      error: String,
      validateStatus: String,
      for: String,
      inlineMessage: {
        type: [String, Boolean],
        default: ''
      },
      showMessage: {
        type: Boolean,
        default: true
      },
      size: String
    },
    components: {
      // use this component to calculate auto width
      LabelWrap
    },
    watch: {
      error: {
        immediate: true,
        handler(value) {
          this.validateMessage = value;
          this.validateState = value ? 'error' : '';
        }
      },
      validateStatus(value) {
        this.validateState = value;
      }
    },
    computed: {
      labelFor() {
        return this.for || this.prop;
      },
      labelStyle() {
        const ret = {};
        if (this.form.labelPosition === 'top') return ret;
        const labelWidth = this.labelWidth || this.form.labelWidth;
        if (labelWidth) {
          ret.width = labelWidth;
        }
        return ret;
      },
      contentStyle() {
        const ret = {};
        const label = this.label;
        if (this.form.labelPosition === 'top' || this.form.inline) return ret;
        if (!label && !this.labelWidth && this.isNested) return ret;
        const labelWidth = this.labelWidth || this.form.labelWidth;
        if (labelWidth === 'auto') {
          if (this.labelWidth === 'auto') {
            ret.marginLeft = this.computedLabelWidth;
          } else if (this.form.labelWidth === 'auto') {
            ret.marginLeft = this.elForm.autoLabelWidth;
          }
        } else {
          ret.marginLeft = labelWidth;
        }
        return ret;
      },
      form() {
        let parent = this.$parent;
        let parentName = parent.$options.componentName;
        while (parentName !== 'ElForm') {
          if (parentName === 'ElFormItem') {
            this.isNested = true;
          }
          parent = parent.$parent;
          parentName = parent.$options.componentName;
        }
        return parent;
      },
      fieldValue() {
        const model = this.form.model;
        if (!model || !this.prop) { return; }

        let path = this.prop;
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }

        return getPropByPath(model, path, true).v;
      },
      isRequired() {
        let rules = this.getRules();
        let isRequired = false;

        if (rules && rules.length) {
          rules.every(rule => {
            if (rule.required) {
              isRequired = true;
              return false;
            }
            return true;
          });
        }
        return isRequired;
      },
      _formSize() {
        return this.elForm.size;
      },
      elFormItemSize() {
        return this.size || this._formSize;
      },
      sizeClass() {
        return this.elFormItemSize || (this.$ELEMENT || {}).size;
      }
    },
    data() {
      return {
        validateState: '',
        validateMessage: '',
        validateDisabled: false,
        validator: {},
        isNested: false,
        computedLabelWidth: ''
      };
    },
    methods: {
      validate(trigger, callback = noop) {
        this.validateDisabled = false;
        const rules = this.getFilteredRule(trigger);
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }

        this.validateState = 'validating';

        const descriptor = {};
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        descriptor[this.prop] = rules;

        const validator = new AsyncValidator(descriptor);
        const model = {};

        model[this.prop] = this.fieldValue;

        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';

          callback(this.validateMessage, invalidFields);
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
      clearValidate() {
        this.validateState = '';
        this.validateMessage = '';
        this.validateDisabled = false;
      },
      resetField() {
        this.validateState = '';
        this.validateMessage = '';

        let model = this.form.model;
        let value = this.fieldValue;
        let path = this.prop;
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }

        let prop = getPropByPath(model, path, true);

        this.validateDisabled = true;
        if (Array.isArray(value)) {
          prop.o[prop.k] = [].concat(this.initialValue);
        } else {
          prop.o[prop.k] = this.initialValue;
        }

        // reset validateDisabled after onFieldChange triggered
        this.$nextTick(() => {
          this.validateDisabled = false;
        });

        this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
      },
      getRules() {
        let formRules = this.form.rules;
        const selfRules = this.rules;
        const requiredRule = this.required !== undefined ? { required: !!this.required } : [];

        const prop = getPropByPath(formRules, this.prop || '');
        formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];

        return [].concat(selfRules || formRules || []).concat(requiredRule);
      },
      getFilteredRule(trigger) {
        const rules = this.getRules();

        return rules.filter(rule => {
          if (!rule.trigger || trigger === '') return true;
          if (Array.isArray(rule.trigger)) {
            return rule.trigger.indexOf(trigger) > -1;
          } else {
            return rule.trigger === trigger;
          }
        }).map(rule => objectAssign({}, rule));
      },
      onFieldBlur() {
        this.validate('blur');
      },
      onFieldChange() {
        if (this.validateDisabled) {
          this.validateDisabled = false;
          return;
        }

        this.validate('change');
      },
      updateComputedLabelWidth(width) {
        this.computedLabelWidth = width ? `${width}px` : '';
      },
      addValidateEvents() {
        const rules = this.getRules();

        if (rules.length || this.required !== undefined) {
          this.$on('el.form.blur', this.onFieldBlur);
          this.$on('el.form.change', this.onFieldChange);
        }
      },
      removeValidateEvents() {
        this.$off();
      }
    },
    mounted() {
      if (this.prop) {
        this.dispatch('ElForm', 'el.form.addField', [this]);

        let initialValue = this.fieldValue;
        if (Array.isArray(initialValue)) {
          initialValue = [].concat(initialValue);
        }
        Object.defineProperty(this, 'initialValue', {
          value: initialValue
        });

        this.addValidateEvents();
      }
    },
    beforeDestroy() {
      this.dispatch('ElForm', 'el.form.removeField', [this]);
    }
  };
</script>

3、将复制过来的源码中使用相对路径引入的模块,将路径更正确

import AsyncValidator from 'async-validator';
    import emitter from 'element-ui/src/mixins/emitter';
    import objectAssign from 'element-ui/src/utils/merge';
    import { noop, getPropByPath } from 'element-ui/src/utils/util';
    //import LabelWrap from './label-wrap';
    //更改后路径
    import LabelWrap from 'element-ui/packages/form/src/label-wrap';

4、将新组件中使用{{}}渲染的错误提示信息改为使用v-html渲染

<div class="el-form-item__error"
     :class="{
          'el-form-item__error--inline': typeof inlineMessage === 'boolean'
          ? inlineMessage
          : (elForm && elForm.inlineMessage || false)
      	}"
      v-html="validateMessage">
       <!--{{validateMessage}}-->
</div>

在需要使用el-form-item且需要在提示信息中解析html标签的组件中引入新组件来替换掉原来的el-form-item组件

<template>
    <div id="box">
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
            <el-form-item label="活动名称" prop="name">
                <el-input v-model="ruleForm.name"></el-input>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
    import formItem from "./form-item"
    export default {
        name: "TestM",
        //直接替换掉改组件中的所有el-form-item组件,当然你还可以换一个名字注册来单独所有
        components:{"el-form-item":formItem},
        data() {
            return {
                ruleForm: {
                    name: ''
                },
                rules: {
                    name: [
                        { required: true, message: '请输入活动名称', trigger: 'blur' },
                        { min: 3, max: 5, message: '长度在 3 到 5 个字符 <span style="color:#0f0" @Click="spanClick">重置表单</span>', trigger: 'blur' }
                    ],
                }
            };
        },
        methods:{
            spanClick(){
                this.ruleForm.name = ''
            }

        }
    }
</script>

标签成功解析

类型Element缺少类型HTMLElement_前端_05


但是我们加的事件并并没有被添加上,原因是这是直接使用html字符串异步添加的。所有我们需要常见一个元素。我们可以给它加一个id,然后监听验证结果变化给它动态绑定上事件(当时,你还可以继续修改源码,这种方式还是比较建议的,因为复用性高)。代码如下

<template>
    <div id="box">
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
            <el-form-item label="活动名称" prop="name">
                <el-input v-model="ruleForm.name" @change="spanClick"></el-input>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
    import formItem from "./form-item"
    export default {
        name: "TestM",
        //直接替换掉改组件中的所有el-form-item组件,当然你还可以换一个名字注册来单独所有
        components:{elFormItem:formItem},
        data() {
            return {
                ruleForm: {
                    name: ''
                },
                rules: {
                    name: [
                        { required: true, message: '请输入活动名称', trigger: 'blur' },
                        { min: 3, max: 5, message: '长度在 3 到 5 个字符 <span style="color:#0f0" id="inputErrMsg">重置表单</span>', trigger: 'blur' }
                    ],
                }
            };
        },
        methods:{
            spanClick(){
                setTimeout(()=>{
                    if(document.getElementById("inputErrMsg")){
                        document.getElementById("inputErrMsg").onclick = ()=>{
                            this.ruleForm.name = ''
                        }
                    }
                },200)
            }
        },
    }
</script>

大功告成

解决方案2:使用js动态获取对应DOM节点,并拼接DOM(不推荐)

优点:灵活一丢丢
缺点:对于使用需求较多的情况,代码量极多,容易乱(不建议)

通过查看浏览器开发面板的DOM结构并给对应el-form-item组件加上唯一标识来获取对应的错误提示dom。

1、查看dom结构

类型Element缺少类型HTMLElement_前端_06


得到结构后,我们只需要一个最近的唯一标识,正所谓 给我一个支点我可以翘起整个地球,给我一个id我可以获取整个页面。

2、接下来我们来给与他应该id

<el-form-item label="活动名称" prop="name" id="inputName">
     <el-input v-model="ruleForm.name"></el-input>
</el-form-item>

再次查看页面dom结构

类型Element缺少类型HTMLElement_表单_07


id已经成功加入,接下来我们只需要获取到对应id下的class即可

获取到错误信息并且追加DOM
第一个问题:应该在哪获取?
当然是在错误提示时获取
第二个问题:什么时候错误提示?
我tm怎么知道?

我觉得应该和解决方案一中一样,检测应该input的change事件,然后里面延时判断一下即可。

有了解决方案,接下来开干

<template>
    <div id="box">
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
            <el-form-item label="活动名称" prop="name" id="inputName">
                <el-input v-model="ruleForm.name" @change="inputChange"></el-input>
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
    export default {
        name: "TestM",
        //直接替换掉改组件中的所有el-form-item组件,当然你还可以换一个名字注册来单独所有
        data() {
            return {
                ruleForm: {
                    name: ''
                },
                rules: {
                    name: [
                        { required: true, message: '请输入活动名称', trigger: 'blur' },
                        { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
                    ],
                }
            };
        },
        methods:{
            spanClick(){
                this.ruleForm.name = ''
            },
            inputChange(){
                //获取到id
                let FI = document.getElementById("inputName");
                //延时尝试获取错误提示
                setTimeout(()=>{
                    let ErrMsgDom = FI.querySelector(".el-form-item__error")

                    if(ErrMsgDom && !ErrMsgDom.querySelector("span")){//存在且span不存在
                        //创建Dom
                        let spanDom = document.createElement("span");
                        spanDom.innerText = "清空表单"
                        spanDom.style.color = "#0f0"
                        spanDom.addEventListener("click",this.spanClick)
                        //追加dom
                        ErrMsgDom.appendChild(spanDom);
                    }
                },1)
            }
        }
    }
</script>

类型Element缺少类型HTMLElement_解决方案_08


完成(其实这种代码量反而有点大)

鸡汤

编程其实并不难,只是你太想速成