Skip to content
Permalink
Browse files

feat(b-form-input, b-form-textarea): add `lazy` modifier prop to upda…

…te v-model on change/blur event (#4169)
  • Loading branch information
tmorehouse committed Oct 17, 2019
1 parent 55ff8c9 commit 55787ddea56c96f6271bb9f832d33887a35919f4
@@ -35,7 +35,7 @@ native browser HTML5 types: `text`, `password`, `email`, `number`, `url`, `tel`,
<b-container fluid>
<b-row class="my-1" v-for="type in types" :key="type">
<b-col sm="3">
<label :for="`type-${type}`">Type {{ type }}:</label>
<label :for="`type-${type}`">Type <code>{{ type }}</code>:</label>
</b-col>
<b-col sm="9">
<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`,
return {
types: [
'text',
'password',
'email',
'number',
'email',
'password',
'search',
'url',
'tel',
'date',
`time`,
'time',
'range',
'color'
]
@@ -75,8 +76,8 @@ rendered and a console warning will be issued.

- Not all browsers support all input types, nor do some types render in the same format across
browser types/versions.
- Browsers that do not support a particular type will fall back to a `text` input type (event
through the rendered `type` attribute markup shows the requested type).
- Browsers that do not support a particular type will fall back to a `text` input type (even though
the rendered `type` attribute markup shows the requested type).
- No testing is performed to see if the requested input type is supported by the browser.
- Chrome lost support for `datetime` in version 26, Opera in version 15, and Safari in iOS 7.
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.
- For date and time style inputs, where supported, the displayed value in the GUI may be different
than what is returned by it's value (i.e. ordering of year-month-date).
- Regardless of input type, the value is **always** returned as a string representation.
- `v-model.lazy` is not supported by `<b-form-input>` (nor any custom Vue component).
- `v-model.lazy` is not supported by `<b-form-input>` (nor any custom Vue component). Use the `lazy`
prop instead.
- `v-model` modifiers `.number` and `.trim` can cause unexpected cursor jumps when the user is
typing (this is a Vue issue with `v-model` on custom components). _Avoid using these modifiers_.
Use the `number` or `trip` props instead.
- Older version of Firefox may not support `readonly` for `range` type inputs.
- Input types that do not support `min`, `max` and `step` (i.e. `text`, `password`, `tel`, `email`,
`url`, etc) will silently ignore these values (although they will still be rendered on the input
markup).
markup) iv values are provided.

### Range type input

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

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

**Notes:**

@@ -416,8 +416,6 @@ describe('form-input', () => {
expect(wrapper.emitted('input').length).toEqual(1)
expect(wrapper.emitted('input')[0][0]).toEqual('test')

expect(input.vm.localValue).toEqual('test')

wrapper.destroy()
})

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

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

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

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

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

expect(input.element.value).toBe('123.450')
// Pre converted value as string
expect(wrapper.emitted('input')).toBeDefined()
expect(wrapper.emitted('input').length).toBe(1)
expect(wrapper.emitted('input')[0].length).toEqual(1)
expect(wrapper.emitted('input')[0][0]).toEqual('123.450')
// v-model update event (should emit a numerical value)
// `v-model` update event (should emit a numerical value)
expect(wrapper.emitted('update')).toBeDefined()
expect(wrapper.emitted('update').length).toBe(1)
expect(wrapper.emitted('update')[0].length).toEqual(1)
expect(wrapper.emitted('update')[0][0]).toBeCloseTo(123.45)
// Pre converted value as string (raw input value)
expect(wrapper.emitted('input')).toBeDefined()
expect(wrapper.emitted('input').length).toBe(1)
expect(wrapper.emitted('input')[0].length).toEqual(1)
expect(wrapper.emitted('input')[0][0]).toEqual('123.450')

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

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

it('"lazy" modifier prop works', async () => {
const wrapper = mount(BFormInput, {
propsData: {
type: 'text',
lazy: true
}
})

const input = wrapper.find('input')
input.element.value = 'a'
input.trigger('input')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('a')
// `v-model` update event should not have emitted
expect(wrapper.emitted('update')).not.toBeDefined()

input.element.value = 'ab'
input.trigger('input')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('ab')
// `v-model` update event should not have emitted
expect(wrapper.emitted('update')).not.toBeDefined()

// trigger a change event
input.trigger('change')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('ab')
// `v-model` update event should have emitted
expect(wrapper.emitted('update')).toBeDefined()
expect(wrapper.emitted('update').length).toEqual(1)
expect(wrapper.emitted('update')[0][0]).toBe('ab')

input.element.value = 'abc'
input.trigger('input')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('abc')
// `v-model` update event should not have emitted new event
expect(wrapper.emitted('update').length).toEqual(1)

input.element.value = 'abcd'
input.trigger('input')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('abcd')
// `v-model` update event should not have emitted new event
expect(wrapper.emitted('update').length).toEqual(1)

// Trigger a blur event
input.trigger('blur')
await waitNT(wrapper.vm)
expect(input.element.value).toBe('abcd')
// `v-model` update event should have emitted
expect(wrapper.emitted('update').length).toEqual(2)
expect(wrapper.emitted('update')[1][0]).toBe('abcd')

wrapper.destroy()
})

it('focus() and blur() methods work', async () => {
const wrapper = mount(BFormInput, {
mountToDocument: true
@@ -21,11 +21,16 @@
},
{
"prop": "trim",
"description": "When set, trims any leading and trailing white space from the input value"
"description": "When set, trims any leading and trailing white space from the input value. Emulates the Vue '.trim' v-model modifier"
},
{
"prop": "number",
"description": "When set attempts to convert the input value to a native number"
"description": "When set attempts to convert the input value to a native number. Emulates the Vue '.number' v-model modifier"
},
{
"prop": "lazy",
"version": "2.1.0",
"description": "When set, updates the v-model on 'change'/'blur' events instead of 'input'. Emulates the Vue '.lazy' v-model modifier"
},
{
"prop": "type",
@@ -63,7 +68,7 @@
"events": [
{
"event": "input",
"description": "Input event triggered by user interaction. Emitted after any formatting and after the v-model is updated",
"description": "Input event triggered by user interaction. Emitted after any formatting (not including 'trim' or 'number' props) and after the v-model is updated",
"args": [
{
"arg": "value",
@@ -77,7 +82,7 @@
},
{
"event": "change",
"description": "Change event triggered by user interaction. Emitted after any formatting and after the v-model is updated.",
"description": "Change event triggered by user interaction. Emitted after any formatting (not including 'trim' or 'number' props) and after the v-model is updated.",
"args": [
{
"arg": "value",
@@ -277,9 +277,9 @@ form field styling and preserve the correct text size, margin, padding and heigh
Vue does not officially support `.lazy`, `.trim`, and `.number` modifiers on the `v-model` of custom
component based inputs, and may generate a bad user experience. Avoid using Vue's native modifiers.

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

**Notes:**

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

## Autofocus

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

## Native and custom events

@@ -259,9 +259,6 @@ describe('form-textarea', () => {
const input = mount(BFormTextarea)

input.element.value = 'test'
// Need to trigger an input event before change can be emitted
input.trigger('input')
expect(input.emitted('change')).not.toBeDefined()

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

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

expect(input.vm.localValue).toEqual(' TEST ')
// `v-model` value stays the same and update event shouldn't be emitted again
expect(input.emitted('update')).toBeDefined()
expect(input.emitted('update').length).toEqual(3)
expect(input.emitted('update')[2][0]).toEqual('TEST')
expect(input.emitted('update').length).toEqual(1)
expect(input.emitted('input')).toBeDefined()
expect(input.emitted('input').length).toEqual(3)
expect(input.emitted('input')[2][0]).toEqual(' TEST ')

input.trigger('input')

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

input.trigger('change')

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

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

expect(input.vm.localValue).toEqual('0123 450')
expect(input.emitted('update')).toBeDefined()
expect(input.emitted('update').length).toEqual(4)
expect(input.emitted('update')[3][0]).toEqual(123)
expect(typeof input.emitted('update')[3][0]).toEqual('number')
expect(input.emitted('update').length).toEqual(3)
expect(input.emitted('update')[2][0]).toEqual(123)
expect(typeof input.emitted('update')[2][0]).toEqual('number')
expect(input.emitted('input')).toBeDefined()
expect(input.emitted('input').length).toEqual(4)
expect(input.emitted('input')[3][0]).toEqual('0123 450')
@@ -903,12 +902,13 @@ describe('form-textarea', () => {
input.destroy()
})

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

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

0 comments on commit 55787dd

Please sign in to comment.
You can’t perform that action at this time.