From be5a41e4b66e9ff4d6340e403bd0c10a652c891f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 25 Feb 2020 10:02:52 +0100 Subject: [PATCH 01/20] fix(perf): avoid useless re-render on parent update --- .../dropdown/dropdown-item-button.js | 5 ++- src/components/dropdown/dropdown-item.js | 5 ++- src/components/form-file/form-file.js | 5 ++- src/components/form-input/form-input.js | 14 ++++--- .../form-spinbutton/_spinbutton.scss | 2 +- .../form-spinbutton/form-spinbutton.js | 9 +++-- src/components/form-textarea/form-textarea.js | 14 ++++--- src/components/link/link.js | 29 +++++++-------- src/components/modal/helpers/bv-modal.js | 8 ++-- src/components/modal/modal.js | 8 +++- .../table/helpers/mixin-table-renderer.js | 2 +- src/components/table/table.js | 11 ++++-- src/components/table/tbody.js | 12 +++--- src/components/table/td.js | 19 +++++++--- src/components/table/tfoot.js | 9 +++-- src/components/table/thead.js | 9 +++-- src/components/table/tr.js | 7 ++-- src/components/toast/helpers/bv-toast.js | 4 +- src/components/toast/toast.js | 5 ++- src/directives/scrollspy/scrollspy.class.js | 4 +- src/mixins/bind-attrs.js | 37 +++++++++++++++++++ src/mixins/form-radio-check.js | 5 ++- 22 files changed, 143 insertions(+), 80 deletions(-) create mode 100644 src/mixins/bind-attrs.js diff --git a/src/components/dropdown/dropdown-item-button.js b/src/components/dropdown/dropdown-item-button.js index 68813e02f9e..89c7c3aa76e 100644 --- a/src/components/dropdown/dropdown-item-button.js +++ b/src/components/dropdown/dropdown-item-button.js @@ -1,4 +1,5 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -23,7 +24,7 @@ export const props = { // @vue/component export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ name: 'BDropdownItemButton', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvDropdown: { @@ -53,7 +54,7 @@ export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ [`text-${this.variant}`]: this.variant && !(this.active || this.disabled) }, attrs: { - ...this.$attrs, + ...this.attrs$, role: 'menuitem', type: 'button', disabled: this.disabled diff --git a/src/components/dropdown/dropdown-item.js b/src/components/dropdown/dropdown-item.js index 51d0a716d3e..2babfc1546a 100644 --- a/src/components/dropdown/dropdown-item.js +++ b/src/components/dropdown/dropdown-item.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' import { requestAF } from '../../utils/dom' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' import { BLink, propsFactory as linkPropsFactory } from '../link/link' @@ -8,7 +9,7 @@ export const props = linkPropsFactory() // @vue/component export const BDropdownItem = /*#__PURE__*/ Vue.extend({ name: 'BDropdownItem', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvDropdown: { @@ -46,7 +47,7 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({ class: { [`text-${this.variant}`]: this.variant && !(this.active || this.disabled) }, - attrs: { ...this.$attrs, role: 'menuitem' }, + attrs: { ...this.attrs$, role: 'menuitem' }, on: { click: this.onClick }, ref: 'item' }, diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 367374b3a56..53b75238442 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -6,6 +6,7 @@ import { isFile, isFunction, isUndefinedOrNull } from '../../utils/inspect' import { File } from '../../utils/safe-types' import { toString } from '../../utils/string' import { warn } from '../../utils/warn' +import bindAttrsMixin from '../../mixins/bind-attrs' import formCustomMixin from '../../mixins/form-custom' import formMixin from '../../mixins/form' import formStateMixin from '../../mixins/form-state' @@ -20,7 +21,7 @@ const VALUE_EMPTY_DEPRECATED_MSG = // @vue/component export const BFormFile = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], + mixins: [idMixin, bindAttrsMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'value', @@ -285,7 +286,7 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ this.stateClass ], attrs: { - ...this.$attrs, + ...this.attrs$, type: 'file', id: this.safeId(), name: this.name, diff --git a/src/components/form-input/form-input.js b/src/components/form-input/form-input.js index 47e736975ef..46526b5e0e3 100644 --- a/src/components/form-input/form-input.js +++ b/src/components/form-input/form-input.js @@ -1,13 +1,14 @@ import Vue from '../../utils/vue' import { arrayIncludes } from '../../utils/array' import { eventOn, eventOff, eventOnOff } from '../../utils/events' -import idMixin from '../../mixins/id' +import bindAttrsMixin from '../../mixins/bind-attrs' import formMixin from '../../mixins/form' +import formSelectionMixin from '../../mixins/form-selection' 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 idMixin from '../../mixins/id' // Valid supported input types const TYPES = [ @@ -33,6 +34,7 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ name: 'BFormInput', mixins: [ idMixin, + bindAttrsMixin, formMixin, formSizeMixin, formStateMixin, @@ -41,15 +43,15 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ formValidityMixin ], props: { - // value prop defined in form-text mixin - // value: { }, + // `value` prop is defined in form-text mixin type: { type: String, default: 'text', validator: type => arrayIncludes(TYPES, type) }, noWheel: { - // Disable mousewheel to prevent wheel from changing values (i.e. number/date). + // Disable mousewheel to prevent wheel from + // changing values (i.e. number/date) type: Boolean, default: false }, @@ -153,7 +155,7 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ value: self.localValue }, on: { - ...self.$listeners, + ...self.listeners$, input: self.onInput, change: self.onChange, blur: self.onBlur diff --git a/src/components/form-spinbutton/_spinbutton.scss b/src/components/form-spinbutton/_spinbutton.scss index e2c28034143..a53ce85f4b0 100644 --- a/src/components/form-spinbutton/_spinbutton.scss +++ b/src/components/form-spinbutton/_spinbutton.scss @@ -9,7 +9,7 @@ height: auto; width: auto; } - + @at-root { // Prevent the buttons from reversing order on in horizontal RTL mode [dir="rtl"] &:not(.flex-column), diff --git a/src/components/form-spinbutton/form-spinbutton.js b/src/components/form-spinbutton/form-spinbutton.js index ec15b92f366..675310a12a0 100644 --- a/src/components/form-spinbutton/form-spinbutton.js +++ b/src/components/form-spinbutton/form-spinbutton.js @@ -1,12 +1,13 @@ import Vue from '../../utils/vue' +import KeyCodes from '../../utils/key-codes' +import identity from '../../utils/identity' import { arrayIncludes, concat } from '../../utils/array' import { getComponentConfig } from '../../utils/config' import { EVENT_OPTIONS_PASSIVE, eventOnOff } from '../../utils/events' import { isFunction, isNull } from '../../utils/inspect' import { toFloat, toInteger } from '../../utils/number' import { toString } from '../../utils/string' -import identity from '../../utils/identity' -import KeyCodes from '../../utils/key-codes' +import bindAttrsMixin from '../../mixins/bind-attrs' import idMixin from '../../mixins/id' import { BIconPlus, BIconDash } from '../../icons/icons' @@ -46,7 +47,7 @@ const defaultInteger = (value, defaultValue = null) => { // @vue/component export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin], + mixins: [idMixin, bindAttrsMixin], inheritAttrs: false, props: { value: { @@ -543,7 +544,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ 'is-invalid': state === false }, attrs: { - ...this.$attrs, + ...this.attrs$, role: 'group', lang: this.computedLocale, tabindex: isDisabled ? null : '-1' diff --git a/src/components/form-textarea/form-textarea.js b/src/components/form-textarea/form-textarea.js index c78cfe5cd9e..de4e37c7c07 100644 --- a/src/components/form-textarea/form-textarea.js +++ b/src/components/form-textarea/form-textarea.js @@ -1,15 +1,16 @@ import Vue from '../../utils/vue' -import { VBVisible } from '../../directives/visible/visible' -import idMixin from '../../mixins/id' +import { getCS, isVisible, requestAF } from '../../utils/dom' +import { isNull } from '../../utils/inspect' +import bindAttrsMixin from '../../mixins/bind-attrs' import formMixin from '../../mixins/form' +import formSelectionMixin from '../../mixins/form-selection' 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 idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' -import { getCS, isVisible, requestAF } from '../../utils/dom' -import { isNull } from '../../utils/inspect' +import { VBVisible } from '../../directives/visible/visible' // @vue/component export const BFormTextarea = /*#__PURE__*/ Vue.extend({ @@ -19,6 +20,7 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ }, mixins: [ idMixin, + bindAttrsMixin, listenOnRootMixin, formMixin, formSizeMixin, @@ -204,7 +206,7 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ value: self.localValue }, on: { - ...self.$listeners, + ...self.listeners$, input: self.onInput, change: self.onChange, blur: self.onBlur diff --git a/src/components/link/link.js b/src/components/link/link.js index 1c8992c03f4..174f95a9ba3 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -1,18 +1,18 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' import { concat } from '../../utils/array' -import { isEvent, isFunction, isUndefined } from '../../utils/inspect' +import { isEvent, isFunction } from '../../utils/inspect' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' /** - * The Link component is used in many other BV components. - * As such, sharing its props makes supporting all its features easier. - * However, some components need to modify the defaults for their own purpose. + * The Link component is used in many other BV components + * As such, sharing its props makes supporting all its features easier + * However, some components need to modify the defaults for their own purpose * Prefer sharing a fresh copy of the props to ensure mutations - * do not affect other component references to the props. + * do not affect other component references to the props * - * https://github.com/vuejs/vue-router/blob/dev/src/components/link.js - * @return {{}} + * See: https://github.com/vuejs/vue-router/blob/dev/src/components/link.js */ export const propsFactory = () => ({ href: { @@ -80,7 +80,7 @@ export const props = propsFactory() // @vue/component export const BLink = /*#__PURE__*/ Vue.extend({ name: 'BLink', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, props: propsFactory(), computed: { @@ -107,7 +107,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ onClick(evt) { const evtIsEvent = isEvent(evt) const isRouterLink = this.isRouterLink - const suppliedHandler = this.$listeners.click + const suppliedHandler = this.listeners$.click if (evtIsEvent && this.disabled) { // Stop event from bubbling up evt.stopPropagation() @@ -148,6 +148,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ } }, render(h) { + const $attrs = this.attrs$ const tag = this.computedTag const rel = this.computedRel const href = this.computedHref @@ -156,14 +157,10 @@ export const BLink = /*#__PURE__*/ Vue.extend({ const componentData = { class: { active: this.active, disabled: this.disabled }, attrs: { - ...this.$attrs, + ...$attrs, rel, target: this.target, - tabindex: this.disabled - ? '-1' - : isUndefined(this.$attrs.tabindex) - ? null - : this.$attrs.tabindex, + tabindex: this.disabled ? '-1' : $attrs.tabindex || null, 'aria-disabled': this.disabled ? 'true' : null }, props: this.computedProps @@ -172,7 +169,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ // ``/`` instead of `on` componentData[isRouterLink ? 'nativeOn' : 'on'] = { // Transfer all listeners (native) to the root element - ...this.$listeners, + ...this.listeners$, // We want to overwrite any click handler since our callback // will invoke the user supplied handler(s) if `!this.disabled` click: this.onClick diff --git a/src/components/modal/helpers/bv-modal.js b/src/components/modal/helpers/bv-modal.js index 27d4875bdf9..2c0770ee54c 100644 --- a/src/components/modal/helpers/bv-modal.js +++ b/src/components/modal/helpers/bv-modal.js @@ -5,10 +5,11 @@ import { getComponentConfig } from '../../../utils/config' import { isUndefined, isFunction } from '../../../utils/inspect' import { assign, + defineProperties, + defineProperty, + hasOwnProperty, keys, omit, - defineProperty, - defineProperties, readonlyDescriptor } from '../../../utils/object' import { pluginFactory } from '../../../utils/plugins' @@ -247,8 +248,7 @@ const plugin = Vue => { // Define our read-only `$bvModal` instance property // Placed in an if just in case in HMR mode - // eslint-disable-next-line no-prototype-builtins - if (!Vue.prototype.hasOwnProperty(PROP_NAME)) { + if (!hasOwnProperty(Vue.prototype, PROP_NAME)) { defineProperty(Vue.prototype, PROP_NAME, { get() { /* istanbul ignore next */ diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index bc2f9702167..b6650a381bb 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -12,6 +12,7 @@ import { stripTags } from '../../utils/html' import { isString, isUndefinedOrNull } from '../../utils/inspect' import { HTMLElement } from '../../utils/safe-types' import { BTransporterSingle } from '../../utils/transporter' +import bindAttrsMixin from '../../mixins/bind-attrs' import idMixin from '../../mixins/id' import listenOnDocumentMixin from '../../mixins/listen-on-document' import listenOnRootMixin from '../../mixins/listen-on-root' @@ -283,6 +284,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ name: NAME, mixins: [ idMixin, + bindAttrsMixin, listenOnDocumentMixin, listenOnRootMixin, listenOnWindowMixin, @@ -1067,7 +1069,11 @@ export const BModal = /*#__PURE__*/ Vue.extend({ { key: `modal-outer-${this._uid}`, style: this.modalOuterStyle, - attrs: { ...scopedStyleAttrs, ...this.$attrs, id: this.safeId('__BV_modal_outer_') } + attrs: { + ...scopedStyleAttrs, + ...this.attrs$, + id: this.safeId('__BV_modal_outer_') + } }, [modal, backdrop] ) diff --git a/src/components/table/helpers/mixin-table-renderer.js b/src/components/table/helpers/mixin-table-renderer.js index 451c7fae640..f7572120747 100644 --- a/src/components/table/helpers/mixin-table-renderer.js +++ b/src/components/table/helpers/mixin-table-renderer.js @@ -152,7 +152,7 @@ export default { // in case user has supplied their own 'aria-rowcount': rowCount, // Merge in user supplied `$attrs` if any - ...this.$attrs, + ...this.attrs$, // Now we can override any `$attrs` here id: this.safeId(), role: 'table', diff --git a/src/components/table/table.js b/src/components/table/table.js index 2042559d789..8eb2b26fd16 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1,6 +1,7 @@ import Vue from '../../utils/vue' // Mixins +import bindAttrsMixin from '../../mixins/bind-attrs' import hasListenerMixin from '../../mixins/has-listener' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -31,19 +32,21 @@ import tableRendererMixin from './helpers/mixin-table-renderer' export const BTable = /*#__PURE__*/ Vue.extend({ name: 'BTable', // Order of mixins is important! - // They are merged from first to last, followed by this component. + // They are merged from first to last, followed by this component mixins: [ - // Required Mixins + // General mixins + bindAttrsMixin, hasListenerMixin, idMixin, normalizeSlotMixin, + // Required table mixins itemsMixin, tableRendererMixin, stackedMixin, theadMixin, tfootMixin, tbodyMixin, - // Features Mixins + // Table features mixins stackedMixin, filteringMixin, sortingMixin, @@ -57,5 +60,5 @@ export const BTable = /*#__PURE__*/ Vue.extend({ busyMixin, providerMixin ] - // render function provided by table-renderer mixin + // Render function is provided by table-renderer mixin }) diff --git a/src/components/table/tbody.js b/src/components/table/tbody.js index 5f2374bf8fc..0d77f87cb64 100644 --- a/src/components/table/tbody.js +++ b/src/components/table/tbody.js @@ -1,4 +1,5 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -15,7 +16,7 @@ export const props = { // @vue/component export const BTbody = /*#__PURE__*/ Vue.extend({ name: 'BTbody', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -67,7 +68,7 @@ export const BTbody = /*#__PURE__*/ Vue.extend({ return this.tbodyTransitionProps || this.tbodyTransitionHandlers }, tbodyAttrs() { - return { role: 'rowgroup', ...this.$attrs } + return { role: 'rowgroup', ...this.attrs$ } }, tbodyProps() { return this.tbodyTransitionProps ? { ...this.tbodyTransitionProps, tag: 'tbody' } : {} @@ -79,13 +80,12 @@ export const BTbody = /*#__PURE__*/ Vue.extend({ attrs: this.tbodyAttrs } if (this.isTransitionGroup) { - // We use native listeners if a transition group - // for any delegated events + // We use native listeners if a transition group for any delegated events data.on = this.tbodyTransitionHandlers || {} - data.nativeOn = this.$listeners || {} + data.nativeOn = this.listeners$ } else { // Otherwise we place any listeners on the tbody element - data.on = this.$listeners || {} + data.on = this.listeners$ } return h( this.isTransitionGroup ? 'transition-group' : 'tbody', diff --git a/src/components/table/td.js b/src/components/table/td.js index 8ebdf0c7173..9a40e0dc534 100644 --- a/src/components/table/td.js +++ b/src/components/table/td.js @@ -1,19 +1,26 @@ import Vue from '../../utils/vue' import { isUndefinedOrNull } from '../../utils/inspect' import { toString } from '../../utils/string' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' -const digitsRx = /^\d+$/ +// --- Constants --- + +const RX_DIGITS = /^\d+$/ + +// --- Utility methods --- // Parse a rowspan or colspan into a digit (or null if < 1 or NaN) const parseSpan = val => { val = parseInt(val, 10) - return digitsRx.test(String(val)) && val > 0 ? val : null + return RX_DIGITS.test(String(val)) && val > 0 ? val : null } /* istanbul ignore next */ const spanValidator = val => isUndefinedOrNull(val) || parseSpan(val) > 0 +// --- Props --- + export const props = { variant: { type: String, @@ -42,7 +49,7 @@ export const props = { // @vue/component export const BTd = /*#__PURE__*/ Vue.extend({ name: 'BTableCell', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvTableTr: { @@ -119,7 +126,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ cellClasses() { // We use computed props here for improved performance by caching // the results of the string interpolation - // TODO: need to add handling for footVariant + // TODO: We need to add handling for `footVariant` let variant = this.variant if ( (!variant && this.isStickyHeader && !this.headVariant) || @@ -163,7 +170,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ role: role, scope: scope, // Allow users to override role/scope plus add other attributes - ...this.$attrs, + ...this.attrs$, // Add in the stacked cell label data-attribute if in // stacked mode (if a stacked heading label is provided) 'data-label': @@ -181,7 +188,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ class: this.cellClasses, attrs: this.cellAttrs, // Transfer any native listeners - on: this.$listeners + on: this.listeners$ }, [this.isStackedCell ? h('div', [content]) : content] ) diff --git a/src/components/table/tfoot.js b/src/components/table/tfoot.js index c5bcfc09fb8..783ca036f34 100644 --- a/src/components/table/tfoot.js +++ b/src/components/table/tfoot.js @@ -1,9 +1,10 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { footVariant: { - type: String, // supported values: 'lite', 'dark', or null + type: String, // Supported values: 'lite', 'dark', or null default: null } } @@ -11,7 +12,7 @@ export const props = { // @vue/component export const BTfoot = /*#__PURE__*/ Vue.extend({ name: 'BTfoot', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -63,7 +64,7 @@ export const BTfoot = /*#__PURE__*/ Vue.extend({ return [this.footVariant ? `thead-${this.footVariant}` : null] }, tfootAttrs() { - return { role: 'rowgroup', ...this.$attrs } + return { role: 'rowgroup', ...this.attrs$ } } }, render(h) { @@ -73,7 +74,7 @@ export const BTfoot = /*#__PURE__*/ Vue.extend({ class: this.tfootClasses, attrs: this.tfootAttrs, // Pass down any native listeners - on: this.$listeners + on: this.listeners$ }, this.normalizeSlot('default') ) diff --git a/src/components/table/thead.js b/src/components/table/thead.js index 4f89683a003..8baa2d21b8e 100644 --- a/src/components/table/thead.js +++ b/src/components/table/thead.js @@ -1,10 +1,11 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { headVariant: { // Also sniffed by / / - type: String, // supported values: 'lite', 'dark', or null + type: String, // Supported values: 'lite', 'dark', or null default: null } } @@ -12,7 +13,7 @@ export const props = { // @vue/component export const BThead = /*#__PURE__*/ Vue.extend({ name: 'BThead', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -66,7 +67,7 @@ export const BThead = /*#__PURE__*/ Vue.extend({ return [this.headVariant ? `thead-${this.headVariant}` : null] }, theadAttrs() { - return { role: 'rowgroup', ...this.$attrs } + return { role: 'rowgroup', ...this.attrs$ } } }, render(h) { @@ -76,7 +77,7 @@ export const BThead = /*#__PURE__*/ Vue.extend({ class: this.theadClasses, attrs: this.theadAttrs, // Pass down any native listeners - on: this.$listeners + on: this.listeners$ }, this.normalizeSlot('default') ) diff --git a/src/components/table/tr.js b/src/components/table/tr.js index ab58d137bcb..d31cff3a093 100644 --- a/src/components/table/tr.js +++ b/src/components/table/tr.js @@ -1,4 +1,5 @@ import Vue from '../../utils/vue' +import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -14,7 +15,7 @@ const DARK = 'dark' // @vue/component export const BTr = /*#__PURE__*/ Vue.extend({ name: 'BTr', - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -88,7 +89,7 @@ export const BTr = /*#__PURE__*/ Vue.extend({ return [this.variant ? `${this.isRowDark ? 'bg' : 'table'}-${this.variant}` : null] }, trAttrs() { - return { role: 'row', ...this.$attrs } + return { role: 'row', ...this.attrs$ } } }, render(h) { @@ -98,7 +99,7 @@ export const BTr = /*#__PURE__*/ Vue.extend({ class: this.trClasses, attrs: this.trAttrs, // Pass native listeners to child - on: this.$listeners + on: this.listeners$ }, this.normalizeSlot('default') ) diff --git a/src/components/toast/helpers/bv-toast.js b/src/components/toast/helpers/bv-toast.js index 3ab4a2bbb8c..8c159ab9b6f 100644 --- a/src/components/toast/helpers/bv-toast.js +++ b/src/components/toast/helpers/bv-toast.js @@ -10,6 +10,7 @@ import { assign, defineProperties, defineProperty, + hasOwnProperty, keys, omit, readonlyDescriptor @@ -177,8 +178,7 @@ const plugin = Vue => { // Define our read-only `$bvToast` instance property // Placed in an if just in case in HMR mode - // eslint-disable-next-line no-prototype-builtins - if (!Vue.prototype.hasOwnProperty(PROP_NAME)) { + if (!hasOwnProperty(Vue.prototype, PROP_NAME)) { defineProperty(Vue.prototype, PROP_NAME, { get() { /* istanbul ignore next */ diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index 7eb5d6c42d4..0e9a73c9523 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -6,6 +6,7 @@ import { getComponentConfig } from '../../utils/config' import { requestAF } from '../../utils/dom' import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events' import { toInteger } from '../../utils/number' +import bindAttrsMixin from '../../mixins/bind-attrs' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -108,7 +109,7 @@ export const props = { // @vue/component export const BToast = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], + mixins: [idMixin, bindAttrsMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], inheritAttrs: false, model: { prop: 'visible', @@ -394,7 +395,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ staticClass: 'toast', class: this.toastClass, attrs: { - ...this.$attrs, + ...this.attrs$, tabindex: '0', id: this.safeId() } diff --git a/src/directives/scrollspy/scrollspy.class.js b/src/directives/scrollspy/scrollspy.class.js index de692c0ac8c..4eafd565d38 100644 --- a/src/directives/scrollspy/scrollspy.class.js +++ b/src/directives/scrollspy/scrollspy.class.js @@ -20,7 +20,7 @@ import { } from '../../utils/dom' import { EVENT_OPTIONS_NO_CAPTURE, eventOn, eventOff } from '../../utils/events' import { isString, isUndefined } from '../../utils/inspect' -import { toString as objectToString } from '../../utils/object' +import { hasOwnProperty, toString as objectToString } from '../../utils/object' import { warn } from '../../utils/warn' /* @@ -96,7 +96,7 @@ const typeCheckConfig = ( configTypes ) => /* istanbul ignore next: not easy to test */ { for (const property in configTypes) { - if (Object.prototype.hasOwnProperty.call(configTypes, property)) { + if (hasOwnProperty(configTypes, property)) { const expectedTypes = configTypes[property] const value = config[property] let valueType = value && isElement(value) ? 'element' : toType(value) diff --git a/src/mixins/bind-attrs.js b/src/mixins/bind-attrs.js new file mode 100644 index 00000000000..db8656d2d60 --- /dev/null +++ b/src/mixins/bind-attrs.js @@ -0,0 +1,37 @@ +import { hasOwnProperty } from '../utils/object' + +// --- Constants --- + +const ATTRS_ATTRIBUTE_NAME = 'attrs$' +const LISTENERS_ATTRIBUTE_NAME = 'listeners$' + +// --- Utility methods --- + +const makeWatcher = property => ({ + handler(newVal, oldVal) { + for (const attr in oldVal) { + if (!hasOwnProperty(newVal, attr)) { + this.$delete(this.$data[property], attr) + } + } + for (const attr in newVal) { + this.$set(this.$data[property], attr, newVal[attr]) + } + }, + immediate: true +}) + +// @vue/component +export default { + data() { + return { + [ATTRS_ATTRIBUTE_NAME]: {}, + [LISTENERS_ATTRIBUTE_NAME]: {} + } + }, + watch: { + // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115 + $attrs: makeWatcher(ATTRS_ATTRIBUTE_NAME), + $listeners: makeWatcher(LISTENERS_ATTRIBUTE_NAME) + } +} diff --git a/src/mixins/form-radio-check.js b/src/mixins/form-radio-check.js index 1704f00f332..773a4be0d61 100644 --- a/src/mixins/form-radio-check.js +++ b/src/mixins/form-radio-check.js @@ -1,8 +1,9 @@ +import bindAttrsMixin from './bind-attrs' import normalizeSlotMixin from './normalize-slot' // @vue/component export default { - mixins: [normalizeSlotMixin], + mixins: [bindAttrsMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'checked', @@ -201,7 +202,7 @@ export default { } ], attrs: { - ...this.$attrs, + ...this.attrs$, id: this.safeId(), type: this.isRadio ? 'radio' : 'checkbox', name: this.getName, From 76fd7dbef22f342d027df62a543379287a0156ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Wed, 26 Feb 2020 00:12:22 +0100 Subject: [PATCH 02/20] Update bind-attrs.js --- src/mixins/bind-attrs.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/mixins/bind-attrs.js b/src/mixins/bind-attrs.js index db8656d2d60..fbacd69a318 100644 --- a/src/mixins/bind-attrs.js +++ b/src/mixins/bind-attrs.js @@ -9,16 +9,15 @@ const LISTENERS_ATTRIBUTE_NAME = 'listeners$' const makeWatcher = property => ({ handler(newVal, oldVal) { - for (const attr in oldVal) { - if (!hasOwnProperty(newVal, attr)) { - this.$delete(this.$data[property], attr) + for (const prop in oldVal) { + if (!hasOwnProperty(newVal, prop)) { + this.$delete(this.$data[property], prop) } } - for (const attr in newVal) { - this.$set(this.$data[property], attr, newVal[attr]) + for (const prop in newVal) { + this.$set(this.$data[property], prop, newVal[prop]) } - }, - immediate: true + } }) // @vue/component @@ -33,5 +32,9 @@ export default { // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115 $attrs: makeWatcher(ATTRS_ATTRIBUTE_NAME), $listeners: makeWatcher(LISTENERS_ATTRIBUTE_NAME) + }, + created() { + this[ATTRS_ATTRIBUTE_NAME] = { ...this.$attrs } + this[LISTENERS_ATTRIBUTE_NAME] = { ...this.$listeners } } } From ff85621a16904cd65e1ec193ecec723e7948b9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 27 Feb 2020 10:51:46 +0100 Subject: [PATCH 03/20] Make sure `bindAttrsMixin` is always the first mixin --- src/components/form-file/form-file.js | 2 +- src/components/form-input/form-input.js | 2 +- src/components/form-spinbutton/form-spinbutton.js | 2 +- src/components/form-textarea/form-textarea.js | 2 +- src/components/modal/modal.js | 2 +- src/components/toast/toast.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 53b75238442..9315e5a5136 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -21,7 +21,7 @@ const VALUE_EMPTY_DEPRECATED_MSG = // @vue/component export const BFormFile = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, bindAttrsMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], + mixins: [bindAttrsMixin, idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'value', diff --git a/src/components/form-input/form-input.js b/src/components/form-input/form-input.js index 46526b5e0e3..b66f8d4ae46 100644 --- a/src/components/form-input/form-input.js +++ b/src/components/form-input/form-input.js @@ -33,8 +33,8 @@ const TYPES = [ export const BFormInput = /*#__PURE__*/ Vue.extend({ name: 'BFormInput', mixins: [ - idMixin, bindAttrsMixin, + idMixin, formMixin, formSizeMixin, formStateMixin, diff --git a/src/components/form-spinbutton/form-spinbutton.js b/src/components/form-spinbutton/form-spinbutton.js index 9fa8e95679d..2290d1cb22e 100644 --- a/src/components/form-spinbutton/form-spinbutton.js +++ b/src/components/form-spinbutton/form-spinbutton.js @@ -47,7 +47,7 @@ const defaultInteger = (value, defaultValue = null) => { // @vue/component export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, bindAttrsMixin], + mixins: [bindAttrsMixin, idMixin], inheritAttrs: false, props: { value: { diff --git a/src/components/form-textarea/form-textarea.js b/src/components/form-textarea/form-textarea.js index de4e37c7c07..0c016dd3d04 100644 --- a/src/components/form-textarea/form-textarea.js +++ b/src/components/form-textarea/form-textarea.js @@ -19,8 +19,8 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ 'b-visible': VBVisible }, mixins: [ - idMixin, bindAttrsMixin, + idMixin, listenOnRootMixin, formMixin, formSizeMixin, diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index b6650a381bb..6bb8438469e 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -283,8 +283,8 @@ export const props = { export const BModal = /*#__PURE__*/ Vue.extend({ name: NAME, mixins: [ - idMixin, bindAttrsMixin, + idMixin, listenOnDocumentMixin, listenOnRootMixin, listenOnWindowMixin, diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index 0e9a73c9523..5d48a77d491 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -109,7 +109,7 @@ export const props = { // @vue/component export const BToast = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, bindAttrsMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], + mixins: [bindAttrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], inheritAttrs: false, model: { prop: 'visible', From cfe7f695514003943a4c8b99bd4db3d4b0db7cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Thu, 27 Feb 2020 13:15:28 +0100 Subject: [PATCH 04/20] Update form-input.js --- src/components/form-input/form-input.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/form-input/form-input.js b/src/components/form-input/form-input.js index b66f8d4ae46..dd4476e08a0 100644 --- a/src/components/form-input/form-input.js +++ b/src/components/form-input/form-input.js @@ -122,7 +122,8 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ } }, render(h) { - var self = this + // We alias `this` to `self` for better minification + const self = this return h('input', { ref: 'input', class: self.computedClass, From d678bc0d168658dcb046af36ed434c2570656e56 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 7 Apr 2020 16:22:39 -0300 Subject: [PATCH 05/20] Update link.js --- src/components/link/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/link/link.js b/src/components/link/link.js index 3b63276f7be..1776d7ae428 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -2,7 +2,7 @@ import Vue from '../../utils/vue' import bindAttrsMixin from '../../mixins/bind-attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' import { concat } from '../../utils/array' -import { isEvent, isFunction } from '../../utils/inspect' +import { isEvent, isFunction, isUndefined } from '../../utils/inspect' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' /** From 71ba13ec68c529394bfad027e4f59328efbfbf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 8 May 2020 11:49:44 +0200 Subject: [PATCH 06/20] Separate `$attrs` and `$listeners` cache mixins --- src/components/calendar/calendar.js | 6 ++- .../dropdown/dropdown-item-button.js | 6 +-- src/components/dropdown/dropdown-item.js | 6 +-- src/components/form-file/form-file.js | 6 +-- src/components/form-input/form-input.js | 7 ++-- .../form-spinbutton/form-spinbutton.js | 9 +++-- src/components/form-textarea/form-textarea.js | 7 ++-- src/components/link/link.js | 14 ++++--- src/components/modal/modal.js | 6 +-- src/components/sidebar/sidebar.js | 6 ++- .../table/helpers/mixin-table-renderer.js | 8 +++- src/components/table/table.js | 4 +- src/components/table/tbody.js | 12 +++--- src/components/table/td.js | 10 +++-- src/components/table/tfoot.js | 10 +++-- src/components/table/thead.js | 10 +++-- src/components/table/tr.js | 10 +++-- src/components/toast/toast.js | 6 +-- src/mixins/attrs.js | 3 ++ src/mixins/bind-attrs.js | 40 ------------------- src/mixins/form-radio-check.js | 6 +-- src/mixins/listeners.js | 3 ++ src/utils/cache.js | 29 ++++++++++++++ 23 files changed, 121 insertions(+), 103 deletions(-) create mode 100644 src/mixins/attrs.js delete mode 100644 src/mixins/bind-attrs.js create mode 100644 src/mixins/listeners.js create mode 100644 src/utils/cache.js diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index ee87e3c53d8..a35387c9681 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -27,6 +27,7 @@ import { isLocaleRTL } from '../../utils/locale' import { mathMax } from '../../utils/math' import { toInteger } from '../../utils/number' import { toString } from '../../utils/string' +import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' import { @@ -56,7 +57,8 @@ export const STR_NARROW = 'narrow' // @vue/component export const BCalendar = Vue.extend({ name: NAME, - mixins: [idMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, idMixin, normalizeSlotMixin], model: { // Even though this is the default that Vue assumes, we need // to add it for the docs to reflect that this is the model @@ -1133,7 +1135,7 @@ export const BCalendar = Vue.extend({ 'aria-describedby': [ // Should the attr (if present) go last? // Or should this attr be a prop? - this.$attrs['aria-describedby'], + this.bvAttrs['aria-describedby'], idValue, idGridHelp ] diff --git a/src/components/dropdown/dropdown-item-button.js b/src/components/dropdown/dropdown-item-button.js index 672818d75d7..c3087d10b0d 100644 --- a/src/components/dropdown/dropdown-item-button.js +++ b/src/components/dropdown/dropdown-item-button.js @@ -1,5 +1,5 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -28,7 +28,7 @@ export const props = { // @vue/component export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ name: 'BDropdownItemButton', - mixins: [bindAttrsMixin, normalizeSlotMixin], + mixins: [attrsMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvDropdown: { @@ -61,7 +61,7 @@ export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ } ], attrs: { - ...this.attrs$, + ...this.bvAttrs, role: 'menuitem', type: 'button', disabled: this.disabled diff --git a/src/components/dropdown/dropdown-item.js b/src/components/dropdown/dropdown-item.js index e8ca6e8f220..58da3f82ef2 100644 --- a/src/components/dropdown/dropdown-item.js +++ b/src/components/dropdown/dropdown-item.js @@ -1,6 +1,6 @@ import Vue from '../../utils/vue' import { requestAF } from '../../utils/dom' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' import { BLink, propsFactory as linkPropsFactory } from '../link/link' @@ -9,7 +9,7 @@ export const props = linkPropsFactory() // @vue/component export const BDropdownItem = /*#__PURE__*/ Vue.extend({ name: 'BDropdownItem', - mixins: [bindAttrsMixin, normalizeSlotMixin], + mixins: [attrsMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvDropdown: { @@ -54,7 +54,7 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({ [`text-${this.variant}`]: this.variant && !(this.active || this.disabled) } ], - attrs: { ...this.attrs$, role: 'menuitem' }, + attrs: { ...this.bvAttrs, role: 'menuitem' }, on: { click: this.onClick }, ref: 'item' }, diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 218d1cfb5c9..2a57b502321 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -6,7 +6,7 @@ import { isFile, isFunction, isUndefinedOrNull } from '../../utils/inspect' import { File } from '../../utils/safe-types' import { toString } from '../../utils/string' import { warn } from '../../utils/warn' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import formCustomMixin from '../../mixins/form-custom' import formMixin from '../../mixins/form' import formStateMixin from '../../mixins/form-state' @@ -27,7 +27,7 @@ const isValidValue = value => isFile(value) || (isArray(value) && value.every(v // @vue/component export const BFormFile = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [bindAttrsMixin, idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], + mixins: [attrsMixin, idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'value', @@ -292,7 +292,7 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ this.stateClass ], attrs: { - ...this.attrs$, + ...this.bvAttrs, type: 'file', id: this.safeId(), name: this.name, diff --git a/src/components/form-input/form-input.js b/src/components/form-input/form-input.js index 9aa2b5ca5c6..5b3a2b24b64 100644 --- a/src/components/form-input/form-input.js +++ b/src/components/form-input/form-input.js @@ -1,7 +1,6 @@ import Vue from '../../utils/vue' import { arrayIncludes } from '../../utils/array' import { eventOn, eventOff, eventOnOff } from '../../utils/events' -import bindAttrsMixin from '../../mixins/bind-attrs' import formMixin from '../../mixins/form' import formSelectionMixin from '../../mixins/form-selection' import formSizeMixin from '../../mixins/form-size' @@ -9,6 +8,7 @@ import formStateMixin from '../../mixins/form-state' import formTextMixin from '../../mixins/form-text' import formValidityMixin from '../../mixins/form-validity' import idMixin from '../../mixins/id' +import listenersMixin from '../../mixins/listeners' // Valid supported input types const TYPES = [ @@ -32,8 +32,9 @@ const TYPES = [ // @vue/component export const BFormInput = /*#__PURE__*/ Vue.extend({ name: 'BFormInput', + // Mixin order is important! mixins: [ - bindAttrsMixin, + listenersMixin, idMixin, formMixin, formSizeMixin, @@ -158,7 +159,7 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ value: self.localValue }, on: { - ...self.listeners$, + ...self.bvListeners, input: self.onInput, change: self.onChange, blur: self.onBlur diff --git a/src/components/form-spinbutton/form-spinbutton.js b/src/components/form-spinbutton/form-spinbutton.js index 4e8ea52ad35..80e65e66598 100644 --- a/src/components/form-spinbutton/form-spinbutton.js +++ b/src/components/form-spinbutton/form-spinbutton.js @@ -9,7 +9,7 @@ import { isLocaleRTL } from '../../utils/locale' import { mathFloor, mathMax, mathPow, mathRound } from '../../utils/math' import { toFloat, toInteger } from '../../utils/number' import { toString } from '../../utils/string' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' import { BIconPlus, BIconDash } from '../../icons/icons' @@ -38,7 +38,8 @@ const DEFAULT_REPEAT_MULTIPLIER = 4 // @vue/component export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [bindAttrsMixin, idMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, idMixin, normalizeSlotMixin], inheritAttrs: false, props: { value: { @@ -525,7 +526,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ }, attrs: { dir: this.computedRTL ? 'rtl' : 'ltr', - ...this.$attrs, + ...this.bvAttrs, id: spinId, role: 'spinbutton', tabindex: isDisabled ? null : '0', @@ -564,7 +565,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ 'is-invalid': state === false }, attrs: { - ...this.attrs$, + ...this.bvAttrs, role: 'group', lang: this.computedLocale, tabindex: isDisabled ? null : '-1', diff --git a/src/components/form-textarea/form-textarea.js b/src/components/form-textarea/form-textarea.js index ef4e90699a6..c3034df3445 100644 --- a/src/components/form-textarea/form-textarea.js +++ b/src/components/form-textarea/form-textarea.js @@ -3,7 +3,6 @@ import { getCS, isVisible, requestAF } from '../../utils/dom' import { isNull } from '../../utils/inspect' import { mathCeil, mathMax, mathMin } from '../../utils/math' import { toInteger, toFloat } from '../../utils/number' -import bindAttrsMixin from '../../mixins/bind-attrs' import formMixin from '../../mixins/form' import formSelectionMixin from '../../mixins/form-selection' import formSizeMixin from '../../mixins/form-size' @@ -12,6 +11,7 @@ import formTextMixin from '../../mixins/form-text' import formValidityMixin from '../../mixins/form-validity' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' +import listenersMixin from '../../mixins/listeners' import { VBVisible } from '../../directives/visible/visible' // @vue/component @@ -20,8 +20,9 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ directives: { 'b-visible': VBVisible }, + // Mixin order is important! mixins: [ - bindAttrsMixin, + listenersMixin, idMixin, listenOnRootMixin, formMixin, @@ -206,7 +207,7 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ value: self.localValue }, on: { - ...self.listeners$, + ...self.bvListeners, input: self.onInput, change: self.onChange, blur: self.onBlur diff --git a/src/components/link/link.js b/src/components/link/link.js index 1776d7ae428..51336477fdf 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -1,9 +1,10 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' -import normalizeSlotMixin from '../../mixins/normalize-slot' import { concat } from '../../utils/array' import { isEvent, isFunction, isUndefined } from '../../utils/inspect' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' +import normalizeSlotMixin from '../../mixins/normalize-slot' /** * The Link component is used in many other BV components @@ -81,7 +82,8 @@ export const props = propsFactory() // @vue/component export const BLink = /*#__PURE__*/ Vue.extend({ name: 'BLink', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, props: propsFactory(), computed: { @@ -108,7 +110,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ onClick(evt) { const evtIsEvent = isEvent(evt) const isRouterLink = this.isRouterLink - const suppliedHandler = this.listeners$.click + const suppliedHandler = this.bvListeners.click if (evtIsEvent && this.disabled) { // Stop event from bubbling up evt.stopPropagation() @@ -149,7 +151,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ } }, render(h) { - const $attrs = this.attrs$ + const $attrs = this.bvAttrs const { active, disabled, target, routerTag, isRouterLink } = this const tag = this.computedTag const rel = this.computedRel @@ -170,7 +172,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ // ``/`` instead of `on` componentData[isRouterLink ? 'nativeOn' : 'on'] = { // Transfer all listeners (native) to the root element - ...this.listeners$, + ...this.bvListeners, // We want to overwrite any click handler since our callback // will invoke the user supplied handler(s) if `!this.disabled` click: this.onClick diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index 6220944640a..6d63a6b771e 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -12,7 +12,7 @@ import { stripTags } from '../../utils/html' import { isString, isUndefinedOrNull } from '../../utils/inspect' import { HTMLElement } from '../../utils/safe-types' import { BTransporterSingle } from '../../utils/transporter' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import listenOnDocumentMixin from '../../mixins/listen-on-document' import listenOnRootMixin from '../../mixins/listen-on-root' @@ -273,7 +273,7 @@ export const props = { export const BModal = /*#__PURE__*/ Vue.extend({ name: NAME, mixins: [ - bindAttrsMixin, + attrsMixin, idMixin, listenOnDocumentMixin, listenOnRootMixin, @@ -1063,7 +1063,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ style: this.modalOuterStyle, attrs: { ...scopedStyleAttrs, - ...this.attrs$, + ...this.bvAttrs, id: this.safeId('__BV_modal_outer_') } }, diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index c18a2059606..3bb3bebc91c 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -4,6 +4,7 @@ import BVTransition from '../../utils/bv-transition' import { contains, getTabables } from '../../utils/dom' import { getComponentConfig } from '../../utils/config' import { toString } from '../../utils/string' +import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -124,7 +125,8 @@ const renderBackdrop = (h, ctx) => { // @vue/component export const BSidebar = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [idMixin, listenOnRootMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'visible', @@ -402,7 +404,7 @@ export const BSidebar = /*#__PURE__*/ Vue.extend({ this.sidebarClass ], attrs: { - ...this.$attrs, + ...this.bvAttrs, id: this.safeId(), tabindex: '-1', role: 'dialog', diff --git a/src/components/table/helpers/mixin-table-renderer.js b/src/components/table/helpers/mixin-table-renderer.js index 88d035af5b1..dec85ef717e 100644 --- a/src/components/table/helpers/mixin-table-renderer.js +++ b/src/components/table/helpers/mixin-table-renderer.js @@ -1,6 +1,7 @@ import identity from '../../../utils/identity' import { isBoolean } from '../../../utils/inspect' import { toString } from '../../../utils/string' +import attrsMixin from '../../../mixins/attrs' // Main `` render mixin // Includes all main table styling options @@ -9,6 +10,8 @@ export default { // Don't place attributes on root element automatically, // as table could be wrapped in responsive `
` inheritAttrs: false, + // Mixin order is important! + mixins: [attrsMixin], provide() { return { bvTable: this @@ -130,7 +133,8 @@ export default { tableAttrs() { // Preserve user supplied aria-describedby, if provided in `$attrs` const adb = - [(this.$attrs || {})['aria-describedby'], this.captionId].filter(identity).join(' ') || null + [(this.bvAttrs || {})['aria-describedby'], this.captionId].filter(identity).join(' ') || + null const items = this.computedItems const filteredItems = this.filteredItems const fields = this.computedFields @@ -152,7 +156,7 @@ export default { // in case user has supplied their own 'aria-rowcount': rowCount, // Merge in user supplied `$attrs` if any - ...this.attrs$, + ...this.bvAttrs, // Now we can override any `$attrs` here id: this.safeId(), role: 'table', diff --git a/src/components/table/table.js b/src/components/table/table.js index 8eb2b26fd16..8c73445dc29 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -1,7 +1,7 @@ import Vue from '../../utils/vue' // Mixins -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import hasListenerMixin from '../../mixins/has-listener' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -35,7 +35,7 @@ export const BTable = /*#__PURE__*/ Vue.extend({ // They are merged from first to last, followed by this component mixins: [ // General mixins - bindAttrsMixin, + attrsMixin, hasListenerMixin, idMixin, normalizeSlotMixin, diff --git a/src/components/table/tbody.js b/src/components/table/tbody.js index 763d00506f0..a96918f44c9 100644 --- a/src/components/table/tbody.js +++ b/src/components/table/tbody.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -19,7 +20,8 @@ export const props = { // @vue/component export const BTbody = /*#__PURE__*/ Vue.extend({ name: 'BTbody', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -72,7 +74,7 @@ export const BTbody = /*#__PURE__*/ Vue.extend({ return this.tbodyTransitionProps || this.tbodyTransitionHandlers }, tbodyAttrs() { - return { role: 'rowgroup', ...this.attrs$ } + return { role: 'rowgroup', ...this.bvAttrs } }, tbodyProps() { return this.tbodyTransitionProps ? { ...this.tbodyTransitionProps, tag: 'tbody' } : {} @@ -86,10 +88,10 @@ export const BTbody = /*#__PURE__*/ Vue.extend({ if (this.isTransitionGroup) { // We use native listeners if a transition group for any delegated events data.on = this.tbodyTransitionHandlers || {} - data.nativeOn = this.listeners$ + data.nativeOn = this.bvListeners } else { // Otherwise we place any listeners on the tbody element - data.on = this.listeners$ + data.on = this.bvListeners } return h( this.isTransitionGroup ? 'transition-group' : 'tbody', diff --git a/src/components/table/td.js b/src/components/table/td.js index 6ba3e2a5898..cb6ce671823 100644 --- a/src/components/table/td.js +++ b/src/components/table/td.js @@ -2,7 +2,8 @@ import Vue from '../../utils/vue' import { isUndefinedOrNull } from '../../utils/inspect' import { toInteger } from '../../utils/number' import { toString } from '../../utils/string' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' // Parse a rowspan or colspan into a digit (or `null` if < `1` ) @@ -47,7 +48,8 @@ export const props = { // @vue/component export const BTd = /*#__PURE__*/ Vue.extend({ name: 'BTableCell', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, inject: { bvTableTr: { @@ -170,7 +172,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ role: role, scope: scope, // Allow users to override role/scope plus add other attributes - ...this.attrs$, + ...this.bvAttrs, // Add in the stacked cell label data-attribute if in // stacked mode (if a stacked heading label is provided) 'data-label': @@ -188,7 +190,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ class: this.cellClasses, attrs: this.cellAttrs, // Transfer any native listeners - on: this.listeners$ + on: this.bvListeners }, [this.isStackedCell ? h('div', [content]) : content] ) diff --git a/src/components/table/tfoot.js b/src/components/table/tfoot.js index 6f7198c1232..9d19f624d3f 100644 --- a/src/components/table/tfoot.js +++ b/src/components/table/tfoot.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -15,7 +16,8 @@ export const props = { // @vue/component export const BTfoot = /*#__PURE__*/ Vue.extend({ name: 'BTfoot', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -68,7 +70,7 @@ export const BTfoot = /*#__PURE__*/ Vue.extend({ return [this.footVariant ? `thead-${this.footVariant}` : null] }, tfootAttrs() { - return { role: 'rowgroup', ...this.attrs$ } + return { role: 'rowgroup', ...this.bvAttrs } } }, render(h) { @@ -78,7 +80,7 @@ export const BTfoot = /*#__PURE__*/ Vue.extend({ class: this.tfootClasses, attrs: this.tfootAttrs, // Pass down any native listeners - on: this.listeners$ + on: this.bvListeners }, this.normalizeSlot('default') ) diff --git a/src/components/table/thead.js b/src/components/table/thead.js index bdbeb3b3f1d..812bfac00cd 100644 --- a/src/components/table/thead.js +++ b/src/components/table/thead.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -16,7 +17,8 @@ export const props = { // @vue/component export const BThead = /*#__PURE__*/ Vue.extend({ name: 'BThead', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -71,7 +73,7 @@ export const BThead = /*#__PURE__*/ Vue.extend({ return [this.headVariant ? `thead-${this.headVariant}` : null] }, theadAttrs() { - return { role: 'rowgroup', ...this.attrs$ } + return { role: 'rowgroup', ...this.bvAttrs } } }, render(h) { @@ -81,7 +83,7 @@ export const BThead = /*#__PURE__*/ Vue.extend({ class: this.theadClasses, attrs: this.theadAttrs, // Pass down any native listeners - on: this.listeners$ + on: this.bvListeners }, this.normalizeSlot('default') ) diff --git a/src/components/table/tr.js b/src/components/table/tr.js index 1a3566e6c5c..d9f9e98bc71 100644 --- a/src/components/table/tr.js +++ b/src/components/table/tr.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' +import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' export const props = { @@ -18,7 +19,8 @@ const DARK = 'dark' // @vue/component export const BTr = /*#__PURE__*/ Vue.extend({ name: 'BTr', - mixins: [bindAttrsMixin, normalizeSlotMixin], + // Mixin order is important! + mixins: [attrsMixin, listenersMixin, normalizeSlotMixin], inheritAttrs: false, provide() { return { @@ -93,7 +95,7 @@ export const BTr = /*#__PURE__*/ Vue.extend({ return [this.variant ? `${this.isRowDark ? 'bg' : 'table'}-${this.variant}` : null] }, trAttrs() { - return { role: 'row', ...this.attrs$ } + return { role: 'row', ...this.bvAttrs } } }, render(h) { @@ -103,7 +105,7 @@ export const BTr = /*#__PURE__*/ Vue.extend({ class: this.trClasses, attrs: this.trAttrs, // Pass native listeners to child - on: this.listeners$ + on: this.bvListeners }, this.normalizeSlot('default') ) diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index c931016c2c5..eb1ed134031 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -7,7 +7,7 @@ import { requestAF } from '../../utils/dom' import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events' import { mathMax } from '../../utils/math' import { toInteger } from '../../utils/number' -import bindAttrsMixin from '../../mixins/bind-attrs' +import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -110,7 +110,7 @@ export const props = { // @vue/component export const BToast = /*#__PURE__*/ Vue.extend({ name: NAME, - mixins: [bindAttrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], + mixins: [attrsMixin, idMixin, listenOnRootMixin, normalizeSlotMixin, scopedStyleAttrsMixin], inheritAttrs: false, model: { prop: 'visible', @@ -398,7 +398,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ staticClass: 'toast', class: this.toastClass, attrs: { - ...this.attrs$, + ...this.bvAttrs, tabindex: '0', id: this.safeId() } diff --git a/src/mixins/attrs.js b/src/mixins/attrs.js new file mode 100644 index 00000000000..9d63d1d12fa --- /dev/null +++ b/src/mixins/attrs.js @@ -0,0 +1,3 @@ +import { makePropCacheMixin } from '../utils/cache' + +export default makePropCacheMixin('$attrs', 'bvAttrs') diff --git a/src/mixins/bind-attrs.js b/src/mixins/bind-attrs.js deleted file mode 100644 index fbacd69a318..00000000000 --- a/src/mixins/bind-attrs.js +++ /dev/null @@ -1,40 +0,0 @@ -import { hasOwnProperty } from '../utils/object' - -// --- Constants --- - -const ATTRS_ATTRIBUTE_NAME = 'attrs$' -const LISTENERS_ATTRIBUTE_NAME = 'listeners$' - -// --- Utility methods --- - -const makeWatcher = property => ({ - handler(newVal, oldVal) { - for (const prop in oldVal) { - if (!hasOwnProperty(newVal, prop)) { - this.$delete(this.$data[property], prop) - } - } - for (const prop in newVal) { - this.$set(this.$data[property], prop, newVal[prop]) - } - } -}) - -// @vue/component -export default { - data() { - return { - [ATTRS_ATTRIBUTE_NAME]: {}, - [LISTENERS_ATTRIBUTE_NAME]: {} - } - }, - watch: { - // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115 - $attrs: makeWatcher(ATTRS_ATTRIBUTE_NAME), - $listeners: makeWatcher(LISTENERS_ATTRIBUTE_NAME) - }, - created() { - this[ATTRS_ATTRIBUTE_NAME] = { ...this.$attrs } - this[LISTENERS_ATTRIBUTE_NAME] = { ...this.$listeners } - } -} diff --git a/src/mixins/form-radio-check.js b/src/mixins/form-radio-check.js index 932867eaca7..9338e333a81 100644 --- a/src/mixins/form-radio-check.js +++ b/src/mixins/form-radio-check.js @@ -1,9 +1,9 @@ -import bindAttrsMixin from './bind-attrs' +import attrsMixin from './attrs' import normalizeSlotMixin from './normalize-slot' // @vue/component export default { - mixins: [bindAttrsMixin, normalizeSlotMixin], + mixins: [attrsMixin, normalizeSlotMixin], inheritAttrs: false, model: { prop: 'checked', @@ -202,7 +202,7 @@ export default { } ], attrs: { - ...this.attrs$, + ...this.bvAttrs, id: this.safeId(), type: this.isRadio ? 'radio' : 'checkbox', name: this.getName, diff --git a/src/mixins/listeners.js b/src/mixins/listeners.js new file mode 100644 index 00000000000..cc61d62f92a --- /dev/null +++ b/src/mixins/listeners.js @@ -0,0 +1,3 @@ +import { makePropCacheMixin } from '../utils/cache' + +export default makePropCacheMixin('$listeners', 'bvListeners') diff --git a/src/utils/cache.js b/src/utils/cache.js new file mode 100644 index 00000000000..c6faef986b0 --- /dev/null +++ b/src/utils/cache.js @@ -0,0 +1,29 @@ +import { hasOwnProperty } from './object' + +export const makePropWatcher = propName => ({ + handler(newVal, oldVal) { + for (const key in oldVal) { + if (!hasOwnProperty(newVal, key)) { + this.$delete(this.$data[propName], key) + } + } + for (const key in newVal) { + this.$set(this.$data[propName], key, newVal[key]) + } + } +}) + +export const makePropCacheMixin = (propName, proxyPropName) => ({ + data() { + return { + [proxyPropName]: {} + } + }, + watch: { + // Work around unwanted re-renders: https://github.com/vuejs/vue/issues/10115 + [propName]: makePropWatcher(proxyPropName) + }, + created() { + this[proxyPropName] = { ...this[propName] } + } +}) From a762b78766debfaf89ef24a7736a65c368652b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 8 May 2020 14:31:05 +0200 Subject: [PATCH 07/20] Further performance optimizations --- src/components/calendar/calendar.js | 73 ++++++--- .../dropdown/dropdown-item-button.js | 17 +- src/components/dropdown/dropdown-item.js | 10 +- src/components/form-file/form-file.js | 31 ++-- .../form-spinbutton/form-spinbutton.js | 148 +++++++++++------- src/components/link/link.js | 83 +++++----- src/components/modal/modal.js | 98 ++++++++---- src/components/sidebar/sidebar.js | 34 ++-- src/components/toast/toast.js | 13 +- src/mixins/form-radio-check.js | 29 ++-- 10 files changed, 329 insertions(+), 207 deletions(-) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index a35387c9681..8961773f576 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -273,6 +273,27 @@ export const BCalendar = Vue.extend({ } }, computed: { + valueId() { + return this.safeId() + }, + widgetId() { + return this.safeId('_calendar-wrapper_') + }, + navId() { + return this.safeId('_calendar-nav_') + }, + gridId() { + return this.safeId('_calendar-grid_') + }, + gridCaptionId() { + return this.safeId('_calendar-grid-caption_') + }, + gridHelpId() { + return this.safeId('_calendar-grid-help_') + }, + activeId() { + return this.activeYMD ? this.safeId(`_cell-${this.activeYMD}_`) : null + }, // TODO: Use computed props to convert `YYYY-MM-DD` to `Date` object selectedDate() { // Selected as a `Date` object @@ -773,24 +794,28 @@ export const BCalendar = Vue.extend({ } }, render(h) { - // If hidden prop is set, render just a placeholder node + // If `hidden` prop is set, render just a placeholder node if (this.hidden) { return h() } - const { isLive, isRTL, activeYMD, selectedYMD, safeId } = this + const { + valueId, + widgetId, + navId, + gridId, + gridCaptionId, + gridHelpId, + activeId, + isLive, + isRTL, + activeYMD, + selectedYMD, + safeId + } = this const hideDecadeNav = !this.showDecadeNav const todayYMD = formatYMD(this.getToday()) const highlightToday = !this.noHighlightToday - // Pre-compute some IDs - // This should be computed props - const idValue = safeId() - const idWidget = safeId('_calendar-wrapper_') - const idNav = safeId('_calendar-nav_') - const idGrid = safeId('_calendar-grid_') - const idGridCaption = safeId('_calendar-grid-caption_') - const idGridHelp = safeId('_calendar-grid-help_') - const idActive = activeYMD ? safeId(`_cell-${activeYMD}_`) : null // Header showing current selected date let $header = h( @@ -799,8 +824,8 @@ export const BCalendar = Vue.extend({ staticClass: 'form-control form-control-sm text-center', class: { 'text-muted': this.disabled, readonly: this.readonly || this.disabled }, attrs: { - id: idValue, - for: idGrid, + id: valueId, + for: gridId, role: 'status', tabindex: this.disabled ? null : '-1', // Mainly for testing purposes, as we do not know @@ -887,11 +912,11 @@ export const BCalendar = Vue.extend({ { staticClass: 'b-calendar-nav d-flex', attrs: { - id: idNav, + id: navId, role: 'group', 'aria-hidden': this.disabled ? 'true' : null, 'aria-label': this.labelNav || null, - 'aria-controls': idGrid + 'aria-controls': gridId } }, [ @@ -959,7 +984,7 @@ export const BCalendar = Vue.extend({ staticClass: 'b-calendar-grid-caption text-center font-weight-bold', class: { 'text-muted': this.disabled }, attrs: { - id: idGridCaption, + id: gridCaptionId, 'aria-live': isLive ? 'polite' : null, 'aria-atomic': isLive ? 'true' : null } @@ -1079,7 +1104,7 @@ export const BCalendar = Vue.extend({ { staticClass: 'b-calendar-grid-help border-top small text-muted text-center bg-light', attrs: { - id: idGridHelp + id: gridHelpId } }, [h('div', { staticClass: 'small' }, this.labelHelp)] @@ -1091,18 +1116,18 @@ export const BCalendar = Vue.extend({ ref: 'grid', staticClass: 'b-calendar-grid form-control h-auto text-center', attrs: { - id: idGrid, + id: gridId, role: 'application', tabindex: this.disabled ? null : '0', 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, - 'aria-labelledby': idGridCaption, - 'aria-describedby': idGridHelp, + 'aria-labelledby': gridCaptionId, + 'aria-describedby': gridHelpId, // `aria-readonly` is not considered valid on `role="application"` // https://www.w3.org/TR/wai-aria-1.1/#aria-readonly // 'aria-readonly': this.readonly && !this.disabled ? 'true' : null, 'aria-disabled': this.disabled ? 'true' : null, - 'aria-activedescendant': idActive + 'aria-activedescendant': activeId }, on: { keydown: this.onKeydownGrid, @@ -1123,7 +1148,7 @@ export const BCalendar = Vue.extend({ staticClass: 'b-calendar-inner', style: this.block ? {} : { width: this.width }, attrs: { - id: idWidget, + id: widgetId, dir: isRTL ? 'rtl' : 'ltr', lang: this.computedLocale || null, role: 'group', @@ -1136,8 +1161,8 @@ export const BCalendar = Vue.extend({ // Should the attr (if present) go last? // Or should this attr be a prop? this.bvAttrs['aria-describedby'], - idValue, - idGridHelp + valueId, + gridHelpId ] .filter(identity) .join(' ') diff --git a/src/components/dropdown/dropdown-item-button.js b/src/components/dropdown/dropdown-item-button.js index c3087d10b0d..1c5998d2f2e 100644 --- a/src/components/dropdown/dropdown-item-button.js +++ b/src/components/dropdown/dropdown-item-button.js @@ -36,6 +36,16 @@ export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ } }, props, + computed: { + computedAttrs() { + return { + ...this.bvAttrs, + role: 'menuitem', + type: 'button', + disabled: this.disabled + } + } + }, methods: { closeDropdown() { if (this.bvDropdown) { @@ -60,12 +70,7 @@ export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({ [`text-${this.variant}`]: this.variant && !(this.active || this.disabled) } ], - attrs: { - ...this.bvAttrs, - role: 'menuitem', - type: 'button', - disabled: this.disabled - }, + attrs: this.computedAttrs, on: { click: this.onClick }, ref: 'button' }, diff --git a/src/components/dropdown/dropdown-item.js b/src/components/dropdown/dropdown-item.js index 58da3f82ef2..992984e4d0a 100644 --- a/src/components/dropdown/dropdown-item.js +++ b/src/components/dropdown/dropdown-item.js @@ -27,6 +27,14 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({ default: null } }, + computed: { + computedAttrs() { + return { + ...this.bvAttrs, + role: 'menuitem' + } + } + }, methods: { closeDropdown() { // Close on next animation frame to allow time to process @@ -54,7 +62,7 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({ [`text-${this.variant}`]: this.variant && !(this.active || this.disabled) } ], - attrs: { ...this.bvAttrs, role: 'menuitem' }, + attrs: this.computedAttrs, on: { click: this.onClick }, ref: 'item' }, diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 2a57b502321..3eda52041f0 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -128,6 +128,22 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ ? toString(this.fileNameFormatter(files)) : files.map(file => file.name).join(', ') } + }, + computedAttrs() { + return { + ...this.bvAttrs, + type: 'file', + id: this.safeId(), + name: this.name, + disabled: this.disabled, + required: this.required, + form: this.form || null, + capture: this.capture || null, + accept: this.accept || null, + multiple: this.multiple, + webkitdirectory: this.directory, + 'aria-required': this.required ? 'true' : null + } } }, watch: { @@ -291,20 +307,7 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ }, this.stateClass ], - attrs: { - ...this.bvAttrs, - type: 'file', - id: this.safeId(), - name: this.name, - disabled: this.disabled, - required: this.required, - form: this.form || null, - capture: this.capture || null, - accept: this.accept || null, - multiple: this.multiple, - webkitdirectory: this.directory, - 'aria-required': this.required ? 'true' : null - }, + attrs: this.computedAttrs, on: { change: this.onFileChange, focusin: this.focusHandler, diff --git a/src/components/form-spinbutton/form-spinbutton.js b/src/components/form-spinbutton/form-spinbutton.js index 80e65e66598..9618ea8f235 100644 --- a/src/components/form-spinbutton/form-spinbutton.js +++ b/src/components/form-spinbutton/form-spinbutton.js @@ -153,6 +153,18 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ } }, computed: { + spinId() { + return this.safeId() + }, + computedInline() { + return this.inline && !this.vertical + }, + computedReadonly() { + return this.readonly && !this.disabled + }, + computedRequired() { + return this.required && !this.computedReadonly && !this.disabled + }, computedStep() { return toFloat(this.step, DEFAULT_STEP) }, @@ -213,6 +225,50 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ }) // Return the format method reference return nf.format + }, + computedFormatter() { + return isFunction(this.formatterFn) ? this.formatterFn : this.defaultFormatter + }, + computedAttrs() { + return { + ...this.bvAttrs, + role: 'group', + lang: this.computedLocale, + tabindex: this.disabled ? null : '-1', + title: this.ariaLabel + } + }, + computedSpinAttrs() { + const { + spinId, + localValue: value, + computedRequired: required, + disabled, + state, + computedFormatter + } = this + const hasValue = !isNull(value) + + return { + dir: this.computedRTL ? 'rtl' : 'ltr', + ...this.bvAttrs, + id: spinId, + role: 'spinbutton', + tabindex: disabled ? null : '0', + 'aria-live': 'off', + 'aria-label': this.ariaLabel || null, + 'aria-controls': this.ariaControls || null, + // TODO: May want to check if the value is in range + 'aria-invalid': state === false || (!hasValue && required) ? 'true' : null, + 'aria-required': required ? 'true' : null, + // These attrs are required for role spinbutton + 'aria-valuemin': toString(this.computedMin), + 'aria-valuemax': toString(this.computedMax), + // These should be `null` if the value is out of range + // They must also be non-existent attrs if the value is out of range or `null` + 'aria-valuenow': hasValue ? value : null, + 'aria-valuetext': hasValue ? computedFormatter(value) : null + } } }, watch: { @@ -421,17 +477,18 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ } }, render(h) { - const spinId = this.safeId() - const value = this.localValue - const isVertical = this.vertical - const isInline = this.inline && !isVertical - const isDisabled = this.disabled - const isReadonly = this.readonly && !isDisabled - const isRequired = this.required && !isReadonly && !isDisabled - const state = this.state - const size = this.size + const { + spinId, + localValue: value, + computedInline: inline, + computedReadonly: readonly, + vertical, + disabled, + state, + size, + computedFormatter + } = this const hasValue = !isNull(value) - const formatter = isFunction(this.formatterFn) ? this.formatterFn : this.defaultFormatter const makeButton = (stepper, label, IconCmp, keyRef, shortcut, btnDisabled, slotName) => { const $icon = h(IconCmp, { @@ -440,7 +497,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ }) const scope = { hasFocus: this.hasFocus } const handler = evt => { - if (!isDisabled && !isReadonly) { + if (!disabled && !readonly) { evt.preventDefault() this.setMouseup(true) try { @@ -456,12 +513,12 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ key: keyRef || null, ref: keyRef, staticClass: 'btn btn-sm border-0 rounded-0', - class: { 'py-0': !isVertical }, + class: { 'py-0': !vertical }, attrs: { tabindex: '-1', type: 'button', - disabled: isDisabled || isReadonly || btnDisabled, - 'aria-disabled': isDisabled || isReadonly || btnDisabled ? 'true' : null, + disabled: disabled || readonly || btnDisabled, + 'aria-disabled': disabled || readonly || btnDisabled ? 'true' : null, 'aria-controls': spinId, 'aria-label': label || null, 'aria-keyshortcuts': shortcut || null @@ -495,7 +552,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ ) let $hidden = h() - if (this.name && !isDisabled) { + if (this.name && !disabled) { $hidden = h('input', { key: 'hidden', attrs: { @@ -516,36 +573,17 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ key: 'output', staticClass: 'flex-grow-1', class: { - 'd-flex': isVertical, - 'align-self-center': !isVertical, - 'align-items-center': isVertical, - 'border-top': isVertical, - 'border-bottom': isVertical, - 'border-left': !isVertical, - 'border-right': !isVertical + 'd-flex': vertical, + 'align-self-center': !vertical, + 'align-items-center': vertical, + 'border-top': vertical, + 'border-bottom': vertical, + 'border-left': !vertical, + 'border-right': !vertical }, - attrs: { - dir: this.computedRTL ? 'rtl' : 'ltr', - ...this.bvAttrs, - id: spinId, - role: 'spinbutton', - tabindex: isDisabled ? null : '0', - 'aria-live': 'off', - 'aria-label': this.ariaLabel || null, - 'aria-controls': this.ariaControls || null, - // TODO: May want to check if the value is in range - 'aria-invalid': state === false || (!hasValue && isRequired) ? 'true' : null, - 'aria-required': isRequired ? 'true' : null, - // These attrs are required for role spinbutton - 'aria-valuemin': toString(this.computedMin), - 'aria-valuemax': toString(this.computedMax), - // These should be `null` if the value is out of range - // They must also be non-existent attrs if the value is out of range or `null` - 'aria-valuenow': hasValue ? value : null, - 'aria-valuetext': hasValue ? formatter(value) : null - } + attrs: this.computedSpinAttrs }, - [h('bdi', hasValue ? formatter(value) : this.placeholder || '')] + [h('bdi', hasValue ? computedFormatter(value) : this.placeholder || '')] ) return h( @@ -553,24 +591,18 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ { staticClass: 'b-form-spinbutton form-control', class: { - disabled: isDisabled, - readonly: isReadonly, + disabled, + readonly, focus: this.hasFocus, [`form-control-${size}`]: !!size, - 'd-inline-flex': isInline || isVertical, - 'd-flex': !isInline && !isVertical, - 'align-items-stretch': !isVertical, - 'flex-column': isVertical, + 'd-inline-flex': inline || vertical, + 'd-flex': !inline && !vertical, + 'align-items-stretch': !vertical, + 'flex-column': vertical, 'is-valid': state === true, 'is-invalid': state === false }, - attrs: { - ...this.bvAttrs, - role: 'group', - lang: this.computedLocale, - tabindex: isDisabled ? null : '-1', - title: this.ariaLabel - }, + attrs: this.computedAttrs, on: { keydown: this.onKeydown, keyup: this.onKeyup, @@ -579,9 +611,7 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ '!blur': this.onFocusBlur } }, - isVertical - ? [$increment, $hidden, $spin, $decrement] - : [$decrement, $hidden, $spin, $increment] + vertical ? [$increment, $hidden, $spin, $decrement] : [$decrement, $hidden, $spin, $increment] ) } }) diff --git a/src/components/link/link.js b/src/components/link/link.js index 51336477fdf..1da2efb2bd3 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -2,6 +2,7 @@ import Vue from '../../utils/vue' import { concat } from '../../utils/array' import { isEvent, isFunction, isUndefined } from '../../utils/inspect' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' +import { omit } from '../../utils/object' import attrsMixin from '../../mixins/attrs' import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' @@ -103,7 +104,41 @@ export const BLink = /*#__PURE__*/ Vue.extend({ return computeHref({ to: this.to, href: this.href }, this.computedTag) }, computedProps() { - return this.isRouterLink ? { ...this.$props, tag: this.routerTag } : {} + const props = this.isRouterLink ? { ...this.$props, tag: this.routerTag } : {} + // Ensure the `href` prop does not exist for router links + return this.computedHref ? props : omit(props, ['href']) + }, + computedAttrs() { + const { + bvAttrs, + computedHref: href, + computedRel: rel, + disabled, + target, + routerTag, + isRouterLink + } = this + + return { + ...bvAttrs, + // If `href` attribute exists on `` (even `undefined` or `null`) + // it fails working on SSR, so we explicitly add it here if needed + // (i.e. if `computedHref()` is truthy) + ...(href ? { href } : {}), + // We don't render `rel` or `target` on non link tags when using `vue-router` + ...(isRouterLink && routerTag !== 'a' && routerTag !== 'area' ? {} : { rel, target }), + tabindex: disabled ? '-1' : isUndefined(bvAttrs.tabindex) ? null : bvAttrs.tabindex, + 'aria-disabled': disabled ? 'true' : null + } + }, + computedListeners() { + return { + // Transfer all listeners (native) to the root element + ...this.bvListeners, + // We want to overwrite any click handler since our callback + // will invoke the user supplied handler(s) if `!this.disabled` + click: this.onClick + } } }, methods: { @@ -151,42 +186,18 @@ export const BLink = /*#__PURE__*/ Vue.extend({ } }, render(h) { - const $attrs = this.bvAttrs - const { active, disabled, target, routerTag, isRouterLink } = this - const tag = this.computedTag - const rel = this.computedRel - const href = this.computedHref + const { active, disabled } = this - const componentData = { - class: { active, disabled }, - attrs: { - ...$attrs, - // We don't render `rel` or `target` on non link tags when using `vue-router` - ...(isRouterLink && routerTag !== 'a' && routerTag !== 'area' ? {} : { rel, target }), - tabindex: disabled ? '-1' : isUndefined($attrs.tabindex) ? null : $attrs.tabindex, - 'aria-disabled': disabled ? 'true' : null + return h( + this.computedTag, + { + class: { active, disabled }, + attrs: this.computedAttrs, + props: this.computedProps, + // We must use `nativeOn` for ``/`` instead of `on` + [this.isRouterLink ? 'nativeOn' : 'on']: this.computedListeners }, - props: this.computedProps - } - // Add the event handlers. We must use `nativeOn` for - // ``/`` instead of `on` - componentData[isRouterLink ? 'nativeOn' : 'on'] = { - // Transfer all listeners (native) to the root element - ...this.bvListeners, - // We want to overwrite any click handler since our callback - // will invoke the user supplied handler(s) if `!this.disabled` - click: this.onClick - } - - // If href attribute exists on (even undefined or null) it fails working on - // SSR, so we explicitly add it here if needed (i.e. if computedHref() is truthy) - if (href) { - componentData.attrs.href = href - } else { - // Ensure the prop HREF does not exist for router links - delete componentData.props.href - } - - return h(tag, componentData, this.normalizeSlot('default')) + this.normalizeSlot('default') + ) } }) diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index 6d63a6b771e..931647c78e4 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -307,6 +307,30 @@ export const BModal = /*#__PURE__*/ Vue.extend({ } }, computed: { + modalId() { + return this.safeId() + }, + modalOuterId() { + return this.safeId('__BV_modal_outer_') + }, + modalHeaderId() { + return this.safeId('__BV_modal_header_') + }, + modalBodyId() { + return this.safeId('__BV_modal_body_') + }, + modalTitleId() { + return this.safeId('__BV_modal_title_') + }, + modalContentId() { + return this.safeId('__BV_modal_content_') + }, + modalFooterId() { + return this.safeId('__BV_modal_footer_') + }, + modalBackdropId() { + return this.safeId('__BV_modal_backdrop_') + }, modalClasses() { return [ { @@ -390,6 +414,34 @@ export const BModal = /*#__PURE__*/ Vue.extend({ .filter(identity) .join(',') .trim() + }, + computedAttrs() { + // If the parent has a scoped style attribute, and the modal + // is portalled, add the scoped attribute to the modal wrapper + const scopedStyleAttrs = !this.static ? this.scopedStyleAttrs : {} + + return { + ...scopedStyleAttrs, + ...this.bvAttrs, + id: this.modalOuterId + } + }, + computedModalAttrs() { + return { + id: this.modalId, + role: 'dialog', + 'aria-hidden': this.isVisible ? null : 'true', + 'aria-modal': this.isVisible ? 'true' : null, + 'aria-label': this.ariaLabel, + 'aria-labelledby': + this.hideHeader || + this.ariaLabel || + // TODO: Rename slot to `title` and deprecate `modal-title` + !(this.hasNormalizedSlot('modal-title') || this.titleHtml || this.title) + ? null + : this.modalTitleId, + 'aria-describedby': this.modalBodyId + } } }, watch: { @@ -450,7 +502,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ ...options, // Options that can't be overridden vueTarget: this, - componentId: this.safeId() + componentId: this.modalId }) }, // Public method to show modal @@ -738,18 +790,18 @@ export const BModal = /*#__PURE__*/ Vue.extend({ }, // Root listener handlers showHandler(id, triggerEl) { - if (id === this.safeId()) { + if (id === this.modalId) { this.return_focus = triggerEl || this.getActiveElement() this.show() } }, hideHandler(id) { - if (id === this.safeId()) { + if (id === this.modalId) { this.hide('event') } }, toggleHandler(id, triggerEl) { - if (id === this.safeId()) { + if (id === this.modalId) { this.toggle(triggerEl) } }, @@ -852,7 +904,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ { staticClass: 'modal-title', class: this.titleClasses, - attrs: { id: this.safeId('__BV_modal_title_') }, + attrs: { id: this.modalTitleId }, domProps }, // TODO: Rename slot to `title` and deprecate `modal-title` @@ -867,7 +919,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ ref: 'header', staticClass: 'modal-header', class: this.headerClasses, - attrs: { id: this.safeId('__BV_modal_header_') } + attrs: { id: this.modalHeaderId } }, [modalHeader] ) @@ -880,7 +932,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ ref: 'body', staticClass: 'modal-body', class: this.bodyClasses, - attrs: { id: this.safeId('__BV_modal_body_') } + attrs: { id: this.modalBodyId } }, this.normalizeSlot('default', this.slotScope) ) @@ -938,7 +990,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ ref: 'footer', staticClass: 'modal-footer', class: this.footerClasses, - attrs: { id: this.safeId('__BV_modal_footer_') } + attrs: { id: this.modalFooterId } }, [modalFooter] ) @@ -953,7 +1005,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ class: this.contentClass, attrs: { role: 'document', - id: this.safeId('__BV_modal_content_'), + id: this.modalContentId, tabindex: '-1' } }, @@ -992,21 +1044,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ directives: [ { name: 'show', rawName: 'v-show', value: this.isVisible, expression: 'isVisible' } ], - attrs: { - id: this.safeId(), - role: 'dialog', - 'aria-hidden': this.isVisible ? null : 'true', - 'aria-modal': this.isVisible ? 'true' : null, - 'aria-label': this.ariaLabel, - 'aria-labelledby': - this.hideHeader || - this.ariaLabel || - // TODO: Rename slot to `title` and deprecate `modal-title` - !(this.hasNormalizedSlot('modal-title') || this.titleHtml || this.title) - ? null - : this.safeId('__BV_modal_title_'), - 'aria-describedby': this.safeId('__BV_modal_body_') - }, + attrs: this.computedModalAttrs, on: { keydown: this.onEsc, click: this.onClickOut } }, [modalDialog] @@ -1044,28 +1082,20 @@ export const BModal = /*#__PURE__*/ Vue.extend({ if (!this.hideBackdrop && this.isVisible) { backdrop = h( 'div', - { staticClass: 'modal-backdrop', attrs: { id: this.safeId('__BV_modal_backdrop_') } }, + { staticClass: 'modal-backdrop', attrs: { id: this.modalBackdropId } }, // TODO: Rename slot to `backdrop` and deprecate `modal-backdrop` [this.normalizeSlot('modal-backdrop')] ) } backdrop = h(BVTransition, { props: { noFade: this.noFade } }, [backdrop]) - // If the parent has a scoped style attribute, and the modal - // is portalled, add the scoped attribute to the modal wrapper - const scopedStyleAttrs = !this.static ? this.scopedStyleAttrs : {} - // Assemble modal and backdrop in an outer
return h( 'div', { key: `modal-outer-${this._uid}`, style: this.modalOuterStyle, - attrs: { - ...scopedStyleAttrs, - ...this.bvAttrs, - id: this.safeId('__BV_modal_outer_') - } + attrs: this.computedAttrs }, [modal, backdrop] ) diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index 3bb3bebc91c..dc94bca7a5c 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -261,6 +261,24 @@ export const BSidebar = /*#__PURE__*/ Vue.extend({ right: this.right, hide: this.hide } + }, + computedTile() { + return this.normalizeSlot('title', this.slotScope) || toString(this.title) || null + }, + titleId() { + return this.computedTile ? this.safeId('__title__') : null + }, + computedAttrs() { + return { + ...this.bvAttrs, + id: this.safeId(), + tabindex: '-1', + role: 'dialog', + 'aria-modal': this.backdrop ? 'true' : 'false', + 'aria-hidden': this.localShow ? null : 'true', + 'aria-label': this.ariaLabel || null, + 'aria-labelledby': this.ariaLabelledby || this.titleId || null + } } }, watch: { @@ -381,11 +399,6 @@ export const BSidebar = /*#__PURE__*/ Vue.extend({ render(h) { const localShow = this.localShow const shadow = this.shadow === '' ? true : this.shadow - const title = this.normalizeSlot('title', this.slotScope) || toString(this.title) || null - const titleId = title ? this.safeId('__title__') : null - const ariaLabel = this.ariaLabel || null - // `ariaLabel` takes precedence over `ariaLabelledby` - const ariaLabelledby = this.ariaLabelledby || titleId || null let $sidebar = h( this.tag, @@ -403,16 +416,7 @@ export const BSidebar = /*#__PURE__*/ Vue.extend({ }, this.sidebarClass ], - attrs: { - ...this.bvAttrs, - id: this.safeId(), - tabindex: '-1', - role: 'dialog', - 'aria-modal': this.backdrop ? 'true' : 'false', - 'aria-hidden': localShow ? null : 'true', - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledby - }, + attrs: this.computedAttrs, style: { width: this.width } }, [renderContent(h, this)] diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index eb1ed134031..f3e40224606 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -158,6 +158,13 @@ export const BToast = /*#__PURE__*/ Vue.extend({ beforeLeave: this.onBeforeLeave, afterLeave: this.onAfterLeave } + }, + computedAttrs() { + return { + ...this.bvAttrs, + id: this.safeId(), + tabindex: '0' + } } }, watch: { @@ -397,11 +404,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ ref: 'toast', staticClass: 'toast', class: this.toastClass, - attrs: { - ...this.bvAttrs, - tabindex: '0', - id: this.safeId() - } + attrs: this.computedAttrs }, [$header, $body] ) diff --git a/src/mixins/form-radio-check.js b/src/mixins/form-radio-check.js index 9338e333a81..0d97d4be04e 100644 --- a/src/mixins/form-radio-check.js +++ b/src/mixins/form-radio-check.js @@ -141,6 +141,21 @@ export default { focus: this.hasFocus } ] + }, + computedAttrs() { + return { + ...this.bvAttrs, + id: this.safeId(), + type: this.isRadio ? 'radio' : 'checkbox', + name: this.getName, + form: this.getForm, + disabled: this.isDisabled, + required: this.isRequired, + autocomplete: 'off', + 'aria-required': this.isRequired || null, + 'aria-label': this.ariaLabel || null, + 'aria-labelledby': this.ariaLabelledby || null + } } }, watch: { @@ -201,19 +216,7 @@ export default { expression: 'computedLocalChecked' } ], - attrs: { - ...this.bvAttrs, - id: this.safeId(), - type: this.isRadio ? 'radio' : 'checkbox', - name: this.getName, - form: this.getForm, - disabled: this.isDisabled, - required: this.isRequired, - autocomplete: 'off', - 'aria-required': this.isRequired || null, - 'aria-label': this.ariaLabel || null, - 'aria-labelledby': this.ariaLabelledby || null - }, + attrs: this.computedAttrs, domProps: { value: this.value, checked: this.isChecked From db125d43cc8399f1412925aa7ff26a77b6a919a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 8 May 2020 15:03:27 +0200 Subject: [PATCH 08/20] Update form-input.js --- src/components/form-input/form-input.js | 70 ++++++++++++------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/components/form-input/form-input.js b/src/components/form-input/form-input.js index 5b3a2b24b64..9a9688f848c 100644 --- a/src/components/form-input/form-input.js +++ b/src/components/form-input/form-input.js @@ -77,6 +77,35 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ localType() { // We only allow certain types return arrayIncludes(TYPES, this.type) ? this.type : 'text' + }, + computedAttrs() { + const { localType: type, disabled, placeholder, required, min, max, step } = this + + return { + id: this.safeId(), + name: this.name || null, + form: this.form || null, + type, + disabled, + placeholder, + required, + autocomplete: this.autocomplete || null, + readonly: this.readonly || this.plaintext, + min, + max, + step, + list: type !== 'password' ? this.list : null, + 'aria-required': required ? 'true' : null, + 'aria-invalid': this.computedAriaInvalid + } + }, + computedListeners() { + return { + ...this.bvListeners, + input: this.onInput, + change: this.onChange, + blur: this.onBlur + } } }, watch: { @@ -125,45 +154,12 @@ export const BFormInput = /*#__PURE__*/ Vue.extend({ } }, render(h) { - // We alias `this` to `self` for better minification - const self = this return h('input', { ref: 'input', - class: self.computedClass, - directives: [ - { - name: 'model', - rawName: 'v-model', - value: self.localValue, - expression: 'localValue' - } - ], - attrs: { - id: self.safeId(), - name: self.name, - form: self.form || null, - type: self.localType, - disabled: self.disabled, - placeholder: self.placeholder, - required: self.required, - autocomplete: self.autocomplete || null, - readonly: self.readonly || self.plaintext, - min: self.min, - max: self.max, - step: self.step, - list: self.localType !== 'password' ? self.list : null, - 'aria-required': self.required ? 'true' : null, - 'aria-invalid': self.computedAriaInvalid - }, - domProps: { - value: self.localValue - }, - on: { - ...self.bvListeners, - input: self.onInput, - change: self.onChange, - blur: self.onBlur - } + class: this.computedClass, + attrs: this.computedAttrs, + domProps: { value: this.localValue }, + on: this.computedListeners }) } }) From d0e5ec1fd3bf746fc7bebe0d31383903704189d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 8 May 2020 15:03:31 +0200 Subject: [PATCH 09/20] Update form-textarea.js --- src/components/form-textarea/form-textarea.js | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/form-textarea/form-textarea.js b/src/components/form-textarea/form-textarea.js index c3034df3445..15ae4b2e166 100644 --- a/src/components/form-textarea/form-textarea.js +++ b/src/components/form-textarea/form-textarea.js @@ -92,6 +92,32 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ // This is used to set the attribute 'rows' on the textarea // If auto-height is enabled, then we return `null` as we use CSS to control height return this.computedMinRows === this.computedMaxRows ? this.computedMinRows : null + }, + computedAttrs() { + const { disabled, required } = this + + return { + id: this.safeId(), + name: this.name || null, + form: this.form || null, + disabled, + placeholder: this.placeholder || null, + required, + autocomplete: this.autocomplete || null, + readonly: this.readonly || this.plaintext, + rows: this.computedRows, + wrap: this.wrap || null, + 'aria-required': this.required ? 'true' : null, + 'aria-invalid': this.computedAriaInvalid + } + }, + computedListeners() { + return { + ...this.bvListeners, + input: this.onInput, + change: this.onChange, + blur: this.onBlur + } } }, watch: { @@ -171,17 +197,11 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ } }, render(h) { - // Using self instead of this helps reduce code size during minification - const self = this return h('textarea', { ref: 'input', - class: self.computedClass, - style: self.computedStyle, + class: this.computedClass, + style: this.computedStyle, directives: [ - { - name: 'model', - value: self.localValue - }, { name: 'b-visible', value: this.visibleCallback, @@ -189,29 +209,9 @@ export const BFormTextarea = /*#__PURE__*/ Vue.extend({ modifiers: { '640': true } } ], - attrs: { - id: self.safeId(), - name: self.name || null, - form: self.form || null, - disabled: self.disabled, - placeholder: self.placeholder || null, - required: self.required, - autocomplete: self.autocomplete || null, - readonly: self.readonly || self.plaintext, - rows: self.computedRows, - wrap: self.wrap || null, - 'aria-required': self.required ? 'true' : null, - 'aria-invalid': self.computedAriaInvalid - }, - domProps: { - value: self.localValue - }, - on: { - ...self.bvListeners, - input: self.onInput, - change: self.onChange, - blur: self.onBlur - } + attrs: this.computedAttrs, + domProps: { value: this.localValue }, + on: this.computedListeners }) } }) From daa8877963dc50e2e3b30201156c56fda383bd50 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 8 May 2020 20:50:48 -0300 Subject: [PATCH 10/20] Create attrs.spec.js --- src/mixins/attrs.spec.js | 92 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/mixins/attrs.spec.js diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js new file mode 100644 index 00000000000..15a1544361d --- /dev/null +++ b/src/mixins/attrs.spec.js @@ -0,0 +1,92 @@ +import { mount } from '@vue/test-utils' +import attrsMixin from './attrs' + +const BTest = { + name 'BTest', + mixins: [attrsMixin], + inheritAttrs: false, + render(h) { + return h('section', [h('article', { attrs: this.bvAttrs })]) + } +} + +const App = { + name 'App', + props: { + attrs: { + type: Object, + default: {} + } + }, + render(h) { + return h(BTest, { attrs: this.attrs }) + } +} + +describe('mixins > attrs', () => { + it('works (indirectly tests utils/cache)', async () => { + const wrapper = mount(App) + + expect(wrapper).toBeDefined() + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('SECTION') + + const $test = wrapper.findComponent(BTest) + + expect($test.exists()).toBe(true) + expect($test.vm).toBeDefined() + + const $section = $test.find('section') + expect($section.exists()).toBe(true) + + const $article = $test.find('article') + expect($article.exists()).toBe(true) + + expect($section.attributes()).toEqual({}) + expect($article.attributes()).toEqual({}) + + expect(BTest.vm.bvAttrs).toBeDefined() + expect(BTest.vm.bvAttrs.foo).not.toBeDefined() + expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + + // Correctly adds new attrs data + await wrapper.setProps({ + attrs: { 'foo': 'bar' } + }) + + expect($section.attributes()).toEqual({}) + expect($article.attributes()).toEqual({ 'foo': 'bar' }) + expect(BTest.vm.bvAttrs.foo).toEqual('bar') + expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + + // Correctly adds new attrs data + await wrapper.setProps({ + attrs: { 'foo': 'bar', 'baz': 'biz' } + }) + + expect($section.attributes()).toEqual({}) + expect($article.attributes()).toEqual({ 'foo': 'bar', 'baz': 'biz' }) + expect(BTest.vm.bvAttrs.foo).toEqual('bar') + expect(BTest.vm.bvAttrs.baz).toEqual('biz') + + // Correctly removes attrs data + await wrapper.setProps({ + attrs: { 'foo': 'bar' } + }) + + expect($section.attributes()).toEqual({}) + expect($article.attributes()).toEqual({ 'foo': 'bar' }) + expect(BTest.vm.bvAttrs.foo).toEqual('bar') + expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + + // Correctly removes all attrs data + await wrapper.setProps({ attrs: {} }) + + expect($section.attributes()).toEqual({}) + expect($article.attributes()).toEqual({}) + expect(BTest.vm.bvAttrs.foo).not.toBeDefined() + expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + + wrapper.destroy() + }) +}) From 12a8f61abbd75e7e4cd7268848f059d87c592937 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 8 May 2020 20:55:46 -0300 Subject: [PATCH 11/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 15a1544361d..720114cf217 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils' import attrsMixin from './attrs' const BTest = { - name 'BTest', + name: 'BTest', mixins: [attrsMixin], inheritAttrs: false, render(h) { @@ -11,7 +11,7 @@ const BTest = { } const App = { - name 'App', + name: 'App', props: { attrs: { type: Object, From ae9d204110f4a78c8df875dd06d36e21d091c8cf Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 8 May 2020 21:03:14 -0300 Subject: [PATCH 12/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 720114cf217..7e50d534f5f 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -15,7 +15,7 @@ const App = { props: { attrs: { type: Object, - default: {} + default: () => ({}) } }, render(h) { @@ -45,19 +45,19 @@ describe('mixins > attrs', () => { expect($section.attributes()).toEqual({}) expect($article.attributes()).toEqual({}) - expect(BTest.vm.bvAttrs).toBeDefined() - expect(BTest.vm.bvAttrs.foo).not.toBeDefined() - expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + expect($test.vm.bvAttrs).toBeDefined() + expect($test.vm.bvAttrs.foo).not.toBeDefined() + expect($test.vm.bvAttrs.baz).not.toBeDefined() // Correctly adds new attrs data await wrapper.setProps({ - attrs: { 'foo': 'bar' } + attrs: { foo: 'bar' } }) expect($section.attributes()).toEqual({}) - expect($article.attributes()).toEqual({ 'foo': 'bar' }) - expect(BTest.vm.bvAttrs.foo).toEqual('bar') - expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + expect($article.attributes()).toEqual({ foo: 'bar' }) + expect($test.vm.bvAttrs.foo).toEqual('bar') + expect($test.vm.bvAttrs.baz).not.toBeDefined() // Correctly adds new attrs data await wrapper.setProps({ @@ -65,27 +65,27 @@ describe('mixins > attrs', () => { }) expect($section.attributes()).toEqual({}) - expect($article.attributes()).toEqual({ 'foo': 'bar', 'baz': 'biz' }) - expect(BTest.vm.bvAttrs.foo).toEqual('bar') - expect(BTest.vm.bvAttrs.baz).toEqual('biz') + expect($article.attributes()).toEqual({ foo: 'bar', baz: 'biz' }) + expect($test.vm.bvAttrs.foo).toEqual('bar') + expect($test.vm.bvAttrs.baz).toEqual('biz') // Correctly removes attrs data await wrapper.setProps({ - attrs: { 'foo': 'bar' } + attrs: { foo: 'bar' } }) expect($section.attributes()).toEqual({}) - expect($article.attributes()).toEqual({ 'foo': 'bar' }) - expect(BTest.vm.bvAttrs.foo).toEqual('bar') - expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + expect($article.attributes()).toEqual({ foo: 'bar' }) + expect($test.vm.bvAttrs.foo).toEqual('bar') + expect($test.vm.bvAttrs.baz).not.toBeDefined() // Correctly removes all attrs data await wrapper.setProps({ attrs: {} }) expect($section.attributes()).toEqual({}) expect($article.attributes()).toEqual({}) - expect(BTest.vm.bvAttrs.foo).not.toBeDefined() - expect(BTest.vm.bvAttrs.baz).not.toBeDefined() + expect($test.vm.bvAttrs.foo).not.toBeDefined() + expect($test.vm.bvAttrs.baz).not.toBeDefined() wrapper.destroy() }) From f8a1b4d2cbe7490e75cfdb51d25d55843656c239 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 8 May 2020 21:08:37 -0300 Subject: [PATCH 13/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 7e50d534f5f..c3ebd15a111 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -61,7 +61,7 @@ describe('mixins > attrs', () => { // Correctly adds new attrs data await wrapper.setProps({ - attrs: { 'foo': 'bar', 'baz': 'biz' } + attrs: { foo: 'bar', baz: 'biz' } }) expect($section.attributes()).toEqual({}) From 3f07b971a7457d47a31ef5ced8dbca2af70c7f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 12:24:50 +0200 Subject: [PATCH 14/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 105 +++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index c3ebd15a111..4fce28a0458 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -1,30 +1,29 @@ import { mount } from '@vue/test-utils' import attrsMixin from './attrs' -const BTest = { - name: 'BTest', - mixins: [attrsMixin], - inheritAttrs: false, - render(h) { - return h('section', [h('article', { attrs: this.bvAttrs })]) - } -} - -const App = { - name: 'App', - props: { - attrs: { - type: Object, - default: () => ({}) - } - }, - render(h) { - return h(BTest, { attrs: this.attrs }) - } -} - describe('mixins > attrs', () => { it('works (indirectly tests utils/cache)', async () => { + const BTest = { + name: 'BTest', + mixins: [attrsMixin], + inheritAttrs: false, + render(h) { + return h('section', [h('article', { attrs: this.bvAttrs })]) + } + } + const App = { + name: 'App', + props: { + attrs: { + type: Object, + default: () => ({}) + } + }, + render(h) { + return h(BTest, { attrs: this.attrs }) + } + } + const wrapper = mount(App) expect(wrapper).toBeDefined() @@ -89,4 +88,66 @@ describe('mixins > attrs', () => { wrapper.destroy() }) + + it('does not re-render parent child components', async () => { + let renderCount = 0 + + const BTest = { + name: 'BTest', + mixins: [attrsMixin], + inheritAttrs: false, + render(h) { + renderCount++ + return h('section', [h('article', { attrs: this.bvAttrs })]) + } + } + const App = { + name: 'App', + props: { + test1Attrs: { + type: Object, + default: () => ({}) + }, + test2Attrs: { + type: Object, + default: () => ({}) + } + }, + render(h) { + return h('div', [ + h(BTest, { attrs: this.test1Attrs }), + h(BTest, { attrs: this.test2Attrs }) + ]) + } + } + + const wrapper = mount(App) + + const $tests = wrapper.findAllComponents(BTest) + expect($tests.length).toBe(2) + expect($tests.at(0)).toBeDefined() + expect($tests.at(1)).toBeDefined() + expect(renderCount).toBe(2) + + const $section1 = $tests.at(0).find('section') + const $section2 = $tests.at(1).find('section') + const $article1 = $section1.find('article') + const $article2 = $section2.find('article') + + await wrapper.setProps({ test1Attrs: { foo: 'bar' } }) + expect($section1.attributes()).toEqual({}) + expect($article1.attributes()).toEqual({ foo: 'bar' }) + expect($section2.attributes()).toEqual({}) + expect($article2.attributes()).toEqual({}) + expect(renderCount).toBe(3) + + await wrapper.setProps({ test2Attrs: { baz: 'biz' } }) + expect($section1.attributes()).toEqual({}) + expect($article1.attributes()).toEqual({ foo: 'bar' }) + expect($section2.attributes()).toEqual({}) + expect($article2.attributes()).toEqual({ baz: 'biz' }) + expect(renderCount).toBe(4) + + wrapper.destroy() + }) }) From 745f75c9b9932e9860083dd3a0d6616993ddffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 13:11:34 +0200 Subject: [PATCH 15/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 135 ++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 4fce28a0458..3fb37cfb4cd 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -1,8 +1,10 @@ import { mount } from '@vue/test-utils' import attrsMixin from './attrs' +// Note: The following tests indirectly test `utils/cache` + describe('mixins > attrs', () => { - it('works (indirectly tests utils/cache)', async () => { + it('works', async () => { const BTest = { name: 'BTest', mixins: [attrsMixin], @@ -90,64 +92,95 @@ describe('mixins > attrs', () => { }) it('does not re-render parent child components', async () => { - let renderCount = 0 + let input1RenderCount = 0 + let input2RenderCount = 0 - const BTest = { - name: 'BTest', - mixins: [attrsMixin], - inheritAttrs: false, + const Input1 = { + props: ['value'], render(h) { - renderCount++ - return h('section', [h('article', { attrs: this.bvAttrs })]) + input1RenderCount++ + return h('input', { + attrs: { ...this.$attrs, value: this.value }, + domProps: { value: this.value }, + on: { input: e => this.$emit('input', e.target.value) } + }) } } - const App = { - name: 'App', - props: { - test1Attrs: { - type: Object, - default: () => ({}) - }, - test2Attrs: { - type: Object, - default: () => ({}) - } - }, + const Input2 = { + props: ['value'], + mixins: [attrsMixin], render(h) { - return h('div', [ - h(BTest, { attrs: this.test1Attrs }), - h(BTest, { attrs: this.test2Attrs }) - ]) + input2RenderCount++ + return h('input', { + attrs: { ...this.bvAttrs, value: this.value }, + domProps: { value: this.value }, + on: { input: e => this.$emit('input', e.target.value) } + }) } } - const wrapper = mount(App) - - const $tests = wrapper.findAllComponents(BTest) - expect($tests.length).toBe(2) - expect($tests.at(0)).toBeDefined() - expect($tests.at(1)).toBeDefined() - expect(renderCount).toBe(2) - - const $section1 = $tests.at(0).find('section') - const $section2 = $tests.at(1).find('section') - const $article1 = $section1.find('article') - const $article2 = $section2.find('article') - - await wrapper.setProps({ test1Attrs: { foo: 'bar' } }) - expect($section1.attributes()).toEqual({}) - expect($article1.attributes()).toEqual({ foo: 'bar' }) - expect($section2.attributes()).toEqual({}) - expect($article2.attributes()).toEqual({}) - expect(renderCount).toBe(3) - - await wrapper.setProps({ test2Attrs: { baz: 'biz' } }) - expect($section1.attributes()).toEqual({}) - expect($article1.attributes()).toEqual({ foo: 'bar' }) - expect($section2.attributes()).toEqual({}) - expect($article2.attributes()).toEqual({ baz: 'biz' }) - expect(renderCount).toBe(4) + const App1 = { + components: { Input1 }, + props: ['value1', 'value2'], + template: `
+ + +
` + } + const App2 = { + components: { Input2 }, + props: ['value1', 'value2'], + template: `
+ + +
` + } - wrapper.destroy() + const wrapper1 = mount(App1) + const wrapper2 = mount(App2) + + const $inputs1 = wrapper1.findAllComponents(Input1) + expect($inputs1.length).toBe(2) + expect($inputs1.at(0)).toBeDefined() + expect($inputs1.at(0).vm.value).toBe(undefined) + expect($inputs1.at(1)).toBeDefined() + expect($inputs1.at(1).vm.value).toBe(undefined) + expect(input1RenderCount).toBe(2) + + const $inputs2 = wrapper2.findAllComponents(Input2) + expect($inputs2.length).toBe(2) + expect($inputs2.at(0)).toBeDefined() + expect($inputs2.at(0).vm.value).toBe(undefined) + expect($inputs2.at(1)).toBeDefined() + expect($inputs2.at(1).vm.value).toBe(undefined) + expect(input2RenderCount).toBe(2) + + // Update the value for the first `Input1` + await wrapper1.setProps({ value1: 'foo' }) + expect($inputs1.at(0).vm.value).toBe('foo') + expect($inputs1.at(1).vm.value).toBe(undefined) + + expect(input1RenderCount).toBe(4) + + // Update the value for the second `Input1` + await wrapper1.setProps({ value2: 'bar' }) + expect($inputs1.at(0).vm.value).toBe('foo') + expect($inputs1.at(1).vm.value).toBe('bar') + expect(input1RenderCount).toBe(6) + + // Update the value for the first `Input2` + await wrapper2.setProps({ value1: 'foo' }) + expect($inputs2.at(0).vm.value).toBe('foo') + expect($inputs2.at(1).vm.value).toBe(undefined) + expect(input2RenderCount).toBe(3) + + // Update the value for the second `Input2` + await wrapper2.setProps({ value2: 'bar' }) + expect($inputs2.at(0).vm.value).toBe('foo') + expect($inputs2.at(1).vm.value).toBe('bar') + expect(input2RenderCount).toBe(4) + + wrapper1.destroy() + wrapper2.destroy() }) }) From 303ad84fd9db000fb41cd97c60bfa4f3bb0c028e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 13:13:19 +0200 Subject: [PATCH 16/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 3fb37cfb4cd..6e463f2a8b7 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -159,13 +159,14 @@ describe('mixins > attrs', () => { await wrapper1.setProps({ value1: 'foo' }) expect($inputs1.at(0).vm.value).toBe('foo') expect($inputs1.at(1).vm.value).toBe(undefined) - + // Both `Input1`'s are re-rendered (See: https://github.com/vuejs/vue/issues/7257) expect(input1RenderCount).toBe(4) // Update the value for the second `Input1` await wrapper1.setProps({ value2: 'bar' }) expect($inputs1.at(0).vm.value).toBe('foo') expect($inputs1.at(1).vm.value).toBe('bar') + // Both `Input1`'s are re-rendered (See: https://github.com/vuejs/vue/issues/7257) expect(input1RenderCount).toBe(6) // Update the value for the first `Input2` From 5ce982f09abf67aa0b08aec58aa5697cb3abe1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 13:14:17 +0200 Subject: [PATCH 17/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 6e463f2a8b7..7bde3aa8e4c 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -173,12 +173,14 @@ describe('mixins > attrs', () => { await wrapper2.setProps({ value1: 'foo' }) expect($inputs2.at(0).vm.value).toBe('foo') expect($inputs2.at(1).vm.value).toBe(undefined) + // With `attrsMixin` only the affected `Input2` is re-rendered expect(input2RenderCount).toBe(3) // Update the value for the second `Input2` await wrapper2.setProps({ value2: 'bar' }) expect($inputs2.at(0).vm.value).toBe('foo') expect($inputs2.at(1).vm.value).toBe('bar') + // With `attrsMixin` only the affected `Input2` is re-rendered expect(input2RenderCount).toBe(4) wrapper1.destroy() From cb098c68c1a7001955e3ed057fc7273e067a014c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 14:51:29 +0200 Subject: [PATCH 18/20] Update attrs.spec.js --- src/mixins/attrs.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/attrs.spec.js b/src/mixins/attrs.spec.js index 7bde3aa8e4c..e6f44b0d363 100644 --- a/src/mixins/attrs.spec.js +++ b/src/mixins/attrs.spec.js @@ -60,7 +60,7 @@ describe('mixins > attrs', () => { expect($test.vm.bvAttrs.foo).toEqual('bar') expect($test.vm.bvAttrs.baz).not.toBeDefined() - // Correctly adds new attrs data + // Correctly updates attrs data await wrapper.setProps({ attrs: { foo: 'bar', baz: 'biz' } }) From 91ad2e08c864e9c92a18fca5dac4e94a47ea5dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 14:54:24 +0200 Subject: [PATCH 19/20] Create listeners.spec.js --- src/mixins/listeners.spec.js | 204 +++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/mixins/listeners.spec.js diff --git a/src/mixins/listeners.spec.js b/src/mixins/listeners.spec.js new file mode 100644 index 00000000000..224087d029d --- /dev/null +++ b/src/mixins/listeners.spec.js @@ -0,0 +1,204 @@ +import { mount } from '@vue/test-utils' +import listenersMixin from './listeners' + +// Note: The following tests indirectly test `utils/cache` + +describe('mixins > listeners', () => { + it('works', async () => { + const BTest = { + name: 'BTest', + mixins: [listenersMixin], + inheritAttrs: false, + render(h) { + return h('button', { on: this.bvListeners }) + } + } + const App = { + name: 'App', + props: ['listenClick', 'listenFocus', 'listenBlur'], + computed: { + listeners() { + const listeners = {} + if (this.listenClick) { + listeners.click = evt => this.$emit('click', evt) + } + if (this.listenFocus) { + listeners.focus = evt => this.$emit('focus', evt) + } + if (this.listenBlur) { + listeners.blur = evt => this.$emit('blur', evt) + } + return listeners + } + }, + render(h) { + return h(BTest, { on: this.listeners }) + } + } + + const wrapper = mount(App) + + expect(wrapper).toBeDefined() + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('BUTTON') + + const $test = wrapper.findComponent(BTest) + + expect($test.exists()).toBe(true) + expect($test.vm).toBeDefined() + + expect($test.vm.bvListeners).toBeDefined() + expect($test.vm.bvListeners.click).not.toBeDefined() + expect($test.vm.bvListeners.focus).not.toBeDefined() + expect($test.vm.bvListeners.blur).not.toBeDefined() + + // Correctly adds new listeners + await wrapper.setProps({ + listenClick: true, + listenFocus: true + }) + + expect($test.vm.bvListeners.click).toBeDefined() + expect($test.vm.bvListeners.focus).toBeDefined() + expect($test.vm.bvListeners.blur).not.toBeDefined() + + // Correctly updates attrs data + await wrapper.setProps({ + listenClick: false, + listenBlur: true + }) + + expect($test.vm.bvListeners.click).not.toBeDefined() + expect($test.vm.bvListeners.focus).toBeDefined() + expect($test.vm.bvListeners.blur).toBeDefined() + + // Correctly removes attrs data + await wrapper.setProps({ + listenClick: false, + listenFocus: false, + listenBlur: false + }) + + expect($test.vm.bvListeners.click).not.toBeDefined() + expect($test.vm.bvListeners.focus).not.toBeDefined() + expect($test.vm.bvListeners.blur).not.toBeDefined() + + wrapper.destroy() + }) + + it('does not re-render parent child components', async () => { + let input1RenderCount = 0 + let input2RenderCount = 0 + + const Input1 = { + props: ['value'], + render(h) { + input1RenderCount++ + return h('input', { + attrs: { value: this.value }, + domProps: { value: this.value }, + on: { ...this.$listeners, input: e => this.$emit('input', e.target.value) } + }) + } + } + const Input2 = { + props: ['value'], + mixins: [listenersMixin], + render(h) { + input2RenderCount++ + return h('input', { + attrs: { value: this.value }, + domProps: { value: this.value }, + on: { ...this.bvListeners, input: e => this.$emit('input', e.target.value) } + }) + } + } + + const App1 = { + components: { Input1 }, + props: ['listenFocus1', 'listenFocus2'], + template: `
+ + +
` + } + const App2 = { + components: { Input2 }, + props: ['listenFocus1', 'listenFocus2'], + template: `
+ + +
` + } + + const wrapper1 = mount(App1) + const wrapper2 = mount(App2) + + // --- `Input1` tests --- + + const $inputs1 = wrapper1.findAllComponents(Input1) + expect($inputs1.length).toBe(2) + expect($inputs1.at(0)).toBeDefined() + expect($inputs1.at(1)).toBeDefined() + expect(wrapper1.emitted().focus1).not.toBeTruthy() + expect(wrapper1.emitted().focus2).not.toBeTruthy() + expect(input1RenderCount).toBe(2) + + await $inputs1.at(0).trigger('focus') + expect(wrapper1.emitted().focus1).not.toBeTruthy() + await $inputs1.at(1).trigger('focus') + expect(wrapper1.emitted().focus2).not.toBeTruthy() + expect(input1RenderCount).toBe(2) + + // Enable focus events for the first input and trigger it + await wrapper1.setProps({ listenFocus1: true }) + await $inputs1.at(0).trigger('focus') + expect(wrapper1.emitted().focus1).toBeTruthy() + expect(wrapper1.emitted().focus2).not.toBeTruthy() + // Both `Input1`'s are re-rendered (See: https://github.com/vuejs/vue/issues/7257) + expect(input1RenderCount).toBe(4) + + // Enable focus events for the second input and trigger it + await wrapper1.setProps({ listenFocus2: true }) + await $inputs1.at(1).trigger('focus') + expect(wrapper1.emitted().focus1).toBeTruthy() + expect(wrapper1.emitted().focus2).toBeTruthy() + // Both `Input1`'s are re-rendered (See: https://github.com/vuejs/vue/issues/7257) + expect(input1RenderCount).toBe(6) + + // --- `Input2` tests --- + + const $inputs2 = wrapper2.findAllComponents(Input2) + expect($inputs2.length).toBe(2) + expect($inputs2.at(0)).toBeDefined() + expect($inputs2.at(1)).toBeDefined() + expect(wrapper2.emitted().focus1).not.toBeTruthy() + expect(wrapper2.emitted().focus2).not.toBeTruthy() + expect(input2RenderCount).toBe(2) + + await $inputs2.at(0).trigger('focus') + expect(wrapper2.emitted().focus1).not.toBeTruthy() + await $inputs2.at(1).trigger('focus') + expect(wrapper2.emitted().focus2).not.toBeTruthy() + expect(input2RenderCount).toBe(2) + + // Enable focus events for the first input and trigger it + await wrapper2.setProps({ listenFocus1: true }) + await $inputs2.at(0).trigger('focus') + expect(wrapper2.emitted().focus1).toBeTruthy() + expect(wrapper2.emitted().focus2).not.toBeTruthy() + // With `listenersMixin` only the affected `Input2` is re-rendered + expect(input2RenderCount).toBe(2) + + // Enable focus events for the second input and trigger it + await wrapper2.setProps({ listenFocus2: true }) + await $inputs2.at(1).trigger('focus') + expect(wrapper2.emitted().focus1).toBeTruthy() + expect(wrapper2.emitted().focus2).toBeTruthy() + // With `listenersMixin` only the affected `Input2` is re-rendered + expect(input2RenderCount).toBe(2) + + wrapper1.destroy() + wrapper2.destroy() + }) +}) From a33febf41883cb7b838d912f4089942132b18688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sun, 10 May 2020 14:55:50 +0200 Subject: [PATCH 20/20] Update listeners.spec.js --- src/mixins/listeners.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mixins/listeners.spec.js b/src/mixins/listeners.spec.js index 224087d029d..736754dcec2 100644 --- a/src/mixins/listeners.spec.js +++ b/src/mixins/listeners.spec.js @@ -62,7 +62,7 @@ describe('mixins > listeners', () => { expect($test.vm.bvListeners.focus).toBeDefined() expect($test.vm.bvListeners.blur).not.toBeDefined() - // Correctly updates attrs data + // Correctly updates listeners await wrapper.setProps({ listenClick: false, listenBlur: true @@ -72,7 +72,7 @@ describe('mixins > listeners', () => { expect($test.vm.bvListeners.focus).toBeDefined() expect($test.vm.bvListeners.blur).toBeDefined() - // Correctly removes attrs data + // Correctly removes listeners await wrapper.setProps({ listenClick: false, listenFocus: false,