Skip to content

Commit 21504df

Browse files
committed
✨Add support for functional elements
The nice thing about JSX is that it is "just javascript" which means that you can add abstractions to it with simple functions. However, the current implementation of JSX in hastscript stops short of allowing elements to be defined this way. This adds this support so you can make dead simple HTML templates in JavaScript: functions that accept parameters and return a `JSX.Element` TODOs and Open Questions - figure out what to do with the "classic" JSX transform since it is currently just a call through to the native `h()` function. We either need to wrap this function or add support for function expansion inside it as well. - Sort out typings, especially for JSX.ElementChildrenAttribute
1 parent e031133 commit 21504df

File tree

5 files changed

+78
-38
lines changed

5 files changed

+78
-38
lines changed

lib/jsx-automatic.d.ts

+32-29
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
11
import type {HProperties, HChild, HResult} from './core.js'
22

3-
export namespace JSX {
4-
/**
5-
* This defines the return value of JSX syntax.
6-
*/
7-
type Element = HResult
3+
declare global {
4+
namespace JSX {
5+
/**
6+
* This defines the return value of JSX syntax.
7+
*/
8+
type Element = HResult
89

9-
/**
10-
* This disallows the use of functional components.
11-
*/
12-
type IntrinsicAttributes = never
10+
/**
11+
* This disallows the use of functional components.
12+
*/
13+
// type IntrinsicAttributes = never
1314

14-
/**
15-
* This defines the prop types for known elements.
16-
*
17-
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
18-
*
19-
* This **must** be an interface.
20-
*/
21-
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
22-
interface IntrinsicElements {
23-
[name: string]:
24-
| HProperties
25-
| {
15+
/**
16+
* This defines the prop types for known elements.
17+
*
18+
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
19+
*
20+
* This **must** be an interface.
21+
*/
22+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
23+
interface IntrinsicElements {
24+
[name: string]:
25+
| HProperties
26+
| {
2627
/**
2728
* The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type.
2829
*/
2930
children?: HChild
3031
}
31-
}
32+
}
3233

33-
/**
34-
* The key of this interface defines as what prop children are passed.
35-
*/
36-
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
37-
interface ElementChildrenAttribute {
3834
/**
39-
* Only the key matters, not the value.
35+
* The key of this interface defines as what prop children are passed.
4036
*/
41-
children?: never
37+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
38+
interface ElementChildrenAttribute {
39+
/**
40+
* Only the key matters, not the value.
41+
*/
42+
//TODO: is there a way to make this work?
43+
//children?: HChild
44+
}
4245
}
4346
}

lib/runtime.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @typedef {import('./core.js').core} Core
1010
*
1111
* @typedef {Record<string, HPropertyValue | HStyle | HChild>} JSXProps
12+
* @typedef {(props: JSXProps) => HResult } HFn
1213
*/
1314

1415
/**
@@ -26,13 +27,16 @@ export function runtime(f) {
2627
*/
2728
(
2829
/**
29-
* @param {string | null} type
30+
* @param {string | null | HFn} typeOrFn
3031
* @param {HProperties & {children?: HChild}} props
3132
* @returns {HResult}
3233
*/
33-
function (type, props) {
34+
function (typeOrFn, props) {
3435
const {children, ...properties} = props
35-
return type === null ? f(type, children) : f(type, properties, children)
36+
if (typeof typeOrFn === 'function') {
37+
return typeOrFn(props);
38+
}
39+
return typeOrFn === null ? f(typeOrFn, children) : f(typeOrFn, properties, children)
3640
}
3741
)
3842

test-d/automatic-h.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,3 @@ expectError(<a invalid={[true]} />)
5151
// The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime.
5252

5353
expectType<Result>(<a children={<b />} />)
54-
55-
declare function Bar(props?: Record<string, unknown>): Element
56-
expectError(<Bar />)

test-d/automatic-s.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,3 @@ expectError(<a invalid={[true]} />)
4141
// The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime.
4242

4343
expectType<Result>(<a children={<b />} />)
44-
45-
declare function Bar(props?: Record<string, unknown>): Element
46-
expectError(<Bar />)

test/jsx.jsx

+39
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,43 @@ test('name', () => {
125125
]),
126126
'should support a fragment in an element (#2)'
127127
)
128+
129+
/**
130+
* @typedef {import('../lib/core.js').HChild} HChild
131+
132+
* @param {{title: string; definition: string, children?: HChild}} options
133+
* @returns {JSX.Element}
134+
*/
135+
const Dl = ({title, definition, children}) => (
136+
<dl>
137+
<dt>{title}</dt>
138+
<dd>{definition}</dd>
139+
{children}
140+
</dl>
141+
);
142+
143+
assert.deepEqual(
144+
<Dl title={'Firefox'} definition={'A red panda.'}/>,
145+
h('dl', [
146+
h('dt', 'Firefox'),
147+
h('dd', 'A red panda.'),
148+
]),
149+
'should support functional elements'
150+
);
151+
152+
assert.deepEqual(
153+
<Dl title={'Firefox'} definition={'A red panda.'}>
154+
<>
155+
<dt>Chrome</dt>
156+
<dd>A chemical element.</dd>
157+
</>
158+
</Dl>,
159+
h('dl', [
160+
h('dt', 'Firefox'),
161+
h('dd', 'A red panda.'),
162+
h('dt', 'Chrome'),
163+
h('dd', 'A chemical element.'),
164+
]),
165+
'should support functional elements that render their children'
166+
)
128167
})

0 commit comments

Comments
 (0)