From 3f2ad5b2a6a2ae96f86fc60f9daeababeb77367f Mon Sep 17 00:00:00 2001 From: Eric Fennis Date: Mon, 24 Feb 2025 10:10:28 +0100 Subject: [PATCH] fix(lucide): Lucide create element function returning SVG Element (#2816) * Fix create element function * Formatting * Adjust documentation --- docs/guide/packages/lucide.md | 20 ++++++++-- packages/lucide/src/createElement.ts | 33 +++++++++++++---- packages/lucide/src/replaceElement.ts | 2 +- .../__snapshots__/createElement.spec.ts.snap | 19 ++++++++++ packages/lucide/tests/createElement.spec.ts | 37 +++++++++++++++++++ packages/lucide/tests/helpers.ts | 17 +++++++++ packages/lucide/tests/lucide.spec.ts | 16 +------- 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 packages/lucide/tests/__snapshots__/createElement.spec.ts.snap create mode 100644 packages/lucide/tests/createElement.spec.ts create mode 100644 packages/lucide/tests/helpers.ts diff --git a/docs/guide/packages/lucide.md b/docs/guide/packages/lucide.md index 813d143a5..5e8613a8f 100644 --- a/docs/guide/packages/lucide.md +++ b/docs/guide/packages/lucide.md @@ -126,11 +126,23 @@ import { createElement, Menu } from 'lucide'; const menuIcon = createElement(Menu); // Returns HTMLElement (svg) -// set custom attributes with browser native functions -menuIcon.setAttribute('stroke', '#333'); -menuIcon.classList.add('my-icon-class'); +// Append HTMLElement in the DOM +const myApp = document.getElementById('app'); +myApp.appendChild(menuIcon); +``` -// Append HTMLElement in webpage +#### Custom Element binding with custom attributes + +```js +import { createElement, Menu } from 'lucide'; + +const menuIcon = createElement(Menu, { + class: ['my-custom-class', 'icon'], + 'stroke-width': 1, + stroke: '#333' +}); // Returns HTMLElement (svg) + +// Append HTMLElement in the DOM const myApp = document.getElementById('app'); myApp.appendChild(menuIcon); ``` diff --git a/packages/lucide/src/createElement.ts b/packages/lucide/src/createElement.ts index 874caa50b..f28e585fc 100644 --- a/packages/lucide/src/createElement.ts +++ b/packages/lucide/src/createElement.ts @@ -1,15 +1,16 @@ +import defaultAttributes from './defaultAttributes'; import { IconNode, SVGProps } from './types'; -type CreateElementParams = [tag: string, attrs: SVGProps, children?: IconNode]; +type CreateSVGElementParams = [tag: string, attrs: SVGProps, children?: IconNode]; /** - * Creates a new HTMLElement from icon node - * @param {string} tag - * @param {object} attrs - * @param {array} children - * @returns {HTMLElement} + * Creates a new SVGElement + * @param {string} tag - Tag name of the element + * @param {object} attrs - Attributes of the element + * @param {array} children - Children of the element + * @returns {SVGElement} */ -const createElement = ([tag, attrs, children]: CreateElementParams) => { +const createSVGElement = ([tag, attrs, children]: CreateSVGElementParams) => { const element = document.createElementNS('http://www.w3.org/2000/svg', tag); Object.keys(attrs).forEach((name) => { @@ -18,7 +19,7 @@ const createElement = ([tag, attrs, children]: CreateElementParams) => { if (children?.length) { children.forEach((child) => { - const childElement = createElement(child); + const childElement = createSVGElement(child); element.appendChild(childElement); }); @@ -27,4 +28,20 @@ const createElement = ([tag, attrs, children]: CreateElementParams) => { return element; }; +/** + * Creates a new HTMLElement from icon node + * @param {array} iconNode - Icon node to be converted to an element + * @param {object} customAttrs - Custom attributes to be added to the element + * @returns {HTMLElement} + */ +const createElement = (iconNode: IconNode, customAttrs: SVGProps = {}) => { + const tag = 'svg'; + const attrs = { + ...defaultAttributes, + ...customAttrs, + }; + + return createSVGElement([tag, attrs, iconNode]); +}; + export default createElement; diff --git a/packages/lucide/src/replaceElement.ts b/packages/lucide/src/replaceElement.ts index 2b26a6b27..5a3fa2e9f 100644 --- a/packages/lucide/src/replaceElement.ts +++ b/packages/lucide/src/replaceElement.ts @@ -98,7 +98,7 @@ const replaceElement = (element: Element, { nameAttr, icons, attrs }: ReplaceEle }); } - const svgElement = createElement(['svg', iconAttrs, iconNode]); + const svgElement = createElement(iconNode, iconAttrs); return element.parentNode?.replaceChild(svgElement, element); }; diff --git a/packages/lucide/tests/__snapshots__/createElement.spec.ts.snap b/packages/lucide/tests/__snapshots__/createElement.spec.ts.snap new file mode 100644 index 000000000..4b1d355d0 --- /dev/null +++ b/packages/lucide/tests/__snapshots__/createElement.spec.ts.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`createElement > should match the snapshot 1`] = ` + + + + + + +`; diff --git a/packages/lucide/tests/createElement.spec.ts b/packages/lucide/tests/createElement.spec.ts new file mode 100644 index 000000000..3c331b74a --- /dev/null +++ b/packages/lucide/tests/createElement.spec.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; +import { House, createElement } from '../src/lucide'; +import { getOriginalSvg } from './helpers'; + +describe('createElement', () => { + it('should create SVG Element', () => { + const HomeSVG = createElement(House); + + expect(HomeSVG.tagName).toBe('svg'); + }); + + it('should match the snapshot', () => { + const HomeSVG = createElement(House); + + expect(HomeSVG.outerHTML).toMatchSnapshot(); + }); + + it('should create SVG Element with attributes', () => { + const HomeSVG = createElement(House, { fill: 'red' }); + + expect(HomeSVG.getAttribute('fill')).toBe('red'); + }); + + it('should create SVG Element with class name', () => { + const HomeSVG = createElement(House, { class: 'icon' }); + + expect(HomeSVG.getAttribute('class')).toBe('icon'); + }); + + it('should create the correct svg element', () => { + const HomeSVG = createElement(House); + + const svg = getOriginalSvg('house', undefined, false); + + expect(HomeSVG.outerHTML).toBe(svg); + }); +}); diff --git a/packages/lucide/tests/helpers.ts b/packages/lucide/tests/helpers.ts new file mode 100644 index 000000000..1e8e90fcd --- /dev/null +++ b/packages/lucide/tests/helpers.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; +import path from 'path'; +import { parseSync, stringify } from 'svgson'; + +const ICONS_DIR = path.resolve(__dirname, '../../../icons'); + +export const getOriginalSvg = (iconName: string, aliasName?: string, setAttrs = true) => { + const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8'); + const svgParsed = parseSync(svgContent); + + if (setAttrs) { + svgParsed.attributes['data-lucide'] = aliasName ?? iconName; + svgParsed.attributes['class'] = `lucide lucide-${aliasName ?? iconName}`; + } + + return stringify(svgParsed, { selfClose: false }); +}; diff --git a/packages/lucide/tests/lucide.spec.ts b/packages/lucide/tests/lucide.spec.ts index 2cd621980..277a5f4ba 100644 --- a/packages/lucide/tests/lucide.spec.ts +++ b/packages/lucide/tests/lucide.spec.ts @@ -1,20 +1,6 @@ import { describe, it, expect } from 'vitest'; import { createIcons, icons } from '../src/lucide'; -import fs from 'fs'; -import path from 'path'; -import { parseSync, stringify } from 'svgson'; - -const ICONS_DIR = path.resolve(__dirname, '../../../icons'); - -const getOriginalSvg = (iconName: string, aliasName?: string) => { - const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8'); - const svgParsed = parseSync(svgContent); - - svgParsed.attributes['data-lucide'] = aliasName ?? iconName; - svgParsed.attributes['class'] = `lucide lucide-${aliasName ?? iconName}`; - - return stringify(svgParsed, { selfClose: false }); -}; +import { getOriginalSvg } from './helpers'; describe('createIcons', () => { it('should read elements from DOM and replace it with icons', () => {