refactor: use a Map instead of an Object in dom/data (#32180)

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
Co-authored-by: Rohit Sharma <rohit2sharma95@gmail.com>
This commit is contained in:
alpadev 2021-03-02 15:55:44 +01:00 committed by GitHub
parent 6d93a1371a
commit 48a95f7280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 131 additions and 166 deletions

View File

@ -38,7 +38,7 @@
}, },
{ {
"path": "./dist/js/bootstrap.bundle.min.js", "path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "21.75 kB" "maxSize": "22 kB"
}, },
{ {
"path": "./dist/js/bootstrap.esm.js", "path": "./dist/js/bootstrap.esm.js",

View File

@ -98,7 +98,7 @@ class Alert extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
if (!data) { if (!data) {
data = new Alert(this) data = new Alert(this)

View File

@ -24,18 +24,18 @@ class BaseComponent {
} }
this._element = element this._element = element
Data.setData(this._element, this.constructor.DATA_KEY, this) Data.set(this._element, this.constructor.DATA_KEY, this)
} }
dispose() { dispose() {
Data.removeData(this._element, this.constructor.DATA_KEY) Data.remove(this._element, this.constructor.DATA_KEY)
this._element = null this._element = null
} }
/** Static */ /** Static */
static getInstance(element) { static getInstance(element) {
return Data.getData(element, this.DATA_KEY) return Data.get(element, this.DATA_KEY)
} }
static get VERSION() { static get VERSION() {

View File

@ -51,7 +51,7 @@ class Button extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
if (!data) { if (!data) {
data = new Button(this) data = new Button(this)
@ -75,7 +75,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
const button = event.target.closest(SELECTOR_DATA_TOGGLE) const button = event.target.closest(SELECTOR_DATA_TOGGLE)
let data = Data.getData(button, DATA_KEY) let data = Data.get(button, DATA_KEY)
if (!data) { if (!data) {
data = new Button(button) data = new Button(button)
} }

View File

@ -527,7 +527,7 @@ class Carousel extends BaseComponent {
// Static // Static
static carouselInterface(element, config) { static carouselInterface(element, config) {
let data = Data.getData(element, DATA_KEY) let data = Data.get(element, DATA_KEY)
let _config = { let _config = {
...Default, ...Default,
...Manipulator.getDataAttributes(element) ...Manipulator.getDataAttributes(element)
@ -586,7 +586,7 @@ class Carousel extends BaseComponent {
Carousel.carouselInterface(target, config) Carousel.carouselInterface(target, config)
if (slideIndex) { if (slideIndex) {
Data.getData(target, DATA_KEY).to(slideIndex) Data.get(target, DATA_KEY).to(slideIndex)
} }
event.preventDefault() event.preventDefault()
@ -605,7 +605,7 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE) const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (let i = 0, len = carousels.length; i < len; i++) { for (let i = 0, len = carousels.length; i < len; i++) {
Carousel.carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY)) Carousel.carouselInterface(carousels[i], Data.get(carousels[i], DATA_KEY))
} }
}) })

View File

@ -147,7 +147,7 @@ class Collapse extends BaseComponent {
const container = SelectorEngine.findOne(this._selector) const container = SelectorEngine.findOne(this._selector)
if (actives) { if (actives) {
const tempActiveData = actives.find(elem => container !== elem) const tempActiveData = actives.find(elem => container !== elem)
activesData = tempActiveData ? Data.getData(tempActiveData, DATA_KEY) : null activesData = tempActiveData ? Data.get(tempActiveData, DATA_KEY) : null
if (activesData && activesData._isTransitioning) { if (activesData && activesData._isTransitioning) {
return return
@ -166,7 +166,7 @@ class Collapse extends BaseComponent {
} }
if (!activesData) { if (!activesData) {
Data.setData(elemActive, DATA_KEY, null) Data.set(elemActive, DATA_KEY, null)
} }
}) })
} }
@ -332,7 +332,7 @@ class Collapse extends BaseComponent {
// Static // Static
static collapseInterface(element, config) { static collapseInterface(element, config) {
let data = Data.getData(element, DATA_KEY) let data = Data.get(element, DATA_KEY)
const _config = { const _config = {
...Default, ...Default,
...Manipulator.getDataAttributes(element), ...Manipulator.getDataAttributes(element),
@ -380,7 +380,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
const selectorElements = SelectorEngine.find(selector) const selectorElements = SelectorEngine.find(selector)
selectorElements.forEach(element => { selectorElements.forEach(element => {
const data = Data.getData(element, DATA_KEY) const data = Data.get(element, DATA_KEY)
let config let config
if (data) { if (data) {
// update parent attribute // update parent attribute

View File

@ -11,57 +11,47 @@
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
*/ */
const mapData = (() => { const elementMap = new Map()
const storeData = {}
let id = 1 export default {
return { set(element, key, instance) {
set(element, key, data) { if (!elementMap.has(element)) {
if (typeof element.bsKey === 'undefined') { elementMap.set(element, new Map())
element.bsKey = {
key,
id
}
id++
} }
storeData[element.bsKey.id] = data const instanceMap = elementMap.get(element)
},
get(element, key) {
if (!element || typeof element.bsKey === 'undefined') {
return null
}
const keyProperties = element.bsKey // make it clear we only want one instance per element
if (keyProperties.key === key) { // can be removed later when multiple key/instances are fine to be used
return storeData[keyProperties.id] if (!instanceMap.has(key) && instanceMap.size !== 0) {
} // eslint-disable-next-line no-console
console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)
return null
},
delete(element, key) {
if (typeof element.bsKey === 'undefined') {
return return
} }
const keyProperties = element.bsKey instanceMap.set(key, instance)
if (keyProperties.key === key) { },
delete storeData[keyProperties.id]
delete element.bsKey
}
}
}
})()
const Data = { get(element, key) {
setData(instance, key, data) { if (elementMap.has(element)) {
mapData.set(instance, key, data) return elementMap.get(element).get(key) || null
},
getData(instance, key) {
return mapData.get(instance, key)
},
removeData(instance, key) {
mapData.delete(instance, key)
}
} }
export default Data return null
},
remove(element, key) {
if (!elementMap.has(element)) {
return
}
const instanceMap = elementMap.get(element)
instanceMap.delete(key)
// free up element references if there are no instances left for an element
if (instanceMap.size === 0) {
elementMap.delete(element)
}
}
}

View File

@ -357,7 +357,7 @@ class Dropdown extends BaseComponent {
// Static // Static
static dropdownInterface(element, config) { static dropdownInterface(element, config) {
let data = Data.getData(element, DATA_KEY) let data = Data.get(element, DATA_KEY)
const _config = typeof config === 'object' ? config : null const _config = typeof config === 'object' ? config : null
if (!data) { if (!data) {
@ -387,7 +387,7 @@ class Dropdown extends BaseComponent {
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE) const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = toggles.length; i < len; i++) { for (let i = 0, len = toggles.length; i < len; i++) {
const context = Data.getData(toggles[i], DATA_KEY) const context = Data.get(toggles[i], DATA_KEY)
const relatedTarget = { const relatedTarget = {
relatedTarget: toggles[i] relatedTarget: toggles[i]
} }

View File

@ -508,7 +508,7 @@ class Modal extends BaseComponent {
static jQueryInterface(config, relatedTarget) { static jQueryInterface(config, relatedTarget) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
const _config = { const _config = {
...Default, ...Default,
...Manipulator.getDataAttributes(this), ...Manipulator.getDataAttributes(this),
@ -556,7 +556,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
}) })
}) })
let data = Data.getData(target, DATA_KEY) let data = Data.get(target, DATA_KEY)
if (!data) { if (!data) {
const config = { const config = {
...Manipulator.getDataAttributes(target), ...Manipulator.getDataAttributes(target),

View File

@ -136,7 +136,7 @@ class Popover extends Tooltip {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' ? config : null const _config = typeof config === 'object' ? config : null
if (!data && /dispose|hide/.test(config)) { if (!data && /dispose|hide/.test(config)) {
@ -145,7 +145,7 @@ class Popover extends Tooltip {
if (!data) { if (!data) {
data = new Popover(this, _config) data = new Popover(this, _config)
Data.setData(this, DATA_KEY, data) Data.set(this, DATA_KEY, data)
} }
if (typeof config === 'string') { if (typeof config === 'string') {

View File

@ -278,7 +278,7 @@ class ScrollSpy extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && config const _config = typeof config === 'object' && config
if (!data) { if (!data) {

View File

@ -182,7 +182,7 @@ class Tab extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
const data = Data.getData(this, DATA_KEY) || new Tab(this) const data = Data.get(this, DATA_KEY) || new Tab(this)
if (typeof config === 'string') { if (typeof config === 'string') {
if (typeof data[config] === 'undefined') { if (typeof data[config] === 'undefined') {
@ -204,7 +204,7 @@ class Tab extends BaseComponent {
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault() event.preventDefault()
const data = Data.getData(this, DATA_KEY) || new Tab(this) const data = Data.get(this, DATA_KEY) || new Tab(this)
data.show() data.show()
}) })

View File

@ -189,7 +189,7 @@ class Toast extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && config const _config = typeof config === 'object' && config
if (!data) { if (!data) {

View File

@ -275,7 +275,7 @@ class Tooltip extends BaseComponent {
this._addAttachmentClass(attachment) this._addAttachmentClass(attachment)
const container = this._getContainer() const container = this._getContainer()
Data.setData(tip, this.constructor.DATA_KEY, this) Data.set(tip, this.constructor.DATA_KEY, this)
if (!this._element.ownerDocument.documentElement.contains(this.tip)) { if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
container.appendChild(tip) container.appendChild(tip)
@ -465,11 +465,11 @@ class Tooltip extends BaseComponent {
_initializeOnDelegatedTarget(event, context) { _initializeOnDelegatedTarget(event, context) {
const dataKey = this.constructor.DATA_KEY const dataKey = this.constructor.DATA_KEY
context = context || Data.getData(event.delegateTarget, dataKey) context = context || Data.get(event.delegateTarget, dataKey)
if (!context) { if (!context) {
context = new this.constructor(event.delegateTarget, this._getDelegateConfig()) context = new this.constructor(event.delegateTarget, this._getDelegateConfig())
Data.setData(event.delegateTarget, dataKey, context) Data.set(event.delegateTarget, dataKey, context)
} }
return context return context
@ -761,7 +761,7 @@ class Tooltip extends BaseComponent {
static jQueryInterface(config) { static jQueryInterface(config) {
return this.each(function () { return this.each(function () {
let data = Data.getData(this, DATA_KEY) let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && config const _config = typeof config === 'object' && config
if (!data && /dispose|hide/.test(config)) { if (!data && /dispose|hide/.test(config)) {

View File

@ -4,128 +4,103 @@ import Data from '../../../src/dom/data'
import { getFixture, clearFixture } from '../../helpers/fixture' import { getFixture, clearFixture } from '../../helpers/fixture'
describe('Data', () => { describe('Data', () => {
const TEST_KEY = 'bs.test'
const UNKNOWN_KEY = 'bs.unknown'
const TEST_DATA = {
test: 'bsData'
}
let fixtureEl let fixtureEl
let div
beforeAll(() => { beforeAll(() => {
fixtureEl = getFixture() fixtureEl = getFixture()
}) })
beforeEach(() => {
fixtureEl.innerHTML = '<div></div>'
div = fixtureEl.querySelector('div')
})
afterEach(() => { afterEach(() => {
Data.remove(div, TEST_KEY)
clearFixture() clearFixture()
}) })
describe('setData', () => { it('should return null for unknown elements', () => {
it('should set data in an element by adding a bsKey attribute', () => { const data = { ...TEST_DATA }
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div') Data.set(div, TEST_KEY, data)
const data = {
test: 'bsData'
}
Data.setData(div, 'test', data) expect(Data.get(null)).toBeNull()
expect(div.bsKey).toBeDefined() expect(Data.get(undefined)).toBeNull()
expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
}) })
it('should change data if something is already stored', () => { it('should return null for unknown keys', () => {
fixtureEl.innerHTML = '<div></div>' const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div') Data.set(div, TEST_KEY, data)
const data = {
test: 'bsData'
}
Data.setData(div, 'test', data) expect(Data.get(div, null)).toBeNull()
expect(Data.get(div, undefined)).toBeNull()
data.test = 'bsData2' expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
Data.setData(div, 'test', data)
expect(div.bsKey).toBeDefined()
})
}) })
describe('getData', () => { it('should store data for an element with a given key and return it', () => {
it('should return stored data', () => { const data = { ...TEST_DATA }
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div') Data.set(div, TEST_KEY, data)
const data = {
test: 'bsData'
}
Data.setData(div, 'test', data) expect(Data.get(div, TEST_KEY)).toBe(data)
expect(Data.getData(div, 'test')).toEqual(data)
}) })
it('should return null on undefined element', () => { it('should overwrite data if something is already stored', () => {
expect(Data.getData(null)).toEqual(null) const data = { ...TEST_DATA }
expect(Data.getData(undefined)).toEqual(null) const copy = { ...data }
Data.set(div, TEST_KEY, data)
Data.set(div, TEST_KEY, copy)
expect(Data.get(div, TEST_KEY)).not.toBe(data)
expect(Data.get(div, TEST_KEY)).toBe(copy)
}) })
it('should return null when an element have nothing stored', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Data.getData(div, 'test')).toEqual(null)
})
it('should return null when an element have nothing stored with the provided key', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
Data.setData(div, 'test', data)
expect(Data.getData(div, 'test2')).toEqual(null)
})
})
describe('removeData', () => {
it('should do nothing when an element have nothing stored', () => { it('should do nothing when an element have nothing stored', () => {
fixtureEl.innerHTML = '<div></div>' Data.remove(div, TEST_KEY)
const div = fixtureEl.querySelector('div')
Data.removeData(div, 'test')
expect().nothing() expect().nothing()
}) })
it('should should do nothing if it\'s not a valid key provided', () => { it('should remove nothing for an unknown key', () => {
fixtureEl.innerHTML = '<div></div>' const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div') Data.set(div, TEST_KEY, data)
const data = { Data.remove(div, UNKNOWN_KEY)
test: 'bsData'
}
Data.setData(div, 'test', data) expect(Data.get(div, TEST_KEY)).toBe(data)
expect(div.bsKey).toBeDefined()
Data.removeData(div, 'test2')
expect(div.bsKey).toBeDefined()
}) })
it('should remove data if something is stored', () => { it('should remove data for a given key', () => {
fixtureEl.innerHTML = '<div></div>' const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div') Data.set(div, TEST_KEY, data)
const data = { Data.remove(div, TEST_KEY)
test: 'bsData'
}
Data.setData(div, 'test', data) expect(Data.get(div, TEST_KEY)).toBeNull()
})
expect(div.bsKey).toBeDefined() it('should console.error a message if called with multiple keys', () => {
/* eslint-disable no-console */
console.error = jasmine.createSpy('console.error')
Data.removeData(div, 'test') const data = { ...TEST_DATA }
const copy = { ...data }
expect(div.bsKey).toBeUndefined() Data.set(div, TEST_KEY, data)
}) Data.set(div, UNKNOWN_KEY, copy)
expect(console.error).toHaveBeenCalled()
expect(Data.get(div, UNKNOWN_KEY)).toBe(null)
}) })
}) })