mirror of
https://github.com/twbs/bootstrap.git
synced 2025-01-18 10:52:19 +01:00
Manipulator: Add JSON parse support (#35077)
Support parsing JSON from each component's main element using the `data-bs-config` attribute. The `bs-config` attribute will be reserved and omitted during `getDataAttributes` parsing. With this commit, every component, will create its config object, using: * defaults * data-bs-config * the rest of data attributes * configuration object given during instance initialization Co-authored-by: XhmikosR <xhmikosr@gmail.com> Co-authored-by: Mark Otto <markd.otto@gmail.com> Co-authored-by: Mark Otto <markdotto@gmail.com>
This commit is contained in:
parent
01cffa6822
commit
584600bda3
@ -19,6 +19,7 @@
|
|||||||
"btnradio",
|
"btnradio",
|
||||||
"callout",
|
"callout",
|
||||||
"callouts",
|
"callouts",
|
||||||
|
"camelCase",
|
||||||
"clearfix",
|
"clearfix",
|
||||||
"Codesniffer",
|
"Codesniffer",
|
||||||
"combinator",
|
"combinator",
|
||||||
|
@ -22,7 +22,15 @@ function normalizeData(value) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
if (typeof value !== 'string') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(decodeURIComponent(value))
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDataKey(key) {
|
function normalizeDataKey(key) {
|
||||||
@ -44,7 +52,7 @@ const Manipulator = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attributes = {}
|
const attributes = {}
|
||||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs'))
|
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))
|
||||||
|
|
||||||
for (const key of bsKeys) {
|
for (const key of bsKeys) {
|
||||||
let pureKey = key.replace(/^bs/, '')
|
let pureKey = key.replace(/^bs/, '')
|
||||||
|
@ -38,8 +38,11 @@ class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_mergeConfigObj(config, element) {
|
_mergeConfigObj(config, element) {
|
||||||
|
const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.constructor.Default,
|
...this.constructor.Default,
|
||||||
|
...(typeof jsonConfig === 'object' ? jsonConfig : {}),
|
||||||
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
|
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
|
||||||
...(typeof config === 'object' ? config : {})
|
...(typeof config === 'object' ? config : {})
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,17 @@ describe('Manipulator', () => {
|
|||||||
target: '#element'
|
target: '#element'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should omit `bs-config` data attribute', () => {
|
||||||
|
fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-bs-config=\'{"testBool":false}\'></div>'
|
||||||
|
|
||||||
|
const div = fixtureEl.querySelector('div')
|
||||||
|
|
||||||
|
expect(Manipulator.getDataAttributes(div)).toEqual({
|
||||||
|
toggle: 'tabs',
|
||||||
|
target: '#element'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getDataAttribute', () => {
|
describe('getDataAttribute', () => {
|
||||||
@ -104,5 +115,21 @@ describe('Manipulator', () => {
|
|||||||
div.setAttribute('data-bs-test', '1')
|
div.setAttribute('data-bs-test', '1')
|
||||||
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1)
|
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should normalize json data', () => {
|
||||||
|
fixtureEl.innerHTML = '<div data-bs-test=\'{"delay":{"show":100,"hide":10}}\'></div>'
|
||||||
|
|
||||||
|
const div = fixtureEl.querySelector('div')
|
||||||
|
|
||||||
|
expect(Manipulator.getDataAttribute(div, 'test')).toEqual({ delay: { show: 100, hide: 10 } })
|
||||||
|
|
||||||
|
const objectData = { 'Super Hero': ['Iron Man', 'Super Man'], testNum: 90, url: 'http://localhost:8080/test?foo=bar' }
|
||||||
|
const dataStr = JSON.stringify(objectData)
|
||||||
|
div.setAttribute('data-bs-test', encodeURIComponent(dataStr))
|
||||||
|
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
|
||||||
|
|
||||||
|
div.setAttribute('data-bs-test', dataStr)
|
||||||
|
expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -730,15 +730,12 @@ describe('Tooltip', () => {
|
|||||||
|
|
||||||
it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => {
|
it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
|
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-delay=\'{"show":0,"hide":150}\'>'
|
||||||
|
|
||||||
const tooltipEl = fixtureEl.querySelector('a')
|
const tooltipEl = fixtureEl.querySelector('a')
|
||||||
const tooltip = new Tooltip(tooltipEl, {
|
const tooltip = new Tooltip(tooltipEl)
|
||||||
delay: {
|
|
||||||
show: 0,
|
expect(tooltip._config.delay).toEqual({ show: 0, hide: 150 })
|
||||||
hide: 150
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(tooltip._getTipElement()).toHaveClass('show')
|
expect(tooltip._getTipElement()).toHaveClass('show')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Config from '../../../src/util/config'
|
import Config from '../../../src/util/config'
|
||||||
|
import { clearFixture, getFixture } from '../../helpers/fixture'
|
||||||
|
|
||||||
class DummyConfigClass extends Config {
|
class DummyConfigClass extends Config {
|
||||||
static get NAME() {
|
static get NAME() {
|
||||||
@ -7,7 +8,17 @@ class DummyConfigClass extends Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Config', () => {
|
describe('Config', () => {
|
||||||
|
let fixtureEl
|
||||||
const name = 'dummy'
|
const name = 'dummy'
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
})
|
||||||
|
|
||||||
describe('NAME', () => {
|
describe('NAME', () => {
|
||||||
it('should return plugin NAME', () => {
|
it('should return plugin NAME', () => {
|
||||||
expect(DummyConfigClass.NAME).toEqual(name)
|
expect(DummyConfigClass.NAME).toEqual(name)
|
||||||
@ -26,6 +37,83 @@ describe('Config', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('mergeConfigObj', () => {
|
||||||
|
it('should parse element\'s data attributes and merge it with default config. Element\'s data attributes must excel Defaults', () => {
|
||||||
|
fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string1="bar"></div>'
|
||||||
|
|
||||||
|
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
|
||||||
|
testBool: true,
|
||||||
|
testString: 'foo',
|
||||||
|
testString1: 'foo',
|
||||||
|
testInt: 7
|
||||||
|
})
|
||||||
|
const instance = new DummyConfigClass()
|
||||||
|
const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test'))
|
||||||
|
|
||||||
|
expect(configResult.testBool).toEqual(false)
|
||||||
|
expect(configResult.testString).toEqual('foo')
|
||||||
|
expect(configResult.testString1).toEqual('bar')
|
||||||
|
expect(configResult.testInt).toEqual(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse element\'s data attributes and merge it with default config, plug these given during method call. The programmatically given should excel all', () => {
|
||||||
|
fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string-1="bar"></div>'
|
||||||
|
|
||||||
|
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
|
||||||
|
testBool: true,
|
||||||
|
testString: 'foo',
|
||||||
|
testString1: 'foo',
|
||||||
|
testInt: 7
|
||||||
|
})
|
||||||
|
const instance = new DummyConfigClass()
|
||||||
|
const configResult = instance._mergeConfigObj({
|
||||||
|
testString1: 'test',
|
||||||
|
testInt: 3
|
||||||
|
}, fixtureEl.querySelector('#test'))
|
||||||
|
|
||||||
|
expect(configResult.testBool).toEqual(false)
|
||||||
|
expect(configResult.testString).toEqual('foo')
|
||||||
|
expect(configResult.testString1).toEqual('test')
|
||||||
|
expect(configResult.testInt).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse element\'s data attribute `config` and any rest attributes. The programmatically given should excel all. Data attribute `config` should excel only Defaults', () => {
|
||||||
|
fixtureEl.innerHTML = '<div id="test" data-bs-config=\'{"testBool":false,"testInt":50,"testInt2":100}\' data-bs-test-int="8" data-bs-test-string-1="bar"></div>'
|
||||||
|
|
||||||
|
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
|
||||||
|
testBool: true,
|
||||||
|
testString: 'foo',
|
||||||
|
testString1: 'foo',
|
||||||
|
testInt: 7,
|
||||||
|
testInt2: 600
|
||||||
|
})
|
||||||
|
const instance = new DummyConfigClass()
|
||||||
|
const configResult = instance._mergeConfigObj({
|
||||||
|
testString1: 'test'
|
||||||
|
}, fixtureEl.querySelector('#test'))
|
||||||
|
|
||||||
|
expect(configResult.testBool).toEqual(false)
|
||||||
|
expect(configResult.testString).toEqual('foo')
|
||||||
|
expect(configResult.testString1).toEqual('test')
|
||||||
|
expect(configResult.testInt).toEqual(8)
|
||||||
|
expect(configResult.testInt2).toEqual(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should omit element\'s data attribute `config` if is not an object', () => {
|
||||||
|
fixtureEl.innerHTML = '<div id="test" data-bs-config="foo" data-bs-test-int="8"></div>'
|
||||||
|
|
||||||
|
spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({
|
||||||
|
testInt: 7,
|
||||||
|
testInt2: 79
|
||||||
|
})
|
||||||
|
const instance = new DummyConfigClass()
|
||||||
|
const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test'))
|
||||||
|
|
||||||
|
expect(configResult.testInt).toEqual(8)
|
||||||
|
expect(configResult.testInt2).toEqual(79)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('typeCheckConfig', () => {
|
describe('typeCheckConfig', () => {
|
||||||
it('should check type of the config object', () => {
|
it('should check type of the config object', () => {
|
||||||
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
|
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
|
||||||
|
@ -308,7 +308,9 @@ var carousel = new bootstrap.Carousel(myCarousel)
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-interval=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table >}}
|
{{< bs-table >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -141,7 +141,9 @@ var collapseList = collapseElementList.map(function (collapseEl) {
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-parent=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -1064,7 +1064,9 @@ Regardless of whether you call your dropdown via JavaScript or instead use the d
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-offset=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, instead of using `data-bs-autoClose="false"`, use `data-bs-auto-close="false"`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -820,7 +820,9 @@ var myModal = new bootstrap.Modal(document.getElementById('myModal'), options)
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-backdrop=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -279,7 +279,9 @@ var offcanvasList = offcanvasElementList.map(function (offcanvasEl) {
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-backdrop=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -166,7 +166,9 @@ Additionally, while it is possible to also include interactive controls (such as
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-animation=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, instead of using `data-bs-customClass="beautifier"`, use `data-bs-custom-class="beautifier"`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< callout warning >}}
|
{{< callout warning >}}
|
||||||
Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
|
Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
|
||||||
|
@ -338,7 +338,9 @@ Target elements that are not visible will be ignored and their corresponding nav
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-root-margin=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -355,7 +355,9 @@ var toastList = toastElList.map(function (toastEl) {
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-animation=""`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< bs-table "table" >}}
|
{{< bs-table "table" >}}
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -193,7 +193,9 @@ Elements with the `disabled` attribute aren't interactive, meaning users cannot
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-animation=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, instead of using `data-bs-customClass="beautifier"`, use `data-bs-custom-class="beautifier"`.
|
{{< markdown >}}
|
||||||
|
{{< partial "js-data-attributes.md" >}}
|
||||||
|
{{< /markdown >}}
|
||||||
|
|
||||||
{{< callout warning >}}
|
{{< callout warning >}}
|
||||||
Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
|
Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
|
||||||
|
3
site/layouts/partials/js-data-attributes.md
Normal file
3
site/layouts/partials/js-data-attributes.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-animation=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, use `data-bs-custom-class="beautifier"` instead of `data-bs-customClass="beautifier"`.
|
||||||
|
|
||||||
|
As of Bootstrap 5.2.0, all components support an **experimental** reserved data attribute `data-bs-config` that can house simple component configuration as a JSON string. When an element has `data-bs-config='{"delay":0, "title":123}'` and `data-bs-title="456"` attributes, the final `title` value will be `456` and the separate data attributes will override values given on `data-bs-config`. In addition, existing data attributes are able to house JSON values like `data-bs-delay='{"show":0,"hide":150}'`.
|
Loading…
x
Reference in New Issue
Block a user