feat(InputBinding): subscribe callback now supports event priority (#4211)

* feat(InputBinding): subscribe callback now supports event priority

* Update NEWS.md

* Update srcts/src/shiny/bind.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* `yarn build` (GitHub Actions)

* Simpler and more consistent typing

* Support a suitable object as input

* Provide a type for the callback itself, not just the valueit's given

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
This commit is contained in:
Carson Sievert
2025-06-19 10:27:45 -05:00
committed by GitHub
parent e6b22d86b6
commit b25e6feabb
8 changed files with 76 additions and 45 deletions

View File

@@ -8,6 +8,8 @@
* The family of `update*Input()` functions can now render HTML content passed to the `label` argument (e.g., `updateInputText(label = tags$b("New label"))`). (#3996)
* The `callback` argument of Shiny.js' `InputBinding.subscribe()` method gains support for a value of `"event"`. This makes it possible for an input binding to use event priority when updating the value (i.e., send immediately and always resend, even if the value hasn't changed). (#4211)
## Changes
* Shiny no longer suspends input changes when _any_ `<input type="submit">` or `<button type="submit">` is on the page. Instead, it now only suspends when a `submitButton()` is present. If you have reason for creating a submit button from custom HTML, add a CSS class of `shiny-submit-button` to the button. (#4209)

View File

@@ -5611,19 +5611,14 @@
function isJQuery(value) {
return Boolean(value && value.jquery);
}
function valueChangeCallback(inputs, binding, el, allowDeferred) {
function valueChangeCallback(inputs, binding, el, priority) {
let id = binding.getId(el);
if (id) {
const value = binding.getValue(el);
const type = binding.getType(el);
if (type)
id = id + ":" + type;
const opts = {
priority: allowDeferred ? "deferred" : "immediate",
binding,
el
};
inputs.setInput(id, value, opts);
inputs.setInput(id, value, { priority, binding, el });
}
}
var bindingsRegistry = (() => {
@@ -5738,8 +5733,18 @@ ${duplicateIdMsg}`;
const thisCallback = function() {
const thisBinding = binding;
const thisEl = el;
return function(allowDeferred) {
valueChangeCallback(inputs, thisBinding, thisEl, allowDeferred);
return function(priority) {
let normalizedPriority;
if (priority === true) {
normalizedPriority = "deferred";
} else if (priority === false) {
normalizedPriority = "immediate";
} else if (typeof priority === "object" && "priority" in priority) {
normalizedPriority = priority.priority;
} else {
normalizedPriority = priority;
}
valueChangeCallback(inputs, thisBinding, thisEl, normalizedPriority);
};
}();
binding.subscribe(el, thisCallback);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,22 @@
import type { EventPriority } from "../../inputPolicies/inputPolicy";
import type { RatePolicyModes } from "../../inputPolicies/inputRateDecorator";
import type { BindScope } from "../../shiny/bind";
type SubscribeEventPriority =
| EventPriority
| boolean
| { priority: EventPriority };
// Historically, the .subscribe()'s callback value only took a boolean. In this
// case:
// * false: send value immediately (i.e., priority = "immediate")
// * true: send value later (i.e., priority = "deferred")
// * The input rate policy is also consulted on whether to debounce or
// throttle
// In recent versions, the value can also be "event", meaning that the
// value should be sent immediately regardless of whether it has changed.
type InputSubscribeCallback = (value: SubscribeEventPriority) => void;
class InputBinding {
name!: string;
@@ -26,10 +42,7 @@ class InputBinding {
el; // unused var
}
// The callback method takes one argument, whose value is boolean. If true,
// allow deferred (debounce or throttle) sending depending on the value of
// getRatePolicy. If false, send value immediately. Default behavior is `false`
subscribe(el: HTMLElement, callback: (value: boolean) => void): void {
subscribe(el: HTMLElement, callback: InputSubscribeCallback): void {
// empty
el; // unused var
callback; // unused var
@@ -102,3 +115,4 @@ class InputBinding {
//// END NOTES FOR FUTURE DEV
export { InputBinding };
export type { InputSubscribeCallback, SubscribeEventPriority };

View File

@@ -1,6 +1,7 @@
import $ from "jquery";
import { Shiny } from "..";
import type { InputBinding, OutputBinding } from "../bindings";
import type { SubscribeEventPriority } from "../bindings/input/inputBinding";
import { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { BindingRegistry } from "../bindings/registry";
import { ShinyClientMessageEvent } from "../components/errorConsole";
@@ -8,6 +9,7 @@ import type {
InputRateDecorator,
InputValidateDecorator,
} from "../inputPolicies";
import type { EventPriority } from "../inputPolicies/inputPolicy";
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
import { sendImageSizeFns } from "./sendImageSize";
@@ -27,7 +29,7 @@ function valueChangeCallback(
inputs: InputValidateDecorator,
binding: InputBinding,
el: HTMLElement,
allowDeferred: boolean
priority: EventPriority
) {
let id = binding.getId(el);
@@ -37,17 +39,7 @@ function valueChangeCallback(
if (type) id = id + ":" + type;
const opts: {
priority: "deferred" | "immediate";
binding: typeof binding;
el: typeof el;
} = {
priority: allowDeferred ? "deferred" : "immediate",
binding: binding,
el: el,
};
inputs.setInput(id, value, opts);
inputs.setInput(id, value, { priority, binding, el });
}
}
@@ -272,8 +264,20 @@ function bindInputs(
const thisBinding = binding;
const thisEl = el;
return function (allowDeferred: boolean) {
valueChangeCallback(inputs, thisBinding, thisEl, allowDeferred);
return function (priority: SubscribeEventPriority) {
// Narrow the type of priority to EventPriority
let normalizedPriority: EventPriority;
if (priority === true) {
normalizedPriority = "deferred";
} else if (priority === false) {
normalizedPriority = "immediate";
} else if (typeof priority === "object" && "priority" in priority) {
normalizedPriority = priority.priority;
} else {
normalizedPriority = priority;
}
valueChangeCallback(inputs, thisBinding, thisEl, normalizedPriority);
};
})();

View File

@@ -1,12 +1,17 @@
import type { EventPriority } from "../../inputPolicies/inputPolicy";
import type { RatePolicyModes } from "../../inputPolicies/inputRateDecorator";
import type { BindScope } from "../../shiny/bind";
type SubscribeEventPriority = EventPriority | boolean | {
priority: EventPriority;
};
type InputSubscribeCallback = (value: SubscribeEventPriority) => void;
declare class InputBinding {
name: string;
find(scope: BindScope): JQuery<HTMLElement>;
getId(el: HTMLElement): string;
getType(el: HTMLElement): string | null;
getValue(el: HTMLElement): any;
subscribe(el: HTMLElement, callback: (value: boolean) => void): void;
subscribe(el: HTMLElement, callback: InputSubscribeCallback): void;
unsubscribe(el: HTMLElement): void;
receiveMessage(el: HTMLElement, data: unknown): Promise<void> | void;
getState(el: HTMLElement): unknown;
@@ -18,3 +23,4 @@ declare class InputBinding {
dispose(el: HTMLElement): void;
}
export { InputBinding };
export type { InputSubscribeCallback, SubscribeEventPriority };