Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow constructors to accept a CSS selector #32245

Merged
merged 11 commits into from
Feb 22, 2021
4 changes: 3 additions & 1 deletion js/src/base-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ const VERSION = '5.0.0-beta2'

class BaseComponent {
constructor(element) {
element = typeof element === 'string' ? document.querySelector(element) : element
XhmikosR marked this conversation as resolved.
Show resolved Hide resolved

if (!element) {
return
}

this._element = element
Data.setData(element, this.constructor.DATA_KEY, this)
Data.setData(this._element, this.constructor.DATA_KEY, this)
}

dispose() {
Expand Down
6 changes: 3 additions & 3 deletions js/src/collapse.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ class Collapse extends BaseComponent {
this._isTransitioning = false
this._config = this._getConfig(config)
this._triggerArray = SelectorEngine.find(
`${SELECTOR_DATA_TOGGLE}[href="#${element.id}"],` +
`${SELECTOR_DATA_TOGGLE}[data-bs-target="#${element.id}"]`
`${SELECTOR_DATA_TOGGLE}[href="#${this._element.id}"],` +
`${SELECTOR_DATA_TOGGLE}[data-bs-target="#${this._element.id}"]`
)

const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
Expand All @@ -82,7 +82,7 @@ class Collapse extends BaseComponent {
const elem = toggleList[i]
const selector = getSelectorFromElement(elem)
const filterElement = SelectorEngine.find(selector)
.filter(foundElem => foundElem === element)
.filter(foundElem => foundElem === this._element)

if (selector !== null && filterElement.length) {
this._selector = selector
Expand Down
2 changes: 1 addition & 1 deletion js/src/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Modal extends BaseComponent {
super(element)

this._config = this._getConfig(config)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, element)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
this._backdrop = null
this._isShown = false
this._isBodyOverflowing = false
Expand Down
2 changes: 1 addition & 1 deletion js/src/scrollspy.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const METHOD_POSITION = 'position'
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element)
this._scrollElement = element.tagName === 'BODY' ? window : element
this._scrollElement = this._element.tagName === 'BODY' ? window : this._element
this._config = this._getConfig(config)
this._selector = `${this._config.target} ${SELECTOR_NAV_LINKS}, ${this._config.target} ${SELECTOR_LIST_ITEMS}, ${this._config.target} .${CLASS_NAME_DROPDOWN_ITEM}`
this._offsets = []
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/alert.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ describe('Alert', () => {
clearFixture()
})

it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="alert"></div>'

const alertEl = fixtureEl.querySelector('.alert')
const alertBySelector = new Alert('.alert')
const alertByElement = new Alert(alertEl)

expect(alertBySelector._element).toEqual(alertEl)
expect(alertByElement._element).toEqual(alertEl)
})

it('should return version', () => {
expect(typeof Alert.VERSION).toEqual('string')
})
Expand Down
10 changes: 10 additions & 0 deletions js/tests/unit/button.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ describe('Button', () => {
clearFixture()
})

it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<button data-bs-toggle="button">Placeholder</button>'
const buttonEl = fixtureEl.querySelector('[data-bs-toggle="button"]')
const buttonBySelector = new Button('[data-bs-toggle="button"]')
const buttonByElement = new Button(buttonEl)

expect(buttonBySelector._element).toEqual(buttonEl)
expect(buttonByElement._element).toEqual(buttonEl)
})

describe('VERSION', () => {
it('should return plugin version', () => {
expect(Button.VERSION).toEqual(jasmine.any(String))
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/carousel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ describe('Carousel', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div id="myCarousel" class="carousel slide"></div>'

const carouselEl = fixtureEl.querySelector('#myCarousel')
const carouselBySelector = new Carousel('#myCarousel')
const carouselByElement = new Carousel(carouselEl)

expect(carouselBySelector._element).toEqual(carouselEl)
expect(carouselByElement._element).toEqual(carouselEl)
})

it('should go to next item if right arrow key is pressed', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/collapse.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ describe('Collapse', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="my-collapse"></div>'

const collapseEl = fixtureEl.querySelector('div.my-collapse')
const collapseBySelector = new Collapse('div.my-collapse')
const collapseByElement = new Collapse(collapseEl)

expect(collapseBySelector._element).toEqual(collapseEl)
expect(collapseByElement._element).toEqual(collapseEl)
})

it('should allow jquery object in parent config', () => {
fixtureEl.innerHTML = [
'<div class="my-collapse">',
Expand Down
18 changes: 18 additions & 0 deletions js/tests/unit/dropdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ describe('Dropdown', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Link</a>',
' </div>',
'</div>'
].join('')

const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]')
const dropdownByElement = new Dropdown(btnDropdown)

expect(dropdownBySelector._element).toEqual(btnDropdown)
expect(dropdownByElement._element).toEqual(btnDropdown)
})

it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
Expand Down
13 changes: 13 additions & 0 deletions js/tests/unit/modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ describe('Modal', () => {
})
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

const modalEl = fixtureEl.querySelector('.modal')
const modalBySelector = new Modal('.modal')
const modalByElement = new Modal(modalEl)

expect(modalBySelector._element).toEqual(modalEl)
expect(modalByElement._element).toEqual(modalEl)
})
})

describe('toggle', () => {
it('should toggle a modal', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/scrollspy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ describe('ScrollSpy', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<nav id="navigation"></nav><div class="content"></div>'

const sSpyEl = fixtureEl.querySelector('#navigation')
const sSpyBySelector = new ScrollSpy('#navigation')
const sSpyByElement = new ScrollSpy(sSpyEl)

expect(sSpyBySelector._element).toEqual(sSpyEl)
expect(sSpyByElement._element).toEqual(sSpyEl)
})

it('should generate an id when there is not one', () => {
fixtureEl.innerHTML = [
'<nav></nav>',
Expand Down
16 changes: 16 additions & 0 deletions js/tests/unit/tab.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ describe('Tab', () => {
})
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = [
'<ul class="nav"><li><a href="#home" role="tab">Home</a></li></ul>',
'<ul><li id="home"></li></ul>'
].join('')

const tabEl = fixtureEl.querySelector('[href="#home"]')
const tabBySelector = new Tab('[href="#home"]')
const tabByElement = new Tab(tabEl)

expect(tabBySelector._element).toEqual(tabEl)
expect(tabByElement._element).toEqual(tabEl)
})
})

describe('show', () => {
it('should activate element by tab id (using buttons, the preferred semantic way)', done => {
fixtureEl.innerHTML = [
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/toast.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ describe('Toast', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<div class="toast"></div>'

const toastEl = fixtureEl.querySelector('.toast')
const toastBySelector = new Toast('.toast')
const toastByElement = new Toast(toastEl)

expect(toastBySelector._element).toEqual(toastEl)
expect(toastByElement._element).toEqual(toastEl)
})

it('should allow to config in js', done => {
fixtureEl.innerHTML = [
'<div class="toast">',
Expand Down
11 changes: 11 additions & 0 deletions js/tests/unit/tooltip.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ describe('Tooltip', () => {
})

describe('constructor', () => {
it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = '<a href="#" id="tooltipEl" rel="tooltip" title="Nice and short title">'

const tooltipEl = fixtureEl.querySelector('#tooltipEl')
const tooltipBySelector = new Tooltip('#tooltipEl')
const tooltipByElement = new Tooltip(tooltipEl)

expect(tooltipBySelector._element).toEqual(tooltipEl)
expect(tooltipByElement._element).toEqual(tooltipEl)
})

it('should not take care of disallowed data attributes', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-sanitize="false" title="Another tooltip">'

Expand Down
9 changes: 9 additions & 0 deletions site/content/docs/5.0/getting-started/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ var modal = new bootstrap.Modal(myModalEl, { keyboard: false }) // initialized w

If you'd like to get a particular plugin instance, each plugin exposes a `getInstance` method. In order to retrieve it directly from an element, do this: `bootstrap.Popover.getInstance(myPopoverEl)`.

### CSS selectors in constructors

You can also use a CSS selector as the first argument instead of a DOM element to initialize the plugin. Currently the element for the plugin is found by the `querySelector` method since our plugins support a single element only.

```js
var modal = new bootstrap.Modal('#myModal')
var dropdown = new bootstrap.Dropdown('[data-bs-toggle="dropdown"]')
```

### Asynchronous functions and transitions

All programmatic API methods are **asynchronous** and return to the caller once the transition is started but **before it ends**.
Expand Down
11 changes: 11 additions & 0 deletions site/content/docs/5.0/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ toc: true

## v5.0.0-beta3

### JavaScript

- All plugins can now accept a CSS selector as the first argument. You can either pass a DOM element or any valid CSS selector to create a new instance of the plugin:

```js
var modal = new bootstrap.Modal('#myModal')
var dropdown = new bootstrap.Dropdown('[data-bs-toggle="dropdown"]')
```

## v5.0.0-beta2

### Utilities

- Dropped the `0` entry in `$border-widths` map to remove the duplicated `.border-0` class.
Expand Down