-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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: Allow bindInputs()
to no-op when attempting to bind currently bound inputs
#3946
Conversation
`input$caption` and `output$caption` may not be the best idea for several reasons, but it was previously allowed Fixes #3943
bindInputs()
no-ops when attempting to bind previously bound inputsbindInputs()
to no-op when attempting to bind currently bound inputs
srcts/src/shiny/bind.ts
Outdated
if (!id) continue; | ||
|
||
bindingsRegistery.addBinding(id, "input"); | ||
// ...or if this element is already registered with a binding | ||
if ($(el).data("shiny-input-binding")) { |
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.
I think el.classList.contains("shiny-bound-input")
is a better test. If I'm reading this correctly, .shiny-bound-input
directly reflects whether this exact element is bound or not; whereas $(el).data("shiny-input-binding")
will be truthy if this element has ever been bound.
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.
And if you replace that test then you can simply continue
without needing to conditionally call bindingsRegistry.addBinding(id, "input")
.
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.
Yeah I went this way because I knew it'd induce conversation about which approach is better.
Lines 404 to 405 in 3e00632
$el.removeClass("shiny-bound-output"); | |
$el.removeData("shiny-output-binding"); |
unbindOutputs()
should remove both the class and the data, so the presence of either should in theory be a solid signal. I'm inclined to lean toward the data value, since it's less likely a user would have included data-shiny-input-binding
in the markup (but maybe they've added .shiny-bound-input
to the class list for some reason?). If we really really want to be certain we could test the value of $(el).data("shiny-input-binding")
but that seems excessive.
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.
Oh wait! Yes unbindInputs()
leaves the data attribute in tact. So there is a currently correct answer of testing the class
Lines 368 to 370 in 3e00632
$(el).removeClass("shiny-bound-input"); | |
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.
srcts/src/shiny/bind.ts
Outdated
// Check if ID is falsy, skip | ||
// Don't bind if ID is falsy... | ||
if (!id) continue; | ||
|
||
bindingsRegistery.addBinding(id, "input"); |
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.
The change here would cause currently-bound inputs to both rebind and to increment the binding registry count, unless an intervening unbindAll()
happened in between.
Which ultimately would conflate binding the same binding to the same element (it's already bound so the binding should no-op) with binding a new element with the same ID.
bindingsRegistery.addBinding(id, "output"); | ||
|
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.
There are a few conditions in the lines below (not seen in the diff) where we would continue
and not bind to el
because it's already bound. If we add this id
to the bindingsRegistry
here, then we'll overcount the bindings on id
.
srcts/src/shiny/bind.ts
Outdated
* @param inputOrOutput Whether the id is for an input or output binding | ||
* @param type Binding type, either "input" or "output" | ||
*/ | ||
function addBinding(id: string, inputOrOutput: "input" | "output"): void { | ||
function addBinding(id: string, type: "input" | "output"): void { | ||
if (id === "") { | ||
throw new ShinyClientError({ | ||
headline: `Empty ${inputOrOutput} ID found`, | ||
headline: `Empty ${type} ID found`, |
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.
I'm biased, but I think the more descriptive parameter/variable name is nice here as it's self-documenting and when using it you don't need to reference the typescript types to understand the possible values. If we think we may add more binding types in the future I could see making it more genericly type
, however. Also, it's not explicitly related to the issue the PR is trying to solve.
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.
I switched it to type
because isRegistered()
would take inputOrOutput: "all" | "input" | "output"
, which wasn't just input or output. I personally found inputOrOutput
to be slightly confusing -- given the longer name it feels like there's something more going on with it. I don't have strong feelings and could easily revert it for a smaller diff.
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.
Given the change suggested here, I'd advocate for bindingType
instead of inputOrOutput
. Then bindingType
is used everywhere to mean the same thing.
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.
Refactored in b124d1b
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.
Oh BindingType
and then ensuring consistency is better!
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 thoughts on the changes related to not flagging duplicate ids when there's only one of each type.
srcts/src/shiny/bind.ts
Outdated
if (nInputs > 1 || nOutputs > 1) { | ||
duplicateIds[id] = { input: nInputs, output: nOutputs }; | ||
} |
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.
Is this the only bit of logic that's different here? If it is I may advocate for reducing changes to just this to keep changes focused.
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.
Sort of, but we also have to count the number of each type earlier. Before you would add an id
to duplicateIds
if bindings.get(id).length > 1
but we need this logic to happen earlier so we can take into account whether there are more than one of the same type as the inclusion criteria.
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.
I think the diff is probably cleaner with this approach, but I'm sure there are other ways to do this same thing and I'm obviously open to any suggestions.
Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>
Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>
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.
I'm mostly good with this.
The one thing I'm uneasy about is the fact that it explicitly allows the duplication of id
s if they happen to be spread across a single pair of inputs and outputs. I definitely agree with the premise that we should not break legacy apps, but I think that by just building this fact into the code itself without an in-progress plan to fix it (or at least alerting the users of its non-idealness) then we risk codifying bad practices right into the codebase. At least previously this was a bug that we just happened to not catch.
One of the main purposes of the error console was to find exactly this type of long-standing bug that was never noticed before.
Maybe we should add a non-throw
ing warning message to the console? I'm curious to get @wch and @jcheng5 s opinions on this though as I very well may be being too dogmatic.
I'm also interested in other's opinions, but I'd like to reiterate: the bad practice here is using the same I've only found a few places in our documentation where we talk about inputs and outputs sharing a global namespace: both (article, package docs) are in sections related to Shiny modules. Neither are strongly worded or well-positioned enough to prevent this usage in real-world apps. And more importantly, the undefined behavior is theoretical, in practice our bindings work as intended in this situation. IMHO, the first iteration of the error console should not break apps that currently work. This PR as it is now, covers both the current state of Shiny and the future world where we've changed how input and output IDs are stored on the bound elements. I think augmenting the console to handle non-error messages is an excellent idea and would be broadly useful. That'd be great work to take up in a follow up PR. |
I wonder if maybe the PR should be split into two parts:
Currently, the changes are a bit spread out. Personally, I think the fixes for 1 are great and ready to go and I think it would be good to let those fixes go through quickly without being dragged down by 2. |
|
||
// count duplicate IDs of each binding type | ||
bindings.forEach((idTypes, id) => { | ||
const counts: { [T in BindingTypes]: number } = { input: 0, output: 0 }; |
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.
Should we use the type that was defined above?
const counts: { [T in BindingTypes]: number } = { input: 0, output: 0 }; | |
const counts: BindingCounts = { input: 0, output: 0 }; |
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.
Oh we should have removed the BindingCounts
type since the definition above is more concise and would expand automatically if we added new binding types. Sorry, this change was made via the suggestion interface, so this was an oversight.
Merging this now to fix the bug with |
Fixes #3943
Fixes #3944
Fixes #3945
Prior to #3931,
bindInputs()
could be called more than once on the same input element and only the first binding would be applied. This behavior was changed in 62eb485This PR makes a few changes to the implementation in #3931, plus one change to improve
bindInputs()
from the previous status quo:bindInputs()
once again no-ops if an attempt is made to bind to a currently bound input. Prior to Client error console and duplicate input/output binding errors #3931, we had a light-weight input binding registry and we would look to see ifid
was marked as bound, and no-op if it was in the registry. With Client error console and duplicate input/output binding errors #3931, we always re-bind the input element regardless of its binding status, most likely assuming thatbindAll()
was paired with a prior unbinding step at the right scope. This doesn't always play out in practice.With this PR, we now inspect the element directly. If it has the input binding data value that signifies it's currently bound, we do nothing -- after we make sure the element is included in the
bindingsRegistry
. (Inspecting the element directly is also whatbindOutputs()
does, so this bringbindInputs()
in line with that function.)For both the binding of inputs and outputs, I relocated the
bindingsRegistry.addBinding()
call to happen after Shiny performs the binding and before it announces the binding to the rest of the world via theshiny:bound
event. There were a few places where we could unintentionally count a new binding when no actual binding was taking place.I've updated the
bindingsRegistry.checkValidity()
method to only throw when an individual binding type has more than one binding. The letsinput$caption
andoutput$caption
work as before, but if a secondoutput$caption
were added, the client error console will report all bindings and alert users. In the future we could update the error console to also handle warnings (so maybe we should call this the Shiny client message console?)There are also a few additional miscellaneous fixes, the most salient of which is fixing the spelling of "registery" to "registry".
I've checked all of the shinycoreci apps affected by the error console (listed in rstudio/shinycoreci#248) and they all work as expected with the changes in this PR.