diff --git a/packages/input/src/input.vue b/packages/input/src/input.vue index 148f49ed3..89b9bce66 100644 --- a/packages/input/src/input.vue +++ b/packages/input/src/input.vue @@ -28,11 +28,9 @@ :disabled="inputDisabled" :readonly="readonly" :autocomplete="autoComplete || autocomplete" - :value="nativeInputValue" ref="input" - @compositionstart="handleComposition" - @compositionupdate="handleComposition" - @compositionend="handleComposition" + @compositionstart="handleCompositionStart" + @compositionend="handleCompositionEnd" @input="handleInput" @focus="handleFocus" @blur="handleBlur" @@ -82,10 +80,8 @@ v-else :tabindex="tabindex" class="el-textarea__inner" - :value="nativeInputValue" - @compositionstart="handleComposition" - @compositionupdate="handleComposition" - @compositionend="handleComposition" + @compositionstart="handleCompositionStart" + @compositionend="handleCompositionEnd" @input="handleInput" ref="textarea" v-bind="$attrs" @@ -130,7 +126,7 @@ textareaCalcStyle: {}, hovering: false, focused: false, - isOnComposition: false, + isComposing: false, passwordVisible: false }; }, @@ -208,7 +204,7 @@ return this.disabled || (this.elForm || {}).disabled; }, nativeInputValue() { - return this.value === null || this.value === undefined ? '' : this.value; + return this.value === null || this.value === undefined ? '' : String(this.value); }, showClear() { return this.clearable && @@ -231,6 +227,12 @@ if (this.validateEvent) { this.dispatch('ElFormItem', 'el.form.change', [val]); } + }, + // native input value is set explicitly + // do not use v-model / :value in template + // see: https://github.com/ElemeFE/element/issues/14521 + nativeInputValue() { + this.setNativeInputValue(); } }, @@ -277,21 +279,27 @@ this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows); }, + setNativeInputValue() { + const input = this.getInput(); + if (!input) return; + if (input.value === this.nativeInputValue) return; + input.value = this.nativeInputValue; + }, handleFocus(event) { this.focused = true; this.$emit('focus', event); }, - handleComposition(event) { - if (event.type === 'compositionstart') { - this.isOnComposition = true; - } - if (event.type === 'compositionend') { - this.isOnComposition = false; - this.handleInput(event); - } + handleCompositionStart() { + this.isComposing = true; + }, + handleCompositionEnd(event) { + this.isComposing = false; + this.handleInput(event); }, handleInput(event) { - if (this.isOnComposition) return; + // should not emit input during composition + // see: https://github.com/ElemeFE/element/issues/10516 + if (this.isComposing) return; // hack for https://github.com/ElemeFE/element/issues/8548 // should remove the following line when we don't support IE @@ -299,12 +307,9 @@ this.$emit('input', event.target.value); - // set input's value, in case parent refuses the change + // ensure native input value is controlled // see: https://github.com/ElemeFE/element/issues/12850 - this.$nextTick(() => { - let input = this.getInput(); - input.value = this.value; - }); + this.$nextTick(this.setNativeInputValue); }, handleChange(event) { this.$emit('change', event.target.value); @@ -355,6 +360,7 @@ }, mounted() { + this.setNativeInputValue(); this.resizeTextarea(); this.updateIconOffset(); }, diff --git a/test/unit/specs/input.spec.js b/test/unit/specs/input.spec.js index 089afe9ee..5c26589c7 100644 --- a/test/unit/specs/input.spec.js +++ b/test/unit/specs/input.spec.js @@ -1,4 +1,4 @@ -import { createVue, destroyVM, wait, waitImmediate } from '../util'; +import { createVue, destroyVM, triggerEvent, wait, waitImmediate } from '../util'; describe('Input', () => { let vm; @@ -6,7 +6,7 @@ describe('Input', () => { destroyVM(vm); }); - it('create', () => { + it('create', async() => { vm = createVue({ template: ` { :maxlength="5" placeholder="请输入内容" @focus="handleFocus" - value="input"> + :value="input"> `, data() { return { + input: 'input', inputFocus: false }; }, @@ -35,6 +36,18 @@ describe('Input', () => { expect(inputElm.value).to.equal('input'); expect(inputElm.getAttribute('minlength')).to.equal('3'); expect(inputElm.getAttribute('maxlength')).to.equal('5'); + + vm.input = 'text'; + await waitImmediate(); + expect(inputElm.value).to.equal('text'); + }); + + it('default to empty', () => { + vm = createVue({ + template: '' + }, true); + let inputElm = vm.$el.querySelector('input'); + expect(inputElm.value).to.equal(''); }); it('disabled', () => { @@ -236,7 +249,7 @@ describe('Input', () => { }); describe('Input Events', () => { - it('event:focus & blur', done => { + it('event:focus & blur', async() => { vm = createVue({ template: ` { vm.$el.querySelector('input').focus(); vm.$el.querySelector('input').blur(); - vm.$nextTick(_ => { - expect(spyFocus.calledOnce).to.be.true; - expect(spyBlur.calledOnce).to.be.true; - done(); - }); + await waitImmediate(); + expect(spyFocus.calledOnce).to.be.true; + expect(spyBlur.calledOnce).to.be.true; }); - it('event:change', done => { + it('event:change', async() => { // NOTE: should be same as native's change behavior vm = createVue({ template: ` @@ -290,13 +301,11 @@ describe('Input', () => { // simplified test, component should emit change when native does simulateEvent('1', 'input'); simulateEvent('2', 'change'); - vm.$nextTick(_ => { - expect(spy.calledWith('2')).to.be.true; - expect(spy.calledOnce).to.be.true; - done(); - }); + await waitImmediate(); + expect(spy.calledWith('2')).to.be.true; + expect(spy.calledOnce).to.be.true; }); - it('event:clear', done => { + it('event:clear', async() => { vm = createVue({ template: ` { // focus to show clear button inputElm.focus(); vm.$refs.input.$on('clear', spyClear); - vm.$nextTick(_ => { - vm.$el.querySelector('.el-input__clear').click(); - vm.$nextTick(_ => { - expect(spyClear.calledOnce).to.be.true; - done(); - }); - }); + await waitImmediate(); + vm.$el.querySelector('.el-input__clear').click(); + await waitImmediate(); + expect(spyClear.calledOnce).to.be.true; + }); + it('event:input', async() => { + vm = createVue({ + template: ` + + + `, + data() { + return { + input: 'a' + }; + } + }, true); + const spy = sinon.spy(); + vm.$refs.input.$on('input', spy); + const nativeInput = vm.$refs.input.$el.querySelector('input'); + nativeInput.value = '1'; + triggerEvent(nativeInput, 'compositionstart'); + triggerEvent(nativeInput, 'input'); + await waitImmediate(); + nativeInput.value = '2'; + triggerEvent(nativeInput, 'compositionupdate'); + triggerEvent(nativeInput, 'input'); + await waitImmediate(); + triggerEvent(nativeInput, 'compositionend'); + await waitImmediate(); + // input event does not fire during composition + expect(spy.calledOnce).to.be.true; + // native input value is controlled + expect(vm.input).to.equal('a'); + expect(nativeInput.value).to.equal('a'); + }); }); describe('Input Methods', () => { - it('method:select', done => { + it('method:select', async() => { const testContent = 'test'; vm = createVue({ @@ -347,11 +389,9 @@ describe('Input', () => { vm.$refs.inputComp.select(); - vm.$nextTick(_ => { - expect(vm.$refs.inputComp.$refs.input.selectionStart).to.equal(0); - expect(vm.$refs.inputComp.$refs.input.selectionEnd).to.equal(testContent.length); - done(); - }); + await waitImmediate(); + expect(vm.$refs.inputComp.$refs.input.selectionStart).to.equal(0); + expect(vm.$refs.inputComp.$refs.input.selectionEnd).to.equal(testContent.length); }); }); });