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

Allow update input labels with HTML #3996

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
20 changes: 10 additions & 10 deletions R/update-input.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
validate_session_object(session)

message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
message <- dropNulls(list(label=processDeps(label, session), value=value, placeholder=placeholder))
cpsievert marked this conversation as resolved.
Show resolved Hide resolved
session$sendInputMessage(inputId, message)
}

Expand Down Expand Up @@ -111,7 +111,7 @@ updateTextAreaInput <- updateTextInput
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
validate_session_object(session)

message <- dropNulls(list(label=label, value=value))
message <- dropNulls(list(label=processDeps(label, session), value=value))
session$sendInputMessage(inputId, message)
}

Expand Down Expand Up @@ -175,13 +175,13 @@ updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, la
validate_session_object(session)

if (!is.null(icon)) icon <- as.character(validateIcon(icon))
message <- dropNulls(list(label=label, icon=icon, disabled=disabled))
message <- dropNulls(list(label=processDeps(label, session), icon=icon, disabled=disabled))
session$sendInputMessage(inputId, message)
}
#' @rdname updateActionButton
#' @export
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
updateActionButton(session, inputId=inputId, label=label, icon=icon)
updateActionButton(session, inputId=inputId, label=processDeps(label, session), icon=icon)
}


Expand Down Expand Up @@ -225,7 +225,7 @@ updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label
min <- dateYMD(min, "min")
max <- dateYMD(max, "max")

message <- dropNulls(list(label=label, value=value, min=min, max=max))
message <- dropNulls(list(label=processDeps(label, session), value=value, min=min, max=max))
session$sendInputMessage(inputId, message)
}

Expand Down Expand Up @@ -275,7 +275,7 @@ updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId,
max <- dateYMD(max, "max")

message <- dropNulls(list(
label = label,
label = processDeps(label, session),
value = dropNulls(list(start = start, end = end)),
min = min,
max = max
Expand Down Expand Up @@ -379,7 +379,7 @@ updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, la
validate_session_object(session)

message <- dropNulls(list(
label = label, value = formatNoSci(value),
label = processDeps(label, session), value = formatNoSci(value),
min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
))
session$sendInputMessage(inputId, message)
Expand Down Expand Up @@ -460,7 +460,7 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
}

message <- dropNulls(list(
label = label,
label = processDeps(label, session),
value = formatNoSci(value),
min = formatNoSci(min),
max = formatNoSci(max),
Expand Down Expand Up @@ -491,7 +491,7 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
))
}

message <- dropNulls(list(label = label, options = options, value = selected))
message <- dropNulls(list(label = processDeps(label, session), options = options, value = selected))

session$sendInputMessage(inputId, message)
}
Expand Down Expand Up @@ -644,7 +644,7 @@ updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, lab
choices <- if (!is.null(choices)) choicesWithNames(choices)
if (!is.null(selected)) selected <- as.character(selected)
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
message <- dropNulls(list(label = label, options = options, value = selected))
message <- dropNulls(list(label = processDeps(label, session), options = options, value = selected))
session$sendInputMessage(inputId, message)
}

Expand Down
6 changes: 3 additions & 3 deletions srcts/src/bindings/input/checkboxgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ class CheckboxGroupInputBinding extends InputBinding {
options: options,
};
}
receiveMessage(
async receiveMessage(
el: CheckboxGroupHTMLElement,
data: CheckboxGroupReceiveMessageData
): void {
): Promise<void> {
const $el = $(el);

// This will replace all the options
Expand All @@ -132,7 +132,7 @@ class CheckboxGroupInputBinding extends InputBinding {
this.setValue(el, data.value);
}

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

$(el).trigger("change");
}
Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,13 @@ class DateInputBinding extends DateInputBindingBase {
startview: startview,
};
}
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): void {
async receiveMessage(
el: HTMLElement,
data: DateReceiveMessageData
): Promise<void> {
const $input = $(el).find("input");

updateLabel(data.label, this._getLabelNode(el));
await updateLabel(data.label, this._getLabelNode(el));

if (hasDefinedProperty(data, "min")) this._setMin($input[0], data.min);

Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/daterange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,16 @@ class DateRangeInputBinding extends DateInputBindingBase {
startview: startview,
};
}
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): void {
async receiveMessage(
el: HTMLElement,
data: DateRangeReceiveMessageData
): Promise<void> {
const $el = $(el);
const $inputs = $el.find("input");
const $startinput = $inputs.eq(0);
const $endinput = $inputs.eq(1);

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

if (hasDefinedProperty(data, "min")) {
this._setMin($startinput[0], data.min);
Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@ class NumberInputBinding extends TextInputBindingBase {
return "shiny.number";
el;
}
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): void {
async receiveMessage(
el: NumberHTMLElement,
data: NumberReceiveMessageData
): Promise<void> {
// Setting values to `""` will remove the attribute value from the DOM element.
// The attr key will still remain, but there is not value... ex: `<input id="foo" type="number" min max/>`
if (hasDefinedProperty(data, "value")) el.value = data.value ?? "";
if (hasDefinedProperty(data, "min")) el.min = data.min ?? "";
if (hasDefinedProperty(data, "max")) el.max = data.max ?? "";
if (hasDefinedProperty(data, "step")) el.step = data.step ?? "";

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

$(el).trigger("change");
}
Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ class RadioInputBinding extends InputBinding {
options: options,
};
}
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): void {
async receiveMessage(
el: RadioHTMLElement,
data: RadioReceiveMessageData
): Promise<void> {
const $el = $(el);
// This will replace all the options

Expand All @@ -122,7 +125,7 @@ class RadioInputBinding extends InputBinding {
this.setValue(el, data.value);
}

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

$(el).trigger("change");
}
Expand Down
6 changes: 3 additions & 3 deletions srcts/src/bindings/input/selectInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ class SelectInputBinding extends InputBinding {
options: options,
};
}
receiveMessage(
async receiveMessage(
el: SelectHTMLElement,
data: SelectInputReceiveMessageData
): void {
): Promise<void> {
const $el = $(el);

// This will replace all the options
Expand Down Expand Up @@ -199,7 +199,7 @@ class SelectInputBinding extends InputBinding {
this.setValue(el, data.value);
}

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

$(el).trigger("change");
}
Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ class SliderInputBinding extends TextInputBindingBase {
unsubscribe(el: HTMLElement): void {
$(el).off(".sliderInputBinding");
}
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): void {
async receiveMessage(
el: HTMLElement,
data: SliderReceiveMessageData
): Promise<void> {
const $el = $(el);
const slider = $el.data("ionRangeSlider");
const msg: {
Expand Down Expand Up @@ -226,7 +229,7 @@ class SliderInputBinding extends TextInputBindingBase {
}
}

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

// (maybe) update data elements
const domElements: Array<"data-type" | "time-format" | "timezone"> = [
Expand Down
7 changes: 5 additions & 2 deletions srcts/src/bindings/input/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ class TextInputBinding extends TextInputBindingBase {
placeholder: el.placeholder,
};
}
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): void {
async receiveMessage(
el: TextHTMLElement,
data: TextReceiveMessageData
): Promise<void> {
if (hasDefinedProperty(data, "value")) this.setValue(el, data.value);

updateLabel(data.label, getLabelNode(el));
await updateLabel(data.label, getLabelNode(el));

if (hasDefinedProperty(data, "placeholder"))
el.placeholder = data.placeholder;
Expand Down
22 changes: 16 additions & 6 deletions srcts/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import $ from "jquery";
import { windowDevicePixelRatio } from "../window/pixelRatio";
import type { MapValuesUnion, MapWithResult } from "./extraTypes";
import type { HtmlDep } from "../shiny/render";
import { hasOwnProperty, hasDefinedProperty } from "./object";
import { renderContent } from "../shiny/render";

function escapeHTML(str: string): string {
/* eslint-disable @typescript-eslint/naming-convention */
Expand Down Expand Up @@ -336,23 +338,31 @@ const compareVersion = function (
else throw `Unknown operator: ${op}`;
};

function updateLabel(
labelTxt: string | undefined,
async function updateLabel(
labelContent: string | { html: string; deps: HtmlDep[] } | undefined,
labelNode: JQuery<HTMLElement>
): void {
): Promise<void> {
// Only update if label was specified in the update method
if (typeof labelTxt === "undefined") return;
if (typeof labelContent === "undefined") return;
if (labelNode.length !== 1) {
throw new Error("labelNode must be of length 1");
}

if (typeof labelContent === "string") {
labelContent = {
html: labelContent,
deps: [],
};
}

// Should the label be empty?
const emptyLabel = Array.isArray(labelTxt) && labelTxt.length === 0;
const emptyLabel =
Array.isArray(labelContent.html) && labelContent.html.length === 0;
Copy link
Collaborator

@cpsievert cpsievert Mar 15, 2024

Choose a reason for hiding this comment

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

I think this check might also now need to check for empty string? I noticed that this PR will break this "clear label" feature:

library(shiny)

ui <- fluidPage(
  checkboxInput("id", label = "foo")
)

server <- function(input, output, session) {
  updateCheckboxInput(inputId = "id", label = character(0))
}

shinyApp(ui, server)

Copy link
Author

Choose a reason for hiding this comment

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

Ah good catch. Do we still need Array.isArray check? I'm not sure what's for to be honest; could we/can we pass a vector of length greater than 1 to label?

Asking because I wonder if it can't be simplified to just labelContent.html.length == 0 since "".length == 0 is true


if (emptyLabel) {
labelNode.addClass("shiny-label-null");
} else {
labelNode.text(labelTxt);
await renderContent(labelNode, labelContent);
labelNode.removeClass("shiny-label-null");
}
}
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/checkboxgroup.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ declare class CheckboxGroupInputBinding extends InputBinding {
value: ReturnType<CheckboxGroupInputBinding["getValue"]>;
options: ValueLabelObject[];
};
receiveMessage(el: CheckboxGroupHTMLElement, data: CheckboxGroupReceiveMessageData): void;
receiveMessage(el: CheckboxGroupHTMLElement, data: CheckboxGroupReceiveMessageData): Promise<void>;
subscribe(el: CheckboxGroupHTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: CheckboxGroupHTMLElement): void;
}
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/date.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ declare class DateInputBinding extends DateInputBindingBase {
format: string;
startview: DatepickerViewModes;
};
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): void;
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): Promise<void>;
}
export { DateInputBinding, DateInputBindingBase };
export type { DateReceiveMessageData };
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/daterange.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ declare class DateRangeInputBinding extends DateInputBindingBase {
language: string;
startview: string;
};
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): void;
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): Promise<void>;
initialize(el: HTMLElement): void;
subscribe(el: HTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: HTMLElement): void;
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/number.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ declare class NumberInputBinding extends TextInputBindingBase {
getValue(el: NumberHTMLElement): string[] | number | string | null | undefined;
setValue(el: NumberHTMLElement, value: number): void;
getType(el: NumberHTMLElement): string;
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): void;
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): Promise<void>;
getState(el: NumberHTMLElement): {
label: string;
value: ReturnType<NumberInputBinding["getValue"]>;
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/radio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ declare class RadioInputBinding extends InputBinding {
value: ReturnType<RadioInputBinding["getValue"]>;
options: ValueLabelObject[];
};
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): void;
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): Promise<void>;
subscribe(el: RadioHTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: RadioHTMLElement): void;
}
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/selectInput.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ declare class SelectInputBinding extends InputBinding {
label: string;
}>;
};
receiveMessage(el: SelectHTMLElement, data: SelectInputReceiveMessageData): void;
receiveMessage(el: SelectHTMLElement, data: SelectInputReceiveMessageData): Promise<void>;
subscribe(el: SelectHTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: HTMLElement): void;
initialize(el: SelectHTMLElement): void;
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/slider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ declare class SliderInputBinding extends TextInputBindingBase {
setValue(el: HTMLElement, value: number | string | [number | string, number | string]): void;
subscribe(el: HTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: HTMLElement): void;
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): void;
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): Promise<void>;
getRatePolicy(el: HTMLElement): {
policy: "debounce";
delay: 250;
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/bindings/input/text.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ declare class TextInputBinding extends TextInputBindingBase {
value: string;
placeholder: string;
};
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): void;
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): Promise<void>;
}
export { TextInputBinding, TextInputBindingBase };
export type { TextHTMLElement, TextReceiveMessageData };
6 changes: 5 additions & 1 deletion srcts/types/src/utils/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MapValuesUnion, MapWithResult } from "./extraTypes";
import type { HtmlDep } from "../shiny/render";
import { hasOwnProperty, hasDefinedProperty } from "./object";
declare function escapeHTML(str: string): string;
declare function randomId(): string;
Expand All @@ -22,7 +23,10 @@ declare function isnan(x: unknown): boolean;
declare function _equal(x: unknown, y: unknown): boolean;
declare function equal(...args: unknown[]): boolean;
declare const compareVersion: (a: string, op: "<" | "<=" | "==" | ">" | ">=", b: string) => boolean;
declare function updateLabel(labelTxt: string | undefined, labelNode: JQuery<HTMLElement>): void;
declare function updateLabel(labelContent: string | {
html: string;
deps: HtmlDep[];
} | undefined, labelNode: JQuery<HTMLElement>): Promise<void>;
declare function getComputedLinkColor(el: HTMLElement): string;
declare function isBS3(): boolean;
declare function toLowerCase<T extends string>(str: T): Lowercase<T>;
Expand Down