]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Manipulator: Add JSON parse support (#35077)
authorGeoSot <geo.sotis@gmail.com>
Thu, 21 Apr 2022 18:41:43 +0000 (21:41 +0300)
committerGitHub <noreply@github.com>
Thu, 21 Apr 2022 18:41:43 +0000 (21:41 +0300)
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>
16 files changed:
.cspell.json
js/src/dom/manipulator.js
js/src/util/config.js
js/tests/unit/dom/manipulator.spec.js
js/tests/unit/tooltip.spec.js
js/tests/unit/util/config.spec.js
site/content/docs/5.1/components/carousel.md
site/content/docs/5.1/components/collapse.md
site/content/docs/5.1/components/dropdowns.md
site/content/docs/5.1/components/modal.md
site/content/docs/5.1/components/offcanvas.md
site/content/docs/5.1/components/popovers.md
site/content/docs/5.1/components/scrollspy.md
site/content/docs/5.1/components/toasts.md
site/content/docs/5.1/components/tooltips.md
site/layouts/partials/js-data-attributes.md [new file with mode: 0644]

index 3d30b10716595da01aea8f17ad26f13a12a0d1af..3995ec1222f97cc60ff411e132f6725d25f8bba8 100644 (file)
@@ -19,6 +19,7 @@
     "btnradio",
     "callout",
     "callouts",
+    "camelCase",
     "clearfix",
     "Codesniffer",
     "combinator",
index 5e6ad92ae76649d01f2cc17f0e30515388a64fe9..2d96d65fc87e61cfe6d9b291e563160d0e083dc0 100644 (file)
@@ -22,7 +22,15 @@ function normalizeData(value) {
     return null
   }
 
-  return value
+  if (typeof value !== 'string') {
+    return value
+  }
+
+  try {
+    return JSON.parse(decodeURIComponent(value))
+  } catch {
+    return value
+  }
 }
 
 function normalizeDataKey(key) {
@@ -44,7 +52,7 @@ const Manipulator = {
     }
 
     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) {
       let pureKey = key.replace(/^bs/, '')
index 19d02955dde4d0a71ae648f17ec9b6c41f707bbe..f6c194276bf340db5eef653142ea6f6e8a223a2a 100644 (file)
@@ -38,8 +38,11 @@ class Config {
   }
 
   _mergeConfigObj(config, element) {
+    const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse
+
     return {
       ...this.constructor.Default,
+      ...(typeof jsonConfig === 'object' ? jsonConfig : {}),
       ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
       ...(typeof config === 'object' ? config : {})
     }
index 2ed1995b60f7a9c8b878c2c0f8bd26bd92dc2479..4561e2e46c5541a98702557a1851351ced419ff2 100644 (file)
@@ -70,6 +70,17 @@ describe('Manipulator', () => {
         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', () => {
@@ -104,5 +115,21 @@ describe('Manipulator', () => {
       div.setAttribute('data-bs-test', '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)
+    })
   })
 })
index 5267305a544cddfed74ee932cf666274e9e58841..ff44d4182adaa075b55add196afe512d19c88032 100644 (file)
@@ -730,15 +730,12 @@ describe('Tooltip', () => {
 
     it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => {
       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 tooltip = new Tooltip(tooltipEl, {
-          delay: {
-            show: 0,
-            hide: 150
-          }
-        })
+        const tooltip = new Tooltip(tooltipEl)
+
+        expect(tooltip._config.delay).toEqual({ show: 0, hide: 150 })
 
         setTimeout(() => {
           expect(tooltip._getTipElement()).toHaveClass('show')
index a8f8962ee39dab9ac89b57e7642a22d5022b73ff..e1693c0c1f255bc7875782c6832f8d84e28425e5 100644 (file)
@@ -1,4 +1,5 @@
 import Config from '../../../src/util/config'
+import { clearFixture, getFixture } from '../../helpers/fixture'
 
 class DummyConfigClass extends Config {
   static get NAME() {
@@ -7,7 +8,17 @@ class DummyConfigClass extends Config {
 }
 
 describe('Config', () => {
+  let fixtureEl
   const name = 'dummy'
+
+  beforeAll(() => {
+    fixtureEl = getFixture()
+  })
+
+  afterEach(() => {
+    clearFixture()
+  })
+
   describe('NAME', () => {
     it('should return plugin 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', () => {
     it('should check type of the config object', () => {
       spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
index 86de96a074e327893edacbf957b6991bde45795e..14f91911d28cccd5659bebf97bd01851ae76e377 100644 (file)
@@ -308,7 +308,9 @@ var carousel = new bootstrap.Carousel(myCarousel)
 
 ### 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 >}}
 | Name | Type | Default | Description |
index 4fb6f5e9443f13ebbb0f98a955fe27ea9c30b42d..60b16826ce539d96509c762e670eda88ed52bedd 100644 (file)
@@ -141,7 +141,9 @@ var collapseList = collapseElementList.map(function (collapseEl) {
 
 ### 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" >}}
 | Name | Type | Default | Description |
index 7751a9bade2464180208650e3c878437596bff6b..86b8491ca7d52fdee23081385d23db89ea8fa951 100644 (file)
@@ -1064,7 +1064,9 @@ Regardless of whether you call your dropdown via JavaScript or instead use the d
 
 ### 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" >}}
 | Name | Type | Default | Description |
index d9bd120d46755a9b57f0d5e37aec2c1381a9905b..011aee2f460d07272f9d73e1f416467f7b727141 100644 (file)
@@ -820,7 +820,9 @@ var myModal = new bootstrap.Modal(document.getElementById('myModal'), 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" >}}
 | Name | Type | Default | Description |
index 56ae26e6ba12eddfcae962f69825e1b797a97c3b..10d184ed573f6fbf0242c226617f6e4fa218af67 100644 (file)
@@ -279,7 +279,9 @@ var offcanvasList = offcanvasElementList.map(function (offcanvasEl) {
 
 ### 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" >}}
 | Name | Type | Default | Description |
index f2cfdb6d0a1fa6190d8bdca8497b50fbe019ec5d..7dce91563512c57bb24a43a81c04c36645be148a 100644 (file)
@@ -166,7 +166,9 @@ Additionally, while it is possible to also include interactive controls (such as
 
 ### 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 >}}
 Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
index 6dfd9732fa9e9b1927f04b9b5176482698412df8..e48cc06f270b7fb342ea2aa572b08534d039e056 100644 (file)
@@ -338,7 +338,9 @@ Target elements that are not visible will be ignored and their corresponding nav
 
 ### 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" >}}
 | Name | Type | Default | Description |
index a54a6656a67cfa9ed631b0d91c51170542969907..32a0f2f41353ef4363dd32a64849adeb2cef18f1 100644 (file)
@@ -355,7 +355,9 @@ var toastList = toastElList.map(function (toastEl) {
 
 ### 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" >}}
 | Name | Type | Default | Description |
index 9460da2a0b8c1317f9e282ac8cffe974526df469..f913ff5fea3dda09be7738dbd8d664422ed4dc23 100644 (file)
@@ -193,7 +193,9 @@ Elements with the `disabled` attribute aren't interactive, meaning users cannot
 
 ### 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 >}}
 Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` options cannot be supplied using data attributes.
diff --git a/site/layouts/partials/js-data-attributes.md b/site/layouts/partials/js-data-attributes.md
new file mode 100644 (file)
index 0000000..c188652
--- /dev/null
@@ -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}'`.