diff --git a/_quarto.yml b/_quarto.yml index e763e8f1..3fa634c5 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -85,6 +85,8 @@ website: - section: "Interactivity" contents: + - text: "Accessing Question Values" + href: docs/accessing-values.qmd - text: "Conditional Logic" href: docs/conditional-logic.qmd - text: "Reactivity" @@ -152,6 +154,8 @@ website: href: templates/random_options.qmd - text: "Random Options Predefined" href: templates/random_options_predefined.qmd + - text: "Option Shuffling" + href: templates/option_shuffling.qmd - section: "Reactivity" contents: diff --git a/blog/2025-11-21-announcing-surveydown-version-1/index.qmd b/blog/2025-11-21-announcing-surveydown-version-1/index.qmd index c585e7bf..c56ef3c3 100644 --- a/blog/2025-11-21-announcing-surveydown-version-1/index.qmd +++ b/blog/2025-11-21-announcing-surveydown-version-1/index.qmd @@ -66,13 +66,13 @@ survey-settings: use-cookies: no auto-scroll: no rate-survey: no - all-questions-required: no + all-required: no start-page: your_first_page system-language: en highlight-unanswered: yes highlight-color: gray capture-metadata: yes - required-questions: [] + required: [] system-messages: cancel: Cancel @@ -245,6 +245,10 @@ In this case, the value stored in the data will be automatically converted to sn | Dark Green | `dark_green` | | Bright Red | `bright_red` | +::: {.callout-caution} +This snake case conversion is **removed** after `v1.1.0`, in which if you provide a single vector of labels, the values stored in the database will be exactly the same as the labels shown to respondents. +::: + ## What's Next? This `v1.0.0` release marks a major milestone for `surveydown`, but we're not stopping here. We have exciting features planned for future releases. Stay tuned! diff --git a/chunks/sdvalue.qmd b/chunks/sdvalue.qmd new file mode 100644 index 00000000..596eb9cb --- /dev/null +++ b/chunks/sdvalue.qmd @@ -0,0 +1,7 @@ +::: {.callout-note} + +The `sd_value()` function returns the chosen value or values for one or more questions. It is a reactive function and can only be used inside the `server()` function in your **app.R** file. + +See the [Accessing Question Values](accessing-values.qmd) page for details on how to use `sd_value()`. + +::: diff --git a/chunks/skip-if.qmd b/chunks/skip-if.qmd index 8cd67bce..68805b30 100644 --- a/chunks/skip-if.qmd +++ b/chunks/skip-if.qmd @@ -1,4 +1,4 @@ -While basic page navigation is handled using `sd_nav()`, you can override this static navigation in your server function with the `sd_skip_if()` function to send the respondent to a forward page based on some condition. +While basic page navigation is handled automatically (or with the `sd_nav()` function for more fine-tuned control), you can override this static navigation in your server function with the `sd_skip_if()` function to send the respondent to a forward page based on some condition. A common example is the need to **screen out** people based on their response(s) to a question. Let's say you need to screen out people who do not own a vehicle. To do this, you would first define a question in your **survey.qmd** file about their vehicle ownership, e.g.: @@ -26,17 +26,11 @@ Sorry, but you are not qualified to take our survey. Then in the server function in the **app.R** file, you can use the `sd_skip_if()` function to define the condition under which the respondent will be sent to the target `screenout` page, like this: -::: {.callout-note} - -The `input` object is a Shiny object that stores each question `id` defined by `sd_question()` in your **survey.qmd** file, so whenever referring to a question in a condition, you must use the format `input$question_id`. - -::: - ```{r} server <- function(input, output, session) { sd_skip_if( - input$vehicle_ownership == "no" ~ "screenout" + sd_value("vehicle_ownership") == "no" ~ "screenout" ) # ...other server code... @@ -48,6 +42,6 @@ You can provide multiple conditions to the `sd_skip_if()` function, each separat > ` ~ "target_page_id"` -In the example above, `input$vehicle_ownership == "no"` is the condition, and `"screenout"` is the target page that the respondent will be sent to if the condition is met. +In the example above, `sd_value("vehicle_ownership") == "no"` is the condition, and `"screenout"` is the target page that the respondent will be sent to if the condition is met. Take a look at the [Common Conditions](conditional-logic.html#common-conditions) section for examples of other types of supported conditions you can use to conditionally control the survey flow. diff --git a/docs/accessing-values.qmd b/docs/accessing-values.qmd new file mode 100644 index 00000000..2e0e76e3 --- /dev/null +++ b/docs/accessing-values.qmd @@ -0,0 +1,328 @@ +--- +title: "Accessing Question Values" +--- + +In surveydown, you often need to access the values that respondents have entered in your survey questions. This is essential for implementing conditional logic, creating reactive content, performing calculations, or storing derived values. + +The primary way to access question values in your **app.R** file is through the `sd_value()` and `sd_values()` functions. The two functions are aliases - they work identically, so use whatever name is easier to remember for you. For the documentation, we'll just be using `sd_value()`. + +## Basic Usage + +The `sd_value()` function retrieves the value(s) that a respondent has entered for one or more questions. + +### Single Question Value + +To get the value of a single question, pass the question ID to `sd_value()`: + +```r +server <- function(input, output, session) { + + # Get the value of a single question + fruit_choice <- sd_value("fruit") + + sd_server() +} +``` + +### Multiple Question Values + +You can retrieve multiple question values at once by passing multiple question IDs. This returns a vector of values: + +```r +server <- function(input, output, session) { + + # Get values from multiple questions + food_choices <- sd_value("fruit", "vegetable", "protein") + # Returns the vector of chosen values, something like c("apple", "lettuce", "chicken") + + sd_server() +} +``` + +## Quoted vs Unquoted IDs + +One of the convenient features of `sd_value()` is that it accepts both quoted and unquoted question IDs: + +```r +# Both of these work identically: +sd_value("fruit") +sd_value(fruit) + +# Also works with multiple IDs: +sd_value("fruit", "vegetable") +sd_value(fruit, vegetable) +``` + +Choose whichever style you prefer - they function the same way. + +## Automatic Type Detection + +One of the most convenient features of `sd_value()` is its **automatic type detection and conversion**. By default, the function intelligently determines the appropriate data type and format for the returned value. + +### Automatic Numeric Conversion + +When `sd_value()` retrieves a value that looks numeric, it automatically converts it to a numeric type. This means you don't need to manually call `as.numeric()`: + +```r +# If age question value is "25", sd_value() returns 25 (numeric) +age <- sd_value("age") + +# You can use it directly in numeric operations +if (age > 18) { + # Do something +} + +# Works in calculations too +age_in_months <- sd_value("age") * 12 +``` + +If a value cannot be converted to numeric (e.g., it contains letters), `sd_value()` returns it as a character string. + +### Automatic Vector Splitting + +For multiple-choice questions that allow multiple selections (like `mc_multiple`), responses are often stored with pipe separators (e.g., `"apple|banana|orange"`). The `sd_value()` function automatically detects and splits these into a vector: + +```r +# If the stored value is "apple|banana|orange" +fruits <- sd_value("favorite_fruits") +# Returns: c("apple", "banana", "orange") + +# You can use it directly in vector operations +if ("apple" %in% fruits) { + # Do something +} +``` + +### Controlling Type Conversion + +While automatic detection works well in most cases, you can override this behavior using the `as_numeric` and `as_vector` parameters: + +**Force numeric conversion:** + +```r +# Always convert to numeric (non-convertible values become NA) +val <- sd_value("some_field", as_numeric = TRUE) +``` + +**Prevent numeric conversion:** + +This is useful for values like ZIP codes that should stay as strings to preserve leading zeros: + +```r +# Keep as string (e.g., "01234" stays "01234", not converted to 1234) +zip <- sd_value("zip_code", as_numeric = FALSE) +``` + +::: {.callout-warning title="ZIP Code Special Case"} + +**ZIP codes require `as_numeric = FALSE`** to preserve leading zeros. Without it, automatic conversion will strip the leading zero: + +```r +# Without as_numeric = FALSE (WRONG for ZIP codes): +zip <- sd_value("zip") +# "01234" becomes 1234 - leading zero lost! + +# With as_numeric = FALSE (CORRECT for ZIP codes): +zip <- sd_value("zip", as_numeric = FALSE) +# "01234" stays as "01234" - preserved correctly +``` + +This matters when you need to check the length: + +```r +# Validate ZIP code length - must use as_numeric = FALSE +sd_stop_if( + nchar(sd_value("zip", as_numeric = FALSE)) != 5 ~ "ZIP code must be 5 digits." +) +``` + +Without `as_numeric = FALSE`, the ZIP code `"01234"` would be converted to `1234`, and `nchar(1234)` would return `4`, incorrectly rejecting a valid ZIP code. + +::: + +**Force vector splitting:** + +```r +# Always split on pipe, even if no pipe is present +vals <- sd_value("some_field", as_vector = TRUE) +``` + +**Prevent vector splitting:** + +```r +# Keep as single string, don't split +raw <- sd_value("favorite_fruits", as_vector = FALSE) +# Returns: "apple|banana|orange" (as single string) +``` + +## Key Features + +### Reactive by Default + +`sd_value()` is a reactive function, which means it automatically updates whenever the question value changes. This makes it ideal for use in reactive contexts like `observe()`, `reactive()`, or within `sd_show_if()` conditions. + +**Important:** Because `sd_value()` is reactive, it can only be used inside the `server()` function in your **app.R** file, not in the **survey.qmd** file. + +### Persistence After Refresh + +When cookies are enabled (the default), `sd_value()` will restore user inputs from the database even after a page refresh. This ensures that respondents don't lose their progress if they accidentally refresh the page or close and reopen the survey. + +### Replacement for `input$` + +If you're familiar with Shiny, you might know about accessing inputs using `input$question_id`. In surveydown, **you should use `sd_value()` instead of `input$`** because: + +1. `sd_value()` restores values after page refresh +2. `sd_value()` provides cleaner syntax with support for unquoted IDs +3. `sd_value()` works seamlessly with surveydown's data persistence features + +## Common Use Cases + +### Conditional Logic + +Use `sd_value()` to check question responses and show/hide content: + +```r +server <- function(input, output, session) { + + sd_show_if( + sd_value("pet_type") == "dog" ~ "dog_breed", + sd_value("pet_type") == "cat" ~ "cat_breed" + ) + + sd_server() +} +``` + +See the [Conditional Logic](conditional-logic.qmd) page for more details. + +### Numeric Calculations + +When working with numeric questions, `sd_value()` automatically converts values to numbers, so you can use them directly in calculations: + +```r +server <- function(input, output, session) { + + # Create a reactive calculation + # sd_value() automatically converts to numeric + total <- reactive({ + sd_value("price") * sd_value("quantity") + }) + + # Store the calculated value + sd_reactive("total_cost", total()) + + sd_server() +} +``` + +### Checking Multiple Choice Selections + +For `mc_multiple` or `mc_multiple_buttons` questions that return vectors: + +```r +server <- function(input, output, session) { + + # Check if a specific option was selected + sd_show_if( + "apple" %in% sd_value("favorite_fruits") ~ "apple_question" + ) + + # Check how many options were selected + sd_show_if( + length(sd_value("favorite_fruits")) >= 3 ~ "fruit_lover" + ) + + sd_server() +} +``` + +### Creating Derived Values + +You can use question values to create new values and store them: + +```r +server <- function(input, output, session) { + + # Create a derived value based on responses + # sd_value() automatically converts age to numeric + category <- reactive({ + age <- sd_value("age") + if (age < 18) { + "minor" + } else if (age < 65) { + "adult" + } else { + "senior" + } + }) + + # Store the category for later analysis + sd_store_value(category = category()) + + sd_server() +} +``` + +### Checking if Questions are Answered + +Use `sd_is_answered()` to check if a question has been answered at all: + +```r +server <- function(input, output, session) { + + sd_show_if( + sd_is_answered("name") ~ "greeting_message" + ) + + sd_server() +} +``` + +This is especially useful when you want to show follow-up content once any answer is provided, regardless of which specific option was chosen. + +## Important Notes + +### NULL Values + +If a question hasn't been answered yet, `sd_value()` returns `NULL`. Always check for this when performing operations that might fail on `NULL`: + +```r +# Safe approach for conditional logic +age <- sd_value("age") +if (!is.null(age) && age > 18) { + # Do something +} +``` + +### Matrix Questions + +For `matrix` type questions, `sd_is_answered()` only returns `TRUE` if **all** sub-questions (rows) have been answered. + +### Automatic Type Detection + +Thanks to automatic type detection, `sd_value()` handles data type conversion for you: + +```r +# Numeric values are automatically converted +# This works directly, no need for as.numeric() +if (sd_value("age") > 18) { + # This works because sd_value() returns 25 (numeric), not "25" (string) +} + +# Pipe-separated values are automatically split into vectors +# This works directly, no need to split manually +if ("apple" %in% sd_value("favorite_fruits")) { + # This works because sd_value() returns c("apple", "banana"), not "apple|banana" +} +``` + +The only time you need to manually control type conversion is for special cases like ZIP codes with leading zeros. In those cases, use `as_numeric = FALSE` to prevent conversion (see [Controlling Type Conversion](#controlling-type-conversion) above). + +## Related Functions + +- `sd_is_answered()` - Check if a question has been answered +- `sd_store_value()` - Store custom values in the survey data +- `sd_reactive()` - Create reactive values that are stored in the data +- `sd_copy_value()` - Create a copy of a question value for display + +See the [Reactivity](reactivity.qmd) page for more information on these related functions. diff --git a/docs/basic-components.qmd b/docs/basic-components.qmd index f7664df3..8b338745 100644 --- a/docs/basic-components.qmd +++ b/docs/basic-components.qmd @@ -171,7 +171,7 @@ By default all questions are optional, but you can make questions required in th ### Navigation buttons -By default, navigation buttons are automatically added to each page to allow respondents to move forward through the survey. To change the global settings (e.g., add a previous button on all pages, change the message shown on all next buttons, etc.), you can edit the survey settings in the YAML header (see the [Survey Settings](survey-settings.html#navigation-buttons) page for details). +By default, navigation buttons are automatically added to each page to allow respondents to move forward through the survey. To change the global settings (e.g., add a previous button on all pages, change the message shown on all next buttons, etc.), you can edit the survey settings in the YAML header (see the [Survey Settings](survey-settings.html#default-settings) page for details). Additionally, you can also manually add navigation buttons using the `sd_nav()` function inside code chunks on any page, which will override the global settings for that specific page, like this: diff --git a/docs/conditional-logic.qmd b/docs/conditional-logic.qmd index 8d4b4ef8..5b11f25e 100644 --- a/docs/conditional-logic.qmd +++ b/docs/conditional-logic.qmd @@ -50,17 +50,13 @@ sd_question( Then in the server function in the **app.R** file, you can use the `sd_show_if()` function to define that the `"penguins_other"` question would only be shown if the respondent chose the `"other"` option in the `"penguins"` question, like this: -::: {.callout-note} - -The `input` object is a Shiny object that stores each question `id` defined by `sd_question()` in your **survey.qmd** file, so whenever referring to a question in a condition, you must use the format `input$question_id`. - -::: +{{< include ../chunks/sdvalue.qmd >}} ```{r} server <- function(input, output, session) { sd_show_if( - input$penguins == "other" ~ "penguins_other" + sd_value("penguins") == "other" ~ "penguins_other" ) sd_server(db = db) @@ -74,7 +70,7 @@ The structure of the `sd_show_if()` function for question display is: You can provide multiple conditions to the `sd_show_if()` function, each separated by a comma. -In the example above, `input$penguins == "other"` is the condition, and `"penguins_other"` is the target question that will be shown if the condition is met. The `~` symbol is used to separate the condition from the target question. +In the example above, `sd_value("penguins") == "other"` is the condition, and `"penguins_other"` is the target question that will be shown if the condition is met. The `~` symbol is used to separate the condition from the target question. Take a look at the [Common Conditions](#common-conditions) section for examples of other types of supported conditions you can use to conditionally display questions. @@ -188,14 +184,23 @@ Then, in the **app.R** file, you can use the `sd_stop_if()` function to define t server <- function(input, output, session) { sd_stop_if( - # The zip question only accepts a 5-digit response - nchar(input$zip) != 5 ~ "Zip code must be 5 digits." + # Use as_numeric = FALSE to preserve leading zeros (e.g., "01234") + # Otherwise sd_value() would convert it to 1234 (only 4 characters) + nchar(sd_value("zip", as_numeric = FALSE)) != 5 ~ "Zip code must be 5 digits." ) sd_server(db = db) } ``` +::: {.callout-tip} + +ZIP codes are a special case where you need to prevent automatic numeric conversion. Without `as_numeric = FALSE`, a ZIP code like `"01234"` would be converted to `1234`, losing the leading zero. This would cause `nchar()` to return `4` instead of `5`, incorrectly flagging valid ZIP codes as invalid. + +::: + + + The structure of the `sd_stop_if()` function is: ```{r} @@ -226,10 +231,10 @@ One of the most common situations is conditioning on the response of a single qu sd_show_if( # Simple condition based on single question choice - input$penguins1 == "other" ~ "penguins1_other", + sd_value("penguins1") == "other" ~ "penguins1_other", # Multiple condition based on multiple question choices - input$penguins2 == "other" & input$show_other == "show" ~ "penguins2_other" + sd_value("penguins2") == "other" & sd_value("show_other") == "show" ~ "penguins2_other" ) ``` @@ -240,11 +245,11 @@ In the second condition, the `penguins2` question is checked to see if the respo ### Numeric values -Another common condition is checking the value of a numeric question. To do so, you need to wrap the `input$question_id` in the `as.numeric()` function because all question values are stored as strings, like this: +Another common condition is checking the value of a numeric question. The `sd_value()` function automatically converts numeric values, so you can use them directly in comparisons: ```{r} sd_show_if( - as.numeric(input$car_number) > 1 ~ "car_ownership" + sd_value("car_number") > 1 ~ "car_ownership" ) ``` @@ -258,10 +263,10 @@ For multiple response question types (e.g. [`mc_multiple`](question-types.html#m sd_show_if( # Check if the respondent chose "apple", "banana", or both - all(input$fav_fruits %in% c("apple", "banana")) ~ "apple_or_banana", + ("apple" %in% sd_value("fav_fruits")) | ("banana" %in% sd_value("fav_fruits")) ~ "apple_or_banana", # Check if the respondent chose more than 3 fruits - length(input$fav_fruits) > 3 ~ "fruit_number" + length(sd_value("fav_fruits")) > 3 ~ "fruit_number" ) ``` @@ -290,23 +295,28 @@ For [`"matrix"`](question-types.html#matrix) type questions, `sd_is_answered()` For situations where the conditional logic is more complex, we recommend defining a custom function that will return a logical value (`TRUE` or `FALSE`). You can then pass this function to `sd_show_if()`, `sd_skip_if()`, or `sd_stop_if()` as a condition. -For example, let's say we had a `mc` type question where we asked how many cars the respondent owned, and we included numeric options `1` through `5` as well as a final option `"6 or more"`. If we wanted to set a condition that would return `TRUE` if the user had more than one car, using `as.numeric(input$question_id) > 1` as the condition would be problematic, because this would return `NA` if the respondent chose the `"6 or more"` option. +For example, let's say we had a `mc` type question where we asked how many cars the respondent owned, and we included numeric options `1` through `5` as well as a final option `"6 or more"`. If we wanted to set a condition that would return `TRUE` if the user had more than one car, using `sd_value("car_number") > 1` as the condition would be problematic, because `sd_value()` would return `NA` when trying to convert `"6 or more"` to a number (since it contains text), and comparisons with `NA` don't work as expected. To address this, we could create a custom function to handle this special condition: ```{r} server <- function(input, output, session) { - more_than_one_car <- function(input) { - if (is.null(input$car_number)) { + more_than_one_car <- function() { + val <- sd_value("car_number") + if (is.null(val)) { return(FALSE) } - num_cars <- as.numeric(input$car_number) - return(num_cars > 1) + # For "6 or more", sd_value() returns NA after trying to convert + # We treat NA as TRUE (more than one car) + if (is.na(val)) { + return(TRUE) + } + return(val > 1) } sd_show_if( - more_than_one_car(input) ~ "car_ownership" + more_than_one_car() ~ "car_ownership" ) sd_server(db = db) @@ -314,7 +324,18 @@ server <- function(input, output, session) { } ``` -In the `more_than_one_car()` function, we first return `FALSE` if the question is not yet answered (that's the `if (is.null(input$car_number))` part). Then we obtain the numeric value of the `car_number` question using `as.numeric(input$car_number)`. Then we return the simple condition `num_cars > 1`, which will return `TRUE` if the respondent chose a number greater than 1, or `FALSE` otherwise (including if they chose `"6 or more"`). +In the `more_than_one_car()` function, we first return `FALSE` if the question is not yet answered (that's the `if (is.null(val))` part). Then we check if the value is `NA` (which happens when `sd_value()` tries to auto-convert `"6 or more"` to numeric and fails) - in that case we return `TRUE` because "6 or more" means more than one car. Otherwise, we return the simple condition `val > 1`, which will return `TRUE` if the respondent chose a number greater than 1. + +Alternatively, you could prevent the automatic numeric conversion for this question by using `as_numeric = FALSE`: + +```{r} +more_than_one_car <- function() { + val <- sd_value("car_number", as_numeric = FALSE) + if (is.null(val)) return(FALSE) + if (val == "6 or more") return(TRUE) + return(as.numeric(val) > 1) +} +``` ### Custom values diff --git a/docs/defining-questions.qmd b/docs/defining-questions.qmd index 5de2e547..2e5f3214 100644 --- a/docs/defining-questions.qmd +++ b/docs/defining-questions.qmd @@ -69,7 +69,7 @@ Note that either `option` or `options` can be used as the argument name for spec The standard way of defining options is to use the `c("Label" = "value")` format, where the left side is the label shown to respondents, and the right side is the internal value stored in the survey results. -The short-hand format `c("Label 1", "Label 2")` is also supported, in which case the values will be the snake case version of the labels (e.g., `"Label 1"` becomes `"label_1"`). +The short-hand format `c("Label 1", "Label 2")` is also supported, in which case the values will be stored as they are. ::: diff --git a/docs/page-navigation.qmd b/docs/page-navigation.qmd index de639479..4079276f 100644 --- a/docs/page-navigation.qmd +++ b/docs/page-navigation.qmd @@ -70,7 +70,7 @@ Here is what the **Survey Page Gadget** looks like in RStudio: ## Navigation buttons -By default, navigation buttons are automatically added to each page to allow respondents to move forward through the survey. To change the global settings (e.g., add a previous button on all pages, change the message shown on all next buttons, etc.), you can edit the survey settings in the YAML header (see the [Survey Settings](survey-settings.html#navigation-buttons) page for details). +By default, navigation buttons are automatically added to each page to allow respondents to move forward through the survey. To change the global settings (e.g., add a previous button on all pages, change the message shown on all next buttons, etc.), you can edit the survey settings in the YAML header (see the [Survey Settings](survey-settings.html#default-settings) page for details). Additionally, you can also manually add navigation buttons using the `sd_nav()` function inside code chunks on any page, which will override the global settings for that specific page, like this: diff --git a/docs/question-formatting.qmd b/docs/question-formatting.qmd index cca4919d..9f37f166 100644 --- a/docs/question-formatting.qmd +++ b/docs/question-formatting.qmd @@ -230,9 +230,41 @@ sd_question( ::: -In addition, [`matrix`](question-types.html#matrix) type questions have two additional size arguments: +In addition, [`matrix`](question-types.html#matrix) type questions have an additional size argument: - `matrix_question_width` -- `matrix_option_width` -These set the relative widths of the question and option parts of `matrix` questions. You should set them to a percentage, such as `"40%"`, for example. +This controls the width of the question column in matrix questions. The option column width is automatically calculated based on the remaining space. + +By default, `matrix_question_width` is set to `NULL`, which automatically sizes the question column based on the length of the longest question text. + +You can override this by setting `matrix_question_width` to a specific value. The parameter accepts several formats: + +- **Percentage string**: `"40%"` +- **Numeric string**: `"40"` +- **Numeric value**: `40` + +All three formats above result in a 40% width for the question column. + +For example: + +```r +sd_question( + type = "matrix", + id = "satisfaction", + label = "Rate your satisfaction:", + option = c( + "Very unsatisfied" = 1, + "Unsatisfied" = 2, + "Neutral" = 3, + "Satisfied" = 4, + "Very satisfied" = 5 + ), + question = c( + "Overall experience" = "experience", + "Customer service" = "service", + "Product quality" = "quality" + ), + matrix_question_width = "40%" # Question column takes 40% of width +) +``` diff --git a/docs/reactivity.qmd b/docs/reactivity.qmd index ab86e729..ac52b9ac 100644 --- a/docs/reactivity.qmd +++ b/docs/reactivity.qmd @@ -78,6 +78,12 @@ This should render as something like this: > Your code is: `r sd_completion_code(10)` +::: {.callout-note title="Why can't I just use `sd_completion_code()` in the **survey.qmd** file?"} + +If you use `sd_completion_code()` in the **survey.qmd** file, you will generate one single completion code that is the same for every respondent, because the **survey.qmd** file can only produce _static_ content. If you want a unique code for each respondent, then you have to generate the code inside the `server()` function in the **app.R** file, then use `sd_output()` to display it in the **survey.qmd** file. + +::: + ## Displaying the same value in multiple places The `sd_output()` function can only be used once per each unique question `id` because the `id` gets used in the rendered HTML divs, and HTML with more than one element with the same id is invalid HTML. This is a general issue for Shiny - outputs can only be used once per each unique `id` (see [this GitHub issue](https://github.com/rstudio/shiny/issues/743) on the topic). @@ -132,8 +138,6 @@ sd_question( label = glue::glue("Are you a {pet_type_chosen} owner?"), option = c("Yes" = "yes", "No" = "no") ) - -sd_next() ``` In this example, if the respondent selects "Cats" in the first question, the label for the second question will display as "Are you a cat owner?". @@ -144,13 +148,15 @@ If both questions are on the same page, you'll probably want to conditionally di ```{r} server <- function(input, output, session) { - # Only show the pet_owner question if pet_type is answered + + #...other code + sd_show_if( sd_is_answered("pet_type") ~ "pet_owner" ) - # Database designation and other settings - sd_server(db = db) + #...other code + } ``` @@ -197,54 +203,50 @@ The product of these 2 numbers is: `r sd_output("product", type = "value")`. The sum of these 2 numbers is: `r sd_output("sum", type = "value")`. -`r sd_output("summary")` - -```{r} -sd_next() -``` +**Summary**: `r sd_output("summary", type = "value")` ```` -To make these values display properly, you can create reactive values with the `sd_reactive()` function in the server, which reactively updates as the user changes any of the question values. Here is an example of how you might create the `product` value: +To make these values display properly, you can create reactive values with the `sd_reactive()` function in the server, which reactively updates as the user changes any of the question values. -```{r} -product <- sd_reactive("product", { - input$first_number * input$second_number -}) -``` +The `sd_reactive()` function takes an `id`, which is the name that will be used in the resulting survey response data to store the returned value. The created object is a reactive expression that can also be used anywhere else in the server using the `()` symbols. Inside the function can be any expression that returns a value. -The `sd_reactive()` function takes an `id` (in this case `"product"`), which is the name that will be used in the resulting survey response data to store the returned value. The created object (in this case named `product`) is a reactive expression that can also be used anywhere else in the server using the `()` symbols, e.g. `product()`. Inside the function can be any expression that returns a value. In this case, we're just multiplying together the two input numbers. +To create all of the objects in the example page above, our server would look like the following below. Note that we first create intermediate objects (`n1` and `n2`) purely for convenience as we use the numbers in multiple places. -To create all of the objects in the example page above, our server would look like this: +{{< include ../chunks/sdvalue.qmd >}} **app.R file**: ```{r} library(surveydown) -server <- function(input, output, session) { - - # Create reactive values for 'product' and 'sum' - product <- sd_reactive("product", { - input$first_number * input$second_number - }) - - sum <- sd_reactive("sum", { - input$first_number + input$second_number - }) +ui <- sd_ui() - # Use the reactive values to create an additional 'summary' output - output$summary <- renderText({ - paste("The product is:", product(), "and the sum is:", sum()) +server <- function(input, output, session) { + observe({ + # Create local objects for each number + n1 <- sd_value("first_number") + n2 <- sd_value("second_number") + + # Create reactive values for 'product' and 'sum' + product <- sd_reactive("product", n1 * n2) + sum <- sd_reactive("sum", n1 + n2) + + # Use the reactive values to create an additional 'summary' output + sd_reactive("summary", { + paste("The product is", product(), "and the sum is", sum()) + }) }) sd_server() } # Launch Survey -shiny::shinyApp(ui = sd_ui(), server = server) +shiny::shinyApp(ui = ui, server = server) ``` -In this server, we create two reactive values, `product` and `sum`, which get stored in our survey data under those respective names. We also use `product()` and `sum()` to create the `output$summary` object, which is just some rendered text to display on the survey page. +In this server, we create two reactive values, `product` and `sum`, which get stored in our survey data under those respective names. We also use `product()` and `sum()` to create the `summary` object, which is just some rendered text to display on the survey page. + +Note also that we wrap the logic for creating these objects inside an `observe()` call. This is because initially the objects `n1` and `n2` will be `NULL` until a respondent answers the question, so using `observe()` will make sure these calculations don't run until a value is present. ## Defining questions in the server function @@ -264,22 +266,26 @@ sd_question( My follow-up question is whether or not the respondent has a pet of the type they chose above. To do this, you would define the follow-up question in the **app.R** file's `server()` function like this: ```{r} -server <- function(input, output, session) { +library(surveydown) +library(glue) +server <- function(input, output, session) { observe({ - pet_type <- input$pet_type + # Get the chosen pet_type value + pet_type <- sd_value("pet_type") - # Make the question label and options - label <-glue::glue("Are you a {pet_type} owner?") + # Make the option vector options <- c('yes', 'no') - names(options)[1] <- glue::glue("Yes, am a {pet_type} owner") - names(options)[2] <- glue::glue("No, I am not a {pet_type} owner") + names(options) <- c( + glue("Yes, am a {pet_type} owner"), + glue("No, I am not a {pet_type} owner") + ) - # Make the question + # Make the question with the chosen pet_type in the label and options sd_question( - type = "mc", - id = "pet_owner", - label = label, + type = "mc", + id = "pet_owner", + label = glue("Are you a {pet_type} owner?"), option = options ) }) @@ -290,18 +296,18 @@ server <- function(input, output, session) { The `pet_owner` question is a reactive question where the label and options will change based on the respondent's answer to the `pet_type` question. -::: {.callout-note} - -The `observe()` function is used to create the reactive question. This is a core concept in Shiny [reactivity](https://shiny.posit.co/r/articles/build/reactivity-overview/) that allows you to create reactive expressions that can change based on the values of other reactive expressions. - -Also, in this example we use the [`glue` package](https://cran.r-project.org/web/packages/glue/readme/README.html) to create the question label and options. This is a powerful package for creating strings that contain variable values. - -::: - -Finally, you can display the `pet_owner` question in the **survey.qmd** file using the `sd_output()` function, like this: +You can then display the `pet_owner` question in the **survey.qmd** file using the `sd_output()` function, like this: ```{r} #| echo: fenced sd_output(id = "pet_owner", type = "question") ``` + +::: {.callout-note} + +The `observe()` function in the above example is used to create the reactive question. This is a core concept in Shiny [reactivity](https://shiny.posit.co/r/articles/build/reactivity-overview/) that allows you to create reactive expressions that can change based on the values of other reactive expressions. + +Also, in this example we use the [`glue` package](https://cran.r-project.org/web/packages/glue/readme/README.html) to create the question label and options. This is a powerful package for creating strings that contain variable values. + +::: diff --git a/docs/survey-settings.qmd b/docs/survey-settings.qmd index 5d73f48c..6b359e7b 100644 --- a/docs/survey-settings.qmd +++ b/docs/survey-settings.qmd @@ -30,13 +30,15 @@ survey-settings: use-cookies: yes auto-scroll: no rate-survey: no - all-questions-required: no + all-required: no start-page: initial_page system-language: en highlight-unanswered: yes highlight-color: gray capture-metadata: yes - required-questions: [] + required: [] + all-shuffled: no + shuffled: [] system-messages: cancel: Cancel @@ -269,24 +271,80 @@ By default, the survey starts at the first page. By default, no questions are required. We provide 2 YAML keys to define required questions: -**Firstly**, set `all-questions-required` to `true` to make all questions required: +**Firstly**, set `all-required` to `true` to make all questions required: ``` yaml -all-questions-required: true +all-required: true ``` -By default, `all-questions-required` is set to `false`. +By default, `all-required` is set to `false`. -**Secondly**, use `required-questions` to provide a list of question IDs to make specific questions required: +**Secondly**, use `required` to provide a list of question IDs to make specific questions required: ``` yaml -required-questions: +required: - question_1 - question_2 ``` The questions set to required will make the respondent unable to proceed until they have answered all of them on the current page. It will also place a red asterisk (*) next to the question label to indicate that the question is required. +### Shuffled questions + +By default, question options and matrix subquestions are displayed in the order they are defined. You can shuffle options/subquestions for supported question types using the `shuffled` and `all-shuffled` settings. + +**Option shuffling** is supported for these question types: + +- `mc` (multiple choice) +- `mc_buttons` (multiple choice buttons) +- `mc_multiple` (multiple choice multiple selection) +- `mc_multiple_buttons` (multiple choice multiple selection buttons) + +**Subquestion shuffling** is supported for: + +- `matrix` (matrix questions) + +#### Shuffle all questions + +Set `all-shuffled` to `true` to shuffle all supported questions: + +``` yaml +all-shuffled: true +``` + +By default, `all-shuffled` is set to `false`. + +#### Shuffle specific questions + +Use `shuffled` to specify which questions should have their options/subquestions shuffled. You can shuffle all options/subquestions, or use indexing to shuffle only specific positions: + +**Shuffle all options/subquestions:** + +``` yaml +shuffled: + - question_1 + - question_2 +``` + +**Indexed shuffling:** + +You can specify which positions to shuffle using several syntax options: + +1. Range notation: `1-5` shuffles positions 1 through 5 +2. List notation: `[1, 2, 4, 7]` shuffles positions 1, 2, 4, and 7 +3. Combined ranges: `[1-5, 8-10]` shuffles positions 1-5 and 8-10 + +``` yaml +shuffled: + - artist: 1-5 # Shuffle first 5 options + - fruit: [1, 2, 4, 7] # Shuffle options at positions 1, 2, 4, 7 + - swift: [1-5, 8-10] # Shuffle options 1-5 and 8-10 + - michael_jackson # Shuffle all options + - car_preference: [1, 3] # Shuffle options at positions 1 and 3 +``` + +For matrix questions, the same syntax applies but shuffles subquestions instead of options. + ## System messages You can customize all system messages and button text elements displayed throughout the survey by modifying the `system-messages` section in YAML. By default, all system messages are in English, though you can set the default language to one of six supported languages by changing the [`system-language`](#system-language) setting. diff --git a/templates/banners/option_shuffling.png b/templates/banners/option_shuffling.png new file mode 100644 index 00000000..51d4582f Binary files /dev/null and b/templates/banners/option_shuffling.png differ diff --git a/templates/option_shuffling.qmd b/templates/option_shuffling.qmd new file mode 100644 index 00000000..4c13da61 --- /dev/null +++ b/templates/option_shuffling.qmd @@ -0,0 +1,30 @@ +--- +title: "Option Shuffling" +date: 2025-02-03 +categories: [Randomization] +image: banners/option_shuffling.png +description: "A template for creating questions with [shuffled options](/docs/survey-settings.html#shuffled-questions)." +--- + +To create this template, run this command in your R console: + +```{r} +surveydown::sd_create_survey( + #path = "path/to/survey", + template = "option_shuffling" +) +``` + +Refer to the [Start with a template](/docs/getting-started.html#start-with-a-template) section for more details. + +::: {.d-flex .justify-content-center .flex-wrap .text-center} + +[ Open in New Tab](https://surveydown4.shinyapps.io/option_shuffling/){.template-button .m-1 style="width: 200px;"} + +[ GitHub Repo](https://github.com/surveydown-dev/template_option_shuffling){.template-button .m-1 style="width: 200px;"} + +::: + +
+ +