Browse Source

fix: 修改input单元测试 (#1128)

ailululu 3 years ago
parent
commit
d74f541ee7

+ 23 - 78
src/packages/__VUE/input/__tests__/input.spec.ts

@@ -28,11 +28,6 @@ test('should emit blur event when input is blur', () => {
   wrapper.find('input').trigger('blur');
   expect(wrapper.emitted('blur')).toBeTruthy();
 });
-test('should emit change event when input is change', () => {
-  const wrapper = mount(Input);
-  wrapper.find('input').trigger('input');
-  expect(wrapper.emitted('change')).toBeTruthy();
-});
 
 test('should render clear icon when using clearable prop', async () => {
   const wrapper = mount(Input, {
@@ -41,20 +36,23 @@ test('should render clear icon when using clearable prop', async () => {
       modelValue: 'test'
     }
   });
-
-  const clearBtn = wrapper.find('.nut-textinput-clear');
   const input = wrapper.find('input');
   await input.trigger('focus');
-  // expect(wrapper.find('.nut-textinput-clear').exists()).toBeTruthy();
-
-  wrapper.find('.nut-textinput-clear').trigger('click');
+  // expect(wrapper.find('.nut-input-clear').exists()).toBeTruthy();
+  wrapper.find('.nut-input-clear').trigger('click');
   // expect((wrapper.emitted('update:modelValue') as any)[0][0]).toEqual('');
   // expect((wrapper.emitted('handleClear') as any)[0][0]).toBeTruthy();
 });
-test('should clear  when event clear', () => {
-  const wrapper = mount(Input, { props: { modelValue: 3 } });
+
+test('should clear when event clear', () => {
+  const wrapper = mount(Input, {
+    props: {
+      clearable: true,
+      modelValue: 'test'
+    }
+  });
   const input = wrapper.find('input');
-  const clear = wrapper.find('.nut-textinput-clear');
+  const clear = wrapper.find('.nut-input-clear');
   wrapper.find('input').trigger('input');
   clear.trigger('click');
   expect(clear.exists()).toBe(true);
@@ -71,28 +69,18 @@ test('should format input value when type is number', () => {
     }
   });
   const input = wrapper.find('input');
+
   input.element.value = '1';
   input.trigger('input');
   expect((wrapper.emitted('change') as any)[0][0]).toEqual('1');
 
-  input.element.value = '1.2';
+  input.element.value = '1.1.';
   input.trigger('input');
-  expect((wrapper.emitted('change') as any)[1][0]).toEqual('12');
+  expect((wrapper.emitted('change') as any)[1][0]).toEqual('1.1');
 
-  // input.element.value = '111qwe';
-  // input.trigger('input');
-  // expect((wrapper.emitted('change') as any)[1][0]).toEqual('111');
-});
-test('should format input value when type is number', () => {
-  const wrapper = mount(Input, {
-    props: {
-      type: 'number',
-      modelValue: '123abc'
-    }
-  });
-  const input = wrapper.find('input');
-  input.trigger('blur');
-  expect((wrapper.emitted('blur') as any)[0][0]).toEqual('');
+  input.element.value = '111qwe';
+  input.trigger('input');
+  expect((wrapper.emitted('change') as any)[2][0]).toEqual('111');
 });
 
 // 测试小数
@@ -104,29 +92,14 @@ test('should format input value when type is digit', () => {
     }
   });
   const input = wrapper.find('input');
+
   input.element.value = '1';
   input.trigger('input');
   expect((wrapper.emitted('change') as any)[0][0]).toEqual('1');
 
-  input.element.value = '1.2';
-  input.trigger('input');
-  expect((wrapper.emitted('change') as any)[1][0]).toEqual('1.2');
-});
-
-test('should limit maxlength of input value when using maxlength prop', async () => {
-  const wrapper = mount(Input, {
-    props: {
-      maxLength: 3,
-      modelValue: '1234'
-    }
-  });
-
-  const input = wrapper.find('input');
-  input.trigger('input');
-  expect((wrapper.emitted('change') as any)[0][0]).toEqual('123');
-  input.element.value = '1234';
-  input.trigger('input');
-  expect((wrapper.emitted('change') as any)[0][0]).toEqual('123');
+  // input.element.value = '1.1';
+  // input.trigger('input');
+  // expect((wrapper.emitted('change') as any)[1][0]).toEqual('11');
 });
 
 test('should no label', () => {
@@ -150,11 +123,11 @@ test('should label', () => {
 test('should require', () => {
   const wrapper = mount(Input, {
     props: {
-      requireShow: true
+      required: true
     }
   });
   const input = wrapper.find('.nut-input');
-  expect(input.classes()).toContain('nut-input-require');
+  expect(input.classes()).toContain('nut-input-required');
 });
 
 test('should disabled', () => {
@@ -176,31 +149,3 @@ test('should readonly', () => {
   const input = wrapper.find('input');
   expect(input.attributes('readonly')).toBe('');
 });
-
-test('should text-align left', () => {
-  const wrapper = mount(Input, {
-    props: {
-      textAlign: 'center'
-    }
-  });
-  const input = wrapper.find('input').element;
-  expect(input.style.textAlign).toEqual('center');
-});
-
-test('should render clear icon when using clearable prop', async () => {
-  const wrapper = mount(Input, {
-    props: {
-      clearable: true,
-      modelValue: 'test'
-    }
-  });
-
-  const clearBtn = wrapper.find('.nut-textinput-clear');
-  const input = wrapper.find('input');
-  await input.trigger('focus');
-  // expect(wrapper.find('.nut-textinput-clear').exists()).toBeTruthy();
-
-  wrapper.find('.nut-textinput-clear').trigger('click');
-  // expect((wrapper.emitted('update:modelValue') as any)[0][0]).toEqual('');
-  // expect((wrapper.emitted('handleClear') as any)[0][0]).toBeTruthy();
-});

+ 3 - 0
src/packages/__VUE/input/index.scss

@@ -81,6 +81,9 @@ textarea {
     color: #c8c9cc;
     cursor: pointer;
   }
+  .nut-button {
+    margin-left: 10px;
+  }
   &.nut-input-required {
     &::before {
       position: absolute;

+ 344 - 87
src/packages/__VUE/input/index.taro.vue

@@ -1,79 +1,139 @@
 <template>
   <view :class="classes">
-    <view v-if="label" class="nut-input-label">
-      <view class="label-string">{{ label }}</view>
+    <view v-if="leftIcon && leftIcon.length > 0" class="nut-input-left-icon" @click="onClickLeftIcon">
+      <nut-icon :name="leftIcon" :size="leftIconSize"></nut-icon>
     </view>
-    <view v-if="readonly" class="nut-input__text--readonly">
-      {{ modelValue }}
-    </view>
-    <input
-      v-else
-      class="input-text"
-      :style="styles"
-      :type="type"
-      :maxlength="maxLength"
-      :placeholder="placeholder"
-      :disabled="disabled || readonly"
-      :readonly="readonly"
-      :value="modelValue"
-      @input="valueChange"
-      @focus="valueFocus"
-      @blur="valueBlur"
-    />
     <view
-      @click="handleClear"
-      class="nut-textinput-clear"
-      v-if="clearable && !readonly"
-      v-show="active && modelValue.length > 0"
+      v-if="label"
+      class="nut-input-label"
+      :class="labelClass"
+      :style="{
+        width: `${labelWidth}px`,
+        textAlign: labelAlign
+      }"
     >
-      <nut-icon name="close-little" size="12px"></nut-icon>
+      <view class="label-string">
+        {{ label }}
+        {{ colon ? ':' : '' }}
+      </view>
+    </view>
+    <view class="nut-input-value">
+      <view class="nut-input-inner" @click="onClickInput">
+        <textarea
+          v-if="type == 'textarea'"
+          class="input-text"
+          ref="inputRef"
+          :style="stylesTextarea"
+          :maxlength="maxLength"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="modelValue"
+          :formatTrigger="formatTrigger"
+          :autofocus="autofocus"
+          @input="onInput"
+          @focus="onFocus"
+          @blur="onBlur"
+        />
+        <input
+          v-else
+          class="input-text"
+          ref="inputRef"
+          :style="styles"
+          :type="inputType(type)"
+          :maxNum="maxNum"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="modelValue"
+          :formatTrigger="formatTrigger"
+          :autofocus="autofocus"
+          @input="onInput"
+          @focus="onFocus"
+          @blur="onBlur"
+        />
+        <nut-icon
+          class="nut-input-clear"
+          v-if="clearable && !readonly"
+          v-show="active && modelValue.length > 0"
+          :name="clearIcon"
+          :size="clearSize"
+          @click="clear"
+        >
+        </nut-icon>
+        <view v-if="rightIcon && rightIcon.length > 0" class="nut-input-right-icon" @click="onClickRightIcon">
+          <nut-icon :name="rightIcon" :size="rightIconSize"></nut-icon>
+        </view>
+        <slot v-if="$slots.button" name="button" class="nut-input-button"></slot>
+      </view>
+      <view v-if="showWordLimit && maxNum" class="nut-input-word-limit">
+        <span class="nut-input-word-num">{{ modelValue ? modelValue.length : 0 }}</span
+        >/{{ maxNum }}
+      </view>
+      <view
+        v-if="errorMessage"
+        class="nut-input-error-message"
+        :style="{
+          textAlign: errorMessageAlign
+        }"
+      >
+        {{ errorMessage }}
+      </view>
     </view>
   </view>
 </template>
 <script lang="ts">
-import { ref, computed } from 'vue';
+import { PropType, ref, reactive, computed, onMounted, watch, nextTick, inject } from 'vue';
 import { createComponent } from '../../utils/create';
-function trimExtraChar(value: string, char: string, regExp: RegExp) {
-  const index = value.indexOf(char);
-
-  if (index === -1) {
-    return value;
-  }
-
-  if (char === '-' && index !== 0) {
-    return value.slice(0, index);
-  }
-
-  return value.slice(0, index + 1) + value.slice(index).replace(regExp, '');
-}
-
-function formatNumber(value: string, allowDot = true, allowMinus = true) {
-  if (allowDot) {
-    value = trimExtraChar(value, '.', /\./g);
-  } else {
-    value = value.replace(/\./g, '');
-  }
-
-  if (allowMinus) {
-    value = trimExtraChar(value, '-', /-/g);
-  } else {
-    value = value.replace(/-/, '');
-  }
-
-  const regExp = allowDot ? /[^-0-9.]/g : /[^-0-9]/g;
-
-  return value.replace(regExp, '');
-}
+import { formatNumber } from './util';
 
 const { componentName, create } = createComponent('input');
 interface Events {
-  eventName: 'change' | 'focus' | 'blur' | 'clear' | 'update:modelValue';
+  eventName: 'focus' | 'blur' | 'clear' | 'change' | 'update:modelValue';
   params: (string | number | Event)[];
 }
+export type InputAlignType = 'left' | 'center' | 'right'; // text-align
+export type InputFormatTrigger = 'onChange' | 'onBlur'; // onChange: 在输入时执行格式化 ; onBlur: 在失焦时执行格式化
+export type InputType =
+  | 'tel'
+  | 'url'
+  | 'date'
+  | 'file'
+  | 'text'
+  | 'time'
+  | 'week'
+  | 'color'
+  | 'digit'
+  | 'email'
+  | 'image'
+  | 'month'
+  | 'radio'
+  | 'range'
+  | 'reset'
+  | 'button'
+  | 'hidden'
+  | 'number'
+  | 'search'
+  | 'submit'
+  | 'checkbox'
+  | 'password'
+  | 'textarea'
+  | 'datetime-local';
+
+export type InputRule = {
+  pattern?: RegExp;
+  message?: string;
+  required?: boolean;
+};
+
 export default create({
   props: {
-    type: {
+    ref: {
       type: String,
+      default: ''
+    },
+    type: {
+      type: String as PropType<InputType>,
       default: 'text'
     },
     modelValue: {
@@ -88,7 +148,31 @@ export default create({
       type: String,
       default: ''
     },
-    requireShow: {
+    labelClass: {
+      type: String,
+      default: ''
+    },
+    labelWidth: {
+      type: [String, Number],
+      default: '80'
+    },
+    labelAlign: {
+      type: String as PropType<InputAlignType>,
+      default: 'left'
+    },
+    colon: {
+      type: Boolean,
+      default: false
+    },
+    inputAlign: {
+      type: String,
+      default: 'left'
+    },
+    center: {
+      type: Boolean,
+      default: false
+    },
+    required: {
       type: Boolean,
       default: false
     },
@@ -100,93 +184,266 @@ export default create({
       type: Boolean,
       default: false
     },
-    textAlign: {
+    error: {
+      type: Boolean,
+      default: false
+    },
+    maxNum: {
+      type: [String, Number],
+      default: ''
+    },
+    leftIcon: {
       type: String,
-      default: 'left'
+      default: ''
     },
-    maxLength: {
+    leftIconSize: {
       type: [String, Number],
-      default: '99999999'
+      default: ''
+    },
+    rightIcon: {
+      type: String,
+      default: ''
+    },
+    rightIconSize: {
+      type: [String, Number],
+      default: ''
     },
     clearable: {
       type: Boolean,
+      default: false
+    },
+    clearIcon: {
+      type: String,
+      default: 'mask-close'
+    },
+    clearSize: {
+      type: [String, Number],
+      default: '14'
+    },
+    border: {
+      type: Boolean,
       default: true
     },
-    hasBorder: {
+    formatTrigger: {
+      type: String as PropType<InputFormatTrigger>,
+      default: 'onChange'
+    },
+    formatter: {
+      type: Function as PropType<(value: string) => string>,
+      default: null
+    },
+    rules: {
+      type: Array as PropType<InputRule>,
+      default: []
+    },
+    errorMessage: {
+      type: String,
+      default: ''
+    },
+    errorMessageAlign: {
+      type: String as PropType<InputAlignType>,
+      default: ''
+    },
+    rows: {
+      type: [String, Number],
+      default: null
+    },
+    showWordLimit: {
       type: Boolean,
       default: true
+    },
+    autofocus: {
+      type: Boolean,
+      default: false
     }
   },
 
-  emits: ['change', 'update:modelValue', 'blur', 'focus', 'clear'],
+  emits: [
+    'update:modelValue',
+    'change',
+    'blur',
+    'focus',
+    'clear',
+    'keypress',
+    'click-input',
+    'click-left-icon',
+    'click-right-icon'
+  ],
 
-  setup(props, { emit }) {
+  setup(props, { emit, slots }) {
     const active = ref(false);
 
+    const inputRef = ref<HTMLInputElement>();
+    const customValue = ref<() => unknown>();
+    const getModelValue = () => String(props.modelValue ?? '');
+    // const form = inject('form');
+
+    const state = reactive({
+      focused: false,
+      validateFailed: false, // 校验失败
+      validateMessage: '' // 校验信息
+    });
+
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
         [prefixCls]: true,
+        center: props.center,
         [`${prefixCls}-disabled`]: props.disabled,
-        [`${prefixCls}-require`]: props.requireShow,
-        [`${prefixCls}-border`]: props.hasBorder
+        [`${prefixCls}-required`]: props.required,
+        [`${prefixCls}-error`]: props.error,
+        [`${prefixCls}-border`]: props.border
       };
     });
 
     const styles = computed(() => {
       return {
-        textAlign: props.textAlign
+        textAlign: props.inputAlign
+      };
+    });
+    const stylesTextarea = computed(() => {
+      return {
+        textAlign: props.inputAlign,
+        height: Number(props.rows) * 24 + 'px'
       };
     });
 
-    const valueChange = (event: Event) => {
-      const input = event.target as HTMLInputElement;
-      let val = input.value;
+    const inputType = (type: string) => {
+      if (type === 'number') {
+        return 'text';
+      } else if (type === 'digit') {
+        return 'tel';
+      } else {
+        return type;
+      }
+    };
 
-      if (props.maxLength && val.length > Number(props.maxLength)) {
-        val = val.slice(0, Number(props.maxLength));
+    const formValue = computed(() => {
+      if (customValue.value && slots.input) {
+        return customValue.value();
       }
+      return props.modelValue;
+    });
+
+    // const inputmode = computed(() => {
+    //   return props.type === 'digit' ? 'decimal' : props.type === 'number' ? 'numeric' : 'text';
+    // });
+
+    const onInput = (event: Event) => {
+      const input = event.target as HTMLInputElement;
+      let value = input.value;
+
+      // if (!event.target!.composing) {
+      //   updateValue((event.target as HTMLInputElement).value);
+      // }
+      updateValue(value);
+    };
+
+    const blur = () => inputRef.value?.blur();
+    const focus = () => inputRef.value?.focus();
+
+    const updateValue = (value: string, trigger: InputFormatTrigger = 'onChange') => {
       if (props.type === 'digit') {
-        val = formatNumber(val, true);
+        value = formatNumber(value, false, false);
       }
       if (props.type === 'number') {
-        val = formatNumber(val, false);
+        // console.log('value', value)
+        value = formatNumber(value, true, true);
+      }
+
+      if (props.formatter && trigger === props.formatTrigger) {
+        value = props.formatter(value);
+      }
+
+      // if (props.maxNum && value.length > Number(props.maxNum)) {
+      //   value = value.slice(0, Number(props.maxNum));
+      // }
+
+      if (inputRef.value && inputRef.value.value !== value) {
+        inputRef.value.value = value;
+      }
+
+      if (value !== props.modelValue) {
+        emit('update:modelValue', value);
+        emit('change', value);
       }
-      emit('update:modelValue', val, event);
-      emit('change', val, event);
     };
 
-    const valueFocus = (event: Event) => {
+    const onFocus = (event: Event) => {
       const input = event.target as HTMLInputElement;
       let value = input.value;
       active.value = true;
       emit('focus', value, event);
+      // if (getProp('readonly')) {
+      //   blur();
+      // }
     };
 
-    const valueBlur = (event: Event) => {
+    const onBlur = (event: Event) => {
       setTimeout(() => {
         active.value = false;
-      }, 0);
+      }, 200);
+
+      // if (getProp('readonly')) {
+      //   return;
+      // }
 
       const input = event.target as HTMLInputElement;
       let value = input.value;
+      if (props.maxNum && value.length > Number(props.maxNum)) {
+        value = value.slice(0, Number(props.maxNum));
+      }
+      updateValue(getModelValue(), 'onBlur');
       emit('blur', value, event);
     };
 
-    const handleClear = (event: Event) => {
+    const clear = (event: Event) => {
       emit('update:modelValue', '', event);
       emit('change', '', event);
-      emit('clear', '');
+      emit('clear', '', event);
+    };
+
+    const resetValidation = () => {
+      if (state.validateFailed) {
+        state.validateFailed = false;
+        state.validateMessage = '';
+      }
+    };
+
+    const onClickInput = (event: MouseEvent) => {
+      emit('click-input', event);
     };
 
+    const onClickLeftIcon = (event: MouseEvent) => emit('click-left-icon', event);
+
+    const onClickRightIcon = (event: MouseEvent) => emit('click-right-icon', event);
+
+    watch(
+      () => props.modelValue,
+      () => {
+        updateValue(getModelValue());
+        resetValidation();
+      }
+    );
+
+    onMounted(() => {
+      updateValue(getModelValue(), props.formatTrigger);
+    });
+
     return {
+      inputRef,
       active,
       classes,
       styles,
-      valueChange,
-      valueFocus,
-      valueBlur,
-      handleClear
+      stylesTextarea,
+      inputType,
+      onInput,
+      onFocus,
+      onBlur,
+      clear,
+      onClickInput,
+      onClickLeftIcon,
+      onClickRightIcon
     };
   }
 });

+ 1 - 84
src/packages/__VUE/input/index.vue

@@ -124,7 +124,6 @@ export type InputRule = {
   pattern?: RegExp;
   message?: string;
   required?: boolean;
-  validator?: FieldRuleValidator; // 通过函数进行校验
 };
 
 export default create({
@@ -277,7 +276,7 @@ export default create({
     const inputRef = ref<HTMLInputElement>();
     const customValue = ref<() => unknown>();
     const getModelValue = () => String(props.modelValue ?? '');
-    const form = inject('form');
+    // const form = inject('form');
 
     const state = reactive({
       focused: false,
@@ -370,18 +369,6 @@ export default create({
       }
     };
 
-    // const limitValueLength = (value: string) => {
-    //   const { maxNum } = props;
-    //   if (isDef(maxNum) && getStringLength(value) > maxNum) {
-    //     const modelValue = getModelValue();
-    //     if (modelValue && getStringLength(modelValue) === +maxNum) {
-    //       return modelValue;
-    //     }
-    //     return cutString(value, +maxLength);
-    //   }
-    //   return value;
-    // };
-
     const onFocus = (event: Event) => {
       const input = event.target as HTMLInputElement;
       let value = input.value;
@@ -408,7 +395,6 @@ export default create({
       }
       updateValue(getModelValue(), 'onBlur');
       emit('blur', value, event);
-      validateWithTrigger('onBlur');
     };
 
     const clear = (event: Event) => {
@@ -417,42 +403,6 @@ export default create({
       emit('clear', '', event);
     };
 
-    // const runRules = (rules: InputRule[]) =>
-    //   rules.reduce(
-    //     (promise, rule) =>
-    //       promise.then(() => {
-    //         console.log('promise', promise, 'rule', rule)
-    //         if (state.validateFailed) {
-    //           return;
-    //         }
-
-    //         let { value } = formValue;
-
-    //         // if (rule.formatter) {
-    //         //   value = rule.formatter(value, rule);
-    //         // }
-
-    //         if (!runSyncRule(value, rule)) {
-    //           state.validateFailed = true;
-    //           state.validateMessage = getRuleMessage(value, rule);
-    //           return;
-    //         }
-
-    //         if (rule.validator) {
-    //           return runRuleValidator(value, rule).then((result) => {
-    //             if (result && typeof result === 'string') {
-    //               state.validateFailed = true;
-    //               state.validateMessage = result;
-    //             } else if (result === false) {
-    //               state.validateFailed = true;
-    //               state.validateMessage = getRuleMessage(value, rule);
-    //             }
-    //           });
-    //         }
-    //       }),
-    //     Promise.resolve()
-    //   );
-
     const resetValidation = () => {
       if (state.validateFailed) {
         state.validateFailed = false;
@@ -460,37 +410,6 @@ export default create({
       }
     };
 
-    // const validate = (rules: any) =>
-    //   new Promise<void>((resolve) => {
-    //     console.log('rules122', form.props.rules)
-    //     resetValidation();
-    //     if (rules) {
-    //       console.log('rules233', rules)
-    //       runRules(rules).then(() => {
-    //         if (state.validateFailed) {
-    //           resolve({
-    //             // name: rules,
-    //             message: state.validateMessage
-    //           });
-    //         } else {
-    //           resolve();
-    //         }
-    //       });
-    //     } else {
-    //       resolve();
-    //     }
-    //   });
-
-    const validateWithTrigger = (trigger: InputFormatTrigger) => {
-      if (form && form.props.rules) {
-        const rules = form.props.rules;
-        if (rules) {
-          // console.log('rules', rules)
-          // validate(rules);
-        }
-      }
-    };
-
     const onClickInput = (event: MouseEvent) => {
       emit('click-input', event);
     };
@@ -504,7 +423,6 @@ export default create({
       () => {
         updateValue(getModelValue());
         resetValidation();
-        validateWithTrigger('onChange');
       }
     );
 
@@ -519,7 +437,6 @@ export default create({
       styles,
       stylesTextarea,
       inputType,
-      // inputmode,
       onInput,
       onFocus,
       onBlur,