Andrei
Andrei

Reputation: 17

Import and render an html element with SolidJS

When my application loads, it will call the backend to obtain a description of components the user decides to add.

These objects should have a function to generate their html, take a button for example: it will export an html() method that returns a string containing an HTML text:

const buttonText = "I'm a button"
export default {
  html() {
    return `<button>${buttonText}</button>`
  }
}

On the frontend side, I will be using import statement to load the aforementioned button module and store it in a JSON object, sort of like a plugins manager.

When it comes time to render it, I tried using Dynamic:

<For each={plugins()} fallback={<p>Loading...</p>}>{ plugin =>
  <div>
    <Dynamic component={plugin.module.html()}>
    </Dynamic>
  <div>
}</For>

But it falls with DOMException: Failed to execute 'createElement' on 'Document': The tag name provided ('<button>I'm a button</button>') is not a valid name.

Which makes sense since it is expecting something like a "div" element, not a string like <button>I'm a button</button>.

What would be the correct way to render HTML strings in SolidJS?

Upvotes: 1

Views: 7057

Answers (2)

snnsnn
snnsnn

Reputation: 13698

If I understand your question correctly you are trying to import a regular HTML element and render it inside a solid component, than answer is simple, just put it between curly brackets like any other expression. Unlike React, Solid can render an HTML element directly.

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

const el = document.createElement('p');
el.innerHTML = 'Some Content';

function App() {
  return (
    <div>{el}</div>
  );
}

render(App, document.getElementById("app")!);

You don't need to use innerHTML property or you don't even need to wrap it inside a JSX element:

const el = document.createElement('p');
el.innerHTML = 'Some Content';

function App() {
  return el;
}

Few issues with your question. The html method is not returning an HTML element but a string:

const buttonText = "I'm a button"
export default {
  html() {
    return `<button>${buttonText}</button>`
  }
}

You can fix it by returning an actual Element. I called the imported object x in order to avoid code bloat since you can import any variable from another module:

const buttonText = "I'm a button";

const x = {
  html() {
    const el = document.createElement("button");
    el.innerText = buttonText;
    return el;
  },
};

function App() {
  return x.html();
}

If you are using x just to return a string, you may omit it completely and just export the element itself:

const el = document.createElement("button");
el.innerText = buttonText;

export default el;

As an alternative you can return the string and have the DOM render for you using innerHTML property. You can either create the element yourself:

const el = document.createElement('div');
el.innerHTML = x.html();

Or have Solid do it for you:

const txt = "<button>I'm a button</button>";

function App() {
  return <div innerHTML={txt}></div>
}

Either way it has nothing to do with Solid, it is browser's feature to render a string into HTML element when assigned to an element's innerHTML. Actually it is what Solid's template function use internally.

As a side note, Solid uses clone method to render multiple elements efficiently, if you are not rendering large HTML text with with lots of elements, createElement is better since it is cleaner and faster.

"Loading components dynamically" refers to something totally different, so does a component. A dynamic component means the rendered output is bound to a variable rather than a condition:

const RedThing = () => <strong style="color: red">Red Thing</strong>;
const GreenThing = () => <strong style="color: green">Green Thing</strong>;
const BlueThing = () => <strong style="color: blue">Blue Thing</strong>;

const [selected, setSelected] = createSignal('red');

const options = {
  red: RedThing,
  green: GreenThing,
  blue: BlueThing
}

<Dynamic component={options[selected()]} />

Still you need to pass a component, not a string. A string containing a valid HTML text is not a JSX component.

It is best to express our problems in simple terms and avoid technical jargon because it makes really hard to understand.

Upvotes: 2

Jesse Good
Jesse Good

Reputation: 52365

A simple solution would be just to use innerHTML like so:

<For each={plugins()} fallback={<p>Loading...</p>}>
{ plugin =>
 <div innerHTML={plugin.module.html()}>
 <div>
}</For>

If you need something more interactive, perhaps using template and cloneNode which is what Solidjs compiles your code to normally. Here is a full example below. I created it on the playground.

import { render, template } from "solid-js/web";
import { For } from "solid-js";

function MakeButton() {
  const plugins = ["<button>Click me</button>"];

  const handleClick = () => {
    console.log("Clicked button");
  }

  const createElem = (el: string) => {
    const elem = template(el, 2);
    const ret = elem.cloneNode(true);
    if (ret.tagName === "BUTTON") {
      ret.onclick = handleClick;
    }
    return ret;
  }

  return (<>
    <For each={plugins} fallback={<div>Loading...</div>}>
      {plugin =>
        <div>{createElem(plugin)}</div>
      }
    </For>
  </>);
}

render(() => <MakeButton />, document.getElementById("app"));

Upvotes: 4

Related Questions