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
+18 -7
View File
@@ -110,14 +110,25 @@ class Node:
"""Total media of this node and its subtree."""
return self.media
# `__html__` marks the value HTML-safe for Django (conditional_escape).
# `__str__` lets f-strings, ``str()`` and ``"".join(str(...))`` render the
# node — the bridge that keeps most existing composition working.
def __html__(self) -> str:
return self._render()
def with_media(self, media: Media) -> "Node":
"""Attach JS dependencies to this node and return it (for fluent use).
def __str__(self) -> str:
return self._render()
Lets a function-built node declare its media without becoming a full
``BaseComponent`` subclass: ``return Div(...).with_media(Media(js=...))``.
"""
self.media = self.media + media
return self
# A node's rendered output is always safe HTML by construction (Element
# escapes unsafe children; Safe wraps trusted markup; Fragment escapes plain
# strings). So both `__html__` (Django's conditional_escape hook) and
# `__str__` return a SafeString — this is what keeps ``str(node)`` safe when
# fed back into a child list or template, matching the old SafeText shims.
def __html__(self) -> SafeText:
return mark_safe(self._render())
def __str__(self) -> SafeText:
return mark_safe(self._render())
def _child_key(child: object) -> tuple[str, bool]: