WebReflection / uhtml

A micro HTML/SVG render

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Interpolated content that should have been removed sometimes "come back" in future renders

richardkazuomiller opened this issue · comments

I found a weird bug in my project where something that was removed from a previous render reappeared unexpectedly when moving the return value of html.for`${something}` to another parent.

For example

import {render, html} from 'uhtml';
const thing1 = html.for({})`Thing 1`;
const thing2 = html.for({})`Thing 2`;

let thing = thing1;

const parentHtml = html.for({});

const renderParent = () => {
  return parentHtml`${thing}`
}

parent = renderParent();

render(container1, parent);

thing = thing2;

renderParent(); // everything works as expected until here

render(container2, parent); container2 says "Thing 1"

If i wrap everthing in a div, like html.for({})`<div>${...}</div>` everything seems to work OK.

I made a codepen that demonstrates the bug better than the above https://codepen.io/richardkazuomiller/pen/vYmPXOV?editors=1111

last renderParent(); call should be parent = renderParent(); instead, right?

Anyway, this works:

import {render, html} from '//unpkg.com/uhtml?module';

const thing1 = html.for({})`Thing 1`;
const thing2 = html.for({})`Thing 2`;

const renderParent = thing => html`${thing}`;

render(container1, renderParent(thing1));
render(container2, renderParent(thing2));

The issue is that you are moving a "wired" reference around, so that you should not have const parentHtml = html.for({}); but just use html instead.

This is yet another case I wish I didn't offer html.for utility, as this use case is not what html.for should be used for: different containers, different logic ... not sure if I'll ever solve this issue, as it's not a real-world use case to me.

This is yet another case I wish I didn't offer html.for utility, as this use case is not what html.for should be used for: different containers, different logic… not sure if I'll ever solve this issue, as it's not a real-world use case to me.

Please do not remove html.for. For me it makes a world of difference.

last renderParent(); call should be parent = renderParent(); instead, right?

Wired references are WeakMapped, so the return value is always the same, which means it doesn't make a difference right?

The issue is that you are moving a "wired" reference around

Sorry, could you explain why moving a wired reference around is a problem? I see the difference, but I can't understand why can't just do render(container, wiredReference) 🙏

To explain a bit more about my actual use case, my code looks like this:

class MyComponent {
  html = html.for(this);

  render () {
    return this.html`...`;
  }

  renderTo(container) {
    render(container, this.render());
  }
}

I want to have a 1-to-1 relationship between each MyComponent instance and the stuff it renders, so if component.renderTo(container1) is called and then component.renderTo(container2) is called later, the results of MyComponent.render aren't duplicated.

I won’t remove for but every app based on for only is likely doing something wrong. The for is fir identity only when it matters, it’s not really about referencing everything, from containers to their nodes, and in this case is misused because an empty container is not necessary 99% if the time, as it brings literally no advantages

Meaning (early comment, sorry), an empty wire solves zero problems and it cannot wire anything … so what is the use case for that empty wire with already wired content?

I’ve read too late your last comment… it’s a fair use case but I don’t understand why your component would be empty, representing no node. Have you looked at uland, ube, or other components libraries based on uhtml? Anyway, I’ll try to see what’s going on there

I forgot to say thanks for the help 🙏 – thanks as always for the help!

Let me try to explain why it's empty as best I can. For example, I have a component that renders completely different things depending on its state

class MyComponent {
  html = html.for(this);

  isOpen = false;

  render() {
    return this.html`${this.isOpen ? this.renderOpen() : this.renderClosed()}`;
  }

  renderOpen() {
    return html.for(this, "open")`
      <style>.container {background: #eee;}</style>
      <div class="container">Hi I'm open! <button type="button">Click me</button></div>
    `;
  }

  renderClosed() {
    return html.for(this, "closed")`I'm closed now`;
  }

  open() {
    this.isOpen = true;
    this.render();
  }

  close() {
    this.isOpen = false;
    this.render();
  }

  renderTo(container) {
    render(container, this.render());
  }
}

I made another codepen for it too.

With this, I expected to be able to move the return value of MyComponent.render() around, but at the end both states are on the screen. I think the fact that it's "empty" isn't actually the problem. If I change the template to have some element as the prefix, it still doesn't work. The only way I can make it show the correct content is to wrap the whole thing in a div (this.html`<div>${this.isOpen? ... : ...}</div>` ), although I think having siblings is meant to work.

Have you looked at uland, ube, or other components libraries based on uhtml?

uland yes, ube no, since it looks like I got started before ube came out. I will take a look 👍 .

I've found the culprit ... basically the issue is that wires are not persistent fragments (as these don't exist, even if I've proposed these centuries ago) ... so while the wire gets updated when no node is used as container, their nodes don't, 'case uhtml by default bypasses wires when it's a single node, plus the differ cannot know if the parentNode of a live node is a wire, as it's bound to its live state on the DOM, not the virtual one.

Heck, dare I say this might be the only case where virtual dom can handle changes better, but to me the main issue is that a component that represent zero nodes is not really a component, and there's no explicit <>...</> fragment, react like, in uhtml.

It's also possible other libraries are somehow affected by this, but the long story short is that if you don't have a DOM reference that gets moved here and there, the for(...) fails if its content is a wire (empty template), and its content gets rendered in multiple containers.

For now, I think the solution is to use <div>...</div> around, or to remember last container and clean it up, or move its nodes once updated.

So ... current wires cannot deal with your use case, and I'm afraid I don't see any easy way to solve this (if any way at all), due lack of persistent fragments in the DOM specifications.

whatwg/dom#736

P.S. I think I'll document this in the README somewhere, but after thinking around this issue, it's probably a won't fix. I'll leave it open until some documentation is in place.

P.S.2 another issue, if not the only one, is that holes are reflected via comments on render, and if you change container, with an empty template and no container reference, you place two different comments in two different places, so you need a container either ways that represents the container, so that different holes are always related to the same comment, within the component container, explaining pretty much everything we see with these examples.

I see. I think keeping track of the container isn't possible in my situation, because I'm rendering to a document fragment that gets appended to the document, which brings us back to the problem of non-persistent fragments 🤦

However, I tried to change my codepen to clean up the container just to see if it would work and it still has the same result. I'm still confused about why the "closed" stuff is coming back at all, because after the second render, I don't expect uhtml to retain any references to the result of renderClosed().

https://codepen.io/richardkazuomiller/pen/abWxQMN?editors=1111

to whom it might concern, there is now a real PersistentFragment class that does not mock or mimic a fragment, it literally is a fragment and it returns its parentNode from the live content so that this issue should've been solved.

The html.for is now an explicit export from uhtml/keyed and it's called htmlFor and it has its counter svgFor utility for SVG content.

Closing as there's no reaction whatsoever.