solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.05k stars 914 forks source link

Runtime element changing/cloning #252

Closed MrFoxPro closed 3 years ago

MrFoxPro commented 3 years ago

I believe it's possible to implement feature like cloneElement in react. It could be really useful.
Example from react: https://stackoverflow.com/a/50441271/8086153

ryansolid commented 3 years ago

Is there a suggestion?

I haven't really come up with a way that this feature makes sense. I do not see how it's possible. On one side there is already an element.cloneNode that is shipped in the browser. On the other side there is nothing to clone. Solid has no actual components. Cloning it would be like cloning a function and its closures with no way to interface with it.

There was discussion about this in #126 (most specifically this comment here: https://github.com/ryansolid/solid/issues/126#issuecomment-597810845). And a couple other places. I honestly have no idea how doing this could be possible.

Now it is possible I'm missing something but as I'm right now I have no ideas.

MrFoxPro commented 3 years ago

Yeah it is suggestion. I think problem is not to clone, but to dynamically change existed nodes Maybe it could be done at dom-expressions level?

MrFoxPro commented 3 years ago

for example, I need to change ref in Parent component

function Child(){
    return <div onClick={()=> console.log('hello, world!')}/>
}
function Parent({children}){
    let childRef;
    const child = children[0];
    child.ref = (ref)=> childRef = ref;
    return <div> Hello, {children}</div>
}

My suggestion would be to create hook that should return new mutated children:

function Child(){
    return <div onClick={()=> console.log('hello, world!')}/>
}
function Parent({children}){
    let childRef;
    childrenMounted((children)=>{
        children[0] = {....children[0], ref: (ref)=> childRef = ref};
    })
    return <div> Hello, {children}</div>
}

I'm not sure about this, could it be possible at least on transpile level? upd seems like not good example, it's too hard to imagine this when you do not have actual understanding how it works under hood

MrFoxPro commented 3 years ago

Actually I do not know how refs are working it solid. Do they create at transpile time as links to nodes?

ryansolid commented 3 years ago

props.children in Solid is a constructor, not a bunch of VDOM nodes like React. So in your examples children[0] is a DOM node. By the time you could read children they'd already be realized. At which point declarative syntax goes out the window mostly. You could use the underpinnings of the Spread operator.. SolidJS DOM does have an spread operator(https://github.com/ryansolid/dom-expressions/blob/master/packages/dom-expressions/src/runtime.js#L116) which sort of fits the bill. Most of the parameters are optional so passing element and bindings would be sufficient. Still question of getting the node.

I mean this far for an actual static DOM node you can:

function Parent(props) {
  const { children } = props; // its static resolve it children === div
  // do whatever you want with children, then
  return <div> Hello, {children}</div>
}

However now insertion looks like:

<Parent>{state.condition && <Child/>}</Parent>

We no longer can static resolve. props.children changes. All we can do is intercept it. You would think something like:

function Parent(props) {
  const children = () => {
    const c = props.children;
    // do whatever with children;
    return c;
  }
  return <div> Hello, {children()}</div>
}

However, that isn't the end of the story. Since if you actually look at what that generates props.children returns a function: https://solid-template-explorer.netlify.app/#DwBQhgTgpgdgLgPgN4Gc5jlAdAYwPYwAmAlnMQQAQBkVFwAwgBbEA2hA9AgL7DvjTwEQA

In fact, depending on how convoluted, you can have functions of functions of arrays of functions of functions etc.. This is what I call the reactive onioning. It is possible to unwind this backwards (I do with Context https://github.com/ryansolid/solid/blob/master/packages/solid/src/reactive/signal.ts#L747) and perhaps that is the method we need. I did a bit of this in solid-transition-group but that is a restrictive case since I know single DOM children. https://github.com/ryansolid/solid-transition-group/blob/master/src/Transition.ts#L28

Inserting in the DOM automatically handles all this resolution but probably too late. So I think you are looking for:

function Parent(props) {
  const [state, setState] = createState({ title: "Something" })
  const children = () => {
    const c = resolveChildren(props.children); // again now we have the actual dom node
    // do whatever with children;
    spread(c, { get title() {  return state.title } })
    return c;
  }
  return <div> Hello, {children()}</div>
}

Refs work like this: https://solid-template-explorer.netlify.app/#DwPgUABBwCYJYDcICcCmAzAvAbwM4HsBbVAFwAs4A7AcwF8IB6cKWRFDHAClQBsBKCJhAQCxclWqCIvekzDAmQA They just do assignment or call functions at create time.

MrFoxPro commented 3 years ago

Now I get it. In my case working example is

const children = createMemo(() => props.children);
***
  return (
    <For each={data()}>
      {(el, i) => {
        const child = children()(el, i);
        child.addEventListener('click', () => {
          onElementClick(i());
        });
        addRef(child);
        return child;
      }}
    </For>
  );

i think it should be further investigated for possible perfomance and ethic issues

ryansolid commented 3 years ago

Yeah.. I mean this is the last resort. Almost necessary to do certain types of DOM manipulation like animation co-ordination. I mostly try to suggests patterns where we inject on create to allow declarative syntax. Like in this case passing the click handler to the child template through the callback function... like:

<MyListWrapper>{
  ({ item, onClick }) => <div onClick={onClick}>{item.title}</div>
}</MyListWrapper>

The benefit here is it goes back to declarative where the compiler controls it. The trickiest thing about what you posted is it will never work on the Server.. which is fine for isomorphic we can just write different code path, say things aren't supported etc.. Like I'm not worried about Transition Groups working on the server. But if moved to declarative code structures we get this for free. Refs and Events won't fire and Effects won't run in the server.

Of course these patterns put onus on the end-user. Another way to do this is to pass up and pass down...

<MyListWrapper clickHandlerRef={getHandler}>{
  (item) => <div onClick={getHandler()}>{item.title}</div>
}</MyListWrapper>

This works less intrusive in cases where you don't already have a callback function...

Hmm... let's think out of the box. Could we create a Component for this? Problem is its internals would be the same as what we are doing here so no different than a helper function. It would need to be an intrinsic compiled element for this to work. Challenge is this would still have to be client only since reopening strings is off the table.

Not sure if anyone has other ideas. How do non-React libraries handle these things?

ryansolid commented 3 years ago

Just was shown this. Felt relevant here: https://kentcdodds.com/blog/how-to-give-rendering-control-to-users-with-prop-getters

ryansolid commented 3 years ago

I've manage to generalize a children helper which will give you a memo with the fully resolved children. I'm not doing any transforms like always an array or anything at this point. So wouldn't mind thoughts there. But I think that is the best we can do here. Can't clone so stuff like prop/getters and the other discussed methods are the only recourse, but atleast you can get the DOM elements and actually interact with them via an easy helper. This adds support for like putting <For> inside <Switch> etc.. it's in beta now but will be 0.24.0 when it is released.

ryansolid commented 3 years ago

Ok I've released it. You use it like:

const getChildren = children(() => props.children)

createEffect(() => {
   const children = getChildren();
   // do something
})

It creates a simple memo but what it does is fully resolve all the dynamic parts in between. So if the end dropped in a static list or use <For> to create those nodes it is all the same. It's the improved version of what you were doing. With this I think we've done all we can here.

imedadel commented 2 years ago

@ryansolid Reading this discussion, I assume that something similar to Slot would be impossible to replicate in Solid? (I'm working on porting parts of Radix to Solid)

ryansolid commented 2 years ago

That's a weird example swapping the anchor in the button so I don't quite get the use case. But I mean reactivity can do anything. Just you have to recognize there is no virtual model in between. So when we update things those are real DOM nodes. But something like Slot doesn't really require anything that special. I used an internal spread helper which is one of the few helpers that can come in handy I think but really you could do any direct DOM manipulation there as it is just a DOM node.

https://playground.solidjs.com/?hash=-2011641040&version=1.3.3

imedadel commented 2 years ago

That's really neat! Is spread documented anywhere?

As for the use case for this, usually, the prop as is used when you want to render your own component, similar to <Dynamic />, however, it comes with a bunch of TypeScript issues, so Radix came up with the idea for this Slot.

ryansolid commented 2 years ago

No that was the thing I've been hesitant documenting some of these just to keep things open for change in the future as the compiler APIs tend to be a bit less stable and it's fine because the runtime and compiler update in lockstep. To be fair this one hasn't really changed interface wise for years.

Biggest challenge with that approach is SSR. Spreading after the fact doesn't really work. It's just a string. Mind you maybe something special purpose. There is a question of if there is a more generalizable too potential here for this too like Dynamic. It's always challenging because this operation is platform specific so sort of vying into making it for all, but something that enhanced an existing element does seem useful. Could be worth looking into. Not going to lie Dynamic is probably the buggiest component to get right for SSR and this would be probably even more so. But better me take that on than anyone else.

imedadel commented 2 years ago

Hmmm. Isn't there a way to somehow do the spreading at the compile step right now? Otherwise, do you recommend sticking with Dynamic?

ryansolid commented 2 years ago

Spreading can't really be done at compile time ever. We don't' know what is going to be passed in from a parent. Dynamic is doing the same thing. Spread is always a runtime de-opt in every framework. Requires pulling in code we wouldn't otherwise need. But what can you do?

More so with these after-the-fact approaches we've already created and serialized the thing. In the case of browser we have real DOM nodes.. in the case of the server it's just a string. In theory, might be able to find the end of the opening tag and insert some more strings.

lordanubi commented 2 years ago

My question is when we write {children} the children Components get printed in the page by Solid. Why can't we write something like: {children(addedProps)} and then Solid knows that the children components need to be printed with some added arguments (if it's function) or with some added attributes if it's a normal dom node (I think this can be done both SSR and CSR) correct me if I'm wrong please.