qwik icon indicating copy to clipboard operation
qwik copied to clipboard

[✨] Expose a function to render a JSXNode into an element in memory

Open DustinJSilk opened this issue 2 years ago • 11 comments

Is your feature request related to a problem?

To attach an element to external libraries like mapbox, we need to create html elements in memory. Currently, we have to create the element using native JavaScript functions.

To improve DX, it would be great to be able to import a Qwik component and to render it into an HTML element. This means the element can be used standalone in Playwright, Vitest, Storybook and in an app, as well as dynamically rendered into external libraries.

Describe the solution you'd like

A function with a potential signature createElement(node: JSX.Element). We can then do this:

const element = createElement(<MyMapMarker someProp={1234} />)
// add to library

Describe alternatives you've considered

Manually creating elements in JavaScript, which sucks

Additional context

No response

DustinJSilk avatar Apr 21 '23 09:04 DustinJSilk

I am not sure I follow, @DustinJSilk. Creating an HTML element would not benefit from any reactivity, as far as I see. Perhaps you are missing something like ref or useRef to programatically hold an element in a variable, so it can be passed to an external library.

That way, instead of creating an HTML element from JSX, you can simply reference any element already rendered.

Please take a look at the hook $onClick in this page. It assigns the current element to a variable. Perhaps such a thing can be done with onload.

In any case, I also found this: https://github.com/BuilderIO/qwik/issues/1100

phcoliveira avatar Apr 21 '23 14:04 phcoliveira

Hey @phcoliveira! Thanks for taking the time to have a think about this! 🙂

Im not looking to capture a reference to an element in the DOM, which using a ref works just fine for, im wanting to turn a JSXNode into an HTML element.

I also dont need to benefit from reactivity, i need to create HTML elements and pass them into a library like Mapbox.

Unfortunately, referencing an element from the DOM wont work either, as the element gets moved by Mapbox, which breaks Qwiks.

I've solved the problem by manually building the element with native JavaScript. Its not difficult, its just a poor DX which other libraries have a solution to.

Hope that helps to clear things up a little!

DustinJSilk avatar Apr 21 '23 14:04 DustinJSilk

Sure, @DustinJSilk. Although I think I understood your argument, I am not sure there is a viable solution to it. By transforming JSX into HTML it is implied that JSX would have to be present in the browser. I am not aware of any light version of it. Bundlephobia shows it weights about 150 kb Gzipped.

https://bundlephobia.com/package/[email protected]

And I am only considering the package size. Unfortunately I am not able to tell if this library can actually run in the browser of if it requires some features from Node or another runtime like Demo or Bun.

phcoliveira avatar Apr 21 '23 14:04 phcoliveira

Qwik builds elements on the frontend when ever it rerenders, so this is possible. If you look at what Qwik returns for component QRLs, its calls to _jsxQ and _jsxC which return JSXNodes. So Qwik is rendering JSXNodes on the frontend already and should be able to expose a function that returns an element without attaching it to the DOM.

DustinJSilk avatar Apr 21 '23 14:04 DustinJSilk

So sorry, @DustinJSilk, the library only weights 146 B. Somehow I confused it with 146 KB. So you might be right.

phcoliveira avatar Apr 21 '23 14:04 phcoliveira

if https://github.com/BuilderIO/qwik/issues/3758 is solved, you can use const e=createElement(...);e.outerHTML=renderToString(...).html

revintec avatar Apr 22 '23 09:04 revintec

Ooo that looks right, thanks for pointing it out @revintec ! Hopefully it gets fixed soon

DustinJSilk avatar Apr 23 '23 04:04 DustinJSilk

Is your feature request related to a problem?

To attach an element to external libraries like mapbox, we need to create html elements in memory. Currently, we have to create the element using native JavaScript functions.

To improve DX, it would be great to be able to import a Qwik component and to render it into an HTML element. This means the element can be used standalone in Playwright, Vitest, Storybook and in an app, as well as dynamically rendered into external libraries.

Describe the solution you'd like

A function with a potential signature createElement(node: JSX.Element). We can then do this:

const element = createElement(<MyMapMarker someProp={1234} />)
// add to library

Describe alternatives you've considered

Manually creating elements in JavaScript, which sucks

Additional context

No response

yes please! We need this badly! Could you please tell me how you are creating the elements? I want to clone a component, convert it to a html element and then append it to another html element.

Dindaleon avatar May 28 '23 22:05 Dindaleon

Adding another example of why this is needed:

When using Leaflet, let's say we want to show a qwik element inside a leaflet popup. If we take qwik's own example in qwik/starters/features/leaflet-map/src/components/leaflet-map/index.tsx:55:

marker(centerPosition).bindPopup(`Soraluze (Gipuzkoa) :)`).addTo(map);

We would like to have something like this:

marker(centerPosition).bindPopup(<QwikElem/>).addTo(map);

...

export const QwikElem = component$(() => <div>qwik</div>);

Well of course that's not working, Leaflet is crying that it Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.

In React we could solve that by rendering to string, like so:

marker(centerPosition).bindPopup(ReactDOMServer.renderToString(<QwikElem/>)).addTo(map);

So one would think that we can use qwik's renderToString in a similar way:

import { renderToString } from "@builder.io/qwik/server";
marker(centerPosition).bindPopup(await renderToString(<QwikElem/>)).addTo(map);

However this fails currently (qwik v1.3.1) with JSXError: <div> can not be rendered because it's not a valid direct children of the <html> element, only <head> and <body> are allowed.

yanivhamo avatar Dec 23 '23 13:12 yanivhamo

I forgot to mention the workaround, can be of value to OP. That's how we can achieve it currently with qwik:

const div = document.createElement('div');
await render(div, <QwikElem/>);
marker(centerPosition).bindPopup(div).addTo(map);

yanivhamo avatar Dec 23 '23 14:12 yanivhamo