-
Notifications
You must be signed in to change notification settings - Fork 125
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
feat: solid-primitives-parser #276
feat: solid-primitives-parser #276
Conversation
|
instead of having a global $TOKEN, $TOKEN is now scoped to createParser this way multiple parsers can co-exists. token.meta is now spread out over token. this way keys passed in tokenize() can be used to typeguard so we have narrower types while parsing
I continued my experimentations and added a cms-implementation as mentioned above: it can be found here A schema written in JSX generates a cms to enter data and typescript-types describing the schema, idea inspired by payload. Nested arrays are supported. These types could be consumed schema-to-ts.mp4A couple of design decisions:
so tokenize((props: Props, parserParams: ParserParams) => ..., { ...meta... }) becomes tokenize({ ...meta... }) instead of inferring the types from the callback-parameters they are now typed in a generic. It takes a type of type Token that looks like to illustrate type Green = Token<{id: 'green-id'}, {color: 'green'}>
// {meta: {id: 'green-id'}, props: {color: 'green'}};
type Red = Token<{id: 'red-id'}, {color: 'red'}>
// {meta: {id: 'red-id'}, props: {color: 'red'}};
type Color = Green | Red;
const x: Color;
if(x.meta.id === 'green-id')
x.props.color // Green | Red while now type Green = Token<{id: 'green-id'}, {color: 'green'}>
// {id: 'green-id', props: {color: 'green'}};
type Red = Token<{id: 'green-id'}, {color: 'green'}>
// {id: 'red-id', props: {color: 'red'}};
type Color = Green | Red;
const x: Color;
if(x.id === 'green-id')
x.props.color // Green there is still some friction with types, for example: type PropsGreen = {color: 'green'}
type Green = Token<PropsGreen, {callback: (props: PropsGreen) => void}>
// {callback: (props: PropsGreen) => void, props: {color: 'green'}}
type PropsRed = {color: 'red'}
type Red = Token<PropsRed, {callback: (props: PropsRed) => void}>
// {callback: (props: PropsRed) => void, props: {color: 'red'}}
type Color = Green | Red;
const x: Color;
x.callback(x.props)
// typerror x.props: PropsGreen | PropsRed is not assignable to parameter of type PropsGreen & PropsRed I would have preferred to keep
I updated this repo accordingly. |
API-idea
becomes
so customer-props are being added to the token becomes an explicit action done by the dev, instead of something we do behind the scenes. |
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.
Looks good.
Here are a couple of things that I caught
Need to look through this one more time later
replace the meta-object with a callback through which you pass the component-properties. remove Token-type helper to not prescribe to a way the tokens should be typed
…allback, tsx to ts
this seems to be a known and unresolved limitation of typescript and will probably stay so so I suppose out of the scope of this pr |
packages/jsx-parser/src/index.ts
Outdated
TProps extends { [key: string]: any }, | ||
TToken extends { [key: string]: any } & { id: string } | ||
>( | ||
function createToken<TProps extends { [key: string]: any }, TToken extends TTokens>( |
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.
is there a way you can make TToken
fit the union TTokens
without extends
?
type GreenToken = {id: 'green'}
const {createToken, childrenTokens} = createJSXParser<GreenToken>('color-parser')
const Green = createToken<Props>(props => ({id: 'green', extra: 'key'})
// no type-error, since {props, id: 'green', extra: 'key'} extends {id: 'green'}
set returnType to Accessor<TTokens> instead of () => TTokens
Until now I have only been testing with static schemas, except for For-loops in the CMS-example. Show returns a signal, to resolve these I added const resolveChild = (child: any) => {
while (true) {
if (typeof child !== 'function') return child
if ($TOKEN in child) return child
child = child()
}
} And added a memo to the callback of But now when implementing it in the ThreeParser, we get the following bug: bug.mp4once a new token is shown, we lose reactivity to the props of the original token. I thought first the props must have been resolved, but when I inspect the props of the token inside the console, I do get bug.mp4(code that implements adding and removing of children is in ThreeParser line 53: [UPDATE]I was able to fix the bug by wrapping the effects in bug.mp4
[UPDATE]For.mp4
|
I just realized you can also access the token-properties immediately from any callbacks by simply accessing createToken((props: Props) => ({
props,
id: 'token',
callback: function(){
console.log(this.id); // 'token'
console.log(this.props); // Props
}
})) that's pretty clean. |
played around with another parser: a poor man's flutter Screen.Recording.2023-01-05.at.23.50.38.movjsx to drive a canvas-layout, with konva as canvas-paint-library and yoga as flexbox-layout-engine. another pattern I discovered was to use the callback to initialize some values: export const Flex = createToken<PropsFlex, TokenFlex>((props) => {
const tokens = childrenTokens(() => props.children)
const node = Yoga.Node.create()
node.setFlexDirection(props.style.flexDirection)
return {
props,
node,
id: 'Flex',
}
} very handy. will start to write docs. |
I have been looking deeper into why The callback-function of the first Mesh is being disposed once another Mesh is shown, it runs when I do
[EDIT] so initially thought that wrapping the callback in a It does however prevent those cleanups that should not be happening: with the callback wrapped in a so yes, this part still needs a bit of work. [EDIT2] you also lose the typing of [EDIT3] if you wrap the whole callback in a sadly it does not seem to work to wrap the values returned by the [EDIT4] ok so i found the reason of the bug: the adding and removing of the meshes to the scene was being done inside an effect: const elements = new Map();
createEffect(() => {
const newElements = new Map()
tokens().forEach((token) => {
if (token.type === 'Object3D' || token.type === 'Light') {
if (elements.has(token)) {
newElements.set(token, elements.get(token))
} else {
untrack(() => newElements.set(token, token.callback(token.props as any)))
}
}
})
newElements.forEach((value, key) => {
if (!elements.has(key)) {
untrack(() => object.add(value))
elements.set(key, value)
}
})
elements.forEach((value, key) => {
if (!newElements.has(key)) {
untrack(() => object.remove(value))
elements.delete(key)
}
})
}) the the solution I found was to do something like:
and use this instead of I guess the bug makes a lot of sense if you understand how those [EDIT5] function mapTokens<T>(tokens: Accessor<T[]>, callback: (token: T) => any) {
const cache = new Map<T, () => void>()
createRootlessEffect(() => {
const current = new Set()
tokens().forEach((token) => {
if (!cache.has(token)) {
createRoot((dispose) => {
callback(token)
cache.set(token, dispose)
})
}
current.add(token)
})
cache.forEach((dispose, token) => {
if (!current.has(token)) {
cache.delete(token)
dispose()
}
})
})
} is a nice abstraction that could help a lot, const childrenEffect = (
object: THREE.Object3D,
tokens: Accessor<Token[]>
) => {
mapTokens(tokens, (token) => {
const element = token.callback(token.props as any)
object.add(element)
onCleanup(() => object.remove(element))
})
} very clean! I don't know if it should be a part of |
This is looking good for me. |
yes, looks great, @thetarnav ! |
@thetarnav
sometimes it's handy to use a token outside jsx, writing it like a callback might make sense there. |
yeah I don't mind, we could do that |
A primitive to tokenize your solid-components to enable custom parsing/custom renderers.
It exists out of two parts,
tokenize()
andchildrenTokens()
:const token = tokenize(callback, meta)
Object.assigned()
to this spoof-function, so they can be accessed from their parent$TOKEN
for validationconst tokens = childrenTokens(() => props.children)
children()
-api$TOKEN
-symbolI made a very simple example in the repo with a calculator, and played around with a threejs-renderer
I originally started playing with these type of hacks for this cms-experiment and @thetarnav mentioned that they also used something similar for
solid-aria
so it would be sweet to have this standardized. I think I will try to make a simplified version of the cms since it is quite a different usecase then the threejs-parser (callback() does return JSX which is being rendered, and data is inject from the parser).The
Object.assign()
-tip came from someone in the typescript-discord, either liquid or foolswisdom. The remark they gave back then was that it's impossible to type it in a way to disallow these spoofed JSX-elements to be used outside the Parser, this can only be done in runtime.talking points:
token.callback
does not take any parameters, but instead the props of the component are being passed into the callback. you can inject or manipulate data withDynamic
in the case of when you would use the callback to render, and I think you might even be able to set them directly on the token-function with another Object.assign, but haven't tested this. Another option possible would be to have the callback-function inside token to take besides the component-props also an object coming from the parent-parser: so something liketokenize((props: {...}, parserParams: {...}) => {...})
.callback()
it should be done explicitlycallback(token.props, {... parserParams ...})
to demistify the inner-workings.tokenize<Props>(() => {...}, {})
. that way the callback is completely decoupled from its props, which feels a bit less spoofy and then you could type callback independently from the component's props. I imagine that in most of the usecase you do want those props though, so it will be a lot oftoken.callback(token.props)
, but that's maybe better then doing it automagically.TODOS:
Also very open for API-suggestions.
I think this can become a very powerful tool in the solid toolchain, if done correctly. Since there is some spoofing going on to trick typescript to accept the JSX, the API-choices feel extra important.