ggpaintr Placeholder Registry
Source:vignettes/ggpaintr-placeholder-registry.Rmd
ggpaintr-placeholder-registry.RmdOverview
ggpaintr now supports per-app placeholder registries.
That means downstream developers can define a new placeholder once, pass
it into ptr_parse_formula(), ptr_app(), or
ptr_server_state(), and keep the rest of the package
runtime unchanged.
The public entry points for this layer are:
Built-in placeholders such as var, text,
num, expr, and upload now use the
same registry path as custom placeholders.
A minimal custom placeholder
This date placeholder adds a Shiny
dateInput() control and turns the chosen date into
as.Date("YYYY-MM-DD") in generated code.
sales <- data.frame(
day = as.Date("2024-01-01") + 0:4,
value = c(10, 13, 12, 16, 18)
)
date_placeholder <- ptr_define_placeholder(
# keyword: the token written inside the formula string (e.g. `date` in
# `geom_vline(xintercept = date)`). Must be a valid R name.
keyword = "date",
# build_ui: produces the Shiny widget for one placeholder occurrence.
# Arguments:
# id — the Shiny input id generated by ggpaintr; pass it to the widget
# so the reactive system can read it with input[[id]].
# copy — resolved label/help text for this occurrence. A named list with
# fields: $label, $help, $placeholder, $empty_text.
# Values come from copy_defaults (defined below), and can be
# overridden per-param or per-layer via ptr_merge_ui_text().
# meta — metadata record for this specific placeholder occurrence:
# meta$id same as `id` above
# meta$keyword "date"
# meta$layer_name the ggplot layer (e.g. "geom_vline")
# meta$param the argument name (e.g. "xintercept")
# Use meta$param or meta$layer_name to vary widget behaviour by
# context (e.g. different defaults per argument).
# context — shared read-only environment:
# context$ptr_obj the full parsed formula object
# context$placeholders the active placeholder registry
# context$ui_text merged copy rules
# context$envir calling environment (for resolving data)
build_ui = function(id, copy, meta, context) {
shiny::dateInput(id, copy$label) # copy$label is filled from copy_defaults below
},
# resolve_expr: converts the current widget value into an R expression that
# replaces the `date` token in the completed formula.
# Arguments:
# value — the current input value (result of input[[id]], or of
# resolve_input() if that hook is defined)
# meta — same metadata record as in build_ui
# context — same shared environment as in build_ui
resolve_expr = function(value, meta, context) {
# ptr_missing_expr() signals that this argument should be dropped from the
# completed formula entirely (rather than inserting a NULL or NA).
if (is.null(value) || identical(as.character(value), "")) {
return(ptr_missing_expr())
}
# !! (unquote) splices the runtime value into the expression at build time.
# The result here is the call `as.Date("2024-01-03")` embedded in the plot code.
rlang::expr(as.Date(!!as.character(value)))
},
# copy_defaults: default label/help text shown for every occurrence of this
# placeholder. {param} is a glue-style template filled with meta$param at
# runtime (e.g. "xintercept" → "Choose a date for xintercept").
# Override specific occurrences with ptr_merge_ui_text().
copy_defaults = list(label = "Choose a date for {param}")
)
# ptr_merge_placeholders() merges custom placeholders into the built-in registry
# (var, text, num, expr, upload). The result is a complete registry that can be
# passed as `placeholders =` to ptr_parse_formula(), ptr_app(), ptr_exec(), etc.
# Custom entries override built-ins of the same keyword name.
placeholders <- ptr_merge_placeholders(
list(date = date_placeholder)
)
names(placeholders) # built-ins + "date"
#> [1] "var" "text" "num" "expr" "upload" "date"Using the registry in parsing and runtime
Pass the registry through placeholders = ... anywhere
you want custom placeholder support.
obj <- ptr_parse_formula(
"ggplot(data = sales, aes(x = day, y = value)) +
geom_line() +
geom_vline(xintercept = date)",
placeholders = placeholders
)
# Input ids follow internal conventions — prefer discovering them via
# ptr_runtime_input_spec(obj) rather than hard-coding strings.
# The patterns shown here are for illustration only:
# "<layer>+checkbox" — toggle that layer on (TRUE) or off (FALSE)
# "<layer>+<n>" — the n-th placeholder in that layer (1-based)
runtime <- ptr_exec(
obj,
list(
"geom_line+checkbox" = TRUE, # include geom_line in the plot
"geom_vline+checkbox" = TRUE, # include geom_vline in the plot
"geom_vline+2" = as.Date("2024-01-03") # value for the date placeholder
)
)
runtime$code_text
#> [1] "ggplot(data = sales, aes(x = day, y = value)) +\n geom_line() +\n geom_vline(xintercept = as.Date(\"2024-01-03\"))"The same registry can also be used in the higher-level wrappers.
ptr_app(
"ggplot(data = sales, aes(x = day, y = value)) +
geom_line() +
geom_vline(xintercept = date)",
placeholders = placeholders
)The placeholder metadata contract
Every placeholder occurrence becomes one metadata record in
ptr_obj$placeholder_map.
meta <- obj$placeholder_map$geom_vline[["geom_vline+2"]]
meta
#> $id
#> [1] "geom_vline+2"
#>
#> $keyword
#> [1] "date"
#>
#> $layer_name
#> [1] "geom_vline"
#>
#> $param
#> [1] "xintercept"
#>
#> $index_path
#> [1] 2Each metadata record contains:
-
id: the Shiny input id for that placeholder occurrence -
keyword: the placeholder keyword, such asdate -
layer_name: the layer where the placeholder was found -
param: the argument name, such asxintercept -
index_path: the expression path used during replacement
These fields are passed into every placeholder hook so custom placeholder code can behave differently by parameter or by layer.
The hook contract
ptr_define_placeholder() supports one required UI hook,
one required resolver hook, and three optional advanced hooks.
build_ui(id, copy, meta, context)
build_ui() returns the UI control for one placeholder
occurrence.
date_placeholder$build_ui
#> function (id, copy, meta, context)
#> {
#> shiny::dateInput(id, copy$label)
#> }The arguments are:
-
id: the generated input id to bind -
copy: resolved copy text for this control -
meta: the metadata record for this occurrence -
context: a named list containingptr_obj,placeholders,ui_text, andenvir
resolve_expr(value, meta, context)
resolve_expr() turns the current input value into an
expression suitable for the completed plot expression.
date_placeholder$resolve_expr
#> function (value, meta, context)
#> {
#> if (is.null(value) || identical(as.character(value), "")) {
#> return(ptr_missing_expr())
#> }
#> rlang::expr(as.Date(!!as.character(value)))
#> }Return ptr_missing_expr() when the argument should be
removed from the completed code instead of inserted.
resolve_input(input, id, meta, context)
Use resolve_input() when the raw Shiny input needs
preprocessing before it is handed to resolve_expr(). If
omitted, ggpaintr uses input[[id]].
trimmed_text_placeholder <- ptr_define_placeholder(
keyword = "trimmed_text",
build_ui = function(id, copy, meta, context) {
shiny::textInput(id, copy$label)
},
# resolve_input: pre-processes the raw Shiny input value before it reaches
# resolve_expr(). If omitted, ggpaintr uses input[[id]] directly.
# Arguments:
# input — the full Shiny input object (access any input, not just this one)
# id — the input id for this placeholder occurrence
# meta, context — same as in build_ui and resolve_expr
# Return the cleaned value; it becomes `value` in resolve_expr().
resolve_input = function(input, id, meta, context) {
trimws(input[[id]]) # strip leading/trailing whitespace before resolve_expr sees it
},
resolve_expr = function(value, meta, context) {
if (identical(value, "")) {
return(ptr_missing_expr())
}
rlang::expr(!!value)
}
)
bind_ui(input, output, metas, context)
Use bind_ui() for deferred or dynamic UI registration.
This is how the built-in var placeholder registers delayed
renderUI() outputs after data is known.
deferred_placeholder <- ptr_define_placeholder(
keyword = "dynamic_text",
build_ui = function(id, copy, meta, context) {
# Return a placeholder container. The real widget is rendered later by bind_ui()
# once the data or other runtime context needed to build it is available.
shiny::uiOutput(paste0("dynamic-", id))
},
# bind_ui: called during ptr_setup_controls() inside the Shiny server.
# Use it to register renderUI() observers for widgets that depend on reactive
# state (e.g. column names from an uploaded file).
# Unlike build_ui() — which runs once at UI construction time and has no
# access to reactives — bind_ui() runs inside the server and can read inputs.
# Arguments:
# input, output — standard Shiny server arguments
# metas — list of metadata records, one per occurrence of this keyword;
# each record has $id, $keyword, $layer_name, $param, $index_path
# context — shared environment (ptr_obj, placeholders, ui_text, envir)
bind_ui = function(input, output, metas, context) {
for (meta in metas) {
# Register a renderUI() for the uiOutput() container built above.
output[[paste0("dynamic-", meta$id)]] <- shiny::renderUI({
shiny::textInput(meta$id, paste("Dynamic control for", meta$param))
})
}
invisible(NULL)
},
resolve_expr = function(value, meta, context) {
if (is.null(value) || identical(value, "")) {
return(ptr_missing_expr())
}
rlang::expr(!!value)
}
)
prepare_eval_env(input, metas, eval_env, context)
Use prepare_eval_env() when a placeholder needs to
inject objects into the evaluation environment before the plot is built.
This is how the built-in upload placeholder assigns
uploaded data objects before code is evaluated.
prepared_data_placeholder <- ptr_define_placeholder(
keyword = "prepared_data",
build_ui = function(id, copy, meta, context) {
shiny::textInput(id, copy$label, placeholder = "dataset_name")
},
resolve_expr = function(value, meta, context) {
if (is.null(value) || identical(value, "")) {
return(ptr_missing_expr())
}
rlang::parse_expr(value)
},
# prepare_eval_env: runs just before the completed formula is evaluated to
# build the plot. Use it to inject R objects that the formula references by
# name (e.g. a dataset variable, a helper function, a colour palette).
# This is how the built-in `upload` placeholder makes uploaded data available
# under the user-supplied name at eval time.
# Arguments:
# input — Shiny input object (read current widget values if needed)
# metas — list of metadata records for all occurrences of this keyword
# eval_env — the environment in which the formula will be eval()'d;
# add or overwrite bindings, then return the modified env
# context — shared environment (ptr_obj, placeholders, ui_text, envir)
prepare_eval_env = function(input, metas, eval_env, context) {
# Bind `prepared_dataset` so the formula can reference it by name.
eval_env$prepared_dataset <- data.frame(x = 1:3, y = c(2, 4, 3))
eval_env # must return the (modified) eval_env
}
)Copy defaults and custom copy rules
Each placeholder can ship its own default copy through
copy_defaults. Those defaults then participate in the
normal ui_text system.
# ptr_merge_ui_text() builds a copy-rules object that overrides default labels
# and help text. Three levels of specificity — higher wins over lower:
#
# defaults → applies to every occurrence of a keyword, across all layers/params
# params → applies to a specific argument name, across all layers
# layers → applies to a keyword inside a specific layer + argument combination
#
# Within each level you can set: label, help, placeholder, empty_text.
date_ui_text <- ptr_merge_ui_text(
list(
# Fallback for any `date` placeholder not matched by a more specific rule.
defaults = list(date = list(label = "Pick any date")),
# Overrides the label when the argument is named `xintercept`, regardless of layer.
params = list(
xintercept = list(date = list(label = "Reference date"))
),
# Most specific: overrides copy only for `date` inside `geom_vline(xintercept = ...)`.
# This wins over both `defaults` and `params` for that exact occurrence.
layers = list(
geom_vline = list(
date = list(
xintercept = list(help = "Choose a cutoff date for the vertical guide.")
)
)
)
),
placeholders = placeholders
)
# ptr_resolve_ui_text() returns the fully merged copy record for one occurrence.
# Precedence applied: layers > params > defaults > copy_defaults from the placeholder.
# The result is what build_ui() receives as its `copy` argument.
ptr_resolve_ui_text(
"control", # component type: "control" for placeholder widgets
keyword = "date",
layer_name = "geom_vline",
param = "xintercept",
ui_text = date_ui_text,
placeholders = placeholders
)
#> $label
#> [1] "Reference date"
#>
#> $help
#> [1] "Choose a cutoff date for the vertical guide."Using custom placeholders with Shiny integration helpers
The phase-1 Shiny integration helpers work with custom placeholder registries too.
ids <- ptr_build_ids(
control_panel = "builder_controls",
draw_button = "render_plot",
plot_output = "main_plot",
error_output = "main_error",
code_output = "main_code"
)
ui <- shiny::fluidPage(
shiny::sidebarLayout(
shiny::sidebarPanel(
ptr_input_ui(ids = ids)
),
shiny::mainPanel(
ptr_output_ui(ids = ids)
)
)
)
server <- function(input, output, session) {
ptr_state <- ptr_server_state(
"ggplot(data = sales, aes(x = day, y = value)) +
geom_line() +
geom_vline(xintercept = date)",
ids = ids,
placeholders = placeholders
)
ptr_setup_controls(input, output, ptr_state, ids = ids)
ptr_register_draw(input, ptr_state, ids = ids)
ptr_register_plot(output, ptr_state, ids = ids)
ptr_register_error(output, ptr_state, ids = ids)
ptr_register_code(output, ptr_state, ids = ids)
}Summary
The placeholder registry is the developer-facing extension point for
adding new control types without editing parser, UI, runtime, or
copy-rule internals. Start with build_ui() plus
resolve_expr(), add the optional hooks only when needed,
and pass the same registry through parsing and app launch.