From 50e3ad7c907824b87e2e888b834a2ebf6992487a Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Tue, 20 Oct 2026 16:35:18 -0400 Subject: [PATCH 1/3] Draw a widget from the host's block inside the page in place of its frame Co-Authored-By: Claude Opus 5.5 --- app/helpers/dash_kit/dashboard_helper.rb | 18 ++++++++++-------- test/helpers/dash_kit/dashboard_helper_test.rb | 9 +++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/helpers/dash_kit/dashboard_helper.rb b/app/helpers/dash_kit/dashboard_helper.rb index 8e75cbd..31abaad 100644 --- a/app/helpers/dash_kit/dashboard_helper.rb +++ b/app/helpers/dash_kit/dashboard_helper.rb @@ -39,11 +39,11 @@ def dash_kit_widget_label(config, widget_key) config.available_widgets.dig(widget_key.to_sym, :label) || widget_key.to_s.humanize end - def dash_kit_render_widgets(config:) + def dash_kit_render_widgets(config:, &draw) return content_tag(:div, "", id: "dashboard-widgets") unless config content_tag(:div, id: dash_kit_widgets_id(config)) do - dash_kit_arrangeable?(config) ? dash_kit_widget_grid(config) : dash_kit_drawn_widgets(config) + dash_kit_arrangeable?(config) ? dash_kit_widget_grid(config, &draw) : dash_kit_drawn_widgets(config, &draw) end end @@ -55,10 +55,10 @@ def dash_kit_arrangeable?(config) DashKit.editable?(config, dash_kit_current_viewer) end - def dash_kit_widget_grid(config) + def dash_kit_widget_grid(config, &draw) safe_join([ react_ui("dash_kit/dashboard-grid", config.layout_data.merge(base: dash_kit.dashboard_path(config), token: dash_kit_form_token)), - block_contents(config.blocks) { |block| dash_kit_block_frame(config, block) } + block_contents(config.blocks) { |block| dash_kit_block_frame(config, block, &draw) } ]) end @@ -66,14 +66,16 @@ def dash_kit_form_token controller.send(:form_authenticity_token) if controller.respond_to?(:form_authenticity_token, true) end - def dash_kit_drawn_widgets(config) + def dash_kit_drawn_widgets(config, &draw) block_layout(config.blocks, kind: config.dashboard_type.to_sym) do |block| - dash_kit_block_frame(config, block) + dash_kit_block_frame(config, block, &draw) end end - def dash_kit_block_frame(config, block) - return dash_kit_widget_frame(block["type"], dashboard_id: config.id) unless block["type"] == WidgetRegistry::BUILT_WIDGET.to_s + def dash_kit_block_frame(config, block, &draw) + unless block["type"] == WidgetRegistry::BUILT_WIDGET.to_s + return draw ? capture(block["type"], &draw) : dash_kit_widget_frame(block["type"], dashboard_id: config.id) + end definition = config.widget_definitions.find_by(id: block.dig("content", "definition_id")) definition ? dash_kit_widget_definition_frame(definition) : dash_kit_loading_skeleton diff --git a/test/helpers/dash_kit/dashboard_helper_test.rb b/test/helpers/dash_kit/dashboard_helper_test.rb index 2a1a743..7a696e4 100644 --- a/test/helpers/dash_kit/dashboard_helper_test.rb +++ b/test/helpers/dash_kit/dashboard_helper_test.rb @@ -121,6 +121,15 @@ def configure_viewer(viewer) assert_match "animate-pulse", html end + test "dash_kit_render_widgets draws a widget from the host's block inside the page" do + dashboard = DashKit::Dashboard.create!(name: "D", dashboard_type: "test_dashboard", owner: Account.create!(name: "Owner")) + dashboard.add_block(KsBlocks.registry.block_types(kind: :test_dashboard).find { |type| type.key == :stats }) + + html = dash_kit_render_widgets(config: dashboard) { |widget_key| content_tag(:p, "Drawn #{widget_key}") } + + assert_match "

Drawn stats

", html + end + test "dash_kit_render_widgets renders a built definition frame" do definition = @config.widget_definitions.create!(source: "revenue", visualization: "single_value") @config.add_built_widget(definition) From be9439fefb1fb164d00f8ea4b2ce21fd2c4e4313 Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Tue, 20 Oct 2026 16:35:44 -0400 Subject: [PATCH 2/3] Check a host-drawn widget sits inside the grid its editor arranges Co-Authored-By: Claude Opus 5.5 --- test/helpers/dash_kit/dashboard_helper_test.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/helpers/dash_kit/dashboard_helper_test.rb b/test/helpers/dash_kit/dashboard_helper_test.rb index 7a696e4..ee7d63f 100644 --- a/test/helpers/dash_kit/dashboard_helper_test.rb +++ b/test/helpers/dash_kit/dashboard_helper_test.rb @@ -130,6 +130,17 @@ def configure_viewer(viewer) assert_match "

Drawn stats

", html end + test "dash_kit_render_widgets draws a widget from the host's block inside the grid its editor arranges" do + owner = Account.create!(name: "Owner") + configure_viewer(owner) + dashboard = DashKit::Dashboard.create!(name: "D", dashboard_type: "test_dashboard", owner: owner) + dashboard.add_block(KsBlocks.registry.block_types(kind: :test_dashboard).find { |type| type.key == :stats }) + + html = dash_kit_render_widgets(config: dashboard) { |widget_key| content_tag(:p, "Drawn #{widget_key}") } + + assert_match "

Drawn stats

", html + end + test "dash_kit_render_widgets renders a built definition frame" do definition = @config.widget_definitions.create!(source: "revenue", visualization: "single_value") @config.add_built_widget(definition) From b39dc7266567c359493134281f9d01a8fc7589de Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Tue, 20 Oct 2026 16:37:37 -0400 Subject: [PATCH 3/3] Describe drawing widgets from the host's block in the guide, its agents and the changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ lib/dash_kit/reference/guide.md | 23 +++++++++++-------- .../the_local/agents/dash_kit-develop.md | 23 +++++++++++-------- .../the_local/agents/dash_kit-info.md | 23 +++++++++++-------- .../the_local/agents/dash_kit-install.md | 23 +++++++++++-------- 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 809ea16..fd977ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Added +- A host can pass a block when it draws a dashboard, and each shown widget is drawn from that block inside the page, in the order and with the visibility the dashboard saved, rather than loading through a request of its own. + ## [2.1.0] - 2026-09-30 ### Added diff --git a/lib/dash_kit/reference/guide.md b/lib/dash_kit/reference/guide.md index df675e2..e75666d 100644 --- a/lib/dash_kit/reference/guide.md +++ b/lib/dash_kit/reference/guide.md @@ -12,8 +12,9 @@ coupling the UI to any specific data backend. - A widget registry: dashboard types map to the widgets available on them. - A persistence layer for user preferences (which widgets show, in what order, with which filters) via an ActiveRecord `Dashboard` model. -- A rendering layer that lazy-loads each widget through a Turbo Frame and - refreshes on configuration change. +- A rendering layer that lazy-loads each widget through a Turbo Frame, or hands + each widget to a block the host passes so the host draws it inside the page, + and refreshes on configuration change. A dashboard renders two kinds of widget side by side: @@ -81,16 +82,19 @@ The layers, all under the `DashKit` namespace: `dash_kit_available_sources`, `dash_kit_visualizations`, and `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets` renders the registered widgets in order, then appends one lazy Turbo Frame per - `widget_definition`. + `widget_definition`. Given a block, it calls the block with each shown + registered widget's key and draws what the block returns in that widget's + place, inside the page's own request and with no Turbo Frame. ### Data flow UI events -> dashboard update -> Turbo refresh -> widgets re-render via -lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a +lazy-loaded Turbo Frames. Without a block, a widget loads through a `` pointing at the widget's own route. The frame carries its dashboard's id, so the widget partial is handed the dashboard's `filter_state` to render against (DashKit passes the blob; the host interprets -it). +it). With a block, DashKit decides only which widgets show and where, and the +host draws each one from data it has already loaded for the page. Built widgets follow the same flow. Each `widget_definition` lazy-loads through its own `` at `/widget_definitions/:id`; @@ -255,7 +259,7 @@ The following manual steps remain: display order. - **Supply the data yourself.** A widget's partial is responsible for fetching and rendering its own data; DashKit only renders the partial inside a lazy - Turbo Frame. Keep partials self-contained around the owner DashKit scopes to. + Turbo Frame, or calls the host's block with the widget's key. Keep partials self-contained around the owner DashKit scopes to. This holds for registered widgets and for built-widget renderer partials alike — both receive `filter_state` and read it to shape their own request. - **Register a renderer for every visualization the builder offers.** A built @@ -274,9 +278,10 @@ The following manual steps remain: `WidgetManagement` methods (`toggle_widget`, `move_widget_up`, `move_widget_down`, `update_filter`) or the engine routes; do not write `widget_order` / `hidden_widgets` / `filter_state` directly. -- **Let widgets lazy-load.** Render through `dash_kit_render_widgets` / - `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not - inline widget bodies. +- **Draw widgets through `dash_kit_render_widgets`.** Without a block, each + widget loads in its own Turbo Frame. Pass a block when the page already holds + the widgets' data, so every widget is drawn in the page's own request rather + than one request per widget. - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind and supports dark mode; host styling should match. The primary surface is often a native webview. diff --git a/lib/dash_kit/the_local/agents/dash_kit-develop.md b/lib/dash_kit/the_local/agents/dash_kit-develop.md index 25892d3..422e316 100644 --- a/lib/dash_kit/the_local/agents/dash_kit-develop.md +++ b/lib/dash_kit/the_local/agents/dash_kit-develop.md @@ -20,8 +20,9 @@ coupling the UI to any specific data backend. - A widget registry: dashboard types map to the widgets available on them. - A persistence layer for user preferences (which widgets show, in what order, with which filters) via an ActiveRecord `Dashboard` model. -- A rendering layer that lazy-loads each widget through a Turbo Frame and - refreshes on configuration change. +- A rendering layer that lazy-loads each widget through a Turbo Frame, or hands + each widget to a block the host passes so the host draws it inside the page, + and refreshes on configuration change. A dashboard renders two kinds of widget side by side: @@ -89,16 +90,19 @@ The layers, all under the `DashKit` namespace: `dash_kit_available_sources`, `dash_kit_visualizations`, and `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets` renders the registered widgets in order, then appends one lazy Turbo Frame per - `widget_definition`. + `widget_definition`. Given a block, it calls the block with each shown + registered widget's key and draws what the block returns in that widget's + place, inside the page's own request and with no Turbo Frame. ### Data flow UI events -> dashboard update -> Turbo refresh -> widgets re-render via -lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a +lazy-loaded Turbo Frames. Without a block, a widget loads through a `` pointing at the widget's own route. The frame carries its dashboard's id, so the widget partial is handed the dashboard's `filter_state` to render against (DashKit passes the blob; the host interprets -it). +it). With a block, DashKit decides only which widgets show and where, and the +host draws each one from data it has already loaded for the page. Built widgets follow the same flow. Each `widget_definition` lazy-loads through its own `` at `/widget_definitions/:id`; @@ -263,7 +267,7 @@ The following manual steps remain: display order. - **Supply the data yourself.** A widget's partial is responsible for fetching and rendering its own data; DashKit only renders the partial inside a lazy - Turbo Frame. Keep partials self-contained around the owner DashKit scopes to. + Turbo Frame, or calls the host's block with the widget's key. Keep partials self-contained around the owner DashKit scopes to. This holds for registered widgets and for built-widget renderer partials alike — both receive `filter_state` and read it to shape their own request. - **Register a renderer for every visualization the builder offers.** A built @@ -282,9 +286,10 @@ The following manual steps remain: `WidgetManagement` methods (`toggle_widget`, `move_widget_up`, `move_widget_down`, `update_filter`) or the engine routes; do not write `widget_order` / `hidden_widgets` / `filter_state` directly. -- **Let widgets lazy-load.** Render through `dash_kit_render_widgets` / - `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not - inline widget bodies. +- **Draw widgets through `dash_kit_render_widgets`.** Without a block, each + widget loads in its own Turbo Frame. Pass a block when the page already holds + the widgets' data, so every widget is drawn in the page's own request rather + than one request per widget. - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind and supports dark mode; host styling should match. The primary surface is often a native webview. diff --git a/lib/dash_kit/the_local/agents/dash_kit-info.md b/lib/dash_kit/the_local/agents/dash_kit-info.md index dc95325..331f56a 100644 --- a/lib/dash_kit/the_local/agents/dash_kit-info.md +++ b/lib/dash_kit/the_local/agents/dash_kit-info.md @@ -20,8 +20,9 @@ coupling the UI to any specific data backend. - A widget registry: dashboard types map to the widgets available on them. - A persistence layer for user preferences (which widgets show, in what order, with which filters) via an ActiveRecord `Dashboard` model. -- A rendering layer that lazy-loads each widget through a Turbo Frame and - refreshes on configuration change. +- A rendering layer that lazy-loads each widget through a Turbo Frame, or hands + each widget to a block the host passes so the host draws it inside the page, + and refreshes on configuration change. A dashboard renders two kinds of widget side by side: @@ -89,16 +90,19 @@ The layers, all under the `DashKit` namespace: `dash_kit_available_sources`, `dash_kit_visualizations`, and `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets` renders the registered widgets in order, then appends one lazy Turbo Frame per - `widget_definition`. + `widget_definition`. Given a block, it calls the block with each shown + registered widget's key and draws what the block returns in that widget's + place, inside the page's own request and with no Turbo Frame. ### Data flow UI events -> dashboard update -> Turbo refresh -> widgets re-render via -lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a +lazy-loaded Turbo Frames. Without a block, a widget loads through a `` pointing at the widget's own route. The frame carries its dashboard's id, so the widget partial is handed the dashboard's `filter_state` to render against (DashKit passes the blob; the host interprets -it). +it). With a block, DashKit decides only which widgets show and where, and the +host draws each one from data it has already loaded for the page. Built widgets follow the same flow. Each `widget_definition` lazy-loads through its own `` at `/widget_definitions/:id`; @@ -263,7 +267,7 @@ The following manual steps remain: display order. - **Supply the data yourself.** A widget's partial is responsible for fetching and rendering its own data; DashKit only renders the partial inside a lazy - Turbo Frame. Keep partials self-contained around the owner DashKit scopes to. + Turbo Frame, or calls the host's block with the widget's key. Keep partials self-contained around the owner DashKit scopes to. This holds for registered widgets and for built-widget renderer partials alike — both receive `filter_state` and read it to shape their own request. - **Register a renderer for every visualization the builder offers.** A built @@ -282,9 +286,10 @@ The following manual steps remain: `WidgetManagement` methods (`toggle_widget`, `move_widget_up`, `move_widget_down`, `update_filter`) or the engine routes; do not write `widget_order` / `hidden_widgets` / `filter_state` directly. -- **Let widgets lazy-load.** Render through `dash_kit_render_widgets` / - `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not - inline widget bodies. +- **Draw widgets through `dash_kit_render_widgets`.** Without a block, each + widget loads in its own Turbo Frame. Pass a block when the page already holds + the widgets' data, so every widget is drawn in the page's own request rather + than one request per widget. - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind and supports dark mode; host styling should match. The primary surface is often a native webview. diff --git a/lib/dash_kit/the_local/agents/dash_kit-install.md b/lib/dash_kit/the_local/agents/dash_kit-install.md index 4ebb183..7be65ef 100644 --- a/lib/dash_kit/the_local/agents/dash_kit-install.md +++ b/lib/dash_kit/the_local/agents/dash_kit-install.md @@ -20,8 +20,9 @@ coupling the UI to any specific data backend. - A widget registry: dashboard types map to the widgets available on them. - A persistence layer for user preferences (which widgets show, in what order, with which filters) via an ActiveRecord `Dashboard` model. -- A rendering layer that lazy-loads each widget through a Turbo Frame and - refreshes on configuration change. +- A rendering layer that lazy-loads each widget through a Turbo Frame, or hands + each widget to a block the host passes so the host draws it inside the page, + and refreshes on configuration change. A dashboard renders two kinds of widget side by side: @@ -89,16 +90,19 @@ The layers, all under the `DashKit` namespace: `dash_kit_available_sources`, `dash_kit_visualizations`, and `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets` renders the registered widgets in order, then appends one lazy Turbo Frame per - `widget_definition`. + `widget_definition`. Given a block, it calls the block with each shown + registered widget's key and draws what the block returns in that widget's + place, inside the page's own request and with no Turbo Frame. ### Data flow UI events -> dashboard update -> Turbo refresh -> widgets re-render via -lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a +lazy-loaded Turbo Frames. Without a block, a widget loads through a `` pointing at the widget's own route. The frame carries its dashboard's id, so the widget partial is handed the dashboard's `filter_state` to render against (DashKit passes the blob; the host interprets -it). +it). With a block, DashKit decides only which widgets show and where, and the +host draws each one from data it has already loaded for the page. Built widgets follow the same flow. Each `widget_definition` lazy-loads through its own `` at `/widget_definitions/:id`; @@ -263,7 +267,7 @@ The following manual steps remain: display order. - **Supply the data yourself.** A widget's partial is responsible for fetching and rendering its own data; DashKit only renders the partial inside a lazy - Turbo Frame. Keep partials self-contained around the owner DashKit scopes to. + Turbo Frame, or calls the host's block with the widget's key. Keep partials self-contained around the owner DashKit scopes to. This holds for registered widgets and for built-widget renderer partials alike — both receive `filter_state` and read it to shape their own request. - **Register a renderer for every visualization the builder offers.** A built @@ -282,9 +286,10 @@ The following manual steps remain: `WidgetManagement` methods (`toggle_widget`, `move_widget_up`, `move_widget_down`, `update_filter`) or the engine routes; do not write `widget_order` / `hidden_widgets` / `filter_state` directly. -- **Let widgets lazy-load.** Render through `dash_kit_render_widgets` / - `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not - inline widget bodies. +- **Draw widgets through `dash_kit_render_widgets`.** Without a block, each + widget loads in its own Turbo Frame. Pass a block when the page already holds + the widgets' data, so every widget is drawn in the page's own request rather + than one request per widget. - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind and supports dark mode; host styling should match. The primary surface is often a native webview.