WebReflection / uhtml

A micro HTML/SVG render

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

fragments in fragments: comment, hence its parentNode, is lost (null)

Golmote opened this issue · comments

Hello !

I'm facing an exception parentNode is null and I can't figure out what I'm doing wrong. The following code renders twice without issue but fails on the third one.

import {render, html} from "uhtml";

const groups = ["1", "2"];
const items = [{"group":"1","title":"Title 1"}, {"group":"2","title":"Title 2"}];

function getGroupsView() {
    return html`
        <h2>Groups</h2>
        ${groups.map(group => html`
            <h3>${group}</h3>
            ${getItems(items.filter(item => item.group === group))}
        `)}
    `;
}

function getNormalView() {
    return html`
        <h2>Normal</h2>
        ${getItems(items)}
    `;
}

function getItems(items) {
    return html`
        <div class="items">
            ${items.map(item => html`<p>${item.title}</p>`)}
        </div>
    `;
}

function renderView(grouped) {
    render(document.body, grouped ? getGroupsView() : getNormalView());
}

renderView(true);
console.log('Rendered 1');
renderView(false);
console.log('Rendered 2');
renderView(true); // This throws TypeError: parentNode is null in udomdiff
console.log('Rendered 3');

View on Codepen

I've found that adding <div> elements in some places fixes the issue. Is it something related to document fragments?

it looks like fragments with placeholders might have issues but it's weird I've never had any other bug filed with this (as in: I don't think it's a very widely used pattern).

This works to me, although I understand it's not exactly what you're after:

import {render, html} from "https://unpkg.com/uhtml?module";

const groups = ["1", "2"];
const items = [{"group":"1","title":"Title 1"}, {"group":"2","title":"Title 2"}];

function getGroupsView() {
    return html`<div>
        <h2>Groups</h2>
        ${groups.map(group => html`<div>
            <h3>${group}</h3>
            ${getItems(items.filter(item => item.group === group))}
        </div>`)}
    </div>`;
}

function getNormalView() {
    return html`<div>
        <h2>Normal</h2>
        ${getItems(items)}
    </div>`;
}

function getItems(items) {
    return html`
        <div class="items">
            ${items.map(item => html`<p>${item.title}</p>`)}
        </div>
    `;
}

function renderView(grouped) {
    render(document.body, grouped ? getGroupsView() : getNormalView());
}

renderView(true);
console.log('Rendered 1');
renderView(false);
console.log('Rendered 2');
renderView(true); // This throws TypeError: parentNode is null
console.log('Rendered 3');

The issue is basically that you have arrays as holes within a fragment and the diffing uses the comment placeholder parent node to diff arrays content so that either you have a <div> or any other container around arrays, or arrays directly used as part of the fragment can't really be reliably rendered because we don't have persistent fragments in the DOM specs, hence once the array is revealed, their pin-pointer is gone.

I think I have an idea how to fix this, but hopefully it won't screw performance for every other common case ...

actually ... it looks like the issue is with fragments within fragments ... just changing only this part produces the expected output:

function getGroupsView() {
    return html`
        <h2>Groups</h2>
        ${groups.map(group => html`
            <h3>${group}</h3>
            <div>${getItems(items.filter(item => item.group === group))}</div>
        `)}
    `;
}

You can twist that around and still see correct outcome:

function getGroupsView() {
    return html`
        <h2>Groups</h2>
        <div>
          ${groups.map(group => html`
            <h3>${group}</h3>
            ${getItems(items.filter(item => item.group === group))}
          `)}
        </div>
    `;
}

so ... definitively a hole that is a comment placeholder, that renders a hole as a comment placeholder doesn't work ... I am not sure when I'll fix this as I have other priorities in life these days, but I'll keep this open until I manage to find a solution to this pretty weird edge case.

I've worked around the issue for now with an extra div as suggested.

Thank you for your answer and for this great library!