Hybrid App 表单组件设计实现(Vue 版)

App 改版了,表单经过重新设计,需要实现新的表单组件。由于参考了 Material Design,故以 md 为命名空间。

设计如图:

new form design

可以将其拆分为以下几个组件:

名称 描述
md-form 多个表单组件的外层容器
md-form-item 表单项,用来包裹标题与输入组件等
md-form-label 表单项标题
md-input 输入组件-单行文本
md-select 输入组件-选择
md-textarea 输入组件-多行文本

拆解如图:

new form struct

md-form

仅作为容器,提供样式。

<template lang="pug">
.md-form
  slot
</template>

<script>
export default {
  name: 'md-form'
};
</script>

md-form-item

基本表单项。

每个 md-form-item 包含:

  • label
  • input
  • bottom

3 个 slot,分别对应标题组件、输入组件和其他元素。

由于一些操作要求点击整个表单项即触发,所以要在 .md-form-item 绑定 click 事件,并在当前实例上触发($emit)它。

md-form-item 需要能够获取到输入组件的 focus 状态:输入组件通过 $parent.$emitmd-form-item 触发事件,md-form-item 可以监听实例上的 focusblur 事件。

<template lang="pug">
.md-form-item(
  @click="handleClick",
  :class="{ active: focus, arrow: arrow, 'input-focus': focus && focusType === 'input' }"
)
  slot(name="label")
  slot(name="input")
  slot(name="bottom")
  .md-input-border(v-if="!borderless")
  arrow-right(v-if="arrow")
</template>

<script>
const getInitialData = () => {
  return {
    focus: false,
    focusType: ''
  }
};

export default {
  name: 'md-form-item',
  data: getInitialData,
  props: {
    borderless: {
      type: Boolean,
      required: false,
      default: false
    },
    arrow: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  mounted() {
    this.$on('focus', this.onFocus);
    this.$on('blur', this.onBlur);
  },
  methods: {
    handleClick() {
      this.$emit('form-item:click');
    },
    onFocus(e, type) {
      this.focus = true;
      this.focusType = type;
    },
    onBlur(e, type) {
      this.focus = false;
      this.focusType = '';
    }
  }
};
</script>

md-form-label

普通的 label 组件,用来展示表单项的标题。由于标题可能不是纯文本格式,所以需要使用 slot 的方式传入标题。

<template lang="pug">
.md-form-label
  slot
</template>

<script>
export default {
  name: 'md-form-label'
};
</script>

md-input

单行文本输入组件,使用 input 元素。

subfix slot 用于显示单位之类的内容。

valuethis.$emit('input', val) 可以使 md-input 支持 v-model

input 元素上 blurfocus 事件触发时,会通过 this.$parent.$emit('focus', event, 'input') 传到父元素,'input' 用于区分 input、select 与 textarea。

<template lang="pug">
.md-input
  input.md-input-input(
    :type="type",
    :value="value",
    :placeholder="placeholder",
    :step="step",
    :maxlength="maxlength",
    @input="handleInput",
    @focus="handleFocus",
    @blur="handleBlur",
  )
  slot(name="subfix")
</template>

<script>
const getInitialData = () => {
  return {
    focus: false
  }
};

export default {
  name: 'md-input',
  data: getInitialData,
  props: {
    value: {
      type: String,
      required: true
    },
    type: {
      type: String,
      required: false,
      default: 'text'
    },
    placeholder: {
      type: String,
      required: false
    },
    maxlength: {
      type: Number,
      required: false
    },
    step: {
      required: false
    }
  },
  methods: {
    handleInput(event) {
      const val = event.target.value;
      this.$emit('input', val);
    },
    handleFocus(event) {
      this.focus = true;
      this.$emit('focus', event);
      this.$parent.$emit('focus', event, 'input');
    },
    handleBlur(event) {
      this.focus = false;
      this.$emit('blur', event);
      this.$parent.$emit('blur', event, 'input');
    }
  }
};
</script>

md-select

选择输入组件,点击外层 md-form-item 会触发本组件上的 click 事件,调出实际的选择组件(actionsheet 等)。

<template lang="pug">
.md-select
  .md-select-placeholder(v-if="value == null") {{ placeholder }}
  .md-select-value(v-if="value != null") {{ value }}
</template>

<script>
const getInitialData = () => {
  return {
  }
};

export default {
  name: 'md-select',
  data: getInitialData,
  mounted() {
    this.$parent.$on('form-item:click', () => {
      this.onFormItemClick();
    });
  },
  props: {
    value: {
      required: true
    },
    placeholder: {
      type: String,
      required: false,
      default: ''
    }
  },
  methods: {
    onFormItemClick() {
      this.$emit('click');
    }
  }
};
</script>

md-textarea

多行文本输入组件,使用 textarea 元素,与 md-input 类似。

<template lang="pug">
.md-textarea
  textarea.md-textarea-textarea(
    :value="value",
    :placeholder="placeholder",
    :rows="rows",
    :maxlength="maxlength",
    @input="handleInput",
    @focus="handleFocus",
    @blur="handleBlur",
  )
</template>

<script>
const getInitialData = () => {
  return {
    focus: false
  }
};

export default {
  name: 'md-input',
  data: getInitialData,
  props: {
    value: {
      type: String,
      required: true
    },
    placeholder: {
      type: String,
      required: false,
      default: ''
    },
    maxlength: {
      type: Number,
      required: false
    },
    rows: {
      required: false,
      default: 4
    }
  },
  methods: {
    handleInput(event) {
      const val = event.target.value;
      this.$emit('input', val);
    },
    handleFocus(event) {
      this.focus = true;
      this.$emit('focus', event);
      this.$parent.$emit('focus', event, 'textarea');
    },
    handleBlur(event) {
      this.focus = false;
      this.$emit('blur', event);
      this.$parent.$emit('blur', event, 'textarea');
    }
  }
};
</script>

总结

由于目前设计比较简洁,所以并不需要使用 functional 组件即可实现。组件嵌套使用了 slot,组件间的通信使用了 vm.$emitvm.$on,通过 vm.$parent 访问父组件实例。