Apify Page Function modification - web-scraping

Previously the page function below
FX: $('span[class="enhanced-table-cell-api"]').text()
has worked OK for a long time on an apify/web-scraper actor on https://www.asb.co.nz/foreign-exchange/foreign-exchange-rates.html
Currently (I have now way of obtaining the previous version prior to September 8) the following html/DOM inspector extract is:
<span class="enhanced-table-cell-api" data-api="">
<div class="api-data-params"
data-api="forex"
data-api-attrs=
"currencycode~USD$description~$isFeatured~ $smallestNote~"
data-default=""
data-prefix=""
data-suffix="" data-subtext=""
data-displayfield="buysPayments"
data-api-url="https://api.asb.co.nz/public/v1/exchange rates"
data-api-key="17xx93b1538ae6564c3fa170f899b645c605"
data-api-keyfields=""
data-api-mwport="">
</div>0.6771
</span>
I am trying to extract the '0.6671' string, and 'USD' from the first column, along with 80 or so other elements of a similar class on the page.
There must have been a change to the website as the output is now only a number of blank rows. I am looking for suggestions as to how to modify the page function to extract the values again.
Thanks for your help/advice.

you may try to use this:
const values = $('span.enhanced-table-cell-api').map((index, el) => {
const $el = $(el);
return {
FX: $el.text().trim(),
currency: $el
.find('[data-api-attrs]')
.first()
.attr('data-api-attrs')
.split(/(currencyCode~|\$)/g, 3)
.filter(s => s)[1]
}
}).toArray();
/*
values now contain an array of { FX, currency } values like:
*/
const values = [
{
"FX": "0.9568",
"currency": "AUD"
},
{
"FX": "0.9376",
"currency": "AUD"
},
{
"FX": "0.9332",
"currency": "AUD"
},
{
"FX": "0.9003",
"currency": "AUD"
},
{
"FX": "0.5511",
"currency": "GBP"
},
{
"FX": "0.5344",
"currency": "GBP"
},
{
"FX": "0.5308",
"currency": "GBP"
},
{
"FX": "0.5109",
"currency": "GBP"
},
{
"FX": "0.5978",
"currency": "EUR"
},
{
"FX": "0.5804",
"currency": "EUR"
},
{
"FX": "0.5776",
"currency": "EUR"
},
{
"FX": "0.5513",
"currency": "EUR"
},
{
"FX": "75.1556",
"currency": "JPY"
},
{
"FX": "72.7472",
"currency": "JPY"
},
{
"FX": "72.2514",
"currency": "JPY"
},
{
"FX": "69.3433",
"currency": "JPY"
},
{
"FX": "0.7049",
"currency": "USD"
},
{
"FX": "0.6849",
"currency": "USD"
},
{
"FX": "0.6802",
"currency": "USD"
},
{
"FX": "0.6543",
"currency": "USD"
},
{
"FX": "0.9568",
"currency": "AUD"
},
{
"FX": "0.9376",
"currency": "AUD"
},
{
"FX": "0.9332",
"currency": "AUD"
},
{
"FX": "0.9003",
"currency": "AUD"
},
{
"FX": "0.5511",
"currency": "GBP"
},
{
"FX": "0.5344",
"currency": "GBP"
},
{
"FX": "0.5308",
"currency": "GBP"
},
{
"FX": "0.5109",
"currency": "GBP"
},
{
"FX": "0.9346",
"currency": "CAD"
},
{
"FX": "0.9003",
"currency": "CAD"
},
{
"FX": "0.8968",
"currency": "CAD"
},
{
"FX": "0.8620",
"currency": "CAD"
},
{
"FX": "72.5986",
"currency": "XPF"
},
{
"FX": "69.4658",
"currency": "XPF"
},
{
"FX": "65.2153",
"currency": "XPF"
},
{
"FX": "5.1225",
"currency": "CNY"
},
{
"FX": "4.2675",
"currency": "CNY"
},
{
"FX": "4.4771",
"currency": "DKK"
},
{
"FX": "4.2799",
"currency": "DKK"
},
{
"FX": "4.0914",
"currency": "DKK"
},
{
"FX": "0.5978",
"currency": "EUR"
},
{
"FX": "0.5804",
"currency": "EUR"
},
{
"FX": "0.5776",
"currency": "EUR"
},
{
"FX": "0.5513",
"currency": "EUR"
},
{
"FX": "1.5422",
"currency": "FJD"
},
{
"FX": "1.4569",
"currency": "FJD"
},
{
"FX": "1.3595",
"currency": "FJD"
},
{
"FX": "5.5051",
"currency": "HKD"
},
{
"FX": "5.2880",
"currency": "HKD"
},
{
"FX": "5.2724",
"currency": "HKD"
},
{
"FX": "5.0683",
"currency": "HKD"
},
{
"FX": "47.5891",
"currency": "INR"
},
{
"FX": "75.1556",
"currency": "JPY"
},
{
"FX": "72.7472",
"currency": "JPY"
},
{
"FX": "72.2514",
"currency": "JPY"
},
{
"FX": "69.3433",
"currency": "JPY"
},
{
"FX": "6.4380",
"currency": "NOK"
},
{
"FX": "6.1602",
"currency": "NOK"
},
{
"FX": "5.8979",
"currency": "NOK"
},
{
"FX": "31.4086",
"currency": "PHP"
},
{
"FX": "1.9609",
"currency": "WST"
},
{
"FX": "1.8693",
"currency": "WST"
},
{
"FX": "1.6274",
"currency": "WST"
},
{
"FX": "0.9724",
"currency": "SGD"
},
{
"FX": "0.9359",
"currency": "SGD"
},
{
"FX": "0.9313",
"currency": "SGD"
},
{
"FX": "0.8932",
"currency": "SGD"
},
{
"FX": "11.9199",
"currency": "ZAR"
},
{
"FX": "11.4809",
"currency": "ZAR"
},
{
"FX": "10.9864",
"currency": "ZAR"
},
{
"FX": "6.3117",
"currency": "SEK"
},
{
"FX": "5.9611",
"currency": "SEK"
},
{
"FX": "5.7011",
"currency": "SEK"
},
{
"FX": "0.6439",
"currency": "CHF"
},
{
"FX": "0.6203",
"currency": "CHF"
},
{
"FX": "0.6190",
"currency": "CHF"
},
{
"FX": "0.5935",
"currency": "CHF"
},
{
"FX": "22.6152",
"currency": "THB"
},
{
"FX": "21.3390",
"currency": "THB"
},
{
"FX": "20.3135",
"currency": "THB"
},
{
"FX": "1.7419",
"currency": "TOP"
},
{
"FX": "1.6212",
"currency": "TOP"
},
{
"FX": "1.4157",
"currency": "TOP"
},
{
"FX": "2.8737",
"currency": "AED"
},
{
"FX": "2.3155",
"currency": "AED"
},
{
"FX": "0.7049",
"currency": "USD"
},
{
"FX": "0.6849",
"currency": "USD"
},
{
"FX": "0.6802",
"currency": "USD"
},
{
"FX": "0.6543",
"currency": "USD"
}
]

Related

Shiny searchInput

Does anyone know if it is possible to create a "shinyWidget :: searchInput" designed with a grid that would allow entering only 9 digits in the field?
https://pasteboard.co/J3mduIS.png
Here is a shiny implementation of bootstrap-pincode-input.
Download the files bootstrap-pincode-input.js and bootstrap-pincode-input.css from here. Put them in the www subfolder of the folder containing the shiny app.
Also add the JS file below to the www subfolder, call it pincodeBinding.js.
var pincodeBinding = new Shiny.InputBinding();
$.extend(pincodeBinding, {
find: function (scope) {
return $(scope).find(".pincode");
},
getValue: function (el) {
return $(el).val();
},
setValue: function(el, value) {
$(el).val(value);
},
subscribe: function (el, callback) {
$(el).on("change.pincodeBinding", function (e) {
callback();
});
},
unsubscribe: function (el) {
$(el).off(".pincodeBinding");
},
initialize: function(el) {
var $el = $(el);
$el.pincodeInput({
inputs: $el.data("ndigits"),
hidedigits: $el.data("hide"),
complete: function(value, e, errorElement){
Shiny.setInputValue($el.attr("id"), value);
}
});
}
});
Shiny.inputBindings.register(pincodeBinding);
Now, the shiny app:
library(shiny)
pincodeInput <- function(inputId, width = "30%", height = "100px",
label = NULL, ndigits = 4, hideDigits = FALSE){
tags$div(style = sprintf("width: %s; height: %s;", width, height),
shiny:::shinyInputLabel(inputId, label),
tags$input(id = inputId, class = "pincode", type = "text",
`data-ndigits` = ndigits,
`data-hide` = ifelse(hideDigits, "true", "false")
)
)
}
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "bootstrap-pincode-input.css"),
tags$script(src = "bootstrap-pincode-input.js"),
tags$script(src = "pincodeBinding.js")
),
br(),
pincodeInput("pincode", label = "Enter pincode"),
br(),
h3("You entered:"),
verbatimTextOutput("pincodeValue")
)
server <- function(input, output, session){
output[["pincodeValue"]] <- renderPrint({
input[["pincode"]]
})
}
shinyApp(ui, server)
Note that the pincode input only accepts digits, not alphabetical characters. I don't know whether this is what you want?
EDIT: clearable pincode input
Add this CSS file in the www folder, name it pincode-input.css:
.clearable {
padding: 1px 6px 1px 1px;
display: inline-flex;
}
.clearable span {
cursor: pointer;
color: blue;
font-weight: bold;
visibility: hidden;
margin-left: 5px;
}
Replace pincodeBinding.js with this file:
var pincodeBinding = new Shiny.InputBinding();
$.extend(pincodeBinding, {
find: function (scope) {
return $(scope).find(".pincode");
},
getValue: function (el) {
return $(el).val();
},
setValue: function(el, value) {
$(el).val(value);
},
subscribe: function (el, callback) {
$(el).on("change.pincodeBinding", function (e) {
callback();
});
},
unsubscribe: function (el) {
$(el).off(".pincodeBinding");
},
initialize: function(el) {
var $el = $(el);
var clearBtn = el.nextElementSibling;
$el.pincodeInput({
inputs: $el.data("ndigits"),
hidedigits: $el.data("hide"),
complete: function(value, e, errorElement){
Shiny.setInputValue($el.attr("id"), value);
},
change: function(){
clearBtn.style.visibility = ($el.val().length) ? "visible" : "hidden";
}
});
clearBtn.onclick = function() {
this.style.visibility = "hidden";
$el.pincodeInput().data("plugin_pincodeInput").clear();
Shiny.setInputValue($el.attr("id"), "");
};
}
});
Shiny.inputBindings.register(pincodeBinding);
The app:
library(shiny)
pincodeInput <- function(inputId, width = "30%", height = "100px",
label = NULL, ndigits = 4, hideDigits = FALSE){
tags$div(style = sprintf("width: %s; height: %s;", width, height),
shiny:::shinyInputLabel(inputId, label),
tags$span(
class = "clearable",
tags$input(id = inputId, class = "pincode", type = "text",
`data-ndigits` = ndigits,
`data-hide` = ifelse(hideDigits, "true", "false")
),
tags$span(title = "Clear", HTML("×"))
)
)
}
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "bootstrap-pincode-input.css"),
tags$link(rel = "stylesheet", href = "pincode-input.css"),
tags$script(src = "bootstrap-pincode-input.js"),
tags$script(src = "pincodeBinding.js")
),
br(),
pincodeInput("pincode", label = "Enter pincode"),
br(),
h3("You entered:"),
verbatimTextOutput("pincodeValue")
)
server <- function(input, output, session){
output[["pincodeValue"]] <- renderPrint({
input[["pincode"]]
})
}
shinyApp(ui, server)

selectize.js and Shiny : select choices from a remote API

With no prior programming knowledge in JavaScript and APIs, I'm having some troubles to make this example fit my needs : select GitHub repo.
I'm trying to adapt it to work with this API: https://api-adresse.data.gouv.fr/search/? .
The response is a GeoJSON file, where the features are stored in response$features. I want to get the properties$label attribute for each feature.
Here is what I've done so far. I get an array but the items are not displayed in the dropdown...
UI :
########
# ui.R #
########
library(shiny)
fluidPage(
title = 'Selectize examples',
mainPanel(
selectizeInput('addresses', 'Select address', choices = '', options = list(
valueField = 'properties.label',
labelField = 'properties.label',
searchField = 'properties.label',
options = list(),
create = FALSE,
render = I("
{
option: function(item, escape) {
return '<div>' + '<strong>' + escape(item.properties.name) + '</strong>' + '</div>';
}
}" ),
load = I("
function(query, callback) {
if (!query.length) return callback();
$.ajax({
url: 'https://api-adresse.data.gouv.fr/search/?',
type: 'GET',
data: {
q: query
},
dataType: 'json',
error: function() {
callback();
},
success: function(res) {
console.log(res.features);
callback(res.features);
}
});
}"
)
))
)
)
Server :
############
# server.R #
############
library(shiny)
function(input, output) {
output$github <- renderText({
paste('You selected', if (input$github == '') 'nothing' else input$github,
'in the Github example.')
})
}
Thank you for your help.
Got it working thanks to this comment.
selectize doesn't support accessing nested values with dot notation
UI:
########
# ui.R #
########
library(shiny)
fluidPage(
title = 'Selectize examples',
mainPanel(
selectizeInput('addresses', 'Select address', choices = '', options = list(
valueField = 'name',
labelField = 'name',
searchField = 'name',
loadThrottle = '500',
persist = FALSE,
options = list(),
create = FALSE,
render = I("
{
option: function(item, escape) {
return '<div>' + '<strong>' + escape(item.name) + '</strong>' + '</div>';
}
}" ),
load = I("
function(query, callback) {
if (!query.length) return callback();
$.ajax({
url: 'https://api-adresse.data.gouv.fr/search/?',
type: 'GET',
data: {
q: query
},
dataType: 'json',
error: function() {
callback();
},
success: function (data) {
callback(data.features.map(function (item) {
return {name: item.properties.name,
label: item.properties.label,
score: item.properties.score};
}));
}
});
}"
)
))
)
)
Server:
############
# server.R #
############
library(shiny)
function(input, output) {
output$github <- renderText({
paste('You selected', if (input$github == '') 'nothing' else input$github,
'in the Github example.')
})
}

Customizing search box in shiny app

I have a search box that searches states and my selections are able to transferred to another bucket. My goal is for the user to be able to press enter upon searching the state they are interested in and push their result to the selection bucket. For example, user puts New Hampshire in search box and presses enter -- New Hampshire disappears from the list of selections and is transferred to the selection bucket. Currently the user would have to double click on New Hampshire after searching to push the selection to the other box. Furthermore, New Hampshire doesn't disappear from the list of selections if it is pushed to the selected bucket.
server.R
shinyServer(function(input, output, session) {
output$main <- renderUI({
source("chooser.R")
chooserInput("mychooser","Available frobs","Selected frobs",
row.names(USArrests),c(),size=20,multiple=TRUE)})
})
ui.R
source("chooser.R")
shinyUI(fluidPage(
uiOutput("main")
))
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js (in www folder)
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
$(function() {
$('.left').filterByText($('.chooser-input-search'), true);
});
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
As you can see this is pretty much a shameful copy and paste.
I think this works somewhat
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style=
"min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
// Push back options to left select array
if(source == '.right'){
$.each(selected,function(i){
var sel = selected[i];
options.push({value: $(sel).val(), text: $(sel).text()});
});
}
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
/*
Create separate bindings for each chooser widget
*/
$('.chooser').each(function(){
var chooser = $(this);
var left_sel = $(this).find('.left');
var right_sel = $(this).find('.right');
var search_b = $(this).find('.chooser-input-search');
// Search function
$(function() {
$(left_sel).filterByText(search_b, true);
});
//Enter binding
// If element in focus
$('.chooser-input-search').focus(function() {
$(this).keypress(function(e){
// If enter pressed
if(e.which == 13) {
if( $(search_b).val().length > 2){
// Save for debuging
var sel_options = [];
$(left_sel).find('option').each(function() {
var curr_val = $(this).val();
var curr_txt = $(this).text();
// Push to debug array
sel_options.push({value: curr_val, text: curr_txt});
// Append to tight selection
$(right_sel).append(
$('<option>').text(curr_val).val(curr_txt)
);
// Remove option
$(this).remove();
}); // end each
// Remove options from gloabl options array
$.each(options, function(i) {
var option = options[i];
$.each(sel_options,function(j){
var sel_option = sel_options[j];
if (option.value==sel_option.value && option.text==sel_option.text){
options.splice(i, 1);
}
});
});
}
}
});
});
}); // End enter keybinding
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();

Add a search box to custom input control in shiny

My goal is to add a search box on top of the custom input control in shiny. I would like when a user searches Hampshire for example, the selection to pick New Hampshire which is not currently possible as it searches just by the first letter.
server.R
shinyServer(function(input, output, session) {
output$main <- renderUI({
source("chooser.R")
chooserInput("mychooser","Available frobs","Selected frobs",
row.names(USArrests),c(),size=20,multiple=TRUE)})
})
ui.R
source("chooser.R")
shinyUI(fluidPage(
uiOutput("main")
))
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",
div(class="chooser-container chooser-left-container",
tags$select(class="left", size=size, multiple=multiple, leftChoices)
),
div(class="chooser-container chooser-center-container",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
),
div(class="chooser-container chooser-right-container",
tags$select(class="right", size=size, multiple=multiple, rightChoices)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-binding.js (in www folder)
(function() {
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
Cool widget (or whatever the terminology is). This question has actually been answered here so make sure to vote on the persons answer if it helps you.
Here's a super simple implementations of it (could be better):
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
$(function() {
$('.left').filterByText($('.chooser-input-search'), true);
});
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
As you can see this is pretty much a shameful copy and paste.

How can I return options for an HTML select from a Template Helper in Meteor?

I thought I could return the options for an HTML select element something like this:
In the Template's .HTML file:
<select id="stateorprovince" name="stateorprovince">
{{statesAndProvinces}}
</select>
In the Template's .js file (bogus data for now; will replace with US States and Canadian Provinces, at least):
Template.addJobLoc.helpers({
statesAndProvinces: function() {
return
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
}
});
...but it fails to build with a syntax error at the start of the first "option"
If I enclose the options in single quotes, the error is "Unexpected token ILLEGAL'
What is the correct way to accomplish this?
You could use this approach which is better suited than returning HTML from a template helper :
HTML
<template name="addJobLoc">
<select id="stateorprovince" name="stateorprovince">
{{#each statesAndProvinces}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</template>
JS
Template.addJobLoc.helpers({
statesAndProvinces: function(){
return [{
value: "volvo",
label: "Volvo"
},{
value: "saab",
label: "Saab"
}, ...];
}
});
Here we're returning an hardcoded array within the helper for the sake of simplicity but you could also return collection data.
In your helper, you need to wrap your return value in quotes.
I would suggest the following changes
Helpers
statesAndProvinces: function(){
return ["Volvo", "Saab","Mercedes"];
}
HTML(inside the select tags)
{{#each statesAndProvinces}}
<option value="{{this}}">{{this}}</option>
{{/each}}
Adapting saimeunt's accepted answer, this is what I ended up with (just to show the complete code).
If you want both U.S. States (and Territories, etc.) plus Canadian Provinces, you can use this in your Meteor apps as-is, except for replacing the Template name of "addJobLoc" with your template name.
I am using the full place name as the "hover hint" for the item, and the two-letter designation as the content (select options).
I got the JSON list of states and provinces from wholypantalones (great moniker, primo!) here.
HTML:
<select id="stateorprovince" name="stateorprovince">
{{#each statesAndProvinces}}
<option title="{{hint}}">{{abbrcode}}</option>
{{/each}}
</select>
JavaScript (Helper in *.js file):
Template.addJobLoc.helpers({
statesAndProvinces: function() {
return [{
"hint": "Alabama",
"abbrcode": "AL"
}, {
"hint": "Alaska",
"abbrcode": "AK"
}, {
"hint": "American Samoa",
"abbrcode": "AS"
}, {
"hint": "Arizona",
"abbrcode": "AZ"
}, {
"hint": "Arkansas",
"abbrcode": "AR"
}, {
"hint": "British Columbia",
"abbrcode": "BC"
}, {
"hint": "California",
"abbrcode": "CA"
}, {
"hint": "Colorado",
"abbrcode": "CO"
}, {
"hint": "Connecticut",
"abbrcode": "CT"
}, {
"hint": "Delaware",
"abbrcode": "DE"
}, {
"hint": "District Of Columbia",
"abbrcode": "DC"
}, {
"hint": "Federated States Of Micronesia",
"abbrcode": "FM"
}, {
"hint": "Florida",
"abbrcode": "FL"
}, {
"hint": "Georgia",
"abbrcode": "GA"
}, {
"hint": "Guam",
"abbrcode": "GU"
}, {
"hint": "Hawaii",
"abbrcode": "HI"
}, {
"hint": "Idaho",
"abbrcode": "ID"
}, {
"hint": "Illinois",
"abbrcode": "IL"
}, {
"hint": "Indiana",
"abbrcode": "IN"
}, {
"hint": "Iowa",
"abbrcode": "IA"
}, {
"hint": "Kansas",
"abbrcode": "KS"
}, {
"hint": "Kentucky",
"abbrcode": "KY"
}, {
"hint": "Louisiana",
"abbrcode": "LA"
}, {
"hint": "Maine",
"abbrcode": "ME"
}, {
"hint": "Manitoba",
"abbrcode": "MB"
}, {
"hint": "Marshall Islands",
"abbrcode": "MH"
}, {
"hint": "Maryland",
"abbrcode": "MD"
}, {
"hint": "Massachusetts",
"abbrcode": "MA"
}, {
"hint": "Michigan",
"abbrcode": "MI"
}, {
"hint": "Minnesota",
"abbrcode": "MN"
}, {
"hint": "Mississippi",
"abbrcode": "MS"
}, {
"hint": "Missouri",
"abbrcode": "MO"
}, {
"hint": "Montana",
"abbrcode": "MT"
}, {
"hint": "Nebraska",
"abbrcode": "NE"
}, {
"hint": "Nevada",
"abbrcode": "NV"
}, {
"hint": "New Brunswick",
"abbrcode": "NB"
}, {
"hint": "New Hampshire",
"abbrcode": "NH"
}, {
"hint": "New Jersey",
"abbrcode": "NJ"
}, {
"hint": "New Mexico",
"abbrcode": "NM"
}, {
"hint": "New York",
"abbrcode": "NY"
}, {
"hint": "Newfoundland and Labrador",
"abbrcode": "NL"
}, {
"hint": "North Carolina",
"abbrcode": "NC"
}, {
"hint": "North Dakota",
"abbrcode": "ND"
}, {
"hint": "Northern Mariana Islands",
"abbrcode": "MP"
}, {
"hint": "Nova Scotia",
"abbrcode": "NS"
}, {
"hint": "Northwest Territories",
"abbrcode": "NT"
}, {
"hint": "Nunavut",
"abbrcode": "NU"
}, {
"hint": "Ohio",
"abbrcode": "OH"
}, {
"hint": "Oklahoma",
"abbrcode": "OK"
}, {
"hint": "Ontario",
"abbrcode": "ON"
}, {
"hint": "Oregon",
"abbrcode": "OR"
}, {
"hint": "Palau",
"abbrcode": "PW"
}, {
"hint": "Pennsylvania",
"abbrcode": "PA"
}, {
"hint": "Prince Edward Island",
"abbrcode": "PE"
}, {
"hint": "Puerto Rico",
"abbrcode": "PR"
}, {
"hint": "Quebec",
"abbrcode": "QC"
}, {
"hint": "Rhode Island",
"abbrcode": "RI"
}, {
"hint": "Saskatchewan",
"abbrcode": "SK"
}, {
"hint": "South Carolina",
"abbrcode": "SC"
}, {
"hint": "South Dakota",
"abbrcode": "SD"
}, {
"hint": "Tennessee",
"abbrcode": "TN"
}, {
"hint": "Texas",
"abbrcode": "TX"
}, {
"hint": "Utah",
"abbrcode": "UT"
}, {
"hint": "Vermont",
"abbrcode": "VT"
}, {
"hint": "Virgin Islands",
"abbrcode": "VI"
}, {
"hint": "Virginia",
"abbrcode": "VA"
}, {
"hint": "Washington",
"abbrcode": "WA"
}, {
"hint": "West Virginia",
"abbrcode": "WV"
}, {
"hint": "Wisconsin",
"abbrcode": "WI"
}, {
"hint": "Wyoming",
"abbrcode": "WY"
}, {
"hint": "Yukon",
"abbrcode": "YT"
}]
}
});

Resources