S
SolidJS12mo ago
lars

Manually mounting SolidJS component within HTML component via `render`

I'm on an adventure of doing some truly weird shit. I'm parsing YAML and dynamically creating HTML and inserting SolidJS components based on the template properties below:
group/main:
template: |
<div class="main">
{{ widgets }}
</div>
widgets:
- type: 'clock'
template: |
<div class="clock">
{{ hours }}:{{ minutes }}
</div>
group/main:
template: |
<div class="main">
{{ widgets }}
</div>
widgets:
- type: 'clock'
template: |
<div class="clock">
{{ hours }}:{{ minutes }}
</div>
(The user is the one who writes the YAML and runs the app, so it doesn't matter if it's insecure and dangerous) How I'm getting this to work currently:
export function ClockWidget(props) {
const [date, setDate] = createSignal(new Date());

const minutes = createMemo(() => date().getMinutes());
const hours = createMemo(() => date().getHours());
const interval = setInterval(() => setDate(new Date()), 1000);

// Create an `HTMLElement` from `props.template`.
const element = getParsedTemplate();

createEffect(
on(
() => [hours(), minutes()],
// When the hours/minutes change, re-parse the template and mutate
// the original HTMLElement in-place to match the new template. Maybe there's
// some better way to do this?
() => diffAndMutate(element, getParsedTemplate()),
),
);

function getParsedTemplate() {
return parseTemplate(props.template, { // props.template is a string `<div class="clock">{{ hours }} ...`
bindings: {
strings: {
hours: hours(),
minutes: minutes(),
},
},
});
}

return element; // This does not return JSX; it returns a regular `HTMLElement`.
}

export function Group(props) {
// Create an `HTMLElement` from `props.template`.
return parseTemplate(props.template, {
bindings: {
components: {
// Hardcode clock widget to be the only widget for this example.
widgets: () => (
<ClockWidget template={props.widgets[0].template} />
),
},
},
});
}
export function ClockWidget(props) {
const [date, setDate] = createSignal(new Date());

const minutes = createMemo(() => date().getMinutes());
const hours = createMemo(() => date().getHours());
const interval = setInterval(() => setDate(new Date()), 1000);

// Create an `HTMLElement` from `props.template`.
const element = getParsedTemplate();

createEffect(
on(
() => [hours(), minutes()],
// When the hours/minutes change, re-parse the template and mutate
// the original HTMLElement in-place to match the new template. Maybe there's
// some better way to do this?
() => diffAndMutate(element, getParsedTemplate()),
),
);

function getParsedTemplate() {
return parseTemplate(props.template, { // props.template is a string `<div class="clock">{{ hours }} ...`
bindings: {
strings: {
hours: hours(),
minutes: minutes(),
},
},
});
}

return element; // This does not return JSX; it returns a regular `HTMLElement`.
}

export function Group(props) {
// Create an `HTMLElement` from `props.template`.
return parseTemplate(props.template, {
bindings: {
components: {
// Hardcode clock widget to be the only widget for this example.
widgets: () => (
<ClockWidget template={props.widgets[0].template} />
),
},
},
});
}
2 Replies
lars
lars12mo ago
pt2 The parseTemplate fn is where the real magic happens. It compiles the template with Handlebars (a templating engine), and then afterwards SolidJS components are mounted into the compiled HTML
export function parseTemplate(
template: string,
options: {
bindings: {
strings?: Record<string, string | number>;
components?: Record<string, () => JSXElement>;
};
},
): HTMLElement {
const compiledTemplate = handlebars.compile(
template,
options.bindings.strings ?? {},
);

// Create a div from the compiled template.
const element = document.createElement('div');
element.innerHTML = compiledTemplate;

const componentBindings = Object.entries(options.bindings.components ?? {});

for (const [componentName, component] of componentBindings) {
// Get element to mount for the component.
const mount = getElementByText(element, `{{ ${componentName} }}`);

if (mount) {
render(component, mount); // render from "solid-js/web"
}
}

return element;
}
export function parseTemplate(
template: string,
options: {
bindings: {
strings?: Record<string, string | number>;
components?: Record<string, () => JSXElement>;
};
},
): HTMLElement {
const compiledTemplate = handlebars.compile(
template,
options.bindings.strings ?? {},
);

// Create a div from the compiled template.
const element = document.createElement('div');
element.innerHTML = compiledTemplate;

const componentBindings = Object.entries(options.bindings.components ?? {});

for (const [componentName, component] of componentBindings) {
// Get element to mount for the component.
const mount = getElementByText(element, `{{ ${componentName} }}`);

if (mount) {
render(component, mount); // render from "solid-js/web"
}
}

return element;
}
The issue I'm facing is that the manually mounted SolidJS components don't seem to be "wired up" correctly. Their onCleanup hooks aren't being called. Is there a correct way to insert the SolidJS components in the parsed HTML rather than just calling render()?
foxpro 🐍
foxpro 🐍12mo ago
There is hydrate, but I think it looks for hydration markers produced by ssr