-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recursive fragment w/ lost lastChild during target update #102
Comments
You can’t pass array or null conditionally, you can pass array or [] though. See if this helps |
I tried the following code. const tpl = (tree, state) => {
const item = tree.find(x => x.name === state[0])
return html`
<div>
${tree.map(item => html`<span>${item.name}</span>`)}
</div>
${item?.children
? tpl(item.children, state.slice(1))
: [] // changed
}
`
} No errors occurred, but the results were not what I expected. Actual HTML: <div id="root1">
<div>
<span>A</span><span>B</span><!--isµ0-->
</div>
<!--isµ1-->
</div> Expected HTML: <div id="root1">
<div>
<span>A</span><span>B</span>
</div>
<div>
<span>B-A</span>
</div>
</div> "B-A" is not shown. |
I have created a more simplified code that can be reproduced. <!DOCTYPE html>
<meta charset="utf-8" />
<script type="module">
import { render, html } from "https://cdn.jsdelivr.net/npm/uhtml@4.3.4/index.js"
const tpl = (items) => {
const [first, ...rest] = items
return html`
<div>${first}</div>
${
// html or null
rest.length > 0 ? tpl(rest) : null
}
`
}
const tpl2 = (items) => {
const [first, ...rest] = items
return html`
<div>${first}</div>
${
// html or []
rest.length > 0 ? tpl2(rest) : []
}
`
}
const case1 = () => {
const update = (state) => render(root1, tpl(state))
update(["A", "B"])
update(["A", "B", "C"])
update(["A", "B", "C", "D"])
update(["X", "Y"]) // ERROR
}
const case2 = () => {
const update = (state) => render(root2, tpl(state))
update(["A", "B"])
update(["A", "B", "C"])
// update(["A", "B", "C", "D"])
update(["X", "Y"]) // NO ERROR
}
const case3 = () => {
const update = (state) => render(root3, tpl(state))
// update(["A", "B"])
// update(["A", "B", "C"])
update(["A", "B", "C", "D"])
update(["X", "Y"]) // NO ERROR
}
const case4 = () => {
// use tpl2
const update = (state) => render(root4, tpl2(state))
update(["A", "B"])
update(["A", "B", "C"])
update(["A", "B", "C", "D"])
update(["X", "Y"]) // NO ERROR, but it renders only X
}
for (const case_ of [case1, case2, case3, case4]) {
try {
case_()
} catch (err) {
console.error(`[${case_.name} error]`, err)
}
}
</script>
<div id="root1"></div>
<hr/>
<div id="root2"></div>
<hr/>
<div id="root3"></div>
<hr/>
<div id="root4"></div> |
Ok, in the release notes it's pretty clear you need to pass holes.
rest.length > 0 ? tpl(rest) : null
Can you please try to see if never falling back to an unexpected value works? |
I tried the following code. ${rest.length > 0 ? tpl(rest) : html``} But I got the following error.
|
out of curiosity ... this seems to be a fragment related issue ... what if you wrap all your fragments within a div? do you still have the issue? if not, I know what's the issue ... if yes, I need to really understand what's your intent/expectations and having a single conditional would help me smooth out the outcome. Sorry not much extra time so any extra help is welcome. |
I wrapped all the fragments in divs, but an error occurs.
Here is the code: <!DOCTYPE html>
<meta charset="utf-8" />
<script type="module">
import { render, html } from "https://cdn.jsdelivr.net/npm/uhtml@4.3.4/index.js"
const tpl = (items) => {
const [first, ...rest] = items
return html`
<div>
<div>${first}</div>
${rest.length > 0 ? tpl(rest) : html``}
</div>
`
}
const update = (state) => render(root1, tpl(state))
update(["A", "B"])
update(["A", "B", "C"]) // ERROR
</script>
<div id="root1"></div> |
actually the hint to use empty html was my mistake ... I am going to check other cases |
${item?.children
? tpl(item.children, state.slice(1))
: null}
edit that was not the issue, which is now properly described as issue description. |
digging more into it ... there's something too smart around the null case as that node gets lost in fragments ... I am not sure that's due too nested logic and I will find the culprit at some point but ... I think your use case is fairly edge ... nested fragment recursion doesn't look like often used in UI, it's usually always within a container but if you came here with this example I am sure you have a reason to use that pattern. For now, I can say that not "abusing" fragments that way works and when this scenario is desired using a container is likely a better way to present, or even style, the layout. I'll keep this open but I won't fix this too soon, thanks. |
I wanted to make the HTML flat for styling purposes without nesting it, so I used fragments. |
FWIWI I've checked v3 and indeed everything works as expected in there ... this might be a regression due completely new logic implemented in v4. I want these cases to work as well as they did before but I don't have too much extra time to actually figure out why the current logic wouldn't work here ... it's a great issue report after all, and a heck of a regression from the library side but all other demos I have work great so I couldn't fully get the error first. Useless to speculate as what's the culprit but I think the smart template "pre-parsed" upfront might be it ... I hope I'll come back with a better answer, and I am also still working on creating the minimal representation case for the issue ... as in: just one simple recursive case that fail, but the combinations in there might also be misleading so I can't narrow down the real issue. This is just to tell you: bear with me, I've realized these shenanigans are essential to have a stable v4 for all cases, but it looks like I'm not there yet, and this is none of users fault, or expectations. Use v3 if that worked well to date, as that will keep working well "forever" too. I will ping in here once I've got all your examples working without surprises. |
I did investigate and I am leaning toward this conclusion ... see this comment #103 (comment) and the following one ... for instance, your issue with fagments in fragments can easily be solved like this: const tpl = (items) => {
const [first, ...rest] = items
return html`
<div>${first}</div>
${rest.map(tpl)}
`
} Should I make the code slower and the interpolation intent ambiguous when arrays (growing/shrinking list of nodes) actually solve all of this? I am thinking every second more I shouldn't and I'd rather explain what is the issue and why there is such issue. I could fix it in code but every time I try the performance penalty is horrendous and all to avoid people shooting themselves in their foot. |
If interested in why that would work here the answer: that code would translate into this static template: <div><!--0--></div>
<!--1--> That The With recusrive fragment, that collection will look like this: <div><!--0--></div>
<div><!--0--></div>
<div><!--0--></div>
<!--1--><!--1--><!--1--> Every node in it would be pinned/related to the array comment instead of being just a persistent fragment because persistent fragments can exist but cannot have nested persistent fragments in them because nodes will get spread and lost so that when the inner template drop nodes because there's less recursion and the outer template tries to to update, some I could ignore those cases and just re-append nodes to the persistent fragment reference but that's quirk and dirt ... with arrays, every operation is diffed properly out of a specific pin of nodes, where nodes can be persistent fragments too. You provide a new persistent fragment, the previous one content will be replaced with the new node without issues, thanks to udomdiff fragment specific logic but the fragment itself won't try, by itself, to remove its nodes that could already be gone. TL;DR every time content is meant to grow or shrink, the diffing algorithm is the best option everyone have and it's deadly fast too. For code that is never meant to grow or shrink, a hole is all it takes. This case was meant to grow or shrink, hence I believe using an array there is the right solution/conclusion to fix your issue or you wrap the fragment so that none of this becomes an issue. I hope I've managed somehow to explain what's going on and why plus how to solve it or change it to make it work best. |
This MR fixes #102 and fixes #103 + it provides further hydration hints out of the box. Current changes: * each fagment is demilited by `<>` and `</>` comments: see notes * this is a linear render: values are never looped more than once * this version of *uhtml* is even more Memory friendly: a lot has been refactored to consume and recycle as much as possible * the fragment in fragment issue has been resolved * the array hole in tags has been converted into a fragment case * the PersistentFragment has been refactored to survive edge cases * the performance is either better or the same as before * the Array hole now is a `<!--[N]-->` comment where `N` is the amount of nodes handled * holes are still transparent so that the amount of nodes is still ideal * a new code coverage goal has been reached: 100% of everything, including uhtml/dom * a new test has been written to help out with expectations on the DOM world (browsers) as well as SSR * the SSR story is still to be defined but everything is coming out nicely ... there are fragment hints, array hints, only missing hints to produce a DOM to Template transformer are holes which might land on SSR version only, as it would be ugly to have so many comments in the wild for no reason
Latest uhtml fixed the presented The What you want to do there is really: const tpl2 = (items) => {
const [first, ...rest] = items
return html`
<div>${first}</div>
${rest.map(tpl2)}
`
} This preserve the once array, always array contract and doesn't need to deal with a last-entry array nobody would know what to do with. The former case instead just works now though. Please read notes in this MR #105 to understand how the logic works now and expect layout changes as that's inevitable and for good too ... fragments are still not suggested in general, but as these used to work reliably, all I could do was to bring these back in an even better shape. |
Thanks for fixing and the detailed explanation. |
I get an error in complex cases.
case1 gives the following error.
However, it does not occur in case2 and case3.
This can be avoided by wrapping the top level of the html returned by the tpl function in a div.
The text was updated successfully, but these errors were encountered: