Skip to content

Commit

Permalink
feat(jsx): normalize class attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperskei committed Oct 15, 2024
1 parent d87c180 commit 1681ced
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 6 deletions.
4 changes: 2 additions & 2 deletions packages/jsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"scripts": {
"prepublishOnly": "npm run build && npm run test",
"build": "microbundle -f esm,cjs && cp src/jsx.d.ts build/ && cp -r build/ jsx-runtime/build && cp -r build/ jsx-dev-runtime/build",
"test": "tsc && wtr src/index.test.tsx",
"test:watch": "wtr src/index.test.tsx --watch"
"test": "tsc && wtr src/*.test.{ts,tsx}",
"test:watch": "wtr src/*.test.{ts,tsx} --watch"
},
"dependencies": {
"@reatom/core": ">=3.6.0",
Expand Down
23 changes: 22 additions & 1 deletion packages/jsx/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ it('custom component', setup((ctx, h, hf, mount, parent) => {
const Component = (props: JSX.HTMLAttributes) => <div {...props} />

assert.instance(<Component />, window.HTMLElement)
assert.is(((<Component draggable />) as HTMLElement).draggable, true)
assert.is(((<Component draggable="true" />) as HTMLElement).draggable, true)
assert.equal(((<Component>123</Component>) as HTMLElement).innerText, '123')
}))

Expand Down Expand Up @@ -470,3 +470,24 @@ it('style object update', setup((ctx, h, hf, mount, parent) => {

assert.is(component.getAttribute('style'), 'left: 0px; bottom: 0px;')
}))

it('complex class attribute', setup(async (ctx, h, hf, mount, parent) => {
const boolAtom = atom(false)
const classAtom = atom((ctx) => [
ctx.spy(boolAtom) ? '' : 'a',
['b'],
{c: true, d: ctx.spy(boolAtom)},
])

const component = (
<div class={classAtom}></div>
)

mount(parent, component)

assert.is(component.className, 'a b c')

boolAtom(ctx, true)

assert.is(component.className, 'b c d')
}))
11 changes: 10 additions & 1 deletion packages/jsx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { action, Atom, AtomMut, createCtx, Ctx, Fn, isAtom, Rec, throwReatomErro
import { isObject, random } from '@reatom/utils'
import { type LinkedList, type LLNode, isLinkedListAtom, LL_NEXT } from '@reatom/primitives'
import type { JSX } from './jsx'
import { normalizeClass } from './utils'

declare type JSXElement = JSX.Element

Expand Down Expand Up @@ -110,8 +111,16 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
styleId = styles[val] = random().toString()
stylesheet.innerText += '[data-reatom="' + styleId + '"]{' + val + '}\n'
}
/** @see https://www.measurethat.net/Benchmarks/Show/11819/0/dataset-vs-setattribute */
/** @see https://www.measurethat.net/Benchmarks/Show/11819 */
element.setAttribute('data-reatom', styleId)
} else if (key === 'class') {
const className = normalizeClass(val)
if (element instanceof HTMLElement) {
/** @see https://measurethat.net/Benchmarks/Show/54 */
element.className = className
} else {
element.setAttribute('class', className)
}
} else if (key === 'style' && typeof val === 'object') {
for (const key in val) {
if (val[key] == null) element.style.removeProperty(key)
Expand Down
6 changes: 4 additions & 2 deletions packages/jsx/src/jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ export namespace JSX {
// [key in CSSKeys as `style:${key}`]: csstype.PropertiesHyphen[key];
// };

type ClassValue = string | undefined | Record<string, null | undefined | string | number | boolean> | Array<ClassValue>

interface HTMLAttributes<T = HTMLElement>
extends AriaAttributes,
DOMAttributes<T>,
Expand All @@ -669,7 +671,7 @@ export namespace JSX {
$Spread<T> {
// [key: ClassKeys]: boolean;
accessKey?: string
class?: string | undefined
class?: ClassValue
contenteditable?: boolean | 'plaintext-only' | 'inherit'
contextmenu?: string
dir?: HTMLDir
Expand Down Expand Up @@ -1178,7 +1180,7 @@ export namespace JSX {
tabindex?: number | string
}
interface StylableSVGAttributes extends CssAttributes {
class?: string | undefined
class?: ClassValue
style?: CSSProperties | string
}
interface TransformableSVGAttributes {
Expand Down
65 changes: 65 additions & 0 deletions packages/jsx/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as assert from 'uvu/assert'
import { normalizeClass } from './utils'

describe('normalizeClass', () => {
it('handles undefined correctly', () => {
assert.is(normalizeClass(undefined), '')
})

it('handles string correctly', () => {
assert.is(normalizeClass('foo'), 'foo')
})

it('handles array correctly', () => {
assert.is(normalizeClass(['foo', undefined, true, false, 'bar']), 'foo bar')
})

it('handles string containing spaces correctly', () => {
assert.is(normalizeClass('foo1 '), 'foo1')
assert.is(normalizeClass(['foo ', ' baz ']), 'foo baz')
})

it('handles empty array correctly', () => {
assert.is(normalizeClass([]), '')
})

it('handles nested array correctly', () => {
assert.is(normalizeClass(['foo', ['bar'], [['baz']]]), 'foo bar baz')
})

it('handles object correctly', () => {
assert.is(normalizeClass({ foo: true, bar: false, baz: true }), 'foo baz')
})

it('handles empty object correctly', () => {
assert.is(normalizeClass({}), '')
})

it('handles arrays and objects correctly', () => {
assert.is(normalizeClass(['foo', ['bar'], { baz: true }, [{ qux: true }]]), 'foo bar baz qux')
})

it('handles array of objects with falsy values', () => {
assert.is(
normalizeClass([
{ foo: false },
{ bar: 0 },
{ baz: -0 },
{ qux: '' },
{ quux: null },
{ corge: undefined },
{ grault: NaN },
]), '')
})

it('handles array of objects with truthy values', () => {
assert.is(
normalizeClass([
{ foo: true },
{ bar: 'not-empty' },
{ baz: 1 },
{ qux: {} },
{ quux: [] },
]), 'foo bar baz qux quux')
})
})
25 changes: 25 additions & 0 deletions packages/jsx/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isObject } from '@reatom/utils'

/**
* @see https://github.com/vuejs/core/blob/main/packages/shared/src/normalizeProp.ts
*/
export const normalizeClass = (value: unknown): string => {
let className = ''
if (typeof value === 'string') {
className = value
} else if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized) {
className += normalized + ' '
}
}
} else if (isObject(value)) {
for (const name in value) {
if (value[name]) {
className += name + ' '
}
}
}
return className.trim()
}

0 comments on commit 1681ced

Please sign in to comment.