Files
shiny/_includes/tutorial/building-inputs.md
2012-09-06 16:59:39 -07:00

7.7 KiB
Raw Blame History

Building Inputs

If you have some knowledge of HTML/CSS/JavaScript, you can create your own custom input components that can be reused by others.

Shiny input components should try to adhere to the following principles, if possible:

  • Designed to be used from HTML and R: Shiny user interfaces can either be written using R code (that generates HTML), or by writing the HTML directly. A well-designed Shiny input component will take both styles into account: offer an R function for creating the component, but also have thoughtfully designed and documented HTML markup.
  • Configurable using HTML attributes: Avoid requiring the user to make JavaScript calls to configure the component. Instead, it's better to use HTML attributes. In your component's JavaScript logic, you can easily access these values using jQuery (or simply by reading the DOM attribute directly).

Design the Component

Write an Input Binding

Each custom input component you design needs an input binding, an object you create that tells Shiny how to identify instances of your component and how to interact with it. An input binding object needs to have the following methods:

find(scope)

Given an HTML document or element (scope), find any descendant elements that are an instance of your component and return them as an array (or array-like object). The other input binding methods all take an el argument; that value will always be an element that was returned from find.

<p>
  A very common implementation is to use jQuery's <code>find</code> method to identify elements with a specific class, for example:
</p>
exampleInputBinding.find = function(scope) {
  return $(scope).find(".exampleComponentClass");
};
getId(el)
Return the Shiny input ID for the element el, or null if the element doesn't have an ID and should therefore be ignored. The default implementation in Shiny.InputBinding reads the data-input-id attribute and falls back to the element's id if not present.
getValue(el)
Return the Shiny value for the element el. This can be any JSON-compatible value.
setValue(el, value)
Set the element to the specified value. (This is not currently used, but in the future we anticipate adding features that will require the server to push input values to the client.)
subscribe(el, callback)

Subscribe to DOM events on the element el that indicate the value has changed. When the DOM events fire, call callback (a function) which will tell Shiny to retrieve the value.

We recommend using jQuery's event namespacing feature when subscribing, as unsubscribing becomes very easy (see unsubscribe, below). In this example, exampleComponentName is used as a namespace:

exampleInputBinding.subscribe = function(el, callback) {
  $(el).on("keyup.exampleComponentName", function(event) {
    callback(true);
  });
  $(el).on("change.exampleComponentName", function(event) {
    callback();
  });
};

Later on, we can unsubscribe ".exampleComponentName" which will remove all of our handlers without touching anyone else's.

The callback function optionally takes an argument: a boolean value that indicates whether the component's rate policy should apply (true means the rate policy should apply). See getRatePolicy below for more details.

unsubscribe(el)

Unsubscribe DOM event listeners that were bound in subscribe.

Example:

exampleInputBinding.unsubscribe = function(el) {
  $(el).off(".exampleComponentName");
};
getRatePolicy()

Return an object that describes the rate policy of this component (or null for default).

Rate policies are helpful for slowing down the rate at which input events get sent to the server. For example, as the user drags a slider from value A to value B, dozens of change events may occur. It would be wasteful to send all of those events to the server, where each event would potentially cause expensive computations to occur.

A rate policy slows down the rate of events using one of two algorithms (so far). Throttling means no more than one event will be sent per X milliseconds. Debouncing means all of the events will be ignored until no events have been received for X milliseconds, at which time the most recent event will be sent. This blog post goes into more detail about the difference between throttle and debounce.

A rate policy object has two members:

  • policy - Valid values are the strings "direct", "debounce", and "throttle". "direct" means that all events are sent immediately.
  • delay - Number indicating the number of milliseconds that should be used when debouncing or throttling. Has no effect if the policy is direct.

Rate policies are only applied when the callback function in subscribe is called with true as the first parameter. It's important that input components be able to control which events are rate-limited and which are not, as different events may have different expectations to the user. For example, for a textbox, it would make sense to rate-limit events while the user is typing, but if the user hits Enter or focus leaves the textbox, then the input should always be sent immediately.

Register Input Binding

Shiny.inputBindings.register(exampleInputBinding);

Example

For this example, we'll create a button that displays a number, whose value increases by one each time the button is clicked.

Example:

0

<script> $(document).on("click", "button.increment", function(evt) { var el = $(evt.target); el.text(parseInt(el.text()) + 1); }); </script>

HTML:

<button class="increment btn" type="button">0</button>

JavaScript:

// Non-Shiny-specific code that drives the basic behavior

$(document).on("click", "button.increment", function(evt) { var el = $(evt.target); el.text(parseInt(el.text()) + 1); el.trigger("change"); });

// Shiny-specific binding code

var incrementBinding = new Shiny.InputBinding(); $.extend(incrementBinding, { find: function(scope) { return $(scope).find(".increment"); }, getValue: function(el) { return parseInt($(el).text()); }, setValue: function(el, value) { $(el).text(value); }, subscribe: function(el, callback) { $(el).on("change.incrementBinding", function(e) { callback(); }); }, unsubscribe: function(el) { $(el).off(".incrementBinding"); } });

Shiny.inputBindings.register(incrementBinding);