mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-07 03:00:20 -04:00
Merge branch 'joe/feature/undedupe-inputs'
This commit is contained in:
2
NEWS.md
2
NEWS.md
@@ -13,6 +13,8 @@ This is a significant release for Shiny, with a major new feature that was nearl
|
||||
|
||||
* Support for asynchronous operations! Built-in render functions that expected a certain kind of object to be yielded from their `expr`, now generally can handle a promise for that kind of object. Reactive expressions and observers are now promise-aware as well. ([#1932](https://github.com/rstudio/shiny/pull/1932))
|
||||
|
||||
* Introduced two changes to the (undocumented but widely used) JavaScript function `Shiny.onInputChange(name, value)`. First, we changed the function name to `Shiny.setInputValue` (but don't worry--the old function name will continue to work). Second, until now, all calls to `Shiny.onInputChange(inputId, value)` have been "deduplicated"; that is, anytime an input is set to the same value it already has, the set is ignored. With Shiny v1.1, you can now add an options object as the third parameter: `Shiny.setInputValue("name", value, {priority: "event"})`. When the priority option is set to `"event"`, Shiny will always send the value and trigger reactivity, whether it is a duplicate or not. This closes [#928](https://github.com/rstudio/shiny/issues/928), which was the most upvoted open issue by far! Thanks, @daattali. ([#2018](https://github.com/rstudio/shiny/pull/2018))
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Addressed [#1978](https://github.com/rstudio/shiny/issues/1978): `shiny:value` is now triggered when duplicate output data is received from the server. (Thanks, @andrewsali! [#1999](https://github.com/rstudio/shiny/pull/1999))
|
||||
|
||||
@@ -278,8 +278,9 @@ ReactiveValues <- R6Class(
|
||||
.allValuesDeps = 'Dependents',
|
||||
# Dependents for all values
|
||||
.valuesDeps = 'Dependents',
|
||||
.dedupe = logical(0),
|
||||
|
||||
initialize = function() {
|
||||
initialize = function(dedupe = TRUE) {
|
||||
.label <<- paste('reactiveValues',
|
||||
p_randomInt(1000, 10000),
|
||||
sep="")
|
||||
@@ -289,6 +290,7 @@ ReactiveValues <- R6Class(
|
||||
.namesDeps <<- Dependents$new()
|
||||
.allValuesDeps <<- Dependents$new()
|
||||
.valuesDeps <<- Dependents$new()
|
||||
.dedupe <<- dedupe
|
||||
},
|
||||
|
||||
get = function(key) {
|
||||
@@ -317,7 +319,7 @@ ReactiveValues <- R6Class(
|
||||
hidden <- substr(key, 1, 1) == "."
|
||||
|
||||
if (exists(key, envir=.values, inherits=FALSE)) {
|
||||
if (identical(.values[[key]], value)) {
|
||||
if (.dedupe && identical(.values[[key]], value)) {
|
||||
return(invisible())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,8 +706,8 @@ ShinySession <- R6Class(
|
||||
private$flushCallbacks <- Callbacks$new()
|
||||
private$flushedCallbacks <- Callbacks$new()
|
||||
private$inputReceivedCallbacks <- Callbacks$new()
|
||||
private$.input <- ReactiveValues$new()
|
||||
private$.clientData <- ReactiveValues$new()
|
||||
private$.input <- ReactiveValues$new(dedupe = FALSE)
|
||||
private$.clientData <- ReactiveValues$new(dedupe = TRUE)
|
||||
private$timingRecorder <- ShinyServerTimingRecorder$new()
|
||||
self$progressStack <- Stack$new()
|
||||
self$files <- Map$new()
|
||||
|
||||
@@ -762,26 +762,34 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.lastChanceCallback = [];
|
||||
};
|
||||
(function () {
|
||||
this.setInput = function (name, value) {
|
||||
var self = this;
|
||||
|
||||
this.setInput = function (name, value, opts) {
|
||||
this.pendingData[name] = value;
|
||||
|
||||
if (!this.timerId && !this.reentrant) {
|
||||
this.timerId = setTimeout(function () {
|
||||
self.reentrant = true;
|
||||
try {
|
||||
$.each(self.lastChanceCallback, function (i, callback) {
|
||||
callback();
|
||||
});
|
||||
self.timerId = null;
|
||||
var currentData = self.pendingData;
|
||||
self.pendingData = {};
|
||||
self.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
self.reentrant = false;
|
||||
}
|
||||
}, 0);
|
||||
if (!this.reentrant) {
|
||||
if (opts.priority === "event") {
|
||||
this.$sendNow();
|
||||
} else if (!this.timerId) {
|
||||
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$sendNow = function () {
|
||||
if (this.reentrant) {
|
||||
console.trace("Unexpected reentrancy in InputBatchSender!");
|
||||
}
|
||||
|
||||
this.reentrant = true;
|
||||
try {
|
||||
this.timerId = null;
|
||||
$.each(this.lastChanceCallback, function (i, callback) {
|
||||
callback();
|
||||
});
|
||||
var currentData = this.pendingData;
|
||||
this.pendingData = {};
|
||||
this.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
this.reentrant = false;
|
||||
}
|
||||
};
|
||||
}).call(InputBatchSender.prototype);
|
||||
@@ -791,11 +799,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.lastSentValues = this.reset(initialValues);
|
||||
};
|
||||
(function () {
|
||||
this.setInput = function (name, value) {
|
||||
// Note that opts is not passed to setInput at this stage of the input
|
||||
// decorator stack. If in the future this setInput keeps track of opts, it
|
||||
// would be best not to store the `el`, because that could prevent it from
|
||||
// being GC'd.
|
||||
this.setInput = function (name, value, opts) {
|
||||
var _splitInputNameType = splitInputNameType(name);
|
||||
|
||||
var inputName = _splitInputNameType.name;
|
||||
@@ -803,11 +807,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
var jsonValue = JSON.stringify(value);
|
||||
|
||||
if (this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
|
||||
if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
|
||||
return;
|
||||
}
|
||||
this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType };
|
||||
this.target.setInput(name, value);
|
||||
this.target.setInput(name, value, opts);
|
||||
};
|
||||
this.reset = function () {
|
||||
var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||||
@@ -850,6 +854,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
evt.value = value;
|
||||
evt.binding = opts.binding;
|
||||
evt.el = opts.el;
|
||||
evt.priority = opts.priority;
|
||||
|
||||
$(document).trigger(evt);
|
||||
|
||||
@@ -857,9 +862,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
name = evt.name;
|
||||
if (evt.inputType !== '') name += ':' + evt.inputType;
|
||||
|
||||
// opts aren't passed along to lower levels in the input decorator
|
||||
// Most opts aren't passed along to lower levels in the input decorator
|
||||
// stack.
|
||||
this.target.setInput(name, evt.value);
|
||||
this.target.setInput(name, evt.value, { priority: opts.priority });
|
||||
}
|
||||
};
|
||||
}).call(InputEventDecorator.prototype);
|
||||
@@ -872,7 +877,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
this.setInput = function (name, value, opts) {
|
||||
this.$ensureInit(name);
|
||||
|
||||
if (opts.immediate) this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
|
||||
if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
|
||||
};
|
||||
this.setRatePolicy = function (name, mode, millis) {
|
||||
if (mode === 'direct') {
|
||||
@@ -924,11 +929,25 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
// Merge opts with defaults, and return a new object.
|
||||
function addDefaultInputOpts(opts) {
|
||||
return $.extend({
|
||||
immediate: false,
|
||||
|
||||
opts = $.extend({
|
||||
priority: "immediate",
|
||||
binding: null,
|
||||
el: null
|
||||
}, opts);
|
||||
|
||||
if (opts && typeof opts.priority !== "undefined") {
|
||||
switch (opts.priority) {
|
||||
case "deferred":
|
||||
case "immediate":
|
||||
case "event":
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function splitInputNameType(name) {
|
||||
@@ -2968,7 +2987,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
return function (e) {
|
||||
if (e === null) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2976,7 +2995,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
// If outside of plotting region
|
||||
if (!coordmap.isInPanel(offset)) {
|
||||
if (nullOutside) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
if (clip) return;
|
||||
@@ -2997,8 +3016,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
coords.range = panel.range;
|
||||
coords.log = panel.log;
|
||||
|
||||
coords[".nonce"] = Math.random();
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords, { priority: "event" });
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -3185,7 +3203,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
// We're in a new or reset state
|
||||
if (isNaN(coords.xmin)) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
// Must tell other brushes to clear.
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: inputId, outputId: null
|
||||
@@ -3212,7 +3230,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
coords.outputId = outputId;
|
||||
|
||||
// Send data to server
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords);
|
||||
|
||||
$el.data("mostRecentBrush", true);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
|
||||
@@ -3846,7 +3864,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
};
|
||||
|
||||
exports.resetBrush = function (brushId) {
|
||||
exports.onInputChange(brushId, null);
|
||||
exports.setInputValue(brushId, null);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: brushId, outputId: null
|
||||
});
|
||||
@@ -6072,7 +6090,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
|
||||
inputs = new InputValidateDecorator(inputs);
|
||||
|
||||
exports.onInputChange = function (name, value, opts) {
|
||||
exports.setInputValue = exports.onInputChange = function (name, value, opts) {
|
||||
opts = addDefaultInputOpts(opts);
|
||||
inputs.setInput(name, value, opts);
|
||||
};
|
||||
@@ -6086,7 +6104,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
|
||||
var type = binding.getType(el);
|
||||
if (type) id = id + ":" + type;
|
||||
|
||||
var opts = { immediate: !allowDeferred, binding: binding, el: el };
|
||||
var opts = {
|
||||
priority: allowDeferred ? "deferred" : "immediate",
|
||||
binding: binding,
|
||||
el: el
|
||||
};
|
||||
inputs.setInput(id, value, opts);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
6
inst/www/shared/shiny.min.js
vendored
6
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -101,7 +101,7 @@ function initShiny() {
|
||||
|
||||
inputs = new InputValidateDecorator(inputs);
|
||||
|
||||
exports.onInputChange = function(name, value, opts) {
|
||||
exports.setInputValue = exports.onInputChange = function(name, value, opts) {
|
||||
opts = addDefaultInputOpts(opts);
|
||||
inputs.setInput(name, value, opts);
|
||||
};
|
||||
@@ -116,7 +116,11 @@ function initShiny() {
|
||||
if (type)
|
||||
id = id + ":" + type;
|
||||
|
||||
let opts = { immediate: !allowDeferred, binding: binding, el: el };
|
||||
let opts = {
|
||||
priority: allowDeferred ? "deferred" : "immediate",
|
||||
binding: binding,
|
||||
el: el
|
||||
};
|
||||
inputs.setInput(id, value, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,26 +189,34 @@ var InputBatchSender = function(shinyapp) {
|
||||
this.lastChanceCallback = [];
|
||||
};
|
||||
(function() {
|
||||
this.setInput = function(name, value) {
|
||||
var self = this;
|
||||
|
||||
this.setInput = function(name, value, opts) {
|
||||
this.pendingData[name] = value;
|
||||
|
||||
if (!this.timerId && !this.reentrant) {
|
||||
this.timerId = setTimeout(function() {
|
||||
self.reentrant = true;
|
||||
try {
|
||||
$.each(self.lastChanceCallback, function(i, callback) {
|
||||
callback();
|
||||
});
|
||||
self.timerId = null;
|
||||
var currentData = self.pendingData;
|
||||
self.pendingData = {};
|
||||
self.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
self.reentrant = false;
|
||||
}
|
||||
}, 0);
|
||||
if (!this.reentrant) {
|
||||
if (opts.priority === "event") {
|
||||
this.$sendNow();
|
||||
} else if (!this.timerId) {
|
||||
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$sendNow = function() {
|
||||
if (this.reentrant) {
|
||||
console.trace("Unexpected reentrancy in InputBatchSender!");
|
||||
}
|
||||
|
||||
this.reentrant = true;
|
||||
try {
|
||||
this.timerId = null;
|
||||
$.each(this.lastChanceCallback, (i, callback) => {
|
||||
callback();
|
||||
});
|
||||
var currentData = this.pendingData;
|
||||
this.pendingData = {};
|
||||
this.shinyapp.sendInput(currentData);
|
||||
} finally {
|
||||
this.reentrant = false;
|
||||
}
|
||||
};
|
||||
}).call(InputBatchSender.prototype);
|
||||
@@ -219,21 +227,18 @@ var InputNoResendDecorator = function(target, initialValues) {
|
||||
this.lastSentValues = this.reset(initialValues);
|
||||
};
|
||||
(function() {
|
||||
this.setInput = function(name, value) {
|
||||
// Note that opts is not passed to setInput at this stage of the input
|
||||
// decorator stack. If in the future this setInput keeps track of opts, it
|
||||
// would be best not to store the `el`, because that could prevent it from
|
||||
// being GC'd.
|
||||
this.setInput = function(name, value, opts) {
|
||||
const { name: inputName, inputType: inputType } = splitInputNameType(name);
|
||||
const jsonValue = JSON.stringify(value);
|
||||
|
||||
if (this.lastSentValues[inputName] &&
|
||||
if (opts.priority !== "event" &&
|
||||
this.lastSentValues[inputName] &&
|
||||
this.lastSentValues[inputName].jsonValue === jsonValue &&
|
||||
this.lastSentValues[inputName].inputType === inputType) {
|
||||
return;
|
||||
}
|
||||
this.lastSentValues[inputName] = { jsonValue, inputType };
|
||||
this.target.setInput(name, value);
|
||||
this.target.setInput(name, value, opts);
|
||||
};
|
||||
this.reset = function(values = {}) {
|
||||
// Given an object with flat name-value format:
|
||||
@@ -271,6 +276,7 @@ var InputEventDecorator = function(target) {
|
||||
evt.value = value;
|
||||
evt.binding = opts.binding;
|
||||
evt.el = opts.el;
|
||||
evt.priority = opts.priority;
|
||||
|
||||
$(document).trigger(evt);
|
||||
|
||||
@@ -278,9 +284,9 @@ var InputEventDecorator = function(target) {
|
||||
name = evt.name;
|
||||
if (evt.inputType !== '') name += ':' + evt.inputType;
|
||||
|
||||
// opts aren't passed along to lower levels in the input decorator
|
||||
// Most opts aren't passed along to lower levels in the input decorator
|
||||
// stack.
|
||||
this.target.setInput(name, evt.value);
|
||||
this.target.setInput(name, evt.value, { priority: opts.priority });
|
||||
}
|
||||
};
|
||||
}).call(InputEventDecorator.prototype);
|
||||
@@ -294,7 +300,7 @@ var InputRateDecorator = function(target) {
|
||||
this.setInput = function(name, value, opts) {
|
||||
this.$ensureInit(name);
|
||||
|
||||
if (opts.immediate)
|
||||
if (opts.priority !== "deferred")
|
||||
this.inputRatePolicies[name].immediateCall(name, value, opts);
|
||||
else
|
||||
this.inputRatePolicies[name].normalCall(name, value, opts);
|
||||
@@ -359,11 +365,25 @@ const InputValidateDecorator = function(target) {
|
||||
|
||||
// Merge opts with defaults, and return a new object.
|
||||
function addDefaultInputOpts(opts) {
|
||||
return $.extend({
|
||||
immediate: false,
|
||||
|
||||
opts = $.extend({
|
||||
priority: "immediate",
|
||||
binding: null,
|
||||
el: null
|
||||
}, opts);
|
||||
|
||||
if (opts && typeof(opts.priority) !== "undefined") {
|
||||
switch (opts.priority) {
|
||||
case "deferred":
|
||||
case "immediate":
|
||||
case "event":
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ imageutils.initCoordmap = function($el, coordmap) {
|
||||
|
||||
return function(e) {
|
||||
if (e === null) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ imageutils.initCoordmap = function($el, coordmap) {
|
||||
// If outside of plotting region
|
||||
if (!coordmap.isInPanel(offset)) {
|
||||
if (nullOutside) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
return;
|
||||
}
|
||||
if (clip)
|
||||
@@ -466,8 +466,7 @@ imageutils.initCoordmap = function($el, coordmap) {
|
||||
coords.range = panel.range;
|
||||
coords.log = panel.log;
|
||||
|
||||
coords[".nonce"] = Math.random();
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords, {priority: "event"});
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -662,7 +661,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
|
||||
|
||||
// We're in a new or reset state
|
||||
if (isNaN(coords.xmin)) {
|
||||
exports.onInputChange(inputId, null);
|
||||
exports.setInputValue(inputId, null);
|
||||
// Must tell other brushes to clear.
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: inputId, outputId: null
|
||||
@@ -689,7 +688,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
|
||||
coords.outputId = outputId;
|
||||
|
||||
// Send data to server
|
||||
exports.onInputChange(inputId, coords);
|
||||
exports.setInputValue(inputId, coords);
|
||||
|
||||
$el.data("mostRecentBrush", true);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
|
||||
@@ -1373,7 +1372,7 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
|
||||
};
|
||||
|
||||
exports.resetBrush = function(brushId) {
|
||||
exports.onInputChange(brushId, null);
|
||||
exports.setInputValue(brushId, null);
|
||||
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
|
||||
brushId: brushId, outputId: null
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user