Is there a way to suppress 'events' during input to a field?

3,183 views
Skip to first unread message

Bob Anderson

unread,
Jul 24, 2013, 5:19:26 PM7/24/13
to shiny-...@googlegroups.com
When one is typing numbers into a numericInput field, each digit that is typed causes a reactive event. I like to be able to have a single reactive event occur when input is signaled as complete by the user either leaving the field by clicking on some other item, or pressing return or tab.

Is this possible?

David Gonzales

unread,
Jul 24, 2013, 6:33:38 PM7/24/13
to shiny-...@googlegroups.com
I'm seconding this question/request. A good interface would send the event only when the focus switches to a different input element or return is pressed. Perhaps even a specified but long delay would be OK.

Willi Fiebranz

unread,
Jul 25, 2013, 7:17:48 AM7/25/13
to shiny-...@googlegroups.com
The reactive functions you use are updating instantly, because you did not isolate them:

instant update:

renderUI{(

   dosomethingwith(input$yourinput)

)}

patient update:

renderUI{(

   input$trigger.event (e.g. input$goButton)

   dosomethingwith(isolate(input$yourinput))

)}


With the first version the input$yourinputs get invalidated inside your renderUI as soon as is changes its value.
With the second version on the other hand input$yourinput does not force your renderUI into reactivity anymore, because it is isolated, meaning that renderUI grabs the current input$yourinput value only if it reacts to some other unisolated variable (like a button push event of so).

It took me some time get my head wrapped around to it, but once you got the concept, you can do cool stuff.
For example, I use it to dynamically set initial values of UI-elements like

numericInput("number1", value = ifselse(is.null(input$number1), 1, isolate(input$number1)))

You need this if you have a lot of reacitivity in you interface, so that html-divisions get rerendered constantly.

Hope it helps.

Ciao

Willi Fiebranz

unread,
Jul 25, 2013, 7:19:40 AM7/25/13
to shiny-...@googlegroups.com
I don't know, from my expierience delays do not solve the problem of unintended reactivity.

David Gonzales

unread,
Jul 25, 2013, 11:24:02 AM7/25/13
to shiny-...@googlegroups.com
After some roaming around the javascript code I found the following change induces the desired behavior:
in directory /usr/local/lib/R/site-library/shiny/www/shared and file shiny.js, commenting
the three lines 1191-1193 (the exact directory and line numbers may vary), as following:

    subscribe: function(el, callback) {
      $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
        callback(true);
      });
      $(el).on('change.textInputBinding', function(event) {

to (added // in bold)

    subscribe: function(el, callback) {
      // $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
      //   callback(true);
      // });
      $(el).on('change.textInputBinding', function(event) {


In my experience, unless the application is highly interactive and animated, the user prefers to wait until moving on to the next field or pressing <return> before the changes are updated. But still a user might like the reactivity of not having to mouse press a button.

Willi: the bit with the isolate (), especially in my ui.R looks interesting, but I am very newbie to shiny, so give me a couple of days to get to it.

Bob Anderson

unread,
Jul 25, 2013, 11:28:49 AM7/25/13
to shiny-...@googlegroups.com
Willi, thanks for your response.  I am aware of that technique, but that also means adding 'do it' buttons and asking the user to perform extra 'clicks', something that I try to minimize in a GUI.

Currently, the input fields of shiny are giving me onChange events.  What I want are onExit events.  And what I was really hoping for was someone to tell me that if you set a particular flag (possibly undocumented) then you will get the desired onExit notifications.  And then there was always the possibility that I failed to read all the documentation and had therefore overlooked a 'switch'.




--
You received this message because you are subscribed to a topic in the Google Groups "Shiny - Web Framework for R" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/shiny-discuss/BFUgjICEQlc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to shiny-discus...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Bob Anderson

unread,
Jul 25, 2013, 11:33:09 AM7/25/13
to shiny-...@googlegroups.com
David:

That's what I was looking for!

I gather that with that change, all inout fields will be affected.  That's fine for how my application works.


--

Bob Anderson

unread,
Jul 25, 2013, 11:41:34 AM7/25/13
to shiny-...@googlegroups.com
David:

Sad news.  There is no such file (shiny.js) on my machine.


On Thu, Jul 25, 2013 at 8:24 AM, David Gonzales <dvdgon...@gmail.com> wrote:
--

David Gonzales

unread,
Jul 25, 2013, 12:14:32 PM7/25/13
to shiny-...@googlegroups.com
there must be such a file (because, it is part of the shiny page which you access through the browser).
it is exactly where you have all the other files from packages which are installed (it may be in your home directory, if you don't install as root user). keep looking, or use some file search function. ultimately, you can just look at the log of installing some random package and see where it says the files go - hop up one directory and down to the shiny directory.

the effect is on all text input fields - this is even better - since a user naturally wants sliders to keep being interactive immediately.

Bob Anderson

unread,
Jul 25, 2013, 12:47:30 PM7/25/13
to shiny-...@googlegroups.com
I found it --- changed it as you suggested --- works great.

I am on a Mac and I expect Spotlight to be able to find anything.  It failed this time, but it's probably ignorance on my part.  I found it by typing .libPaths() in the R console and following that path...

 "/Library/Frameworks/R.framework/Versions/3.0/Resources/library"


Thanks for the help.

I hope that RStudio will provide a way to accomplish this without a user having to edit/change a system file.  I can do that on my machine, but I have users of my app (some on Windows) that may find the process daunting.

Bob

Vincent Nijs

unread,
Oct 24, 2013, 2:51:37 AM10/24/13
to shiny-...@googlegroups.com
+1

Just tried David's suggestion to comment out the 3 lines in shiny.js and now text input works the way I would want (i.e., change on Enter). This way you don't need to an action button for text fields if you want to wait for the user to stop typing.

I tried to include the changed code in my app in the hopes it would override the original binding in shiny.js. No luck sofar. Any suggestions on how this might be achieved?

ZJ

unread,
Oct 24, 2013, 10:06:07 AM10/24/13
to shiny-...@googlegroups.com
Well I can think of two options

a. create your own component based on Rstudio's original code but with those lines changed. Call this component something other than textInput. 

b. Actually since Shiny is a open source project you can contribute to it. Write the code so that in ui.R the user can specify the behaviour they want using a new argument, then change the js code so that both options are covered. Do a pull request and see what happens.

Vincent Nijs

unread,
Oct 24, 2013, 8:50:31 PM10/24/13
to shiny-...@googlegroups.com
Good suggestions ZJ. Thank you. I adapted the js code from shiny.js

When I try to use the new binding as myTextInput("tr_rename", "Rename", "")

I get a 'could not find function "myTextInput"' error

Could you tell me how I get Shiny to pick-up this new binding? I tried various things but no luck sofar.

  var myTextInputBinding = new Shiny.InputBinding();
  $.extend(myTextInputBinding, {
    find: function(scope) {
      return $(scope).find('input[type="text"]');
    },
    getId: function(el) {
      return InputBinding.prototype.getId.call(this, el) || el.name;
    },
    getValue: function(el) {
      return el.value;
    },
    setValue: function(el, value) {
      el.value = value;
    },
    subscribe: function(el, callback) {
      // $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
      //   callback(true);
      // });
      $(el).on('change.myTextInputBinding', function(event) {
        callback(false);
      });
    },
    unsubscribe: function(el) {
      $(el).off('.myTextInputBinding');
    },
    receiveMessage: function(el, data) {
      if (data.hasOwnProperty('value'))
        this.setValue(el, data.value);

      if (data.hasOwnProperty('label'))
        $(el).parent().find('label[for=' + el.id + ']').text(data.label);

      $(el).trigger('change');
    },
    getState: function(el) {
      return {
        label: $(el).parent().find('label[for=' + el.id + ']').text(),
        value: el.value
      };
    },
    getRatePolicy: function() {
      return {
        policy: 'debounce',
        delay: 250
      };
    }
  });
  Shiny.inputBindings.register(myTextInputBinding, 'shiny.myTextInput');

Vincent Nijs

unread,
Oct 24, 2013, 9:16:39 PM10/24/13
to shiny-...@googlegroups.com
I put the following in ui.R before the call to ShinyUI suggests but get the same error. Just noticed that I put 'button' in the function. Not what is needed I assume :)

myTextInput <- function(inputId, label, value = "") {
  tagList(
    singleton(tags$head(tags$script(src = "js/textInputBindingEnter.js"))),
    tags$button(id = inputId,
                label = label,
                as.character(value))
  )

ZJ

unread,
Oct 24, 2013, 10:26:27 PM10/24/13
to shiny-...@googlegroups.com
find: function(scope) {
return $(scope).find('input[type="text"]');
},


find is what shiny uses to identify your complements. In the above it's looking for an input element with type = "text".

So including a button won't work. U need to modify it further to make sure that shiny can identify ur component. See if I can whip up an example when I am back to my desk

Vincent Nijs

unread,
Oct 25, 2013, 1:04:33 AM10/25/13
to shiny-...@googlegroups.com
I updated the myTextInput function (see below) and put it in global.R. Now I am not getting the 'not found error' so Shiny seems to be picking up the function. Now, however, several of my other ui-elements don't show up.

When I comment-out the last line of the js file (See below) they show up again so it seems there is a problem with registering the binding somehow.

Shiny.inputBindings.register(myTextInputBinding, 'shiny.myTextInput'); // last line of js file

myTextInput <- function(inputId, label, value = "") {
  tagList(
    singleton(tags$head(tags$script(src = "js/textInputBindingEnter.js"))),
    tags$text(id = inputId, label = label, as.character(value))
  )

ZJ

unread,
Oct 25, 2013, 2:44:38 AM10/25/13
to shiny-...@googlegroups.com

Vincent Nijs

unread,
Oct 25, 2013, 2:45:46 PM10/25/13
to shiny-...@googlegroups.com
Interesting. Thanks ZJ. It seems the new textInput actually makes a text area. Ultimately I would want a textInput so that pressing enter will 'submit' the text rather than go the next line. There must some formatting options in the shiny code for the textInput I am missing.

ZJ

unread,
Oct 25, 2013, 5:41:30 PM10/25/13
to shiny-...@googlegroups.com
Check the gist again.
 The key is the subscribe function. It tells shiny when the value has changed. I updated the code so that when enter is pressed it calls callback. If callback is called shiny is alerted of the changes in value otherwise it won't.

 subscribe: function(el, callback) {
             $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
               if(event.keyCode == 13) { //if enter
                callback()
               }
             });
            $(el).on('focusout.myTextInputBinding', function(event) { // on losing focus
              callback();
            });
            },

Vincent Nijs

unread,
Oct 25, 2013, 9:34:02 PM10/25/13
to shiny-...@googlegroups.com
Awesome! Thanks so much ZJ. Works great.

One thing I did notice that is that when I apply updateTextInput to a myTextInput it still works (i.e., it clears out the text in the box) but it does not clear the text out memory (i.e., value previously assigned is still being used). Any ideas?

This is how I call it.

updateTextInput(session = session, inputId = "tr_rename", label = "Rename (separate by ','):", value = '')


The code for updateTextInput from the Shiny code base is as below. Strange that it would cleanup the text in the box but not the variable value.

updateTextInput <- function(session, inputId, label = NULL, value = NULL) {
  message <- dropNulls(list(label=label, value=value))
  session$sendInputMessage(inputId, message)

Vincent Nijs

unread,
Oct 25, 2013, 9:53:19 PM10/25/13
to shiny-...@googlegroups.com
I think I might have it working. Will post back when I am sure.

Vincent Nijs

unread,
Oct 25, 2013, 11:57:23 PM10/25/13
to shiny-...@googlegroups.com
Using ZJ code to map access the binding from the app and some input from the post linked below. I got this to work. Thanks again ZJ!!

I have the following in global.R

returnTextInput <- function(inputId, label, value = "") {
  tagList(
    singleton(tags$head(tags$script(src = "js/returnTextInputBinding.js"))),
    tags$label(label, `for` = inputId),
    tags$input(id = inputId, type = "text", value = value, class = "returnTextInput")
  )
}

and resurnTextInputBinding.js is as follows:

  // Text input
  var returnTextInputBinding = new Shiny.InputBinding();
  $.extend(returnTextInputBinding, {
    find: function(scope) {
      return $(scope).find('input[type="text"]');
    },
    getId: function(el) {
      return Shiny.InputBinding.prototype.getId.call(this, el) || el.name;
    },
    getValue: function(el) {
      return el.value;
    },
    setValue: function(el, value) {
      el.value = value;
    },
    subscribe: function(el, callback) {
      // $(el).on('keyup.textInputBinding input.textInputBinding', function(event) {
      //   callback(true);
      // });
      $(el).on('change.textInputBinding', function(event) {
        callback(false);
      });
    },
    unsubscribe: function(el) {
      $(el).off('.textInputBinding');
    },
    receiveMessage: function(el, data) {
      if (data.hasOwnProperty('value'))
        this.setValue(el, data.value);

      if (data.hasOwnProperty('label'))
        $(el).parent().find('label[for=' + el.id + ']').text(data.label);

      $(el).trigger('change');
    },
    getState: function(el) {
      return {
        label: $(el).parent().find('label[for=' + el.id + ']').text(),
        value: el.value
      };
    },
    getRatePolicy: function() {
      return {
        policy: 'debounce',
        delay: 250
      };
    }
  });
Reply all
Reply to author
Forward
0 new messages