posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.29k stars 78 forks source link

Numeric Input should optionally format input users see #1157

Open corey-dawson opened 8 months ago

corey-dawson commented 8 months ago

There should be optional args to add a comma separator or dollar formatting to numeric inputs. Currently, only an number with no formatting is presented to the user. Would be much more user friendly if large numbers could have thousand separator and dollars could have the dollar symbol + comma separator.

additional args could be "separator": True/False "currency_symbol": "$" # appends to the front of the input

wch commented 8 months ago

There are a number of features that people have asked for for number inputs. The current implementation doesn't give us much flexibility since we're just using an HTML <input type="number">, but a JS-based implementation would allow us to do a lot more, in terms of formatting and validation.

corey-dawson commented 7 months ago

Temporary fix to get currency-like formatted inputs. Will have to turn it back into a number when using it in the server

from pathlib import Path
from shiny import App, render, ui, reactive
# from shiny.types import ImgData
import os
import re

js = """
function formatCurrency(inputId) {
  const inputElement = document.getElementById(inputId);

  inputElement.addEventListener('keyup', function(e) {
    let value = e.target.value;

    // Remove all non-numeric characters
    value = value.replace(/[^0-9.]/g, '');

    // Format the number with commas
    if (value) {
      value = parseInt(value).toLocaleString('en-US', { minimumFractionDigits: 0 });
      value = "$" + value;
    } else {
      value = "";
    }

    // Update the input value
    inputElement.value = value;
  });
}

// Call the function for each input element
formatCurrency('dollars1');
formatCurrency('dollars2');

"""

app_ui = ui.page_fluid(
    ui.div(
        ui.tags.label("Dollars1 input", style="width: 100%; margin-bottom: 0.5rem"),
        ui.tags.input(type="text", id="dollars1", name="dollars", value="$20,000,000", style="padding: 0.375rem 0.75rem; font-size: 0.9375rem; font-weight: 400"),
        stye="width: 300px; margin-bottom: 1rem"        
    ),
    ui.div(
        ui.tags.label("Dollars1 input", style="width: 100%; margin-bottom: 0.5rem"),
        ui.tags.input(type="text", id="dollars2", name="dollars", value="$100,000", style="padding: 0.375rem 0.75rem; font-size: 0.9375rem; font-weight: 400"),
         stye="width: 300px; margin-bottom: 1rem"
    ),
    ui.input_numeric("tst", "formatsteal", value=5),
    ui.input_action_button("btn", "Update costs"),
    ui.tags.script(js)
)

def server(input, output, session):

    @reactive.Effect
    @reactive.event(input.btn)
    def prntVals():
        print(f"input 1: {input.dollars1()}")
        print(f"input 2: {input.dollars2()}")
        print(f"input 3: {input.tst()}")

        # make a number
        rgx = r"[\$,]"
        in_str = input.dollars1()
        in_str = re.sub(rgx, "", in_str)
        in_int = int(in_str)
        print(f"input 1 as int: {in_int}")

app = App(app_ui, server)
gdsutton commented 5 months ago

@corey-dawson this is a great work around, thank you! just what I needed. How might I change it so that that the currency symbol can be varied? I have a use case where depending on another input, the currency symbol required is available on the server side say as a reactive value, i'd like to have the currency symbol be updated in the js function. not sure how to pass this is in dynamically, suspect I could get around it by having different inputs for each symbol (only need three currently) and then conditionally showing the correct one and coalescing the results of all three together afterwards, which would avoid js function changes but some duplication. Any suggestion for a useful approach?

corey-dawson commented 5 months ago

@gdsutton, can update the JavaScript a bit. Function should input all arguments you want to be considered in the formatting. here is a simple example that updates on inputted number OR inputted currency symbol. Another note is you should be able to open the developer tools -> console in your browser and play around with the javascript. For example, could test getting the inputs from your app

Browser Console Testing

//in the browser terminal
curSel
//try printing
console.log("my current selection is: " + curSel)

Python Sample

from shiny import App, Inputs, Outputs, Session, reactive, ui
import shinyswatch

js = """
function formatCurrency(curSel, dollarAmt) {
  let fmtValue = dollarAmt

  //format
  //add all formatting here. simplified for example
  fmtValue = fmtValue.replace(/[^0-9.]/g, '');
  fmtValue = curSel + fmtValue;
  console.log(fmtValue);

  //set value
  dolAmt.value = fmtValue;
}

const curSel = document.getElementById("curSel");
const dolAmt = document.getElementById("dollars");

//Listener for one input
curSel.addEventListener("change", function() {
  formatCurrency(curSel.value, dolAmt.value);
});

//listener for second input
dolAmt.addEventListener("change", function() {
  formatCurrency(curSel.value, dolAmt.value);
});

//need to run the function on startup
formatCurrency(curSel.value, dolAmt.value);

"""

app_ui = ui.page_fluid(
    shinyswatch.theme.darkly(),
    ui.h1("MyApp"),
    ui.input_select("curSel", "Currency Selection", choices=["$", "€"]),
    ui.tags.input(type="text", id="dollars", name="Dollars", value=5),
    ui.tags.script(js)
)

def server(input: Inputs, output: Outputs, session: Session):
    print("do stuff")

app = App(app_ui, server)