Skip to content

Commit 2cb3fe0

Browse files
authored
fix(perf): avoid useless re-renders of component on parent update (#4825)
1 parent 260ef72 commit 2cb3fe0

27 files changed

+931
-334
lines changed

src/components/calendar/calendar.js

+53-26
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { isLocaleRTL } from '../../utils/locale'
2727
import { mathMax } from '../../utils/math'
2828
import { toInteger } from '../../utils/number'
2929
import { toString } from '../../utils/string'
30+
import attrsMixin from '../../mixins/attrs'
3031
import idMixin from '../../mixins/id'
3132
import normalizeSlotMixin from '../../mixins/normalize-slot'
3233
import {
@@ -56,7 +57,8 @@ export const STR_NARROW = 'narrow'
5657
// @vue/component
5758
export const BCalendar = Vue.extend({
5859
name: NAME,
59-
mixins: [idMixin, normalizeSlotMixin],
60+
// Mixin order is important!
61+
mixins: [attrsMixin, idMixin, normalizeSlotMixin],
6062
model: {
6163
// Even though this is the default that Vue assumes, we need
6264
// to add it for the docs to reflect that this is the model
@@ -271,6 +273,27 @@ export const BCalendar = Vue.extend({
271273
}
272274
},
273275
computed: {
276+
valueId() {
277+
return this.safeId()
278+
},
279+
widgetId() {
280+
return this.safeId('_calendar-wrapper_')
281+
},
282+
navId() {
283+
return this.safeId('_calendar-nav_')
284+
},
285+
gridId() {
286+
return this.safeId('_calendar-grid_')
287+
},
288+
gridCaptionId() {
289+
return this.safeId('_calendar-grid-caption_')
290+
},
291+
gridHelpId() {
292+
return this.safeId('_calendar-grid-help_')
293+
},
294+
activeId() {
295+
return this.activeYMD ? this.safeId(`_cell-${this.activeYMD}_`) : null
296+
},
274297
// TODO: Use computed props to convert `YYYY-MM-DD` to `Date` object
275298
selectedDate() {
276299
// Selected as a `Date` object
@@ -771,24 +794,28 @@ export const BCalendar = Vue.extend({
771794
}
772795
},
773796
render(h) {
774-
// If hidden prop is set, render just a placeholder node
797+
// If `hidden` prop is set, render just a placeholder node
775798
if (this.hidden) {
776799
return h()
777800
}
778801

779-
const { isLive, isRTL, activeYMD, selectedYMD, safeId } = this
802+
const {
803+
valueId,
804+
widgetId,
805+
navId,
806+
gridId,
807+
gridCaptionId,
808+
gridHelpId,
809+
activeId,
810+
isLive,
811+
isRTL,
812+
activeYMD,
813+
selectedYMD,
814+
safeId
815+
} = this
780816
const hideDecadeNav = !this.showDecadeNav
781817
const todayYMD = formatYMD(this.getToday())
782818
const highlightToday = !this.noHighlightToday
783-
// Pre-compute some IDs
784-
// This should be computed props
785-
const idValue = safeId()
786-
const idWidget = safeId('_calendar-wrapper_')
787-
const idNav = safeId('_calendar-nav_')
788-
const idGrid = safeId('_calendar-grid_')
789-
const idGridCaption = safeId('_calendar-grid-caption_')
790-
const idGridHelp = safeId('_calendar-grid-help_')
791-
const idActive = activeYMD ? safeId(`_cell-${activeYMD}_`) : null
792819

793820
// Header showing current selected date
794821
let $header = h(
@@ -797,8 +824,8 @@ export const BCalendar = Vue.extend({
797824
staticClass: 'form-control form-control-sm text-center',
798825
class: { 'text-muted': this.disabled, readonly: this.readonly || this.disabled },
799826
attrs: {
800-
id: idValue,
801-
for: idGrid,
827+
id: valueId,
828+
for: gridId,
802829
role: 'status',
803830
tabindex: this.disabled ? null : '-1',
804831
// Mainly for testing purposes, as we do not know
@@ -885,11 +912,11 @@ export const BCalendar = Vue.extend({
885912
{
886913
staticClass: 'b-calendar-nav d-flex',
887914
attrs: {
888-
id: idNav,
915+
id: navId,
889916
role: 'group',
890917
'aria-hidden': this.disabled ? 'true' : null,
891918
'aria-label': this.labelNav || null,
892-
'aria-controls': idGrid
919+
'aria-controls': gridId
893920
}
894921
},
895922
[
@@ -957,7 +984,7 @@ export const BCalendar = Vue.extend({
957984
staticClass: 'b-calendar-grid-caption text-center font-weight-bold',
958985
class: { 'text-muted': this.disabled },
959986
attrs: {
960-
id: idGridCaption,
987+
id: gridCaptionId,
961988
'aria-live': isLive ? 'polite' : null,
962989
'aria-atomic': isLive ? 'true' : null
963990
}
@@ -1077,7 +1104,7 @@ export const BCalendar = Vue.extend({
10771104
{
10781105
staticClass: 'b-calendar-grid-help border-top small text-muted text-center bg-light',
10791106
attrs: {
1080-
id: idGridHelp
1107+
id: gridHelpId
10811108
}
10821109
},
10831110
[h('div', { staticClass: 'small' }, this.labelHelp)]
@@ -1089,18 +1116,18 @@ export const BCalendar = Vue.extend({
10891116
ref: 'grid',
10901117
staticClass: 'b-calendar-grid form-control h-auto text-center',
10911118
attrs: {
1092-
id: idGrid,
1119+
id: gridId,
10931120
role: 'application',
10941121
tabindex: this.disabled ? null : '0',
10951122
'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing
10961123
'aria-roledescription': this.labelCalendar || null,
1097-
'aria-labelledby': idGridCaption,
1098-
'aria-describedby': idGridHelp,
1124+
'aria-labelledby': gridCaptionId,
1125+
'aria-describedby': gridHelpId,
10991126
// `aria-readonly` is not considered valid on `role="application"`
11001127
// https://www.w3.org/TR/wai-aria-1.1/#aria-readonly
11011128
// 'aria-readonly': this.readonly && !this.disabled ? 'true' : null,
11021129
'aria-disabled': this.disabled ? 'true' : null,
1103-
'aria-activedescendant': idActive
1130+
'aria-activedescendant': activeId
11041131
},
11051132
on: {
11061133
keydown: this.onKeydownGrid,
@@ -1121,7 +1148,7 @@ export const BCalendar = Vue.extend({
11211148
staticClass: 'b-calendar-inner',
11221149
style: this.block ? {} : { width: this.width },
11231150
attrs: {
1124-
id: idWidget,
1151+
id: widgetId,
11251152
dir: isRTL ? 'rtl' : 'ltr',
11261153
lang: this.computedLocale || null,
11271154
role: 'group',
@@ -1133,9 +1160,9 @@ export const BCalendar = Vue.extend({
11331160
'aria-describedby': [
11341161
// Should the attr (if present) go last?
11351162
// Or should this attr be a prop?
1136-
this.$attrs['aria-describedby'],
1137-
idValue,
1138-
idGridHelp
1163+
this.bvAttrs['aria-describedby'],
1164+
valueId,
1165+
gridHelpId
11391166
]
11401167
.filter(identity)
11411168
.join(' ')

src/components/dropdown/dropdown-item-button.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Vue from '../../utils/vue'
2+
import attrsMixin from '../../mixins/attrs'
23
import normalizeSlotMixin from '../../mixins/normalize-slot'
34

45
export const props = {
@@ -27,14 +28,24 @@ export const props = {
2728
// @vue/component
2829
export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({
2930
name: 'BDropdownItemButton',
30-
mixins: [normalizeSlotMixin],
31+
mixins: [attrsMixin, normalizeSlotMixin],
3132
inheritAttrs: false,
3233
inject: {
3334
bvDropdown: {
3435
default: null
3536
}
3637
},
3738
props,
39+
computed: {
40+
computedAttrs() {
41+
return {
42+
...this.bvAttrs,
43+
role: 'menuitem',
44+
type: 'button',
45+
disabled: this.disabled
46+
}
47+
}
48+
},
3849
methods: {
3950
closeDropdown() {
4051
if (this.bvDropdown) {
@@ -59,12 +70,7 @@ export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({
5970
[`text-${this.variant}`]: this.variant && !(this.active || this.disabled)
6071
}
6172
],
62-
attrs: {
63-
...this.$attrs,
64-
role: 'menuitem',
65-
type: 'button',
66-
disabled: this.disabled
67-
},
73+
attrs: this.computedAttrs,
6874
on: { click: this.onClick },
6975
ref: 'button'
7076
},

src/components/dropdown/dropdown-item.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Vue from '../../utils/vue'
22
import { requestAF } from '../../utils/dom'
3+
import attrsMixin from '../../mixins/attrs'
34
import normalizeSlotMixin from '../../mixins/normalize-slot'
45
import { BLink, propsFactory as linkPropsFactory } from '../link/link'
56

@@ -8,7 +9,7 @@ export const props = linkPropsFactory()
89
// @vue/component
910
export const BDropdownItem = /*#__PURE__*/ Vue.extend({
1011
name: 'BDropdownItem',
11-
mixins: [normalizeSlotMixin],
12+
mixins: [attrsMixin, normalizeSlotMixin],
1213
inheritAttrs: false,
1314
inject: {
1415
bvDropdown: {
@@ -26,6 +27,14 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({
2627
default: null
2728
}
2829
},
30+
computed: {
31+
computedAttrs() {
32+
return {
33+
...this.bvAttrs,
34+
role: 'menuitem'
35+
}
36+
}
37+
},
2938
methods: {
3039
closeDropdown() {
3140
// Close on next animation frame to allow <b-link> time to process
@@ -53,7 +62,7 @@ export const BDropdownItem = /*#__PURE__*/ Vue.extend({
5362
[`text-${this.variant}`]: this.variant && !(this.active || this.disabled)
5463
}
5564
],
56-
attrs: { ...this.$attrs, role: 'menuitem' },
65+
attrs: this.computedAttrs,
5766
on: { click: this.onClick },
5867
ref: 'item'
5968
},

src/components/form-file/form-file.js

+19-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isFile, isFunction, isUndefinedOrNull } from '../../utils/inspect'
66
import { File } from '../../utils/safe-types'
77
import { toString } from '../../utils/string'
88
import { warn } from '../../utils/warn'
9+
import attrsMixin from '../../mixins/attrs'
910
import formCustomMixin from '../../mixins/form-custom'
1011
import formMixin from '../../mixins/form'
1112
import formStateMixin from '../../mixins/form-state'
@@ -26,7 +27,7 @@ const isValidValue = value => isFile(value) || (isArray(value) && value.every(v
2627
// @vue/component
2728
export const BFormFile = /*#__PURE__*/ Vue.extend({
2829
name: NAME,
29-
mixins: [idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin],
30+
mixins: [attrsMixin, idMixin, formMixin, formStateMixin, formCustomMixin, normalizeSlotMixin],
3031
inheritAttrs: false,
3132
model: {
3233
prop: 'value',
@@ -127,6 +128,22 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({
127128
? toString(this.fileNameFormatter(files))
128129
: files.map(file => file.name).join(', ')
129130
}
131+
},
132+
computedAttrs() {
133+
return {
134+
...this.bvAttrs,
135+
type: 'file',
136+
id: this.safeId(),
137+
name: this.name,
138+
disabled: this.disabled,
139+
required: this.required,
140+
form: this.form || null,
141+
capture: this.capture || null,
142+
accept: this.accept || null,
143+
multiple: this.multiple,
144+
webkitdirectory: this.directory,
145+
'aria-required': this.required ? 'true' : null
146+
}
130147
}
131148
},
132149
watch: {
@@ -290,20 +307,7 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({
290307
},
291308
this.stateClass
292309
],
293-
attrs: {
294-
...this.$attrs,
295-
type: 'file',
296-
id: this.safeId(),
297-
name: this.name,
298-
disabled: this.disabled,
299-
required: this.required,
300-
form: this.form || null,
301-
capture: this.capture || null,
302-
accept: this.accept || null,
303-
multiple: this.multiple,
304-
webkitdirectory: this.directory,
305-
'aria-required': this.required ? 'true' : null
306-
},
310+
attrs: this.computedAttrs,
307311
on: {
308312
change: this.onFileChange,
309313
focusin: this.focusHandler,

0 commit comments

Comments
 (0)