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

fix: Fix checking if scope is a jquery element #4176

Closed
wants to merge 6 commits into from

Conversation

gadenbuie
Copy link
Member

@gadenbuie gadenbuie commented Jan 14, 2025

Fixes rstudio/bslib#1159

Apparently there are cases where .bindAll() can be called on a text node rather than an HTMLElement or a jquery-wrapped element. The code path where this was observed was through the shiny-insert-tab message handler (see linked issue for a reprex).

I couldn't track down how that behavior arises. But it only becomes relevant for us when we want to dispatch the error console event since you can't dispatch an event on a text node.

This PR improves the check that differentiates between jQuery objects and DOM nodes and ensures that checkValidity() doesn't try to dispatch an event or call .get() on a text node.

@gadenbuie gadenbuie force-pushed the fix/binding-check-jquery branch from ec327ab to db123a1 Compare January 14, 2025 18:30
@gadenbuie gadenbuie requested a review from cpsievert January 22, 2025 17:26
Comment on lines 14 to 15
type BindScope = HTMLElement | JQuery<HTMLElement>;
type BindAllScope = HTMLElement | JQuery<HTMLElement> | Text;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will require some downstream changes, but it feels like, if checkValidity() is gonna support a Text type, then so should .bindAll(), etc.

Suggested change
type BindScope = HTMLElement | JQuery<HTMLElement>;
type BindAllScope = HTMLElement | JQuery<HTMLElement> | Text;
type BindScope = HTMLElement | JQuery<HTMLElement> | Text;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's super weird that text is being passed in at all. It bothers me not knowing where that's coming from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The el.innerHTML || el.textContent in this code seems like a likely source

for (const el of $divTag.get()) {
// Must not use jQuery for appending el to the doc, we don't want any
// scripts to run (since they will run when renderContent takes a crack).
$tabContent[0].appendChild(el);
// If `el` itself is a script tag, this approach won't work (the script
// won't be run), since we're only sending innerHTML through renderContent
// and not the whole tag. That's fine in this case because we control the
// R code that generates this HTML, and we know that the element is not
// a script tag.
await renderContentAsync(el, el.innerHTML || el.textContent);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just above it that section I found this comment

// C) $divTag may be of length > 1 (e.g. navbarMenu). I also noticed text
// elements consisting of just "\n" being included in the nodeset of
// $divTag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I refactored this to avoid adding a new type, and we just exit validity checking if it's passed something other than an HTMLElement or a jQuery instance. 33d6686

A few take-aways:

  1. Validity checks happen with each new renderContentAsync() and which nav insertion calls many times in one nav_insert(). Thus feat: De-duplicate client console messages #4177 will be helpful.
  2. There are dragons in the nav insertion logic and I think it's okay to take the easy way out and treat the symptom in checkValidity().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The el.innerHTML || el.textContent in this code seems like a likely source

I don't think that's right. It's the type of el (i.e., the first argument to renderContentAsync()) that's the problem. The type checker incorrectly thinks el is always HTMLElement, when apparently it can also be Text. This can be confirmed by adding console.log(el) just before the renderContentAsync(el, el.innerHTML || el.textContent) call using the reprex you provided. And in this case, the Text nodes are just new lines. Apparently jQuery does this for HTML strings that look like this: $("<div></div>\n<div></div>"). Given that, it's really unfortunate that $divTag's type is JQuery<HTMLElement> because it really should be something more like JQuery<HTMLElement | Text>?

All that said, ideally we'd get to the bottom of the jQuery typing issues, but if that seems like a lot of work/impossible, I think I'd be OK with just adding the if check around that renderContentAsync() call (along with a comment about how el's type isn't quite right). I can't think of a scenario where el is Text that you actually want to render?

Copy link
Member Author

@gadenbuie gadenbuie Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jcheng5 and I looked through this and came to the same conclusion as you. So I added the check to only call that renderContentAsync() for element nodes in 4a01dde.

That said, I remembered while looking at this that about a year ago I ran into issues with a web component in an inserted nav panel being initialized twice and as a result having different behavior when finally added to the DOM because the component changes some attributes on load.

What we'd really like to do is to avoid doing both $(message.divTag.html) and then later calling renderContentAsync(). If you have any advice or context around why we currently do both or how we can get out of the situation, it'd be appreciated!

Comment on lines +1056 to 1062
// TODO: Only render tab content HTML once
// The following lines turn the content/nav control HTML into DOM nodes,
// but we don't insert these directly, instead we take the HTML from
// these nodes and pass it through `renderContentAsync()`. This means
// the inserted HTML may not perfectly match the message HTML, esp. if
// the content uses web components that alter their HTML when loaded.
const $divTag = $(message.divTag.html);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this shouldn't be a comment in the source, but the idea is to come back to this in a subsequent PR and see if we can do this with only one html -> DOM pass instead of two.

@gadenbuie
Copy link
Member Author

Closing in favor of #4179, which includes core changes from this PR and solves the underlying problem of rendering the nav HTML twice.

@gadenbuie gadenbuie closed this Jan 23, 2025
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.

bslib::nav_insert() with repeated inputs fails with error console changes in shiny 1.10.0
3 participants