Phase 3: declare component media that bubbles through the node tree

The JS-bearing widgets now declare their script dependencies, so a view
no longer needs to know which scripts a component requires:

- SearchSelect / FilterSelect → search_select.js
- RangeSlider → range_slider.js
- DateRangePicker → date_range_picker.js
- YearPicker → datepicker.umd.js (external, from Phase 2)
- FilterBar chrome → filter_bar.js

Because the filter-bar internals now build a node tree (the legacy
Component() string-builder calls became Element/Div), each bar's
collect_media() returns its own filter_bar.js merged with the scripts
that bubble up from the FilterSelect / RangeSlider / DateRangePicker
widgets it contains — exactly the set the views thread by hand today.

Adds Node.with_media() so a function-built node can declare media
without a full BaseComponent subclass, and tests proving the bubbling.

Note: the six *FilterBar functions still share the _filter_bar chrome
helper rather than a BaseComponent class hierarchy; folding them into
one is a follow-up that does not affect media collection (Phase 4).

https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
This commit is contained in:
Claude
2026-06-13 07:24:29 +00:00
parent 4031657bb5
commit 0819ddb87d
5 changed files with 116 additions and 44 deletions
+12 -9
View File
@@ -23,9 +23,12 @@ from typing import TypedDict
from django.utils.safestring import SafeText
from common.components.core import Component, HTMLAttribute
from common.components.core import Element, HTMLAttribute, Media, Node
from common.components.primitives import Div, Input, Pill, Span, Template
# Both comboboxes are wired by search_select.js.
_SEARCH_SELECT_MEDIA = Media(js=("search_select.js",))
class SearchSelectOption(TypedDict):
value: str | int
@@ -322,12 +325,12 @@ def SearchSelect(
always_visible=always_visible,
items_visible=items_visible,
templates=templates,
)
).with_media(_SEARCH_SELECT_MEDIA)
def _filter_remove_button() -> SafeText:
return Component(
tag_name="button",
def _filter_remove_button() -> Node:
return Element(
"button",
attributes=[
("type", "button"),
("data-pill-remove", ""),
@@ -369,9 +372,9 @@ def _filter_modifier_pill(modifier_value: str, label: str) -> SafeText:
)
def _filter_action_button(action: str, symbol: str, title: str) -> SafeText:
return Component(
tag_name="button",
def _filter_action_button(action: str, symbol: str, title: str) -> Node:
return Element(
"button",
attributes=[
("type", "button"),
("data-search-select-action", action),
@@ -557,7 +560,7 @@ def FilterSelect(
always_visible=False,
items_visible=items_visible,
templates=templates,
)
).with_media(_SEARCH_SELECT_MEDIA)
def searchselect_selected(