Skip to content

feat(form-input): Use new form-text mixin and add trim and number modifiers #2204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions src/components/form-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ as two separate inputs.
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` 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.
- `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_.
- 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).

Expand Down Expand Up @@ -135,7 +135,7 @@ export default {

**Note:** Range inputs (as do all input types) return their value as a string. You may
need to convert the value to a native number by using `Number(value)`, `parseInt(value, 10)`,
`parseFloat(value)`, or use the `.number` modifier on the `v-model`.
`parseFloat(value)`, or use the `number` prop.

**Note:** Bootsttrap V4.1 CSS does not include styling for range inputs inside input groups,
nor validation styling on range inputs. However, Bootstrap-Vue includes custom styling to handle
Expand Down Expand Up @@ -192,8 +192,8 @@ Generally speaking, you’ll want to use a particular state for specific types o

To apply one of the contextual state icons on `<b-form-input>`, set the `state` prop
to:
- `'invalid'` or `false` for invalid contextual state
- `'valid'` or `true` for the valid contextual state
- The string `'invalid'` or boolean `false` for invalid contextual state
- The string `'valid'` or boolean `true` for the valid contextual state
- `null` for no validation contextual state (default)

```html
Expand Down Expand Up @@ -291,19 +291,17 @@ then the `aria-invalid` attribute on the input will automatically be set to `'tr


## Formatter support
`<b-form-input>` optionally supports formatting by passing a function reference to
`<b-form-input>` and `<b-form-textarea>` optionally supports formatting by passing a function reference to
the `formatter` prop.

Formatting (when a formatter funtion is supplied) occurs when the control's native `input`
event fires. You can use the boolean prop `lazy-formatter` to restrict the formatter
function to being called on the control's native `change` event (which usually occurs on blur).
Formatting (when a formatter funtion is supplied) occurs when the control's native `input` and `change`
events fire. You can use the boolean prop `lazy-formatter` to restrict the formatter
function to being called on the control's native `blur` event.

The `formatter` function receives two arguments: the raw `value` of the input element,
and the native `event` object (if available). If the formatter is triggered during a
`v-model` update (or by running the component `.format()` method), then the event argument
will be `null`.
and the native `event` object that triggered teh format (if available).

The `formatter` function should return the formatted value (as a string).
The `formatter` function should return the formatted value as a _string_.

Formatting does not occur if a `formatter` is not provided.

Expand All @@ -322,7 +320,7 @@ Formatting does not occur if a `formatter` is not provided.
</b-form-text>
<p>Value: {{ text1 }}</p>

<label for="inputLazy">Text input with lazy formatter (on change)</label>
<label for="inputLazy">Text input with lazy formatter (on blur)</label>
<b-form-input id="inputLazy"
v-model="text2"
type="text"
Expand Down Expand Up @@ -374,20 +372,40 @@ field styling and preserve the correct margin and padding.

The `plaintext` option is not supported by input types `color` or `range`.


## Disabling mousewheel events on numeric-like inputs
On some browsers, scrolling the mousewheel while a numeric-like input is focused will
increment or decrement the input's value. To disable this browser feture, just set
the `no-wheel` prop to `true`.

## Native input events
All native events (other than the cuustom `input` and `change` events) are supported, without
the need for the `.native` modifier. Available events will vary based on input type.

The custom `input` and `change` events receive to paramters: the input value (after
custom formatter has been applied), and the native event object.
## V-model modifiers
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-for-textarea>` have two boolean props `trim` and `number`
which emulate the native Vue `v-model` modifiers `.trim` and `.number` respectivley. Emulation of the
`.lazy` modifier is _not_ supported (listen for `change` or `blur` events instead).

**Notes:**
- The `number` prop takes precedence over the `trim` prop (i.e. `trim` will have no effect when `number` is set).
- When using the `number` prop, and if the value can be parsed as a number (via `parseFloat`) it will return a value of type `Number` to the `v-model`, otherwise the original input value is returned as type `String`. This is the same behaviour as the native `.number` modifier.
- The `trim` and `number` modifier props do not affect the value returned by the `input` or `change` events. These events will aways return the string value of the content of `<textarea>` after optional formatting (which may not match the value returned via the `v-model` `update` event, which handles the modifiers).


## Native and custom events
All native events (other than the custom `input` and `change` events) are supported, without
the need for the `.native` modifier.

The custom `input` and `change` events receive a single argument of the current `value` (after any
formatting has been applied), and are triggerd by user interaction.

The custom `update` event is passed the input value, and is emitted wehenever the v-model needs
updating (it is emitted before `input`, `change`. and `blur` as needed).

You can always access the native `input` and `change` events by using the `.native` modifier.


## Exposed input properties and methods
`<b-form-input>` exposes several of the native input element's properties and methods on the
component reference (i.e. assign a `ref` to your `<b-form-input ref="foo" ...>` and
Expand Down Expand Up @@ -421,12 +439,8 @@ Refer to https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement for
more information on these methods and properties. Support will vary based on
input type.

### Custom input methods
`b-form-input` also exposes the following custom method(s):

| Method | Notes
| ------ | -----
| `.format()` | Forces the input to run the formatter. The event arument passed to the formatter will be `null`
## Component alias
You can use `<b-form-input>` by it's shorter alias `<b-input>`.


<!-- Component reference added automatically from component package.json -->
159 changes: 31 additions & 128 deletions src/components/form-input/form-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import idMixin from '../../mixins/id'
import formMixin from '../../mixins/form'
import formSizeMixin from '../../mixins/form-size'
import formStateMixin from '../../mixins/form-state'
import formTextMixin from '../../mixins/form-text'
import formSelectionMixin from '../../mixins/form-selection'
import formValidityMixin from '../../mixins/form-validity'
import { arrayIncludes } from '../../utils/array'
Expand Down Expand Up @@ -31,26 +32,39 @@ const TYPES = [
'week'
]

// Custom event to update the model
const MODEL_EVENT = 'update:value'

export default {
mixins: [idMixin, formMixin, formSizeMixin, formStateMixin, formSelectionMixin, formValidityMixin],
mixins: [
idMixin,
formMixin,
formSizeMixin,
formStateMixin,
formTextMixin,
formSelectionMixin,
formValidityMixin
],
render (h) {
var self = this
return h('input', {
ref: 'input',
class: self.inputClass,
class: self.computedClass,
directives: [
{
name: 'model',
rawName: 'v-model',
value: self.localValue,
expression: 'localValue'
}
],
attrs: {
id: self.safeId(),
name: self.name,
form: this.form || null,
form: self.form || null,
type: self.localType,
disabled: self.disabled,
required: self.required,
readonly: self.readonly || self.plaintext,
placeholder: self.placeholder,
required: self.required,
autocomplete: self.autocomplete || null,
readonly: self.readonly || self.plaintext,
min: self.min,
max: self.max,
step: self.step,
Expand All @@ -60,61 +74,20 @@ export default {
domProps: {
value: self.localValue
},
directives: [
{ name: 'model', rawName: 'v-model', value: self.localValue, expression: 'localValue' }
],
on: {
...self.$listeners,
input: self.onInput,
change: self.onChange
change: self.onChange,
blur: self.onBlur
}
})
},
data () {
return {
localValue: this.value
}
},
model: {
prop: 'value',
event: MODEL_EVENT
},
props: {
value: {
default: ''
},
type: {
type: String,
default: 'text',
validator: type => arrayIncludes(TYPES, type)
},
ariaInvalid: {
type: [Boolean, String],
default: false
},
readonly: {
type: Boolean,
default: false
},
plaintext: {
type: Boolean,
default: false
},
autocomplete: {
type: String,
default: null
},
placeholder: {
type: String,
default: null
},
formatter: {
type: Function
},
lazyFormatter: {
type: Boolean,
default: false
},
noWheel: {
// Disable mousewheel to prevent wheel from changing values (i.e. number/date).
type: Boolean,
Expand All @@ -137,35 +110,9 @@ export default {
localType () {
// We only allow certain types
return arrayIncludes(TYPES, this.type) ? this.type : 'text'
},
inputClass () {
return [
{
'custom-range': this.type === 'range',
// plaintext not supported by type=range or type=color
'form-control-plaintext': this.plaintext && this.type !== 'range' && this.type !== 'color',
// form-control not used by type=range or plaintext. Always used by type=color
'form-control': (!this.plaintext && this.type !== 'range') || this.type === 'color'
},
this.sizeFormClass,
this.stateClass
]
},
computedAriaInvalid () {
if (!this.ariaInvalid || this.ariaInvalid === 'false') {
// this.ariaInvalid is null or false or 'false'
return this.computedState === false ? 'true' : null
}
if (this.ariaInvalid === true) {
// User wants explicit aria-invalid=true
return 'true'
}
// Most likely a string value (which could be 'true')
return this.ariaInvalid
}
},
mounted () {
this.setValue(this.lazyFormatter ? this.value : this.getFormatted(this.value, null))
this.setWheelStopper(this.noWheel)
},
deactivated () {
Expand All @@ -183,76 +130,32 @@ export default {
this.setWheelStopper(false)
},
watch: {
value (newVal) {
this.setValue(this.lazyFormatter ? newVal : this.getFormatted(newVal, null))
},
noWheel (newVal) {
this.setWheelStopper(newVal)
}
},
methods: {
setValue (val) {
if (val !== this.localVal) {
// Only update value if changed, to minimize duplicte emits
this.localValue = val
this.$emit(MODEL_EVENT, this.localValue)
}
},
onInput (evt) {
if (evt.target.composing) return
const value = evt.target.value
this.setValue(this.lazyFormatter ? value : this.getFormatted(value, evt))
this.$emit('input', this.localValue, evt)
},
onChange (evt) {
if (evt.target.composing) return
this.setValue(this.format(evt.target.value, evt))
this.$emit('change', this.localValue, evt)
},
getFormatted (value, event = null) {
return this.formatter ? this.formatter(value, event) : value
},
setWheelStopper (on) {
const input = this.$el
// We use native events, so that we don't interfere with prepgation
// We use native events, so that we don't interfere with propgation
if (on) {
eventOn(input, 'focus', this.onFocus)
eventOn(input, 'blur', this.onBlur)
eventOn(input, 'focus', this.onWheelFocus)
eventOn(input, 'blur', this.onWheelBlur)
} else {
eventOff(input, 'focus', this.onFocus)
eventOff(input, 'blur', this.onBlur)
eventOff(input, 'focus', this.onWheelFocus)
eventOff(input, 'blur', this.onWheelBlur)
eventOff(document, 'wheel', this.stopWheel)
}
},
onFocus (evt) {
onWheelFocus (evt) {
eventOn(document, 'wheel', this.stopWheel)
},
onBlur (evt) {
onWheelBlur (evt) {
eventOff(document, 'wheel', this.stopWheel)
},
stopWheel (evt) {
evt.preventDefault()
this.$el.blur()
},
// Exposed methods
format () {
// Force the formatter to run
this.setValue(this.getFormatted(this.localValue, null))
return this.localValue
},
focus () {
// Expose the input focus() method
/* istanbul ignore next */
if (!this.disabled) {
this.$el.focus()
}
},
blur () {
// Expose the input blur() method
/* istanbul ignore next */
if (!this.disabled) {
this.$el.blur()
}
}
}
}
Loading