-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Allow multiple top-level nodes when rendering StreamField blocks #11958
Allow multiple top-level nodes when rendering StreamField blocks #11958
Conversation
Manage this branch in SquashTest this branch here: https://laymonageboundwidget-multi-top-znj81.squash.io |
d0e7252
to
8567b34
Compare
But keep passing just the first element to the BoundWidget class
8567b34
to
7bcd49e
Compare
/* execute any scripts in the new element(s) */ | ||
runInlineScripts(tempContainer); | ||
|
||
/* execute any scripts in the new element */ | ||
runInlineScripts(dom); | ||
/* replace the placeholder with the new element(s) */ | ||
placeholder.replaceWith(...tempContainer.childNodes); |
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.
With this, we replace the placeholder with all the nodes in the HTML (that was put in the div
tempContainer
). However, we keep only passing the tempContainer.firstChild
as the element to the bound widget class (see that the above const dom = tempContainer.firstChild;
and the below return value do not change).
We change the runInlineScripts
call from the dom
(first child) to the tempContainer
in case there are scripts at the top-level (as the case with #11935).
Then, we need to move the runInlineScripts
call to before the placeholder is swapped, because once it is, tempContainer
will be empty. We can't do runInlineScripts(dom.parentElement)
since we can't be sure that the parent does not have other scripts already in place.
I suppose one possible gotcha is if the script relies on the fact that it used to run after the widget was put inside the DOM.
A bit related, in TeleportController
, we run script before the template element is put inside the DOM:
target.append(...this.templateFragment.childNodes); |
wagtail/client/src/controllers/TeleportController.ts
Lines 94 to 108 in 4f5ffa8
get templateFragment() { | |
const content = this.element.content; | |
// https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode (returns the same type) | |
// https://github.com/microsoft/TypeScript/issues/283 (TypeScript will return as Node, incorrectly) | |
const templateFragment = content.cloneNode(true) as typeof content; | |
// HACK: | |
// cloneNode doesn't run scripts, so we need to create new script elements | |
// and copy the attributes and innerHTML over. This is necessary when we're | |
// teleporting a template that contains legacy init code, e.g. initDateChooser. | |
// Only do this for inline scripts, as that's what we're expecting. | |
runInlineScripts(templateFragment); | |
return templateFragment; | |
} |
(It's inside a getter for templateFragment
which is called before the .append()
call.)
Alternatively I guess we could create an array that holds any script elements in tempContainer.childNodes
, then pass that to runInlineScripts
, but that means the util needs to be updated to accept an array of script elements.
Thanks for investigating, @laymonage! If we're not going to take a hard line on insisting that the widget HTML must be a single element, then I think we ought to make it as accommodating as possible, rather than an in-between state where we're making unwritten assumptions such as the first element being the correct one to pass to BoundWidget. (For example, this would be broken again just by adding a Given that the only thing BoundWidget does with the passed-in I'm also sceptical about whether it's safe to invoke Will pick these changes up now. |
@laymonage Updated version now pushed as b310b11. |
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 @gasman! Your changes work well, tested on Safari 17.5, Chrome 125, and Firefox 125.
Just one small comment, but otherwise it looks good to me! (Can't approve as I'm the PR author 😄)
const scripts = element.querySelectorAll( | ||
'script:not([src])', | ||
) as NodeListOf<HTMLScriptElement>; |
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.
Since we've extracted the selector to a variable...
const scripts = element.querySelectorAll( | |
'script:not([src])', | |
) as NodeListOf<HTMLScriptElement>; | |
const scripts = element.querySelectorAll<HTMLScriptElement>(selector); |
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.
Oops, yes, that was indeed what I was aiming for :-) Thanks!
But don't change what's passed to the
BoundWidget
class (which means still just the first element), so this shouldn't affect any of theBoundWidget
s.Fixes #11935.
Will add tests if this is indeed what we want...Test added.