Single-source combobox markup via <template> cloning
Eliminate the Python/JS class-string duplication: the server renders hidden <template> prototypes (row, pill, include/exclude/modifier pills) using the same component functions, and search_select.js clones them, filling only the [data-ss-label] slot, value, and data-* attrs. All Tailwind class strings and DOM structure now live solely in the Python components — the JS no longer hardcodes any class. Pill gains an opt-in label_slot; the shell takes a templates list. Companion issue #8 tracks the further HTMX-idiomatic step of returning rendered row HTML from the search endpoint. https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
@@ -105,7 +105,19 @@ class SearchSelectComponentTest(unittest.TestCase):
|
||||
html = SearchSelect(
|
||||
name="t", options=[("1", "One")], search_url="/api/games/search"
|
||||
)
|
||||
self.assertNotIn('data-ss-option=""', html)
|
||||
# No pre-rendered rows in the live panel; the row prototype lives only in
|
||||
# the cloneable <template>.
|
||||
panel = html.split("data-ss-tpl")[0]
|
||||
self.assertNotIn('data-ss-option=""', panel)
|
||||
self.assertIn('data-ss-tpl="row"', html)
|
||||
|
||||
def test_templates_carry_label_slot_for_js_cloning(self):
|
||||
# The dynamic shapes the JS clones expose a [data-ss-label] slot so the JS
|
||||
# only fills text — classes/structure stay server-side.
|
||||
html = SearchSelect(name="t", search_url="/api/games/search", multi_select=True)
|
||||
self.assertIn('data-ss-tpl="row"', html)
|
||||
self.assertIn('data-ss-tpl="pill"', html)
|
||||
self.assertIn("data-ss-label", html)
|
||||
|
||||
def test_shell_region_order_pills_search_options(self):
|
||||
# The shared shell assembles the three regions in a fixed order; option
|
||||
@@ -150,10 +162,14 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
included=[("1", "Steam")],
|
||||
excluded=[("2", "GOG")],
|
||||
)
|
||||
# Labels live in a [data-ss-label] slot (so JS can fill clones); the ✓/✗
|
||||
# symbol is a sibling text node.
|
||||
self.assertIn('data-ss-type="include"', html)
|
||||
self.assertIn("✓ Steam", html)
|
||||
self.assertIn("✓", html)
|
||||
self.assertIn(">Steam</span>", html)
|
||||
self.assertIn('data-ss-type="exclude"', html)
|
||||
self.assertIn("✗ GOG", html)
|
||||
self.assertIn("✗", html)
|
||||
self.assertIn(">GOG</span>", html)
|
||||
self.assertIn("line-through", html) # excluded pill styling
|
||||
|
||||
def test_modifier_options_render_pinned_rows(self):
|
||||
@@ -172,9 +188,12 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
modifier_options=self.MODIFIERS,
|
||||
)
|
||||
# The lone modifier pill is shown; include/exclude pills are suppressed.
|
||||
# (Scope the check to the live pills region — the cloneable pill <template>s
|
||||
# legitimately contain data-ss-type.)
|
||||
pills_region = html.split("data-ss-tpl")[0]
|
||||
self.assertIn('data-ss-modifier="IS_NULL"', html)
|
||||
self.assertIn("(None)", html)
|
||||
self.assertNotIn('data-ss-type="include"', html)
|
||||
self.assertNotIn('data-ss-type="include"', pills_region)
|
||||
self.assertIn('data-modifier="IS_NULL"', html) # container carries it too
|
||||
|
||||
def test_search_url_omits_value_rows_but_keeps_modifiers(self):
|
||||
@@ -184,7 +203,11 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
prefetch=20,
|
||||
modifier_options=self.MODIFIERS,
|
||||
)
|
||||
self.assertNotIn('data-ss-option=""', html) # value rows fetched by JS
|
||||
# No value rows in the live panel (they're fetched); the row prototype
|
||||
# lives only in a <template>.
|
||||
panel = html.split("data-ss-tpl")[0]
|
||||
self.assertNotIn('data-ss-option=""', panel)
|
||||
self.assertIn('data-ss-tpl="row"', html)
|
||||
self.assertIn('data-ss-modifier-option="NOT_NULL"', html) # still pinned
|
||||
self.assertIn('data-prefetch="20"', html)
|
||||
|
||||
@@ -195,7 +218,7 @@ class FilterSelectComponentTest(unittest.TestCase):
|
||||
search_url="/api/games/search",
|
||||
excluded=[{"value": 4172, "label": "Obscure Game", "data": {}}],
|
||||
)
|
||||
self.assertIn("✗ Obscure Game", html)
|
||||
self.assertIn(">Obscure Game</span>", html)
|
||||
self.assertIn('data-value="4172"', html)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user