blockr.ui::link_menu_ui()
is a bidirectional card-list link picker. It lives
inside the sidebar primitive and replaces the
legacy selectize-style chooser used by blockr.dock’s
add-link flow. Each card represents a board instance that the
new link can attach to, and the menu computes eligibility against a
fixed anchor block (the right-clicked block in the dock
flow).
link_menu_ui(id, board, anchor) renders up to two
category-grouped sections relative to anchor. When both
render, INPUT FROM appears above OUTPUT TO so the order matches the
natural left-to-right data flow (sources feed the anchor; the anchor
feeds targets):
If both pools are empty the panel renders an in-place empty-state
instead of returning NULL. This means right-clicking a
downstream block with free inputs no longer surfaces the legacy “No
inputs are currently available” warning - the menu opens with its
INCOMING section populated.
The JS binding publishes
list(source, target, link_id, block_input, nonce) on
input$commit. source / target are
derived from the clicked card’s data-direction and the
panel’s data-anchor:
data-direction = "outgoing" ->
source = anchor, target = carddata-direction = "incoming" ->
source = card, target = anchorlink_menu_server(id) strips the internal
nonce and returns a reactive whose value is
the spec. The consumer doesn’t have to know the orientation up front -
the spec carries both ends explicitly.
Single click commits one link, but the menu is designed for repeated
adds: the consumer pushes a typed pool-update payload to
the open panel via session$sendInputMessage() after each
commit, and the binding’s receiveMessage handler hides the
just-wired card client-side. Card-expansion state, scroll position, and
the search filter value are preserved. If both pools drain to zero, the
consumer calls hide_sidebar() and the panel closes
itself.
link_eligible_pools(board, anchor) is exported so the
consumer recomputes the post-commit pool against the same eligibility
logic the menu uses for its initial render - one source of truth shared
across both call sites.
library(shiny)
library(blockr.ui)
library(blockr.core)
board <- new_board(
blocks = as_blocks(list(
a = new_dataset_block(),
h = new_head_block(),
m = new_merge_block()
))
)
ui <- fluidPage(
sidebar_ui("panel", side = "right", width = "420px"),
actionButton("open", "Connect a"),
verbatimTextOutput("spec")
)
server <- function(input, output, session) {
committed <- link_menu_server("menu")
observeEvent(input$open, {
show_sidebar(
"panel",
title = "Connect a",
ui = link_menu_ui(session$ns("menu"), board, anchor = "a")
)
})
output$spec <- renderPrint(committed())
}
shinyApp(ui, server)A runnable copy is shipped at:
The bundled example exercises all four anchor cases (source-only,
arity-1 target, arity-N target, variadic) and demonstrates the live
pool-update flow.