-
-
Notifications
You must be signed in to change notification settings - Fork 13
✨Add support for functional elements #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This comment has been minimized.
This comment has been minimized.
21504df
to
663455e
Compare
663455e
to
af30cad
Compare
Looks like I got the capitalization of the "description of changes" wrong. Also, might be out of order (I moved it up to the top) It looks like the code linter is failing, but hard to figure out where. |
You added undone todos, which is why it says it’s not ready yet! You can run Line 81 in e031133
|
@wooorm Do you have any thoughts on how to implement the "classic" runtime? As far as I can tell it is a pass through to the |
Depends on whether we want to support |
c20acd7
to
c1e22bd
Compare
jsx-factory.js
Outdated
*/ | ||
export function h(typeOrFn, props, ...children) { | ||
if (typeof typeOrFn === 'function') { | ||
return typeOrFn({...props, children}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be nice to infinitely flatten children, so we can pass this in JSX:
function Foo({ children }) {
// children is ['bar', 'baz', 'fooz']
}
<Foo>
{'bar'}
{['baz']}
{[[[['fooz']]]]}
<Foo>
This is just a silly hardcoded example, but in practice I think it’s common to use a lot of .map()
and .filter()
with JSX.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@remcohaszing Do you think this should be a blocker?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because changing it later would be a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- There are cases where
children
is currently not an array, right? (i saw a test for a function). Do we even want to support that? - if anything can be passed (such as functions), that also means a user can pass an array by reference, which we shouldn’t touch, how to handle that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can just create a flat copy. It should be as simple as:
return typeOrFn({...props, children: [children].flat()})
And we need to do something similar for the automatic runtime.
We could go a step further and also flatten root
nodes. That requires a bit more additional code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think children is just an iterable of values that have meaning to the function. If that function creates an intrinsic element like A
, then it makes no sense to have children that are anything other than child nodes. However, if it is a function component, then it's up to the function how it wants to interpret its arguments.
I would say you definitely do not want to flatten children in the case of a component, but it would be an option for creating low level html nodes. I don't have enough experience with what flattening would feel like to have an opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is, in this example, Component
gets a children
prop as a string.
<Component>
{'foo'}
</Component>
In this example, it’s an array of two strings.
<Component>
{'foo'}
{'bar'}
</Component>
React normalizes this. I think it’s a good idea to do this here as well. It makes sense to me to always pass a flat array, but I’m open to other suggestions as well. Perhaps strings should even be transformed to text nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React normalizes this.
That’s not what React does, it’s how Babel and others do their JSX compilation. And there’s as way around it: by passing children
. children
is just a prop. <X children={SomeFunctionOrClassOrWhatever} />
or so is fine.
I don’t think we should touch children
. a) React and similar don’t either, b) it would be slow to iterate / call .flat()
on every children array?
e87af51
to
e6f69d9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Some initial nitpicks on the code!
jsx-factory.js
Outdated
* @typedef {(props: JSXProps) => HResult } HFn | ||
*/ | ||
|
||
import {h as hast} from './index.js' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This doesn’t support SVG
- Instead of creating another factory, and an HTML and SVG function (and in the future mathml), I think I now prefer to just add this to the regular
h
; also the affect on the docs gets a bit complex with a separate/jsx-factory
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so to confirm, we'll enhance the core h()
function to accept null
, string
, or function
. Do you want me to document this feature? It seems like it's not very useful when you are using hypersrcipt because you can just call the function yourself. I.e. why say:
h(DLEntry, { key: "x", value: "y" })
When you could just as easily say:
DLEntry({ key: "x", value: "y" });
There might be a reason to invoke it with the former, but I can't think of it. What if I just note on the @param
declaration "accepts a function in order to support JSX"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React and preact are the same? React.createElement(DlEntry, {key, value})
vs DlEntry({key, value})
vs <DlEntry key={key} value={value})>
. The main reason is that you don’t have to create 2 wrappers and explain to users that one h
is different from another h
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that this is simpler. I'm just asking how to explain to users who don't use JSX why this is also something that the h
function can do:
h(DLEntry, { key: "x", value: "y" })
If I wasn't using JSX, I would be like "why is that ever useful", so I was suggesting a brief note in the jsdoc that this is for JSX support.
test/jsx.jsx
Outdated
]) | ||
}, | ||
{depth: 20} | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console call should be removed when done
47e8d4f
to
f1b4d19
Compare
f1b4d19
to
890a9d4
Compare
Here's an attempt at what we discussed. It simplifies things greatly to have everything flowing through a single codepath. However, it looks like the coverage tests are failing? Unfortunately, I'm nearing the end of the amount of time I can spend on this and there is still the coverage to sort out and also the issue of to-flatten or not-to-flatten remaining. |
@@ -58,18 +62,38 @@ export function core(schema, defaultTagName, caseSensitive) { | |||
* (selector: null | undefined, ...children: Array<HChild>): Root | |||
* (selector: string, properties: HProperties, ...children: Array<HChild>): Element | |||
* (selector: string, ...children: Array<HChild>): Element | |||
* (component: HComponent, props: HComponentProperties, ...children: Array<HChild>): HResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we support h(Component)
? h(Component, null)
? h(Component, Child)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And h(Component, {children: ['a']}, 'b')
?
@@ -98,6 +122,7 @@ export function core(schema, defaultTagName, caseSensitive) { | |||
} | |||
} | |||
} else { | |||
// @ts-expect-error cannot be `HComponentProperties` because we're not on that codepath. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn’t it be easier to reuse HProperties
for props, instead of a different but similar HComponentProperties
?
That would solve this. And probably make it easier to create a component MyLink
which wraps a
and internally calls h('a', {...props, something: true})
, which would currently fail I think?
jsx-factory.js
Outdated
*/ | ||
export function h(typeOrFn, props, ...children) { | ||
if (typeof typeOrFn === 'function') { | ||
return typeOrFn({...props, children}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React normalizes this.
That’s not what React does, it’s how Babel and others do their JSX compilation. And there’s as way around it: by passing children
. children
is just a prop. <X children={SomeFunctionOrClassOrWhatever} />
or so is fine.
I don’t think we should touch children
. a) React and similar don’t either, b) it would be slow to iterate / call .flat()
on every children array?
I'm going to park this for now because it seems like there are still several loose ends that need to be sorted out, and I'm officially over my time budget for this. I ended up creating a simple adapter directly from JSX to HAST that satisfies my immediate need, and that I published here: https://github.com/thefrontside/hastx I'd love to see somebody else perhaps use this as a blueprint to push this forward (or not), but either way, it was a pleasure to have made the attempt. |
Description of changes
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
h()
function. We either need to wrap this function or add support for function expansion inside it as well.Initial checklist