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

可以将其拆分为以下几个组件:
| 名称 | 描述 | 
|---|---|
| md-form | 多个表单组件的外层容器 | 
| md-form-item | 表单项,用来包裹标题与输入组件等 | 
| md-form-label | 表单项标题 | 
| md-input | 输入组件-单行文本 | 
| md-select | 输入组件-选择 | 
| md-textarea | 输入组件-多行文本 | 
拆解如图:

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.$emit 在 md-form-item 触发事件,md-form-item 可以监听实例上的 focus 和 blur 事件。
<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 用于显示单位之类的内容。
value 与 this.$emit('input', val) 可以使 md-input 支持 v-model。
input 元素上 blur、focus 事件触发时,会通过 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.$emit 与 vm.$on,通过 vm.$parent 访问父组件实例。