0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-02-19 16:54:24 +01:00

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",
"maxSize": "21.75 kB"
"maxSize": "22 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class Button extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
let data = Data.getData(this, DATA_KEY)
let data = Data.get(this, DATA_KEY)
if (!data) {
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)
let data = Data.getData(button, DATA_KEY)
let data = Data.get(button, DATA_KEY)
if (!data) {
data = new Button(button)
}

View File

@ -527,7 +527,7 @@ class Carousel extends BaseComponent {
// Static
static carouselInterface(element, config) {
let data = Data.getData(element, DATA_KEY)
let data = Data.get(element, DATA_KEY)
let _config = {
...Default,
...Manipulator.getDataAttributes(element)
@ -586,7 +586,7 @@ class Carousel extends BaseComponent {
Carousel.carouselInterface(target, config)
if (slideIndex) {
Data.getData(target, DATA_KEY).to(slideIndex)
Data.get(target, DATA_KEY).to(slideIndex)
}
event.preventDefault()
@ -605,7 +605,7 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
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)
if (actives) {
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) {
return
@ -166,7 +166,7 @@ class Collapse extends BaseComponent {
}
if (!activesData) {
Data.setData(elemActive, DATA_KEY, null)
Data.set(elemActive, DATA_KEY, null)
}
})
}
@ -332,7 +332,7 @@ class Collapse extends BaseComponent {
// Static
static collapseInterface(element, config) {
let data = Data.getData(element, DATA_KEY)
let data = Data.get(element, DATA_KEY)
const _config = {
...Default,
...Manipulator.getDataAttributes(element),
@ -380,7 +380,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
const selectorElements = SelectorEngine.find(selector)
selectorElements.forEach(element => {
const data = Data.getData(element, DATA_KEY)
const data = Data.get(element, DATA_KEY)
let config
if (data) {
// update parent attribute

View File

@ -11,57 +11,47 @@
* ------------------------------------------------------------------------
*/
const mapData = (() => {
const storeData = {}
let id = 1
return {
set(element, key, data) {
if (typeof element.bsKey === 'undefined') {
element.bsKey = {
key,
id
}
id++
}
const elementMap = new Map()
storeData[element.bsKey.id] = data
},
get(element, key) {
if (!element || typeof element.bsKey === 'undefined') {
return null
}
export default {
set(element, key, instance) {
if (!elementMap.has(element)) {
elementMap.set(element, new Map())
}
const keyProperties = element.bsKey
if (keyProperties.key === key) {
return storeData[keyProperties.id]
}
const instanceMap = elementMap.get(element)
return null
},
delete(element, key) {
if (typeof element.bsKey === 'undefined') {
return
}
// make it clear we only want one instance per element
// can be removed later when multiple key/instances are fine to be used
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
}
const keyProperties = element.bsKey
if (keyProperties.key === key) {
delete storeData[keyProperties.id]
delete element.bsKey
}
instanceMap.set(key, instance)
},
get(element, key) {
if (elementMap.has(element)) {
return elementMap.get(element).get(key) || null
}
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)
}
}
})()
const Data = {
setData(instance, key, data) {
mapData.set(instance, key, data)
},
getData(instance, key) {
return mapData.get(instance, key)
},
removeData(instance, key) {
mapData.delete(instance, key)
}
}
export default Data

View File

@ -357,7 +357,7 @@ class Dropdown extends BaseComponent {
// Static
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
if (!data) {
@ -387,7 +387,7 @@ class Dropdown extends BaseComponent {
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
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 = {
relatedTarget: toggles[i]
}

View File

@ -508,7 +508,7 @@ class Modal extends BaseComponent {
static jQueryInterface(config, relatedTarget) {
return this.each(function () {
let data = Data.getData(this, DATA_KEY)
let data = Data.get(this, DATA_KEY)
const _config = {
...Default,
...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) {
const config = {
...Manipulator.getDataAttributes(target),

View File

@ -136,7 +136,7 @@ class Popover extends Tooltip {
static jQueryInterface(config) {
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
if (!data && /dispose|hide/.test(config)) {
@ -145,7 +145,7 @@ class Popover extends Tooltip {
if (!data) {
data = new Popover(this, _config)
Data.setData(this, DATA_KEY, data)
Data.set(this, DATA_KEY, data)
}
if (typeof config === 'string') {

View File

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

View File

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

View File

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

View File

@ -275,7 +275,7 @@ class Tooltip extends BaseComponent {
this._addAttachmentClass(attachment)
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)) {
container.appendChild(tip)
@ -465,11 +465,11 @@ class Tooltip extends BaseComponent {
_initializeOnDelegatedTarget(event, context) {
const dataKey = this.constructor.DATA_KEY
context = context || Data.getData(event.delegateTarget, dataKey)
context = context || Data.get(event.delegateTarget, dataKey)
if (!context) {
context = new this.constructor(event.delegateTarget, this._getDelegateConfig())
Data.setData(event.delegateTarget, dataKey, context)
Data.set(event.delegateTarget, dataKey, context)
}
return context
@ -761,7 +761,7 @@ class Tooltip extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
let data = Data.getData(this, DATA_KEY)
let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && 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'
describe('Data', () => {
const TEST_KEY = 'bs.test'
const UNKNOWN_KEY = 'bs.unknown'
const TEST_DATA = {
test: 'bsData'
}
let fixtureEl
let div
beforeAll(() => {
fixtureEl = getFixture()
})
beforeEach(() => {
fixtureEl.innerHTML = '<div></div>'
div = fixtureEl.querySelector('div')
})
afterEach(() => {
Data.remove(div, TEST_KEY)
clearFixture()
})
describe('setData', () => {
it('should set data in an element by adding a bsKey attribute', () => {
fixtureEl.innerHTML = '<div></div>'
it('should return null for unknown elements', () => {
const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
Data.set(div, TEST_KEY, data)
Data.setData(div, 'test', data)
expect(div.bsKey).toBeDefined()
})
it('should change data if something is already stored', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
Data.setData(div, 'test', data)
data.test = 'bsData2'
Data.setData(div, 'test', data)
expect(div.bsKey).toBeDefined()
})
expect(Data.get(null)).toBeNull()
expect(Data.get(undefined)).toBeNull()
expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
})
describe('getData', () => {
it('should return stored data', () => {
fixtureEl.innerHTML = '<div></div>'
it('should return null for unknown keys', () => {
const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
Data.set(div, TEST_KEY, data)
Data.setData(div, 'test', data)
expect(Data.getData(div, 'test')).toEqual(data)
})
it('should return null on undefined element', () => {
expect(Data.getData(null)).toEqual(null)
expect(Data.getData(undefined)).toEqual(null)
})
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)
})
expect(Data.get(div, null)).toBeNull()
expect(Data.get(div, undefined)).toBeNull()
expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
})
describe('removeData', () => {
it('should do nothing when an element have nothing stored', () => {
fixtureEl.innerHTML = '<div></div>'
it('should store data for an element with a given key and return it', () => {
const data = { ...TEST_DATA }
const div = fixtureEl.querySelector('div')
Data.set(div, TEST_KEY, data)
Data.removeData(div, 'test')
expect().nothing()
})
expect(Data.get(div, TEST_KEY)).toBe(data)
})
it('should should do nothing if it\'s not a valid key provided', () => {
fixtureEl.innerHTML = '<div></div>'
it('should overwrite data if something is already stored', () => {
const data = { ...TEST_DATA }
const copy = { ...data }
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
Data.set(div, TEST_KEY, data)
Data.set(div, TEST_KEY, copy)
Data.setData(div, 'test', data)
expect(Data.get(div, TEST_KEY)).not.toBe(data)
expect(Data.get(div, TEST_KEY)).toBe(copy)
})
expect(div.bsKey).toBeDefined()
it('should do nothing when an element have nothing stored', () => {
Data.remove(div, TEST_KEY)
Data.removeData(div, 'test2')
expect().nothing()
})
expect(div.bsKey).toBeDefined()
})
it('should remove nothing for an unknown key', () => {
const data = { ...TEST_DATA }
it('should remove data if something is stored', () => {
fixtureEl.innerHTML = '<div></div>'
Data.set(div, TEST_KEY, data)
Data.remove(div, UNKNOWN_KEY)
const div = fixtureEl.querySelector('div')
const data = {
test: 'bsData'
}
expect(Data.get(div, TEST_KEY)).toBe(data)
})
Data.setData(div, 'test', data)
it('should remove data for a given key', () => {
const data = { ...TEST_DATA }
expect(div.bsKey).toBeDefined()
Data.set(div, TEST_KEY, data)
Data.remove(div, TEST_KEY)
Data.removeData(div, 'test')
expect(Data.get(div, TEST_KEY)).toBeNull()
})
expect(div.bsKey).toBeUndefined()
})
it('should console.error a message if called with multiple keys', () => {
/* eslint-disable no-console */
console.error = jasmine.createSpy('console.error')
const data = { ...TEST_DATA }
const copy = { ...data }
Data.set(div, TEST_KEY, data)
Data.set(div, UNKNOWN_KEY, copy)
expect(console.error).toHaveBeenCalled()
expect(Data.get(div, UNKNOWN_KEY)).toBe(null)
})
})