Skip to content

chore(refactor): improved code sharing between form components #6100

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 8 commits into from
Nov 29, 2020
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
33 changes: 7 additions & 26 deletions src/components/form-checkbox/form-checkbox-group.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,41 @@
import Vue from '../../vue'
import { NAME_FORM_CHECKBOX_GROUP } from '../../constants/components'
import { makePropsConfigurable } from '../../utils/config'
import formControlMixin, { props as formControlProps } from '../../mixins/form-control'
import formOptionsMixin, { props as formOptionsProps } from '../../mixins/form-options'
import formRadioCheckGroupMixin, {
props as formRadioCheckGroupProps
} from '../../mixins/form-radio-check-group'
import formSizeMixin, { props as formSizeProps } from '../../mixins/form-size'
import formStateMixin, { props as formStateProps } from '../../mixins/form-state'
import idMixin from '../../mixins/id'

// --- Props ---

export const props = makePropsConfigurable(
{
...formControlProps,
...formOptionsProps,
...formRadioCheckGroupProps,
...formSizeProps,
...formStateProps,
checked: {
type: Array,
default: () => []
},
switches: {
// Custom switch styling
type: Boolean,
default: false
},
checked: {
type: Array,
default: null
}
},
NAME_FORM_CHECKBOX_GROUP
)

// --- Main component ---

// @vue/component
export const BFormCheckboxGroup = /*#__PURE__*/ Vue.extend({
name: NAME_FORM_CHECKBOX_GROUP,
mixins: [
idMixin,
formControlMixin,
formRadioCheckGroupMixin, // Includes render function
formOptionsMixin,
formSizeMixin,
formStateMixin
],
// Includes render function
mixins: [formRadioCheckGroupMixin],
provide() {
return {
bvCheckGroup: this
}
},
props,
data() {
return {
localChecked: this.checked || []
}
},
computed: {
isRadioGroup() {
return false
Expand Down
4 changes: 2 additions & 2 deletions src/components/form-checkbox/form-checkbox-group.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,11 @@ describe('form-checkbox-group', () => {
}
})

expect(wrapper.classes()).toBeDefined()
expect(wrapper.vm.isRadioGroup).toEqual(false)
expect(wrapper.vm.localChecked).toEqual([])

const $inputs = wrapper.findAll('input')
expect($inputs.length).toBe(3)
expect(wrapper.vm.localChecked).toEqual([])
expect($inputs.wrappers.every(c => c.find('input[type=checkbox]').exists())).toBe(true)

wrapper.destroy()
Expand Down
2 changes: 1 addition & 1 deletion src/components/form-group/form-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export const BFormGroup = {
id: this.safeId(),
disabled: isFieldset ? this.disabled : null,
role: isFieldset ? null : 'group',
'aria-invalid': state === false ? 'true' : null,
'aria-invalid': this.computedAriaInvalid,
// Only apply aria-labelledby if we are a horizontal fieldset
// as the legend is no longer a direct child of fieldset
'aria-labelledby': isFieldset && isHorizontal ? labelId : null,
Expand Down
35 changes: 3 additions & 32 deletions src/components/form-radio/form-radio-group.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,26 @@
import Vue from '../../vue'
import { NAME_FORM_RADIO_GROUP } from '../../constants/components'
import { makePropsConfigurable } from '../../utils/config'
import idMixin from '../../mixins/id'
import formControlMixin, { props as formControlProps } from '../../mixins/form-control'
import formOptionsMixin, { props as formOptionsProps } from '../../mixins/form-options'
import formRadioCheckGroupMixin, {
props as formRadioCheckGroupProps
} from '../../mixins/form-radio-check-group'
import formSizeMixin, { props as formSizeProps } from '../../mixins/form-size'
import formStateMixin, { props as formStateProps } from '../../mixins/form-state'

// --- Props ---

export const props = makePropsConfigurable(
{
...formControlProps,
...formOptionsProps,
...formRadioCheckGroupProps,
...formSizeProps,
...formStateProps,
checked: {
// type: [String, Number, Boolean, Object],
default: null
}
},
NAME_FORM_RADIO_GROUP
)
export const props = makePropsConfigurable(formRadioCheckGroupProps, NAME_FORM_RADIO_GROUP)

// --- Main component ---

// @vue/component
export const BFormRadioGroup = /*#__PURE__*/ Vue.extend({
name: NAME_FORM_RADIO_GROUP,
mixins: [
idMixin,
formControlMixin,
formRadioCheckGroupMixin, // Includes render function
formOptionsMixin,
formSizeMixin,
formStateMixin
],
mixins: [formRadioCheckGroupMixin],
provide() {
return {
bvRadioGroup: this
}
},
props,
data() {
return {
localChecked: this.checked
}
},
computed: {
isRadioGroup() {
return true
Expand Down
6 changes: 4 additions & 2 deletions src/components/form-radio/form-radio-group.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,12 @@ describe('form-radio-group', () => {
checked: ''
}
})
expect(wrapper.classes()).toBeDefined()

expect(wrapper.vm.isRadioGroup).toEqual(true)
expect(wrapper.vm.localChecked).toEqual('')

const radios = wrapper.findAll('input')
expect(radios.length).toBe(3)
expect(wrapper.vm.localChecked).toEqual('')
expect(radios.wrappers.every(c => c.find('input[type=radio]').exists())).toBe(true)

wrapper.destroy()
Expand Down
6 changes: 0 additions & 6 deletions src/components/form-select/form-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ export const BFormSelect = /*#__PURE__*/ Vue.extend({
this.size && !this.plain ? `custom-select-${this.size}` : null,
this.stateClass
]
},
computedAriaInvalid() {
if (this.ariaInvalid === true || this.ariaInvalid === 'true') {
return 'true'
}
return this.stateClass === 'is-invalid' ? 'true' : null
}
},
watch: {
Expand Down
66 changes: 48 additions & 18 deletions src/mixins/form-radio-check-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@ import { makePropsConfigurable } from '../utils/config'
import { htmlOrText } from '../utils/html'
import { BFormCheckbox } from '../components/form-checkbox/form-checkbox'
import { BFormRadio } from '../components/form-radio/form-radio'
import formControlMixin, { props as formControlProps } from './form-control'
import formCustomMixin, { props as formCustomProps } from './form-custom'
import formOptionsMixin, { props as formOptionsProps } from './form-options'
import formSizeMixin, { props as formSizeProps } from './form-size'
import formStateMixin, { props as formStateProps } from './form-state'
import idMixin from './id'
import normalizeSlotMixin from './normalize-slot'

// --- Props ---

export const props = makePropsConfigurable(
{
...formControlProps,
...formOptionsProps,
...formSizeProps,
...formStateProps,
...formCustomProps,
checked: {
// type: [Boolean, Number, Object, String]
default: null
},
validated: {
type: Boolean,
default: false
Expand Down Expand Up @@ -39,14 +52,28 @@ export const props = makePropsConfigurable(
)

// --- Mixin ---

// @vue/component
export default {
mixins: [formCustomMixin, normalizeSlotMixin],
mixins: [
idMixin,
normalizeSlotMixin,
formControlMixin,
formOptionsMixin,
formSizeMixin,
formStateMixin,
formCustomMixin
],
model: {
prop: 'checked',
event: 'input'
},
props,
data() {
return {
localChecked: this.checked
}
},
computed: {
inline() {
return !this.stacked
Expand All @@ -57,28 +84,28 @@ export default {
return this.name || this.safeId()
},
groupClasses() {
const { inline, size, validated } = this

let classes = { 'was-validated': validated }
if (this.buttons) {
return [
classes = [
classes,
'btn-group-toggle',
this.inline ? 'btn-group' : 'btn-group-vertical',
this.size ? `btn-group-${this.size}` : '',
this.validated ? `was-validated` : ''
{
'btn-group': inline,
'btn-group-vertical': !inline,
[`btn-group-${size}`]: !!size
}
]
}
return [this.validated ? `was-validated` : '']
},
computedAriaInvalid() {
const ariaInvalid = this.ariaInvalid
if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
return 'true'
}
return this.computedState === false ? 'true' : null

return classes
}
},
watch: {
checked(newVal) {
if (!looseEqual(newVal, this.localChecked)) {
this.localChecked = newVal
checked(newValue) {
if (!looseEqual(newValue, this.localChecked)) {
this.localChecked = newValue
}
},
localChecked(newValue, oldValue) {
Expand All @@ -88,11 +115,14 @@ export default {
}
},
render(h) {
const { isRadioGroup } = this
const optionComponent = isRadioGroup ? BFormRadio : BFormCheckbox

const $inputs = this.formOptions.map((option, index) => {
const key = `BV_option_${index}`

return h(
this.isRadioGroup ? BFormRadio : BFormCheckbox,
optionComponent,
{
props: {
id: this.safeId(key),
Expand All @@ -116,7 +146,7 @@ export default {
class: [this.groupClasses, 'bv-no-focus-ring'],
attrs: {
id: this.safeId(),
role: this.isRadioGroup ? 'radiogroup' : 'group',
role: isRadioGroup ? 'radiogroup' : 'group',
// Add `tabindex="-1"` to allow group to be focused if needed by screen readers
tabindex: '-1',
'aria-required': this.required ? 'true' : null,
Expand Down
7 changes: 7 additions & 0 deletions src/mixins/form-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export default {
stateClass() {
const state = this.computedState
return state === true ? 'is-valid' : state === false ? 'is-invalid' : null
},
computedAriaInvalid() {
const { ariaInvalid } = this
if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
return 'true'
}
return this.computedState === false ? 'true' : ariaInvalid
}
}
}
12 changes: 0 additions & 12 deletions src/mixins/form-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,6 @@ export default {
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 the string 'true')
return this.ariaInvalid
},
computedDebounce() {
// Ensure we have a positive number equal to or greater than 0
return mathMax(toInteger(this.debounce, 0), 0)
Expand Down