fix(packages/lucide): replace elements inside <template> (#2635) (#3576)

* fix(packages/lucide): replace elements inside `<template>` (#2635)

* Added replaceInsideTemplates option

* Format code

* Simply code and add some documentation

* Fix vercel build

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
This commit is contained in:
Khalid Alansary
2025-10-17 12:46:55 +03:00
committed by GitHub
parent 32e93c043f
commit be55088e75
4 changed files with 79 additions and 10 deletions

View File

@@ -93,6 +93,7 @@ In the `createIcons` function you can pass some extra parameters:
- you can pass `nameAttr` to adjust the attribute name to replace for
- you can pass `attrs` to pass additional custom attributes, for instance CSS classes or stroke options.
- you can pass `root` to provide a custom DOM element the icons should be replaced in (useful when manipulating small sections of a large DOM or elements in the shadow DOM)
- you can pass `inTemplates: true` to also replace icons inside `<template>` tags.
Here is a full example:
@@ -107,6 +108,7 @@ createIcons({
},
nameAttr: 'data-lucide', // attribute for the icon name.
root: element, // DOM element to replace icons in.
inTemplates: true // Also replace icons inside <template> tags.
});
```
@@ -124,6 +126,34 @@ createIcons({
});
```
### Custom Document root
Apply icons in a custom root element, for instance a shadow DOM root.
```js
import { createIcons } from 'lucide';
// Custom root element, for instance a shadow DOM root.
const shadowRoot = element.attachShadow({ mode: 'open' });
createIcons({
root: shadowRoot
});
```
### Apply icons inside `<template>` tags
By default icons inside `<template>` tags are not added.
By setting the `inTemplates` option to `true`, icons inside templates will also be replaced.
```js
import { createIcons } from 'lucide';
createIcons({
inTemplates: true
});
```
### Custom Element binding
```js

View File

@@ -1,16 +1,26 @@
import replaceElement from './replaceElement';
import * as iconAndAliases from './iconsAndAliases';
import { Icons, SVGProps } from './types';
export interface CreateIconsOptions {
icons?: Icons;
nameAttr?: string;
attrs?: SVGProps;
root?: Element | Document | DocumentFragment;
inTemplates?: boolean;
}
/**
* Replaces all elements with matching nameAttr with the defined icons
* @param {{ icons?: object, nameAttr?: string, attrs?: object, root?: Element | Document }} options
* @param {CreateIconsOptions} options
*/
const createIcons = ({
icons = {},
nameAttr = 'data-lucide',
attrs = {},
root = document,
}: { icons?: object; nameAttr?: string; attrs?: object; root?: Element | Document } = {}) => {
inTemplates,
}: CreateIconsOptions) => {
if (!Object.values(icons).length) {
throw new Error(
"Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`",
@@ -21,10 +31,23 @@ const createIcons = ({
throw new Error('`createIcons()` only works in a browser environment.');
}
const elementsToReplace = root.querySelectorAll(`[${nameAttr}]`);
Array.from(elementsToReplace).forEach((element) =>
replaceElement(element, { nameAttr, icons, attrs }),
);
const elementsToReplace = Array.from(root.querySelectorAll(`[${nameAttr}]`));
elementsToReplace.forEach((element) => replaceElement(element, { nameAttr, icons, attrs }));
if (inTemplates) {
const templates = Array.from(root.querySelectorAll('template'));
templates.forEach((template) =>
createIcons({
icons,
nameAttr,
attrs,
root: template.content,
inTemplates,
}),
);
}
/** @todo: remove this block in v1.0 */
if (nameAttr === 'data-lucide') {

View File

@@ -1,6 +1,6 @@
import createElement from './createElement';
import defaultAttributes from './defaultAttributes';
import { Icons } from './types';
import { Icons, SVGProps } from './types';
export type CustomAttrs = { [attr: string]: any };
@@ -21,7 +21,7 @@ export const getAttrs = (element: Element): Record<string, string> =>
* @returns {Array}
*/
export const getClassNames = (
attrs: Record<string, string | string[]> | string,
attrs: Record<string, number | string | string[]> | string,
): string | string[] => {
if (typeof attrs === 'string') return attrs;
if (!attrs || !attrs.class) return '';
@@ -40,7 +40,7 @@ export const getClassNames = (
* @returns {string}
*/
export const combineClassNames = (
arrayOfClassnames: (string | Record<string, string | string[]>)[],
arrayOfClassnames: (string | Record<string, number | string | string[]>)[],
) => {
const classNameArray = arrayOfClassnames.flatMap(getClassNames);
@@ -57,7 +57,7 @@ const toPascalCase = (string: string): string =>
interface ReplaceElementOptions {
nameAttr: string;
icons: Icons;
attrs: Record<string, string>;
attrs: SVGProps;
}
/**

View File

@@ -104,4 +104,20 @@ describe('createIcons', () => {
expect(document.body.innerHTML).toBe(svg);
expect(document.body.innerHTML).toMatchSnapshot();
});
it('should not replace icons inside template elements by default', () => {
document.body.innerHTML = `<template><i data-lucide="house"></i></template>`;
createIcons({ icons });
const hasIcon = !!document.querySelector('template')?.content.querySelector('svg');
expect(hasIcon).toBeFalsy();
});
it('should replace icons inside template elements when replaceInsideTemplates is true', () => {
document.body.innerHTML = `<template><i data-lucide="house"></i></template>`;
createIcons({ icons, inTemplates: true });
const hasIcon = !!document.querySelector('template')?.content.querySelector('svg');
expect(hasIcon).toBeTruthy();
});
});