Skip to content

Commit 55787dd

Browse files
authored
feat(b-form-input, b-form-textarea): add lazy modifier prop to update v-model on change/blur event (#4169)
1 parent 55ff8c9 commit 55787dd

File tree

7 files changed

+197
-115
lines changed

7 files changed

+197
-115
lines changed

src/components/form-input/README.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ native browser HTML5 types: `text`, `password`, `email`, `number`, `url`, `tel`,
3535
<b-container fluid>
3636
<b-row class="my-1" v-for="type in types" :key="type">
3737
<b-col sm="3">
38-
<label :for="`type-${type}`">Type {{ type }}:</label>
38+
<label :for="`type-${type}`">Type <code>{{ type }}</code>:</label>
3939
</b-col>
4040
<b-col sm="9">
4141
<b-form-input :id="`type-${type}`" :type="type"></b-form-input>
@@ -50,13 +50,14 @@ native browser HTML5 types: `text`, `password`, `email`, `number`, `url`, `tel`,
5050
return {
5151
types: [
5252
'text',
53-
'password',
54-
'email',
5553
'number',
54+
'email',
55+
'password',
56+
'search',
5657
'url',
5758
'tel',
5859
'date',
59-
`time`,
60+
'time',
6061
'range',
6162
'color'
6263
]
@@ -75,8 +76,8 @@ rendered and a console warning will be issued.
7576

7677
- Not all browsers support all input types, nor do some types render in the same format across
7778
browser types/versions.
78-
- Browsers that do not support a particular type will fall back to a `text` input type (event
79-
through the rendered `type` attribute markup shows the requested type).
79+
- Browsers that do not support a particular type will fall back to a `text` input type (even though
80+
the rendered `type` attribute markup shows the requested type).
8081
- No testing is performed to see if the requested input type is supported by the browser.
8182
- Chrome lost support for `datetime` in version 26, Opera in version 15, and Safari in iOS 7.
8283
Instead of using `datetime`, since support should be deprecated, use `date` and `time` as two
@@ -85,13 +86,15 @@ rendered and a console warning will be issued.
8586
- For date and time style inputs, where supported, the displayed value in the GUI may be different
8687
than what is returned by it's value (i.e. ordering of year-month-date).
8788
- Regardless of input type, the value is **always** returned as a string representation.
88-
- `v-model.lazy` is not supported by `<b-form-input>` (nor any custom Vue component).
89+
- `v-model.lazy` is not supported by `<b-form-input>` (nor any custom Vue component). Use the `lazy`
90+
prop instead.
8991
- `v-model` modifiers `.number` and `.trim` can cause unexpected cursor jumps when the user is
9092
typing (this is a Vue issue with `v-model` on custom components). _Avoid using these modifiers_.
93+
Use the `number` or `trip` props instead.
9194
- Older version of Firefox may not support `readonly` for `range` type inputs.
9295
- Input types that do not support `min`, `max` and `step` (i.e. `text`, `password`, `tel`, `email`,
9396
`url`, etc) will silently ignore these values (although they will still be rendered on the input
94-
markup).
97+
markup) iv values are provided.
9598

9699
### Range type input
97100

@@ -461,9 +464,9 @@ from an array of options.
461464
Vue does not officially support `.lazy`, `.trim`, and `.number` modifiers on the `v-model` of custom
462465
component based inputs, and may generate a bad user experience. Avoid using Vue's native modifiers.
463466

464-
To get around this, `<b-form-input>` and `<b-form-textarea>` have two boolean props `trim` and
465-
`number` which emulate the native Vue `v-model` modifiers `.trim` and `.number` respectively.
466-
Emulation of the `.lazy` modifier is _not_ supported (listen for `change` or `blur` events instead).
467+
To get around this, `<b-form-input>` and `<b-form-textarea>` have three boolean props `trim`,
468+
`number`, and `lazy` which emulate the native Vue `v-model` modifiers `.trim` and `.number` and
469+
`.lazy` respectively. The `lazy` prop will update the v-model on `change`/`blur`events.
467470

468471
**Notes:**
469472

src/components/form-input/form-input.spec.js

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,6 @@ describe('form-input', () => {
416416
expect(wrapper.emitted('input').length).toEqual(1)
417417
expect(wrapper.emitted('input')[0][0]).toEqual('test')
418418

419-
expect(input.vm.localValue).toEqual('test')
420-
421419
wrapper.destroy()
422420
})
423421

@@ -593,7 +591,7 @@ describe('form-input', () => {
593591
input.element.focus()
594592
input.trigger('wheel', { deltaY: 33.33, deltaX: 0, deltaZ: 0, deltaMode: 0 })
595593

596-
// no-wheel=true will fire a blur event on the input when wheel fired
594+
// `:no-wheel="true"` will fire a blur event on the input when wheel fired
597595
expect(spy).toHaveBeenCalled()
598596

599597
wrapper.destroy()
@@ -620,7 +618,7 @@ describe('form-input', () => {
620618
input.element.focus()
621619
input.trigger('wheel', { deltaY: 33.33, deltaX: 0, deltaZ: 0, deltaMode: 0 })
622620

623-
// no-wheel=false will not fire a blur event on the input when wheel fired
621+
// `:no-wheel="false"` will not fire a blur event on the input when wheel fired
624622
expect(spy).not.toHaveBeenCalled()
625623

626624
wrapper.destroy()
@@ -677,16 +675,16 @@ describe('form-input', () => {
677675
await waitNT(wrapper.vm)
678676

679677
expect(input.element.value).toBe('123.450')
680-
// Pre converted value as string
681-
expect(wrapper.emitted('input')).toBeDefined()
682-
expect(wrapper.emitted('input').length).toBe(1)
683-
expect(wrapper.emitted('input')[0].length).toEqual(1)
684-
expect(wrapper.emitted('input')[0][0]).toEqual('123.450')
685-
// v-model update event (should emit a numerical value)
678+
// `v-model` update event (should emit a numerical value)
686679
expect(wrapper.emitted('update')).toBeDefined()
687680
expect(wrapper.emitted('update').length).toBe(1)
688681
expect(wrapper.emitted('update')[0].length).toEqual(1)
689682
expect(wrapper.emitted('update')[0][0]).toBeCloseTo(123.45)
683+
// Pre converted value as string (raw input value)
684+
expect(wrapper.emitted('input')).toBeDefined()
685+
expect(wrapper.emitted('input').length).toBe(1)
686+
expect(wrapper.emitted('input')[0].length).toEqual(1)
687+
expect(wrapper.emitted('input')[0][0]).toEqual('123.450')
690688

691689
// Update the input to be different string-wise, but same numerically
692690
input.element.value = '123.4500'
@@ -697,11 +695,11 @@ describe('form-input', () => {
697695
// Should emit a new input event
698696
expect(wrapper.emitted('input').length).toEqual(2)
699697
expect(wrapper.emitted('input')[1][0]).toEqual('123.4500')
700-
// Should emit a new update event
701-
expect(wrapper.emitted('update').length).toBe(2)
698+
// `v-model` value stays the same and update event shouldn't be emitted again
699+
expect(wrapper.emitted('update').length).toBe(1)
702700
expect(wrapper.emitted('update')[0][0]).toBeCloseTo(123.45)
703701

704-
// Updating the v-model to new numeric value
702+
// Updating the `v-model` to new numeric value
705703
wrapper.setProps({
706704
value: 45.6
707705
})
@@ -711,6 +709,63 @@ describe('form-input', () => {
711709
wrapper.destroy()
712710
})
713711

712+
it('"lazy" modifier prop works', async () => {
713+
const wrapper = mount(BFormInput, {
714+
propsData: {
715+
type: 'text',
716+
lazy: true
717+
}
718+
})
719+
720+
const input = wrapper.find('input')
721+
input.element.value = 'a'
722+
input.trigger('input')
723+
await waitNT(wrapper.vm)
724+
expect(input.element.value).toBe('a')
725+
// `v-model` update event should not have emitted
726+
expect(wrapper.emitted('update')).not.toBeDefined()
727+
728+
input.element.value = 'ab'
729+
input.trigger('input')
730+
await waitNT(wrapper.vm)
731+
expect(input.element.value).toBe('ab')
732+
// `v-model` update event should not have emitted
733+
expect(wrapper.emitted('update')).not.toBeDefined()
734+
735+
// trigger a change event
736+
input.trigger('change')
737+
await waitNT(wrapper.vm)
738+
expect(input.element.value).toBe('ab')
739+
// `v-model` update event should have emitted
740+
expect(wrapper.emitted('update')).toBeDefined()
741+
expect(wrapper.emitted('update').length).toEqual(1)
742+
expect(wrapper.emitted('update')[0][0]).toBe('ab')
743+
744+
input.element.value = 'abc'
745+
input.trigger('input')
746+
await waitNT(wrapper.vm)
747+
expect(input.element.value).toBe('abc')
748+
// `v-model` update event should not have emitted new event
749+
expect(wrapper.emitted('update').length).toEqual(1)
750+
751+
input.element.value = 'abcd'
752+
input.trigger('input')
753+
await waitNT(wrapper.vm)
754+
expect(input.element.value).toBe('abcd')
755+
// `v-model` update event should not have emitted new event
756+
expect(wrapper.emitted('update').length).toEqual(1)
757+
758+
// Trigger a blur event
759+
input.trigger('blur')
760+
await waitNT(wrapper.vm)
761+
expect(input.element.value).toBe('abcd')
762+
// `v-model` update event should have emitted
763+
expect(wrapper.emitted('update').length).toEqual(2)
764+
expect(wrapper.emitted('update')[1][0]).toBe('abcd')
765+
766+
wrapper.destroy()
767+
})
768+
714769
it('focus() and blur() methods work', async () => {
715770
const wrapper = mount(BFormInput, {
716771
mountToDocument: true

src/components/form-input/package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@
2121
},
2222
{
2323
"prop": "trim",
24-
"description": "When set, trims any leading and trailing white space from the input value"
24+
"description": "When set, trims any leading and trailing white space from the input value. Emulates the Vue '.trim' v-model modifier"
2525
},
2626
{
2727
"prop": "number",
28-
"description": "When set attempts to convert the input value to a native number"
28+
"description": "When set attempts to convert the input value to a native number. Emulates the Vue '.number' v-model modifier"
29+
},
30+
{
31+
"prop": "lazy",
32+
"version": "2.1.0",
33+
"description": "When set, updates the v-model on 'change'/'blur' events instead of 'input'. Emulates the Vue '.lazy' v-model modifier"
2934
},
3035
{
3136
"prop": "type",
@@ -63,7 +68,7 @@
6368
"events": [
6469
{
6570
"event": "input",
66-
"description": "Input event triggered by user interaction. Emitted after any formatting and after the v-model is updated",
71+
"description": "Input event triggered by user interaction. Emitted after any formatting (not including 'trim' or 'number' props) and after the v-model is updated",
6772
"args": [
6873
{
6974
"arg": "value",
@@ -77,7 +82,7 @@
7782
},
7883
{
7984
"event": "change",
80-
"description": "Change event triggered by user interaction. Emitted after any formatting and after the v-model is updated.",
85+
"description": "Change event triggered by user interaction. Emitted after any formatting (not including 'trim' or 'number' props) and after the v-model is updated.",
8186
"args": [
8287
{
8388
"arg": "value",

src/components/form-textarea/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ form field styling and preserve the correct text size, margin, padding and heigh
277277
Vue does not officially support `.lazy`, `.trim`, and `.number` modifiers on the `v-model` of custom
278278
component based inputs, and may generate a bad user experience. Avoid using Vue's native modifiers.
279279

280-
To get around this, `<b-for-textarea>` and `<b-form-input>` have two boolean props `trim` and
281-
`number` which emulate the native Vue `v-model` modifiers `.trim` and `.number` respectively.
282-
Emulation of the `.lazy` modifier is _not_ supported (listen for `change` or `blur` events instead).
280+
To get around this, `<b-form-input>` and `<b-form-textarea>` have three boolean props `trim`,
281+
`number`, and `lazy` which emulate the native Vue `v-model` modifiers `.trim` and `.number` and
282+
`.lazy` respectively. The `lazy` prop will update the v-model on `change`/`blur`events.
283283

284284
**Notes:**
285285

@@ -295,10 +295,10 @@ Emulation of the `.lazy` modifier is _not_ supported (listen for `change` or `bl
295295

296296
## Autofocus
297297

298-
When the `autofocus` prop is set on `<b-form-textarea>`, the tetarea will be auto-focused when it is
299-
inserted (i.e. **mounted**) into the document or re-activated when inside a Vue `<keep-alive>`
300-
component. Note that this prop **does not** set the `autofocus` attribute on the tetarea, nor can it
301-
tell when the textarea becomes visible.
298+
When the `autofocus` prop is set on `<b-form-textarea>`, the textarea will be auto-focused when it
299+
is inserted (i.e. **mounted**) into the document or re-activated when inside a Vue `<keep-alive>`
300+
component. Note that this prop **does not** set the `autofocus` attribute on the textarea, nor can
301+
it tell when the textarea becomes visible.
302302

303303
## Native and custom events
304304

src/components/form-textarea/form-textarea.spec.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,6 @@ describe('form-textarea', () => {
259259
const input = mount(BFormTextarea)
260260

261261
input.element.value = 'test'
262-
// Need to trigger an input event before change can be emitted
263-
input.trigger('input')
264-
expect(input.emitted('change')).not.toBeDefined()
265262

266263
input.trigger('change')
267264
expect(input.emitted('change')).toBeDefined()
@@ -799,9 +796,9 @@ describe('form-textarea', () => {
799796
input.trigger('input')
800797

801798
expect(input.vm.localValue).toEqual('TEST ')
799+
// `v-model` value stays the same and update event shouldn't be emitted again
802800
expect(input.emitted('update')).toBeDefined()
803-
expect(input.emitted('update').length).toEqual(2)
804-
expect(input.emitted('update')[1][0]).toEqual('TEST')
801+
expect(input.emitted('update').length).toEqual(1)
805802
expect(input.emitted('input')).toBeDefined()
806803
expect(input.emitted('input').length).toEqual(2)
807804
expect(input.emitted('input')[1][0]).toEqual('TEST ')
@@ -810,27 +807,29 @@ describe('form-textarea', () => {
810807
input.trigger('input')
811808

812809
expect(input.vm.localValue).toEqual(' TEST ')
810+
// `v-model` value stays the same and update event shouldn't be emitted again
813811
expect(input.emitted('update')).toBeDefined()
814-
expect(input.emitted('update').length).toEqual(3)
815-
expect(input.emitted('update')[2][0]).toEqual('TEST')
812+
expect(input.emitted('update').length).toEqual(1)
816813
expect(input.emitted('input')).toBeDefined()
817814
expect(input.emitted('input').length).toEqual(3)
818815
expect(input.emitted('input')[2][0]).toEqual(' TEST ')
819816

820817
input.trigger('input')
821818

822819
expect(input.vm.localValue).toEqual(' TEST ')
820+
// `v-model` value stays the same and update event shouldn't be emitted again
823821
expect(input.emitted('update')).toBeDefined()
824-
expect(input.emitted('update').length).toEqual(3) // Not emitted because no change in value
822+
expect(input.emitted('update').length).toEqual(1)
825823
expect(input.emitted('input')).toBeDefined()
826824
expect(input.emitted('input').length).toEqual(4)
827825
expect(input.emitted('input')[3][0]).toEqual(' TEST ')
828826

829827
input.trigger('change')
830828

831829
expect(input.vm.localValue).toEqual(' TEST ')
830+
// `v-model` value stays the same and update event shouldn't be emitted again
832831
expect(input.emitted('update')).toBeDefined()
833-
expect(input.emitted('update').length).toEqual(3) // Not emitted because no change in value
832+
expect(input.emitted('update').length).toEqual(1)
834833
expect(input.emitted('change')).toBeDefined()
835834
expect(input.emitted('change').length).toEqual(1)
836835
expect(input.emitted('change')[0][0]).toEqual(' TEST ')
@@ -878,10 +877,10 @@ describe('form-textarea', () => {
878877
input.trigger('input')
879878

880879
expect(input.vm.localValue).toEqual('0123.450')
880+
// `v-model` value stays the same and update event shouldn't be emitted again
881881
expect(input.emitted('update')).toBeDefined()
882-
expect(input.emitted('update').length).toEqual(3)
883-
expect(input.emitted('update')[2][0]).toEqual(123.45)
884-
expect(typeof input.emitted('update')[2][0]).toEqual('number')
882+
expect(input.emitted('update').length).toEqual(2)
883+
expect(input.emitted('update')[1][0]).toEqual(123.45)
885884
expect(input.emitted('input')).toBeDefined()
886885
expect(input.emitted('input').length).toEqual(3)
887886
expect(input.emitted('input')[2][0]).toEqual('0123.450')
@@ -892,9 +891,9 @@ describe('form-textarea', () => {
892891

893892
expect(input.vm.localValue).toEqual('0123 450')
894893
expect(input.emitted('update')).toBeDefined()
895-
expect(input.emitted('update').length).toEqual(4)
896-
expect(input.emitted('update')[3][0]).toEqual(123)
897-
expect(typeof input.emitted('update')[3][0]).toEqual('number')
894+
expect(input.emitted('update').length).toEqual(3)
895+
expect(input.emitted('update')[2][0]).toEqual(123)
896+
expect(typeof input.emitted('update')[2][0]).toEqual('number')
898897
expect(input.emitted('input')).toBeDefined()
899898
expect(input.emitted('input').length).toEqual(4)
900899
expect(input.emitted('input')[3][0]).toEqual('0123 450')
@@ -903,12 +902,13 @@ describe('form-textarea', () => {
903902
input.destroy()
904903
})
905904

906-
// These tests are wrapped in a new describe to limit the scope of the getBCR Mock
905+
// These tests are wrapped in a new describe to limit
906+
// the scope of the `getBoundingClientRect` mock
907907
describe('prop `autofocus`', () => {
908908
const origGetBCR = Element.prototype.getBoundingClientRect
909909

910910
beforeEach(() => {
911-
// Mock getBCR so that the isVisible(el) test returns true
911+
// Mock `getBoundingClientRect` so that the `isVisible(el)` test returns `true`
912912
// In our test below, all pagination buttons would normally be visible
913913
Element.prototype.getBoundingClientRect = jest.fn(() => ({
914914
width: 24,

0 commit comments

Comments
 (0)