Skip to content

Commit e9c5bea

Browse files
committed
upd dev docu
1 parent a3e6d79 commit e9c5bea

3 files changed

Lines changed: 171 additions & 116 deletions

File tree

docs/dev/create-block.md

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
const blockSteps = [
33
{
44
label: 'Skeleton',
5-
code: 'new_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = ...,\n expr_server = ...,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
5+
code: 'new_my_block <- function(column = NULL, threshold = 0, ...) {\n blockr.core::new_transform_block(\n server = function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n # ...\n })\n },\n ui = function(id) {\n # ...\n },\n class = "my_block",\n expr_type = "bquoted",\n external_ctrl = TRUE,\n allow_empty_state = "column",\n ...\n )\n}'
66
},
77
{
8-
label: 'Add UI function',
9-
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = ...,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
8+
label: 'Add UI',
9+
code: 'new_my_block <- function(column = NULL, threshold = 0, ...) {\n blockr.core::new_transform_block(\n server = function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n # ...\n })\n },\n ui = function(id) {\n shiny::tagList(\n shiny::selectInput(shiny::NS(id, "column"), "Column", choices = NULL),\n shiny::numericInput(shiny::NS(id, "threshold"), "Threshold", value = threshold)\n )\n },\n class = "my_block",\n expr_type = "bquoted",\n external_ctrl = TRUE,\n allow_empty_state = "column",\n ...\n )\n}'
1010
},
1111
{
12-
label: 'Add server function',
13-
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nmy_block_server <- function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n shiny::observe({\n shiny::updateSelectInput(session, "column",\n choices = names(data()))\n })\n\n list(\n expr = shiny::reactive({\n rlang::expr(\n dplyr::filter(data, !!rlang::sym(input$column) > !!input$threshold)\n )\n }),\n state = shiny::reactive(list(\n column = input$column,\n threshold = input$threshold\n ))\n )\n })\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = my_block_server,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
12+
label: 'Add server',
13+
code: 'new_my_block <- function(column = NULL, threshold = 0, ...) {\n blockr.core::new_transform_block(\n server = function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n r_column <- shiny::reactiveVal(column)\n r_threshold <- shiny::reactiveVal(threshold)\n\n shiny::observeEvent(input$column, r_column(input$column))\n shiny::observeEvent(input$threshold, r_threshold(input$threshold))\n\n shiny::observeEvent(names(data()), {\n shiny::updateSelectInput(\n session, "column",\n choices = names(data()), selected = r_column()\n )\n })\n\n list(\n expr = shiny::reactive({\n shiny::req(r_column())\n blockr.core::bbquote(\n dplyr::filter(.(data), .(col) > .(thr)),\n list(col = as.name(r_column()), thr = r_threshold())\n )\n }),\n state = list(column = r_column, threshold = r_threshold)\n )\n })\n },\n ui = function(id) {\n shiny::tagList(\n shiny::selectInput(shiny::NS(id, "column"), "Column", choices = NULL),\n shiny::numericInput(shiny::NS(id, "threshold"), "Threshold", value = threshold)\n )\n },\n class = "my_block",\n expr_type = "bquoted",\n external_ctrl = TRUE,\n allow_empty_state = "column",\n ...\n )\n}'
1414
},
1515
{
16-
label: 'Add registration',
17-
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nmy_block_server <- function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n shiny::observe({\n shiny::updateSelectInput(session, "column",\n choices = names(data()))\n })\n\n list(\n expr = shiny::reactive({\n rlang::expr(\n dplyr::filter(data, !!rlang::sym(input$column) > !!input$threshold)\n )\n }),\n state = shiny::reactive(list(\n column = input$column,\n threshold = input$threshold\n ))\n )\n })\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = my_block_server,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}\n\n.onLoad <- function(libname, pkgname) {\n blockr.core::register_block(\n constructor = new_my_block,\n name = "My custom filter",\n description = "Filter rows by a numeric threshold",\n category = "transform",\n package = pkgname\n )\n}'
16+
label: 'Register',
17+
code: '# R/zzz.R\n.onLoad <- function(libname, pkgname) {\n blockr.core::register_block(\n ctor = "new_my_block",\n name = "Threshold filter",\n description = "Filter rows where a numeric column exceeds a threshold",\n category = "transform",\n package = pkgname\n )\n}'
1818
}
1919
]
2020
</script>
@@ -23,113 +23,147 @@ const blockSteps = [
2323

2424
<VideoEmbed id="-PdixmAscQI" title="Creating blocks in blockr" />
2525

26-
Write custom blocks in pure R to extend blockr with your own logic. A block is a specialized [Shiny module](https://mastering-shiny.org/scaling-modules.html) that returns an **expression** (the R code it generates) and a **state** (its current input values). A workflow is a Shiny app composed of connected blocks.
26+
Write custom blocks in pure R to extend blockr with your own logic. A block is a [Shiny module](https://mastering-shiny.org/scaling-modules.html) that returns an `expr` (the R code it generates) and a `state` (its current input values). A workflow is a Shiny app composed of connected blocks.
2727

28-
Blocks should live in an R package so they can be registered, shared, and tested. The examples below show the package-based approach.
28+
Blocks should live in an R package so they can be registered, shared, and tested.
2929

30-
## Block anatomy
30+
::: tip Just getting started?
31+
The fastest way to your first block is to let a coding agent scaffold it. See the [cat facts walkthrough](/learn/03-create-a-block). It ships a working block (package, tests, registration) in one prompt. Come back here when you want the technical reference.
32+
:::
3133

32-
Every block has three parts: a **UI function**, a **server function**, and a **constructor**. Step through the animation below to see how they come together:
34+
::: info Source of truth
35+
Block patterns are documented canonically in [blockr.docs](https://github.com/cynkra/blockr.docs/tree/main/patterns). `r-driven-blocks.md` covers everything below in more depth, plus the JS-driven path for polished UIs.
36+
:::
3337

34-
<CodeStepper :steps="blockSteps" />
38+
## Block anatomy
3539

36-
The sections below break down each part in detail.
40+
Every block is built from a constructor that wires together a server function and a UI function, then forwards them to a typed parent constructor (`new_data_block`, `new_transform_block`, `new_join_block`, `new_plot_block`, or `new_variadic_block`).
3741

38-
```
39-
Block
40-
├── UI function → defines the user interface (Shiny UI)
41-
├── Server function → handles reactive logic, returns expr + state
42-
└── Constructor → wraps UI + server, initializes defaults
43-
```
42+
Step through the animation to see how the pieces come together:
4443

45-
### UI function
44+
<CodeStepper :steps="blockSteps" />
45+
46+
### Constructor
4647

47-
A standard Shiny module UI. Takes a single `id` argument and returns `shiny.tag` objects:
48+
The constructor exposes every UI-controllable parameter as an argument and forwards `...` to the parent so framework options (`class`, `allow_empty_state`, `expr_type`, `external_ctrl`, …) can pass through:
4849

4950
```r
50-
my_block_ui <- function(id) {
51-
ns <- shiny::NS(id)
52-
shiny::tagList(
53-
shiny::selectInput(ns("column"), "Column", choices = NULL),
54-
shiny::numericInput(ns("threshold"), "Threshold", value = 0)
51+
new_my_block <- function(column = NULL, threshold = 0, ...) {
52+
blockr.core::new_transform_block(
53+
server = ...,
54+
ui = ...,
55+
class = "my_block",
56+
expr_type = "bquoted",
57+
external_ctrl = TRUE,
58+
allow_empty_state = "column",
59+
...
5560
)
5661
}
5762
```
5863

64+
Pick the parent based on the block's role:
65+
66+
| Block does... | Parent | Server signature |
67+
|---|---|---|
68+
| Loads from API / file / database | `new_data_block()` | `function(id)` |
69+
| Reshapes one upstream input | `new_transform_block()` | `function(id, data)` |
70+
| Joins two inputs | `new_join_block()` | `function(id, x, y)` |
71+
| Takes N inputs | `new_variadic_block()` | `function(id, ...args)` |
72+
| Renders a plot | `new_plot_block()` | `function(id, data)` |
73+
5974
### Server function
6075

61-
Returns a list with two reactive elements: `expr` (a quoted R expression) and `state` (a named list of current input values):
76+
Wraps a `shiny::moduleServer()` and returns `list(expr = ..., state = ...)`:
6277

6378
```r
64-
my_block_server <- function(id, data) {
79+
function(id, data) {
6580
shiny::moduleServer(id, function(input, output, session) {
66-
# Update choices based on incoming data
67-
shiny::observe({
68-
shiny::updateSelectInput(session, "column",
69-
choices = names(data()))
81+
r_column <- shiny::reactiveVal(column)
82+
r_threshold <- shiny::reactiveVal(threshold)
83+
84+
shiny::observeEvent(input$column, r_column(input$column))
85+
shiny::observeEvent(input$threshold, r_threshold(input$threshold))
86+
87+
shiny::observeEvent(names(data()), {
88+
shiny::updateSelectInput(
89+
session, "column",
90+
choices = names(data()), selected = r_column()
91+
)
7092
})
7193

7294
list(
7395
expr = shiny::reactive({
74-
rlang::expr(
75-
dplyr::filter(data, !!rlang::sym(input$column) > !!input$threshold)
96+
shiny::req(r_column())
97+
blockr.core::bbquote(
98+
dplyr::filter(.(data), .(col) > .(thr)),
99+
list(col = as.name(r_column()), thr = r_threshold())
76100
)
77101
}),
78-
state = shiny::reactive(list(
79-
column = input$column,
80-
threshold = input$threshold
81-
))
102+
state = list(column = r_column, threshold = r_threshold)
82103
)
83104
})
84105
}
85106
```
86107

87-
### Constructor
108+
Rules that bite:
109+
110+
- **`expr` is a quoted call**, not a string. Use `blockr.core::bbquote()` with `.(x)` splices and pair it with `expr_type = "bquoted"` on the parent constructor. Splice the upstream data via `.(data)`.
111+
- **`state` is a list of reactives**, one per constructor parameter. Names must match constructor argument names exactly. Serialization breaks silently otherwise.
112+
- **The expression must evaluate outside a reactive context.** If `expr` only works because a reactive happens to be in scope, the export pipeline will fail.
113+
- **Don't expose data inputs as constructor arguments.** `data` / `x` / `y` / `...args` are wired by the framework via the server signature.
114+
115+
### UI function
88116

89-
Wraps UI and server together with default state values:
117+
A standard Shiny module UI taking `id` and returning `shiny.tag` objects. Initialise inputs with the constructor's defaults (not empty), so unsaved blocks render their starting state:
90118

91119
```r
92-
new_my_block <- function(column = character(), threshold = 0) {
93-
new_block(
94-
ui = my_block_ui,
95-
expr_server = my_block_server,
96-
state = list(column = column, threshold = threshold),
97-
class = c("my_block", "transform_block")
120+
function(id) {
121+
shiny::tagList(
122+
shiny::selectInput(shiny::NS(id, "column"), "Column", choices = NULL),
123+
shiny::numericInput(shiny::NS(id, "threshold"), "Threshold", value = threshold)
98124
)
99125
}
100126
```
101127

102128
## Registering your block
103129

104-
Blocks must be registered so they appear in the block menu. Use `.onLoad()` in your package:
130+
Register on package load so the block has metadata (without it, every constructor call emits a "No block metadata available" warning, and the block doesn't show up in board / AI / MCP discovery):
105131

106132
```r
133+
# R/zzz.R
107134
.onLoad <- function(libname, pkgname) {
108135
blockr.core::register_block(
109-
constructor = new_my_block,
110-
name = "My custom filter",
111-
description = "Filter rows by a numeric threshold",
136+
ctor = "new_my_block",
137+
name = "Threshold filter",
138+
description = "Filter rows where a numeric column exceeds a threshold",
112139
category = "transform",
113140
package = pkgname
114141
)
115142
}
116143
```
117144

118-
You can also register blocks dynamically at runtime:
145+
`category` must be one of `blockr.core::suggested_categories()`: `input`, `transform`, `structured`, `plot`, `table`, `model`, `output`, `utility`, `uncategorized`. Data-fetching blocks are `input`, not `data`. To register multiple blocks at once, use `register_blocks()` (vectorised).
119146

120-
```r
121-
register_block(
122-
constructor = new_my_block,
123-
name = "My custom filter",
124-
description = "Filter rows by a numeric threshold",
125-
category = "transform"
126-
)
127-
```
147+
## AI-controllable blocks
148+
149+
Blockr's AI assistant configures blocks for end users by writing to their state from outside the block's server. To opt in, set `external_ctrl` on the parent constructor:
150+
151+
| `external_ctrl` value | Meaning |
152+
|---|---|
153+
| `FALSE` (default) | Block state is read-only from outside. AI can't change it. |
154+
| `TRUE` | All constructor arguments are externally writable. |
155+
| `"column"` (a string) | Only the named state slot is externally writable. |
156+
| `c("column", "threshold")` | Multiple named slots are writable. |
157+
158+
State names handed to `external_ctrl` must match the names in the server's `state` list (and therefore the constructor argument names). The framework validates writes by re-evaluating the block expression: if evaluation fails, the previous state is restored and downstream evaluation is gated until the next successful submit. See `?blockr.core::ctrl_block` for the plugin that drives this.
128159

129-
Query what's available with `available_blocks()`.
160+
::: tip
161+
Per-package convention: opt blocks in by default (`external_ctrl = TRUE`) unless you have a reason not to. Blocks that aren't externally controllable disappear from AI/MCP suggestions.
162+
:::
130163

131164
## Further reading
132165

166+
- [blockr.docs patterns](https://github.com/cynkra/blockr.docs/tree/main/patterns): canonical R-driven and JS-driven references
133167
- [Full create-block vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/create-block.html): detailed walkthrough with advanced examples
134168
- [Block registry vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/blocks-registry.html): registry system details
135169
- [Extend blockr vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/extend-blockr.html): plugins and custom UI

docs/dev/extend-blockr.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,39 @@ block_ui.my_block <- function(block, id) {
5151

5252
## Block registry
5353

54-
The registry is the "supermarket" for blocks. It tracks all available blocks with metadata (name, description, category, package). When you load a blockr extension package, its blocks are automatically registered via `.onLoad()` and appear in the block menu.
54+
The registry is the "supermarket" for blocks. It tracks all available blocks with metadata (name, description, category, package). When you load a blockr extension package, its blocks are registered via `.onLoad()` and appear in the block menu, and in the AI/MCP discovery surface, which reads the same registry.
5555

5656
```r
5757
# Query available blocks
58-
available_blocks()
58+
list_blocks()
5959

60-
# Register a new block
60+
# Register a new block (in R/zzz.R or at runtime)
6161
register_block(
62-
constructor = new_my_block,
62+
ctor = "new_my_block",
6363
name = "My block",
6464
description = "Does something useful",
6565
category = "transform"
6666
)
6767

68-
# Unregister a block
69-
unregister_block("my_block")
68+
# Register many at once (vectorised)
69+
register_blocks(
70+
ctor = c("new_filter_block", "new_select_block"),
71+
name = c("Filter rows", "Select columns"),
72+
description = c("Filter by predicate", "Pick a subset of columns"),
73+
category = c("transform", "transform")
74+
)
75+
76+
# Unregister
77+
unregister_blocks("my_block")
7078
```
7179

72-
This makes collaboration easy: one team builds a package of domain-specific blocks, registers them, and they appear in every user's block menu.
80+
`category` must come from `blockr.core::suggested_categories()` (`input`, `transform`, `structured`, `plot`, `table`, `model`, `output`, `utility`, `uncategorized`). Anything else warns. Data-fetching blocks register as `input`, not `data`.
81+
82+
This makes collaboration easy: one team builds a package of domain-specific blocks, registers them, and they appear in every user's block menu. They also become available to the AI assistant for configuration.
7383

7484
## Further reading
7585

86+
- [blockr.docs](https://github.com/cynkra/blockr.docs): canonical block patterns and skills
7687
- [Full extend-blockr vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/extend-blockr.html): complete plugin examples
7788
- [Block registry vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/blocks-registry.html): registry internals
7889
- [blockr.core API reference](https://bristolmyerssquibb.github.io/blockr.core/): full function documentation

0 commit comments

Comments
 (0)