-
Notifications
You must be signed in to change notification settings - Fork 14
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
Fix: Repeat/Map now always rerender. #183
Conversation
Ping @klebba who was affected by this issue, and should be able to confirm if I understood it correctly and have resolved it satisfactorily. |
Hi @Kelketek — thank you for taking this on. We will take a look at get back to you; just to set your expectations I would anticipate this taking a few weeks due to Summer vacations and various internal ongoings. One question: did you intend to merge to |
x-element.js
Outdated
if (typeof key !== 'function') { | ||
throw new Error(`Unexpected map key "${key}" provided, expected a function.`); | ||
} | ||
return Template.map(items, identify, callback, key,'map'); | ||
} | ||
|
||
/** Shim for prior "repeat" function. Use "map". */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@theengineear is repeat
a backward compatible thing? on the deprecation path in favor of map
?
.gitignore
Outdated
@@ -1 +1,2 @@ | |||
node_modules | |||
.nvmrc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it's customary to add this type of thing to your workstation's global .gitignore
because nvm
is not a prerequisite of this lib
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this-- I had no idea that .gitignores above a repository root would be respected, but on reflection I can't think of why they wouldn't be. I'll remove this.
Edit: Haha, turns out they indeed wouldn't be. There's an extra step, git config --global core.excludesfile ~/.gitignore
. Writing this for anyone else who learned it for the first time and finds this later.
Also FYI — I gave this a cursory glance but still need time to complete a more thorough review — thanks! |
@klebba |
@klebba Thanks for the initial pass-- I've addressed the one particular note you had for me so far, and look forward to the full review. I'll wait on squashing 'til that's complete. :) |
Got it — thanks for the context @Kelketek — would it be problematic for you if we were to merge |
@klebba No, not if you believe that |
e027639
to
a9acda7
Compare
@Kelketek just pinging here to let you know that we haven't forgotten about this. Thanks for your patience |
@klebba You're welcome! I see I've got some conflicts now as well so I'll rebase when I get a minute. |
d221111
to
fc7f6d9
Compare
@klebba Rebased! :) |
@Kelketek — thanks for your continued patience. @theengineear and I met today to thoroughly review these proposed changes. Great work! We also revisited #179 to generate a reproduction case for the original issue: #179 (comment) After some discussion we think that the XElement We will leave a few comments on this pull request for your consideration, but also want to set your expectations:
|
Hi @klebba ! I'd love to see the comments and will see what I can do to address your concerns. I'd like to see this through. |
x-element.js
Outdated
const reference = Template.#createWeakMapReference(); | ||
const context = { identify, callback }; | ||
const updater = (type, lastValue, details) => Template.#map(type, value, lastValue, details, context, name); | ||
updater.value = value; | ||
updater.value = key(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We think perhaps map
should always be marked for re-render (and the call signature here would not change). It might be as simple as removing the guard here:
Line 1586 in e60005b
if (value !== lastValue) { |
It could take some back and forth to get the behavior/spec/semantics right, but we could always take this up in a future change set. Hope that makes sense!
test/test-template-engine.js
Outdated
@@ -532,6 +532,48 @@ describe('html updaters', () => { | |||
container.remove(); | |||
}); | |||
|
|||
it('repeat re-runs each time by default', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets rework this so that it covers the repro case in #179 — e.g. the spec change should cover the user-facing confusion around trying to bind to changing outer context within map
test/test-template-engine.js
Outdated
container.remove(); | ||
}); | ||
|
||
it('repeat respects a cache key', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this spec change covers an implementation detail that we would prefer not to leak to the end user (in other words: we should not include this interface in the spec — map
and repeat
should not have changes to their call signature)
1beedcc
to
2ba0b42
Compare
@klebba I've added a commit that removes the additional key function/interface change. Let me know if this looks good to you, and I'll squash these down so they're ready for merge. :) |
Thanks for re-writing that spec and updating the implementation — it’s much clearer to see the real-world requirement here 🤘 I’m going to go through and leave some nit-picky comments in there to make sure the changes land in a consistent way with proximal code. |
Glad to hear it, @theengineear :) I'm heading off for the night but expect to clear out the nits tomorrow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some minor comments in here. At a high-level:
- Fix up some stylistic nit-picks in here to make it match proximal code.
- Pull documentation updates into a different PR.
Thanks again for driving this change 🤘
test/test-template-engine.js
Outdated
<div> | ||
<ul id="target"> | ||
${repeat(items, item => item.id, item => { | ||
return html`<li id="${ item.id }">${ lookup?.[item.id] }</li>`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super-nit: I think proximal code doesn’t put spaces around interpolations:
return html`<li id="${item.id}">${lookup?.[item.id]}</li>`;
(same goes for the other test)
test/test-template-engine.js
Outdated
const container = document.createElement('div'); | ||
document.body.append(container); | ||
const items = [{ id: 'a' }, { id: 'b'}, { id: 'c' }]; | ||
let lookup = {a: 'foo', b: 'bar', c: 'baz'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Add spaces around object literals:
let lookup = { a: 'foo', b: 'bar', c: 'baz' };
(same goes for the other test)
test/test-template-engine.js
Outdated
assert(container.querySelector('#a').textContent === 'foo'); | ||
assert(container.querySelector('#b').textContent === 'bar'); | ||
assert(container.querySelector('#c').textContent === 'baz'); | ||
lookup = {a: 'fizzle', b: 'bop', c: 'fuzz'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Add spaces around object literals:
lookup = { a: 'fizzle', b: 'bop', c: 'fuzz' }
(same goes for the other test)
doc/TEMPLATES.md
Outdated
``` | ||
|
||
Another method might be to use an observation library to watch for changes to the objects you're iterating over, and then run the render function as a callback. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kelketek — I think what you’re trying to highlight here is that re-rendering is only triggered based on a change-by-reference detection (which is definitely by design).
I wonder if we could work this out in a separate documentation-focused PR? The implementation changes seem like they could be ready to merge pretty soon and I wouldn’t want to hold that up while we discuss the high-level documentation changes 🤙
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@theengineear Removed-- and I'll create a follow-up PR for this, probably with some revised wording. Yes, that indeed is what I'm trying to point out, but I maybe I should phrase it differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@theengineear Started rewriting this, and decided I'd like to use the library a bit more before giving it another stab. I'll create the PR when I have a more complete view of what to recommend to the reader and how to frame it.
4e4883d
to
a5af069
Compare
@theengineear I believe all nits are addressed (and I also found a couple of more since I realized eslint was set up here and I could run |
a5af069
to
397b178
Compare
@theengineear Just one more thing-- I've changed the base branch to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks again for sticking with this one — apologies that it took us so long to circle back!
No worries, @theengineear . I had a great time! @klebba Github indicates this is still waiting for your signoff. Did you want to give it one more look? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LG2M, thanks @Kelketek!
Resolves #179
Previously, the repeat and map functions were unable to rerender their contents at all unless the reference to the value they're iterating over changed. This change makes it to where, by default, repeat and map will run their template every time the template function renders, and adds a feature for memoizing the output with a cache key function if desired.
This merge request also adds documentation about the limitations of reactively re-rendering based on arrays and other complex data, and details workarounds/strategies for dealing with this.
As one more change, I've added
.nvmrc
to gitignore so that people using it don't pollute the repo. If desired, I could instead add the target npm version to its contents.Testing instructions:
Index:
let thing = document.querySelector('my-select')
thing.selectedId = 'bar'
() => entries
.selectedId
again.npm run test
to run the test suite, or let CI work its magic.Author notes and concerns:
I think this approach is solid overall, but it raises a few questions. First is that there could be performance impacts by making repeat/map render all the time by default. I could change the default to NOT rendering all the time, though I think this behavior would be more surprising than the current one.
The second is that repeat appears to be begrudgingly supported right now, and I've had to add a good bit of repeated code to continue supporting it. This might be a good argument to remove it entirely while we're here, but I don't know what the downstream impact would be for Netflix.
The third is that adding the key function changes the function signature (albeit in a backwards-compatible way) of repeat and map, and these signatures are already a little long.
The fourth is I don't know the code which uses this code-- so I don't have a strong sense of how often repeat and map are used in practice, and whether instead of adding a cache key to repeat/map, I should instead make it to where they always re-render, and instead add some kind of memoization function that can be embedded into the template arbitrarily. Let me know if you'd prefer I do that, instead.
Finally, I'm new to web components and this library-- I only started reading up on the both of them a few days ago in my spare hours. So there's a chance I'm missing something that would be obvious to someone who has worked with them further. Seeing as the issue has been open a while, I figured I'd take a crack at it anyhow :)