Skip to content

Commit

Permalink
feat(jsx): style property helper (#1011)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperskei authored Jan 25, 2025
1 parent c6d8ac9 commit 2fe6f85
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 28 deletions.
12 changes: 12 additions & 0 deletions packages/jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ Correct:
></div>
```

To define a style property value, you should prepend the namespace `style:`:

```tsx
// <div style="top: 10px; right: 0;"></div>
<div
style:top={atom('10px')}
style:right={0}
style:bottom={undefined}
style:left={null}
></div>
```

### Class name utility

The `cn` function is designed for creating a string of CSS classes. It allows the use of multiple data types: strings, objects, arrays, functions, and atoms, which are converted into a class string.
Expand Down
2 changes: 1 addition & 1 deletion packages/jsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"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",
"build": "microbundle -f esm,cjs && cp src/jsx.d.ts build/ && cp -r build jsx-runtime/ && cp -r build jsx-dev-runtime/",
"test": "tsc && wtr src/*.test.{ts,tsx}",
"test:watch": "wtr src/*.test.{ts,tsx} --watch"
},
Expand Down
41 changes: 28 additions & 13 deletions packages/jsx/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,27 +506,42 @@ it('ref mount and unmount callbacks order', setup(async (ctx, h, hf, mount, pare
}))

it('style object update', setup((ctx, h, hf, mount, parent) => {
const styleAtom = atom({
top: '0',
right: undefined,
bottom: null as unknown as undefined,
left: '0',
} as JSX.CSSProperties)
const styleTopAtom = atom<JSX.StyleProperties['top']>('0')
const styleRightAtom = atom<JSX.StyleProperties['right']>(undefined)
const styleBottomAtom = atom<JSX.StyleProperties['bottom']>(null)
const styleLeftAtom = atom<JSX.StyleProperties['left']>('0')
const styleAtom = atom<JSX.StyleProperties>((ctx) => ({
top: ctx.spy(styleTopAtom),
right: ctx.spy(styleRightAtom),
bottom: ctx.spy(styleBottomAtom),
left: ctx.spy(styleLeftAtom),
}))

const firstEl = (<div style={styleAtom}></div>)
const secondEl = (<div
style:top={styleTopAtom}
style:right={styleRightAtom}
style:bottom={styleBottomAtom}
style:left={styleLeftAtom}
></div>)

const component = (
<div style={styleAtom}></div>
<div>
{firstEl}
{secondEl}
</div>
)

mount(parent, component)

assert.is(component.getAttribute('style'), 'top: 0px; left: 0px;')
assert.is(firstEl.getAttribute('style'), 'top: 0px; left: 0px;')
assert.is(secondEl.getAttribute('style'), 'top: 0px; left: 0px;')

styleAtom(ctx, {
top: undefined,
bottom: '0',
})
styleTopAtom(ctx, undefined)
styleBottomAtom(ctx, 0)

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

it('render different atom children', setup((ctx, h, hf, mount, parent) => {
Expand Down
13 changes: 9 additions & 4 deletions packages/jsx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ const walkLinkedList = (ctx: Ctx, el: JSX.Element, list: Atom<LinkedList<LLNode<
)
}

const patchStyleProperty = (style: CSSStyleDeclaration, key: string, value: any): void => {
if (value == null) style.removeProperty(key)
else style.setProperty(key, value)
}

export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
const StylesheetId = 'reatom-jsx-styles'
let styles: Rec<string> = {}
Expand Down Expand Up @@ -115,10 +120,10 @@ export const reatomJsx = (ctx: Ctx, DOM: DomApis = globalThis.window) => {
/** @see https://measurethat.net/Benchmarks/Show/11819 */
element.setAttribute('data-reatom', styleId)
} else if (key === 'style' && typeof val === 'object') {
for (const key in val) {
if (val[key] == null) element.style.removeProperty(key)
else element.style.setProperty(key, val[key])
}
for (const key in val) patchStyleProperty(element.style, key, val[key])
} else if (key.startsWith('style:')) {
key = key.slice(6)
patchStyleProperty(element.style, key, val)
} else if (key.startsWith('prop:')) {
// @ts-expect-error
element[key.slice(5)] = val
Expand Down
24 changes: 14 additions & 10 deletions packages/jsx/src/jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,6 @@ export namespace JSX {
'on:wheel'?: EventHandler<T, WheelEvent>
}

interface CSSProperties extends csstype.PropertiesHyphen {
// Override
[key: `-${string}`]: string | number | undefined
}

/** Controls automatic capitalization in inputted text. */
type HTMLAutocapitalize = 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters'
// TODO add combinations
Expand Down Expand Up @@ -655,16 +650,25 @@ export namespace JSX {

// TODO: Should we allow this?
// type ClassKeys = `class:${string}`;
// type CSSKeys = Exclude<keyof csstype.PropertiesHyphen, `-${string}`>;
type StylePropertiesKeys = Exclude<keyof csstype.PropertiesHyphen, `-${string}`>
/** @todo Should we use `csstype.PropertiesHyphenFallback`? */
type StyleProperties = {
[key in StylePropertiesKeys]?: csstype.PropertiesHyphen[key] | null
}
type StylePropertyAttributes = {
[key in StylePropertiesKeys as `style:${key}`]?: StyleProperties[key]
}

// type CSSAttributes = {
// [key in CSSKeys as `style:${key}`]: csstype.PropertiesHyphen[key];
// };
interface CSSProperties extends StyleProperties {
// Override
[key: `-${string}`]: string | number | null | undefined
}

interface HTMLAttributes<T = HTMLElement>
extends AriaAttributes,
DOMAttributes<T>,
CssAttributes,
StylePropertyAttributes,
ElementAttributes<T>,
ElementProperties<T>,
$Spread<T> {
Expand Down Expand Up @@ -1175,7 +1179,7 @@ export namespace JSX {
| 'defer xMidYMax slice'
| 'defer xMaxYMax slice'
type SVGUnits = 'userSpaceOnUse' | 'objectBoundingBox'
interface CoreSVGAttributes<T> extends AriaAttributes, DOMAttributes<T> {
interface CoreSVGAttributes<T> extends AriaAttributes, DOMAttributes<T>, StylePropertyAttributes {
id?: string
lang?: string
tabindex?: number | string
Expand Down

0 comments on commit 2fe6f85

Please sign in to comment.