Skip to content
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

Back to the chalk board #105

Merged
merged 2 commits into from Jan 18, 2024
Merged

Back to the chalk board #105

merged 2 commits into from Jan 18, 2024

Conversation

WebReflection
Copy link
Owner

@WebReflection WebReflection commented Jan 18, 2024

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

Notes

html``; // results into an empty text node
html`${hole}`; // results into a fragment
// <!--<>-->$<!--</>-->

html`a${'b'}c`; // also results into a fragment
// result
// <!--<>-->a$c<!--</>-->

All fragments, which are those templates used just to propagate other stuff, including arrays or holes or whatever, are now delimited via <!--<>--> and <!--</>--> comments.

PersistentFragment automatically handle and deal with those cases internally and that's mandatory to have fragments boundaries.

All arrays pinned comments will show at runtime the amount of nodes they are dealing with: <!--[0]-->, as well as <!--[10000]--> are all valid pinned comments.

For the SSR story and hydration done well, holes are the only missing bit and these will be eventually wrapped around a <!--{}--> and a <!--{/}--> comment.

Once that lands in uhtml/dom or better, uhtml/ssr, hydration will be a piece of cake and it will be possible to transform the DOM into mapped template + interpolations entries without ever needing to trash a single DOM node that is already live.

This MR does the following:

  * the `isArray` operation happens only at the template level
  * text and attributes never need to brand check for instanceof Hole or isArray
  * arrays as holes never need nested `isArray` checks neither: DOM or Hole are all they care about
  * the loop over many items is done once, no matter if first time render or update
  * the removal of many rows is now back to the fast path
  * when the template is different, the previous stack is simply trashed, hopefully reducing edge cases shenanigans
  * both keyed and non-keyed cases have been stress-tested with js-frameworks-benchmark
  * on node update, if the current node is the same as before nothing happens
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
@WebReflection WebReflection merged commit 2d91c1a into main Jan 18, 2024
4 checks passed
@gbishop
Copy link

gbishop commented Jan 18, 2024

Whoa! Amazing!

Do we still need to keep the types consistent? (I.E. Can a Hole become a Hole[]?)

I really like not having to wrap everything in <span> (or whatever).

I'll be monkey testing it shortly.

@gbishop
Copy link

gbishop commented Jan 18, 2024

It survives 500 random changes (insert, delete, move, undo, redo, etc) followed by undoing them all to get back to the original configuration on my most complicated design.

A quick test answers my question about consistency. If I allow a Hole to become a Hole[] it breaks. Easy enough to avoid.

@WebReflection
Copy link
Owner Author

(I.E. Can a Hole become a Hole[]?)

again, the contract with arrays is: once array, always array ... the contract works the other way around: once a hole, always a hole ... this should be simple to think about or digest ... but ...

The current logic allows you to pass a hole and, after that, a hole that contains an array:

const holeInHole = value => html`${value}`;

// this works
render(document.body, holeInHole('a'));

// this also works
render(document.body, holeInHole(html`a`));

// and this works too
render(document.body, holeInHole(html`${'a'}`));

// so that this can work as well
render(document.body, holeInHole(html`${[
  html`a`,
  html`b`,
]}`));

Ths hole becoming an array out of a sudden won't work but we're back to the contract: once array, always array so that once hole, always a hole.

If a specific place in the template might ever accept a list of node, use an array there.

If there is this weird case where such place is never an array but then life happens and it needs to be one, pass a hole that wraps an array:

html`${html`${[]}`}`

That should now work out of the box.

Did I answer you?

@gbishop
Copy link

gbishop commented Jan 18, 2024

Excellent! This is much less error prone.

Thank you for this amazing work!

@WebReflection
Copy link
Owner Author

WebReflection commented Jan 18, 2024

If it helps anyhow to understand the current design, hence logic, each template literal is unique and it's parsed only once.

If the interpolation is an array in any place of the template, and such array is part of the content, not an attribute value, that place in the content is considered mutable, not in the sense that interpolations don't mutate the DOM already, but in the sense that such place is a special one for list of items operations.

Then there is the hole primitive, which is a much more convoluted thing to deal with, but if it's used to just carry more complex values not previously meant, it can represent a persistent fragment instead of a regular TEXT or ELEMENT node, and withing that persistent fragment all the same rules apply: if that hole (fragment) has a hole thta it's an array, we're back to the contract and the only placeholder it needs around is a node, in this case a persistent fragment unless wrapped in <div></div> or spans, so that normal array operations can work out of that parentNode (which is eventually the persistent fragment) without ever interfering with the rest of the surrouding DOM.

@WebReflection
Copy link
Owner Author

if anything, this conversation made think to export a holed utility that basically uses the following behind the scene:

export const holed = (value, xml = false) => (xml ? svg : html)`${value}`;

That would avoid parsing at all for the very same template literal returned by that function so that creating random fragment holes would be also faster out of the box (and dare I say debugging might be also simplified if used in the wild).

The details are evil though ... if used as template literal it will backfire so I need some more convoluted logic or a way o disambiguate upfront html VS svg.

const holey = (value, xml = false) => (xml ? svg : html)`${value}`;
export const holed = (template, ...values) => (
  Array.isArray(template) && values.length ?
    holey(values[0]) :
    holey(template)
);

But then again, we're back to isArray check all over the place, which is YAGNI to me.

@gbishop
Copy link

gbishop commented Jan 18, 2024

Wow. The injected comment nodes are actually useful now! I can easily see that a thing is an array and how many there are. It also feels like there are fewer comments (though I hardly understand why people care).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

This sequence of renders crashes in both V3 and V4 Recursive fragment w/ lost lastChild during target update
2 participants