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

Targeted parsing and initialization of components #824

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions content/getting-started/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,23 @@ initFlowbite();

Basically, the `initFlowbite()` function parses your DOM for all of the data attributes and creates new instances of the appropriate components like modals or dropdowns and sets up the behaviour of the examples from the Flowbite Docs - applying the functionality of showing and hiding the components such as hiding the modal when clicking on the "X" (close) button.

### Targeted initialisation

All the initialisation functions can be called with an optional argument. This optional argument allows you to specify which node from the DOM you want to target for initialisation. It can be especially useful when content is created or loaded after the document has been parsed and needs to be made interactive.

Consider the following example for [HTMX](https://htmx.org/):

```javascript
import { initFlowbite } from 'flowbite'

// this method is called when new content is loaded
htmx.onLoad(function (content) {
initFlowbite(content);
})
```

**Be mindful that only the ***descendants*** of the node will be made interactive, and not the node itself.**

## Instance manager

Since version `2.0.0`, the Flowbite JS API also provides a way to globally access all of the instances even if they were created via the data attributes interface. This allows you to programmatically handle the components while also giving you the possibility to use the recommended and quick way of accessing the data attributes interface and UI component examples.
Expand Down
85 changes: 42 additions & 43 deletions src/components/accordion/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import type { AccordionItem, AccordionOptions } from './types';
import type { InstanceOptions } from '../../dom/types';
import type { InstanceOptions, RootElement } from '../../dom/types';
import { AccordionInterface } from './interface';
import instances from '../../dom/instances';

Expand Down Expand Up @@ -185,54 +185,53 @@ class Accordion implements AccordionInterface {
}
}

export function initAccordions() {
document.querySelectorAll('[data-accordion]').forEach(($accordionEl) => {
const alwaysOpen = $accordionEl.getAttribute('data-accordion');
const activeClasses = $accordionEl.getAttribute('data-active-classes');
const inactiveClasses = $accordionEl.getAttribute(
'data-inactive-classes'
);

const items = [] as AccordionItem[];
$accordionEl
.querySelectorAll('[data-accordion-target]')
.forEach(($triggerEl) => {
// Consider only items that directly belong to $accordionEl
// (to make nested accordions work).
if ($triggerEl.closest('[data-accordion]') === $accordionEl) {
const item = {
id: $triggerEl.getAttribute('data-accordion-target'),
triggerEl: $triggerEl,
targetEl: document.querySelector(
$triggerEl.getAttribute('data-accordion-target')
),
iconEl: $triggerEl.querySelector(
'[data-accordion-icon]'
),
active:
$triggerEl.getAttribute('aria-expanded') === 'true'
? true
: false,
} as AccordionItem;
items.push(item);
}
});
export function initAccordions($rootElement: RootElement = document) {
$rootElement
.querySelectorAll('[data-accordion]')
.forEach(initAccordionByElement);
}

new Accordion($accordionEl as HTMLElement, items, {
alwaysOpen: alwaysOpen === 'open' ? true : false,
activeClasses: activeClasses
? activeClasses
: Default.activeClasses,
inactiveClasses: inactiveClasses
? inactiveClasses
: Default.inactiveClasses,
} as AccordionOptions);
});
export function initAccordionByElement($accordionEl: HTMLElement) {
const alwaysOpen = $accordionEl.getAttribute('data-accordion');
const activeClasses = $accordionEl.getAttribute('data-active-classes');
const inactiveClasses = $accordionEl.getAttribute('data-inactive-classes');

const items = [] as AccordionItem[];
$accordionEl
.querySelectorAll('[data-accordion-target]')
.forEach(($triggerEl) => {
// Consider only items that directly belong to $accordionEl
// (to make nested accordions work).
if ($triggerEl.closest('[data-accordion]') === $accordionEl) {
const item = {
id: $triggerEl.getAttribute('data-accordion-target'),
triggerEl: $triggerEl,
targetEl: document.querySelector(
$triggerEl.getAttribute('data-accordion-target')
),
iconEl: $triggerEl.querySelector('[data-accordion-icon]'),
active:
$triggerEl.getAttribute('aria-expanded') === 'true'
? true
: false,
} as AccordionItem;
items.push(item);
}
});

new Accordion($accordionEl as HTMLElement, items, {
alwaysOpen: alwaysOpen === 'open' ? true : false,
activeClasses: activeClasses ? activeClasses : Default.activeClasses,
inactiveClasses: inactiveClasses
? inactiveClasses
: Default.inactiveClasses,
} as AccordionOptions);
}

if (typeof window !== 'undefined') {
window.Accordion = Accordion;
window.initAccordions = initAccordions;
window.initAccordionByElement = initAccordionByElement;
}

export default Accordion;
113 changes: 56 additions & 57 deletions src/components/carousel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
IndicatorItem,
RotationItems,
} from './types';
import type { InstanceOptions } from '../../dom/types';
import type { InstanceOptions, RootElement } from '../../dom/types';
import { CarouselInterface } from './interface';
import instances from '../../dom/instances';

Expand Down Expand Up @@ -308,20 +308,22 @@ class Carousel implements CarouselInterface {
}
}

export function initCarousels() {
document.querySelectorAll('[data-carousel]').forEach(($carouselEl) => {
const interval = $carouselEl.getAttribute('data-carousel-interval');
const slide =
$carouselEl.getAttribute('data-carousel') === 'slide'
? true
: false;

const items: CarouselItem[] = [];
let defaultPosition = 0;
if ($carouselEl.querySelectorAll('[data-carousel-item]').length) {
Array.from(
$carouselEl.querySelectorAll('[data-carousel-item]')
).map(($carouselItemEl: HTMLElement, position: number) => {
export function initCarousels($rootElement: RootElement = document) {
$rootElement
.querySelectorAll('[data-carousel]')
.forEach(initCarouselByElement);
}

export function initCarouselByElement($carouselEl: Element) {
const interval = $carouselEl.getAttribute('data-carousel-interval');
const slide =
$carouselEl.getAttribute('data-carousel') === 'slide' ? true : false;

const items: CarouselItem[] = [];
let defaultPosition = 0;
if ($carouselEl.querySelectorAll('[data-carousel-item]').length) {
Array.from($carouselEl.querySelectorAll('[data-carousel-item]')).map(
($carouselItemEl: HTMLElement, position: number) => {
items.push({
position: position,
el: $carouselItemEl,
Expand All @@ -333,60 +335,57 @@ export function initCarousels() {
) {
defaultPosition = position;
}
});
}
}
);
}

const indicators: IndicatorItem[] = [];
if ($carouselEl.querySelectorAll('[data-carousel-slide-to]').length) {
Array.from(
$carouselEl.querySelectorAll('[data-carousel-slide-to]')
).map(($indicatorEl: HTMLElement) => {
indicators.push({
position: parseInt(
$indicatorEl.getAttribute('data-carousel-slide-to')
),
el: $indicatorEl,
});
const indicators: IndicatorItem[] = [];
if ($carouselEl.querySelectorAll('[data-carousel-slide-to]').length) {
Array.from(
$carouselEl.querySelectorAll('[data-carousel-slide-to]')
).map(($indicatorEl: HTMLElement) => {
indicators.push({
position: parseInt(
$indicatorEl.getAttribute('data-carousel-slide-to')
),
el: $indicatorEl,
});
}
});
}

const carousel = new Carousel($carouselEl as HTMLElement, items, {
defaultPosition: defaultPosition,
indicators: {
items: indicators,
},
interval: interval ? interval : Default.interval,
} as CarouselOptions);
const carousel = new Carousel($carouselEl as HTMLElement, items, {
defaultPosition: defaultPosition,
indicators: {
items: indicators,
},
interval: interval ? interval : Default.interval,
} as CarouselOptions);

if (slide) {
carousel.cycle();
}
if (slide) {
carousel.cycle();
}

// check for controls
const carouselNextEl = $carouselEl.querySelector(
'[data-carousel-next]'
);
const carouselPrevEl = $carouselEl.querySelector(
'[data-carousel-prev]'
);
// check for controls
const carouselNextEl = $carouselEl.querySelector('[data-carousel-next]');
const carouselPrevEl = $carouselEl.querySelector('[data-carousel-prev]');

if (carouselNextEl) {
carouselNextEl.addEventListener('click', () => {
carousel.next();
});
}
if (carouselNextEl) {
carouselNextEl.addEventListener('click', () => {
carousel.next();
});
}

if (carouselPrevEl) {
carouselPrevEl.addEventListener('click', () => {
carousel.prev();
});
}
});
if (carouselPrevEl) {
carouselPrevEl.addEventListener('click', () => {
carousel.prev();
});
}
}

if (typeof window !== 'undefined') {
window.Carousel = Carousel;
window.initCarousels = initCarousels;
window.initCarouselByElement = initCarouselByElement;
}

export default Carousel;
85 changes: 43 additions & 42 deletions src/components/clipboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import type { CopyClipboardOptions } from './types';
import type { InstanceOptions } from '../../dom/types';
import type { InstanceOptions, RootElement } from '../../dom/types';
import { CopyClipboardInterface } from './interface';
import instances from '../../dom/instances';

Expand Down Expand Up @@ -140,54 +140,55 @@ class CopyClipboard implements CopyClipboardInterface {
}
}

export function initCopyClipboards() {
document
export function initCopyClipboards($rootElement: RootElement = document) {
$rootElement
.querySelectorAll('[data-copy-to-clipboard-target]')
.forEach(($triggerEl) => {
const targetId = $triggerEl.getAttribute(
'data-copy-to-clipboard-target'
);
const $targetEl = document.getElementById(targetId);
const contentType = $triggerEl.getAttribute(
'data-copy-to-clipboard-content-type'
);
const htmlEntities = $triggerEl.getAttribute(
'data-copy-to-clipboard-html-entities'
);
.forEach(initCopyClipboardByElement);
}

// check if the target element exists
if ($targetEl) {
if (
!instances.instanceExists(
'CopyClipboard',
$targetEl.getAttribute('id')
)
) {
new CopyClipboard(
$triggerEl as HTMLElement,
$targetEl as HTMLInputElement,
{
htmlEntities:
htmlEntities && htmlEntities === 'true'
? true
: Default.htmlEntities,
contentType: contentType
? contentType
: Default.contentType,
} as CopyClipboardOptions
);
}
} else {
console.error(
`The target element with id "${targetId}" does not exist. Please check the data-copy-to-clipboard-target attribute.`
);
}
});
export function initCopyClipboardByElement($triggerEl: Element) {
const targetId = $triggerEl.getAttribute('data-copy-to-clipboard-target');
const $targetEl = document.getElementById(targetId);
const contentType = $triggerEl.getAttribute(
'data-copy-to-clipboard-content-type'
);
const htmlEntities = $triggerEl.getAttribute(
'data-copy-to-clipboard-html-entities'
);

// check if the target element exists
if ($targetEl) {
if (
!instances.instanceExists(
'CopyClipboard',
$targetEl.getAttribute('id')
)
) {
new CopyClipboard(
$triggerEl as HTMLElement,
$targetEl as HTMLInputElement,
{
htmlEntities:
htmlEntities && htmlEntities === 'true'
? true
: Default.htmlEntities,
contentType: contentType
? contentType
: Default.contentType,
} as CopyClipboardOptions
);
}
} else {
console.error(
`The target element with id "${targetId}" does not exist. Please check the data-copy-to-clipboard-target attribute.`
);
}
}

if (typeof window !== 'undefined') {
window.CopyClipboard = CopyClipboard;
window.initClipboards = initCopyClipboards;
window.initCopyClipboardByElement = initCopyClipboardByElement;
}

export default CopyClipboard;
Loading