Shiny doesn't bind to elements that are 'hidden' when first initializing

1,135 views
Skip to first unread message

Kevin Ushey

unread,
Oct 4, 2013, 1:58:34 PM10/4/13
to shiny-...@googlegroups.com
This is a crosspost from StackOverflow here: http://stackoverflow.com/questions/19166322/shiny-with-html-ui-limited-by-javascript-onload/19177028?noredirect=1#comment28388537_19177028; although there's a workaround, it seems like this is still a bug, or at least unexpected behavior.

It looks like, if an element is hidden when the DOM is loaded, then messages from that binding will not passed around (even though the binding is generated). This status doesn't get reset even if the element is later re-displayed.

Starting on line 2618 of shiny.js:

    // Return true if the object or one of its ancestors in the DOM tree has
    // style='display:none'; otherwise return false.
    function isHidden(obj) {
      // null means we've hit the top of the tree. If width or height is
      // non-zero, then we know that no ancestor has display:none.
      if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
        return false;
      } else if (getStyle(obj, 'display') === 'none') {
        return true;
      } else {
        return(isHidden(obj.parentNode));
      }
    }
    var lastKnownVisibleOutputs = {};
    // Set initial state of outputs to hidden, if needed
    $('.shiny-bound-output').each(function() {
      if (isHidden(this)) {
        initialValues['.clientdata_output_' + this.id + '_hidden'] = true;
      } else {
        lastKnownVisibleOutputs[this.id] = true;
        initialValues['.clientdata_output_' + this.id + '_hidden'] = false;
      }
    });
    // Send update when hidden state changes
    function sendOutputHiddenState() {
      var visibleOutputs = {};
      $('.shiny-bound-output').each(function() {
        delete lastKnownVisibleOutputs[this.id];
        // Assume that the object is hidden when width and height are 0
        if (isHidden(this)) {
          inputs.setInput('.clientdata_output_' + this.id + '_hidden', true);
        } else {
          visibleOutputs[this.id] = true;
          inputs.setInput('.clientdata_output_' + this.id + '_hidden', false);
        }
      });
      // Anything left in lastKnownVisibleOutputs is orphaned
      for (var name in lastKnownVisibleOutputs) {
        if (lastKnownVisibleOutputs.hasOwnProperty(name)) {
          inputs.setInput('.clientdata_output_' + name + '_hidden', true);
        }
      }
      // Update the visible outputs for next time
      lastKnownVisibleOutputs = visibleOutputs;
    }

It seems like, when loading, this .clientdata_output_<foo>_hidden flag gets set to true, and it does not get unset even if the element is later displayed through 'standard' JavaScript (e.g. $("#foo").show())

Although 'outputOptions(output, 'x', suspendWhenHidden=FALSE)' fixes it, it seems more like a workaround than a real solution...

Hopefully someone on the Shiny team has comments / a solution.

Yihui Xie

unread,
Oct 8, 2013, 12:07:56 AM10/8/13
to shiny-...@googlegroups.com, Kevin Ushey
Hi Kevin,

This is a good question. The problem here, as you said, is Shiny does
not refresh the visibility status of output elements if you manipulate
their visibility manually. The reason is when you call .show() or
.hide(), the function sendOutputHiddenState() is not triggered at all;
what really triggers it is $('body').on('shown.sendOutputHiddenState
hidden.sendOutputHiddenState') (see the next couple of lines in
shiny.js).

To solve this problem without hacking at suspendWhenHidden=FALSE, you
can trigger the internal events shown/hidden manually, e.g.

function showFoo(){
$('#foodiv').show();
$('#bardiv').hide().trigger('hidden');
}

function showBar(){
$('#foodiv').hide();
$('#bardiv').show().trigger('shown');
}

Then Shiny will be aware of the real visibility of #bardiv.

Perhaps what I said is a bad idea and Joe can share more wisdom on this issue.

Regards,
Yihui
--
Yihui Xie <yi...@rstudio.com>
Web: http://yihui.name
> --
> You received this message because you are subscribed to the Google Groups
> "Shiny - Web Framework for R" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to shiny-discus...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.

Guillem Vicens

unread,
Oct 8, 2013, 2:33:59 AM10/8/13
to shiny-...@googlegroups.com, Kevin Ushey
Hi,

I am the one that asked in Stackoverflow about this issue. I might be misunderstanding you but you seem to imply this issue should happen every time you use .hide() or .show(). That is not exactly what is happening.

  • If I do not call .hide() in the OnShow event, afterwards I can call both .show() and .hide() as much as I want. Shiny will continue to react to inputs.
  • Only if I do call .hide() in the OnShow event Shiny stops to react to the hidden input, regardless of whether I use .show() or not.

El martes, 8 de octubre de 2013 06:07:56 UTC+2, Yihui Xie escribió:
Hi Kevin,

This is a good question. The problem here, as you said, is Shiny does
not refresh the visibility status of output elements if you manipulate
their visibility manually. The reason is when you call .show() or
.hide(), the function sendOutputHiddenState() is not triggered at all;
what really triggers it is $('body').on('shown.sendOutputHiddenState
hidden.sendOutputHiddenState') (see the next couple of lines in
shiny.js).

<snip>

Winston Chang

unread,
Oct 8, 2013, 11:40:21 AM10/8/13
to shiny-...@googlegroups.com

Although 'outputOptions(output, 'x', suspendWhenHidden=FALSE)' fixes it, it seems more like a workaround than a real solution...


The short answer is that `outputOptions(output, 'x', suspendWhenHidden=FALSE)` is the solution - it's not a workaround. It's possible that in the future, there will be an option to control the behavior from the client side.

-Winston
 

Yihui Xie

unread,
Oct 8, 2013, 12:04:18 PM10/8/13
to shiny-...@googlegroups.com, Kevin Ushey
Well, it all depends on the initial status:

- if an element is hidden initially, shiny will _always_ ignore it,
even if you .show() it later
- if an element is shown initially, shiny will always use it, even if
you .hide() it later

What I said was, it is not enough to call .show()/.hide() -- you have
to trigger the internal events "shown"/"hidden" of shiny, so that
shiny can refresh its memory about the visibility of that element.

outputOptions(output, 'x', suspendWhenHidden=FALSE) can solve the
problem, but it depends on what you really want: do you want the
hidden elements to react or not? If you do want them to react, then
use suspendWhenHidden=FALSE; if you want to forget about hidden
elements, you need to trigger the shown/hidden events after you
.show()/.hide().

Regards,
Yihui
--
Yihui Xie <yi...@rstudio.com>
Web: http://yihui.name


Guillem Vicens

unread,
Oct 9, 2013, 2:26:02 AM10/9/13
to shiny-...@googlegroups.com, Kevin Ushey
ok, that makes total sense. Thanks for the information, Yihui!
Reply all
Reply to author
Forward
0 new messages