softr-vibe-coding 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/README.md +3 -0
- package/SKILL.md +1 -0
- package/package.json +1 -1
- package/references/native-block-filters.md +300 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,9 @@ All notable changes to this skill are documented here. Versions follow [Semantic
|
|
|
4
4
|
|
|
5
5
|
Entries from 1.3.1 onward are generated automatically from git commit subjects between version bumps (see `.github/workflows/publish.yml`). Entries before 1.3.1 were backfilled by hand from the existing commit history.
|
|
6
6
|
|
|
7
|
+
## [1.11.0] - 2026-06-12
|
|
8
|
+
- Add references/native-block-filters.md — dynamic date / URL-param filters and custom filter controls on native Softr List/Grid blocks. Covers driving a native block's conditional filter from a Custom Code Static block via {URL_PARAM:…}, the empty-param 'match nothing' wide-range sentinel that otherwise empties the list on load, injecting the control into the block's filter row scoped by data-block-id (a page-wide text search wrongly matches the nav 'Clients' link), resolving the row by filter-label-text + lowest-common-ancestor instead of hashed chip classes, and the key gotcha that Softr re-renders its List/Grid block and discards injected nodes so the control must be re-homed on a short interval rather than relocated once. Includes a full worked date-range picker (wide-range sentinel + wait-for-both + Clear + relocation + re-render survival) and a DevTools how-to for finding data-block-id and the filter row. Wire into the SKILL.md reference table and README structure listing; bump to 1.11.0
|
|
9
|
+
|
|
7
10
|
## [1.10.2] - 2026-06-12
|
|
8
11
|
- Document the autoNumber-formula blank-read gotcha — a formula that references an Airtable autoNumber field frequently reads back empty through Softr's data layer even though Airtable shows the value and a data re-sync doesn't fix it (sibling formulas without an autoNumber dependency read fine). Add to datasources/airtable.md Gotchas + the Formula row of the field table, with the JS rebuild-from-autoNumber fix and the plain-text-stamped-by-automation alternative; bump to 1.10.2
|
|
9
12
|
|
package/README.md
CHANGED
|
@@ -189,6 +189,9 @@ softr-vibe-coding/
|
|
|
189
189
|
│ │ # nav, dropdowns, page background) via global
|
|
190
190
|
│ │ # Custom Code CSS — stable selectors, floating
|
|
191
191
|
│ │ # islands, dropdown grid fix, multi-layer page-bg
|
|
192
|
+
│ ├── native-block-filters.md # Dynamic date / URL-param filters + custom filter
|
|
193
|
+
│ │ # controls on native List/Grid blocks — wide-range
|
|
194
|
+
│ │ # sentinel, inject into filter row, survive re-renders
|
|
192
195
|
│ ├── anti-patterns.md # Categorized violation catalog (86 lines)
|
|
193
196
|
│ │ # Data access, mutations, hooks, layout,
|
|
194
197
|
│ │ # permissions, helper blocks
|
package/SKILL.md
CHANGED
|
@@ -159,6 +159,7 @@ For advanced patterns beyond data fetching, load the relevant reference when the
|
|
|
159
159
|
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |
|
|
160
160
|
| AI-assisted Softr DB schema discovery / field-ID lookup / record reads via the official Softr MCP server (sibling to the in-block `useRecords` workflow) | [references/softr-database-mcp.md](references/softr-database-mcp.md) |
|
|
161
161
|
| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace | [references/native-chrome-styling.md](references/native-chrome-styling.md) |
|
|
162
|
+
| Adding a **dynamic date filter or custom filter control to a native List/Grid block** (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with `{URL_PARAM:…}`, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders | [references/native-block-filters.md](references/native-block-filters.md) |
|
|
162
163
|
|
|
163
164
|
## Code Structure
|
|
164
165
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"softr-vibe-coding": "./bin/cli.js"
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Dynamic Filters & Custom Controls on Native Softr Blocks
|
|
2
|
+
|
|
3
|
+
Softr's native **List / Grid** blocks support category chips and *static* filters, but they have **no dynamic date filter** ("show me records due between X and Y") and no way to add your own filter control to the block's chrome. You can add both with a **Custom Code Static block** — raw HTML/JS rendered in the **main document** (NOT a shadow-DOM Vibe Coding block) — that:
|
|
4
|
+
|
|
5
|
+
1. writes the user's choice into a **URL parameter** and reloads, while
|
|
6
|
+
2. the native block's own **Conditional Filter** reads that parameter via `{URL_PARAM:...}`, and (optionally)
|
|
7
|
+
3. the same script **relocates the control into the native block's filter row** so it looks built-in.
|
|
8
|
+
|
|
9
|
+
> **Sibling to [native-chrome-styling.md](native-chrome-styling.md).** That doc restyles Softr's *shell* (header/footer/nav) with global CSS. This one *drives and augments native blocks* with custom-code JS. Both reach the **main document**, never a block's shadow DOM — so this is a **Custom Code Static block**, not a Vibe Coding (JSX) block.
|
|
10
|
+
|
|
11
|
+
House code style still applies in the `<script>`: `var`, `function(){}`, **no** optional chaining (`?.`) or nullish coalescing (`??`).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. The mechanism — steer a native filter with a URL param
|
|
16
|
+
|
|
17
|
+
The custom control never filters anything itself. It writes the user's selection into the URL and reloads; Softr's *own* conditional filter does the work.
|
|
18
|
+
|
|
19
|
+
**Wire the native block (Studio):** open the List/Grid block → **Conditional Filters** → add rules that reference a URL param instead of a static value. For a date range:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Due date is on or after {URL_PARAM:start_date}
|
|
23
|
+
Due date is on or before {URL_PARAM:end_date}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**The control (Custom Code Static block):** write the param + reload.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<input id="startDate" type="date" />
|
|
30
|
+
<script>
|
|
31
|
+
(function () {
|
|
32
|
+
var input = document.getElementById("startDate");
|
|
33
|
+
var url = new URL(window.location.href);
|
|
34
|
+
if (url.searchParams.has("start_date")) input.value = url.searchParams.get("start_date");
|
|
35
|
+
input.addEventListener("change", function () {
|
|
36
|
+
url.searchParams.set("start_date", input.value);
|
|
37
|
+
window.history.replaceState({}, "", url);
|
|
38
|
+
window.location.reload(); // Softr re-reads {URL_PARAM:...} on load
|
|
39
|
+
});
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's the whole steering loop: **control → URL param → native conditional filter.**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. The empty-param trap → the wide-range sentinel ⚠️
|
|
49
|
+
|
|
50
|
+
**Softr treats an empty or missing `{URL_PARAM:x}` as "match NOTHING", not "ignore this filter."** So on first load with no params, a filtered list renders **empty** — the single most confusing symptom of this technique.
|
|
51
|
+
|
|
52
|
+
**Fix: seed a wide sentinel range that means "show all", and keep the inputs visually blank.** When the user clears the control, fall back to the sentinel instead of an empty value.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
var WIDE_START = "2000-01-01";
|
|
56
|
+
var WIDE_END = "2099-12-31";
|
|
57
|
+
|
|
58
|
+
var rawStart = url.searchParams.get("start_date");
|
|
59
|
+
var rawEnd = url.searchParams.get("end_date");
|
|
60
|
+
|
|
61
|
+
// Blank/first load → seed the wide range so EVERYTHING shows, then reload once.
|
|
62
|
+
// Falsy check (not .has) so an EMPTY param self-heals the same as a missing one.
|
|
63
|
+
if (!rawStart && !rawEnd) {
|
|
64
|
+
url.searchParams.set("start_date", WIDE_START);
|
|
65
|
+
url.searchParams.set("end_date", WIDE_END);
|
|
66
|
+
window.history.replaceState({}, "", url);
|
|
67
|
+
window.location.reload();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Reflect only REAL user dates back into the inputs; hide the sentinels (inputs stay empty).
|
|
72
|
+
if (rawStart && rawStart !== WIDE_START) startInput.value = rawStart;
|
|
73
|
+
if (rawEnd && rawEnd !== WIDE_END) endInput.value = rawEnd;
|
|
74
|
+
|
|
75
|
+
// On apply, empty input → sentinel (so "no bound" still means "show all", not "show none").
|
|
76
|
+
function apply() {
|
|
77
|
+
url.searchParams.set("start_date", startInput.value ? startInput.value : WIDE_START);
|
|
78
|
+
url.searchParams.set("end_date", endInput.value ? endInput.value : WIDE_END);
|
|
79
|
+
window.history.replaceState({}, "", url);
|
|
80
|
+
window.location.reload();
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Date filters need a `date_list` helper table
|
|
87
|
+
|
|
88
|
+
For **date** comparisons, Softr's conditional filter can only compare against dates that *exist as data*. Two patterns:
|
|
89
|
+
|
|
90
|
+
- **Direct range (simplest):** the `{URL_PARAM:start_date}` / `{URL_PARAM:end_date}` rules above compare against the record's own date field. No helper table needed if your "is on or after / before" comparator accepts a raw param value. (Works for List/Grid filtering by a record date.)
|
|
91
|
+
- **Bridge for charts/metrics/many blocks (AppGrape pattern):** to drive *many* blocks off one date without repeating URL-param rules, add a hidden **Item Details** block bound to a **`date_list`** table (one record per day), conditionally filtered to `Date is {URL_PARAM:selected_date}`. That exposes the chosen date as a page-level **Current Record**; every chart/metric then filters `Date is → Current Record > Date`. Hide the bridge block with page CSS (`#item-details1 { display:none !important; }`).
|
|
92
|
+
|
|
93
|
+
Either way, if you compare against `date_list`, it must hold **one record for every day** in range. Populate it **once** with an Airtable automation/Scripting script (one record per calendar day, batched 50/call, re-runnable via a dedupe read). See [airtable-automations.md](airtable-automations.md) for the script shape; field-ID discipline applies.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 4. Injecting the control into the native block's filter row
|
|
98
|
+
|
|
99
|
+
A Custom Code block renders wherever you place it on the page — Softr gives no setting to drop it *inside* a list's filter bar. So after the page renders, the script finds the native filter row in the DOM and **re-parents** the control into it. Three hard-won rules:
|
|
100
|
+
|
|
101
|
+
### 4a. Scope to the block by `data-block-id` — never search page-wide
|
|
102
|
+
Every Softr block renders as `<section data-block-id="...">` (stable; read it off the block in DevTools). **Always scope DOM queries to that section.** A page-wide search will grab the wrong thing — e.g. matching filter buttons by the text "Client" also hits a **"Clients" link in the nav**, dropping your control into the sidebar.
|
|
103
|
+
|
|
104
|
+
### 4b. Find the filter row WITHOUT hashed classes
|
|
105
|
+
Softr's filter chips carry only **hashed build classes** (e.g. `a0e85ef_wsthdk3`) that change on every deploy — same rule as native-chrome-styling.md: don't anchor on them. Durable approach: match the filter **buttons by the label text you configured**, then take their **lowest common ancestor** = the filter row. Keep the hashed class as a fast fallback.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
var YOUR_BLOCK_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; // the List/Grid block
|
|
109
|
+
// The filter labels YOU set on the block — content, not Softr hashes; survives deploys.
|
|
110
|
+
var FILTER_LABELS = ["Client", "Status", "Owner", "Region"]; // only need any 2 to still match
|
|
111
|
+
|
|
112
|
+
function lowestCommonAncestor(els) {
|
|
113
|
+
if (!els.length) return null;
|
|
114
|
+
var anc = els[0];
|
|
115
|
+
for (var i = 1; i < els.length; i++) {
|
|
116
|
+
while (anc && !anc.contains(els[i])) anc = anc.parentElement;
|
|
117
|
+
if (!anc) return null;
|
|
118
|
+
}
|
|
119
|
+
return anc;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findFilterRow() {
|
|
123
|
+
var block = document.querySelector('[data-block-id="' + YOUR_BLOCK_ID + '"]');
|
|
124
|
+
if (!block) return null;
|
|
125
|
+
|
|
126
|
+
// PRIMARY (deploy-proof): the filter buttons by their label text → their shared container.
|
|
127
|
+
var btns = block.querySelectorAll("button");
|
|
128
|
+
var hits = [];
|
|
129
|
+
for (var i = 0; i < btns.length; i++) {
|
|
130
|
+
var t = (btns[i].textContent || "").replace(/\s+/g, " ").trim();
|
|
131
|
+
for (var j = 0; j < FILTER_LABELS.length; j++) {
|
|
132
|
+
if (t === FILTER_LABELS[j]) { hits.push(btns[i]); break; }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (hits.length >= 2) {
|
|
136
|
+
var lca = lowestCommonAncestor(hits);
|
|
137
|
+
if (lca && !lca.querySelector(".softr-list-container")) return lca; // guard: not too high
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// FALLBACK (works today, may break on a Softr CSS rebuild): the hashed chip wrapper.
|
|
141
|
+
var chip = block.querySelector(".a0e85ef_wsthdk3");
|
|
142
|
+
if (chip && chip.parentElement) return chip.parentElement;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 4c. Survive re-renders — re-home on an interval ⚠️ (THE big one)
|
|
148
|
+
**Softr re-renders its List/Grid block on every data or filter change, and a re-render discards any node you injected into it.** A one-shot relocation works once, then the control **vanishes** the first time the user touches a native chip or the data refreshes. Don't relocate once — **re-home on a short interval** (or a `MutationObserver`) so the same node snaps back whenever Softr rebuilds the row.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
var wrap = document.getElementById("yourControlWrap");
|
|
152
|
+
var host = wrap ? (wrap.closest("section") || wrap.parentElement) : null;
|
|
153
|
+
|
|
154
|
+
function ensureHomed() {
|
|
155
|
+
try {
|
|
156
|
+
var row = findFilterRow();
|
|
157
|
+
if (!row || !wrap) return;
|
|
158
|
+
if (wrap.parentElement !== row) { // only acts when knocked out → no loop
|
|
159
|
+
wrap.style.marginLeft = "8px";
|
|
160
|
+
row.appendChild(wrap);
|
|
161
|
+
}
|
|
162
|
+
// Collapse the now-empty Custom Code block — but NEVER hide the list itself.
|
|
163
|
+
if (host && host.style.display !== "none" && !host.querySelector(".softr-list-container")) {
|
|
164
|
+
host.style.display = "none";
|
|
165
|
+
}
|
|
166
|
+
} catch (err) { /* leave the control where it is on any error */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ensureHomed(); // place ASAP
|
|
170
|
+
setInterval(ensureHomed, 500); // and re-home after every Softr re-render
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`ensureHomed` is idempotent (it only re-appends when the control is *not* already in the row), so the interval is cheap and can't loop.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 5. Full worked example — date-range picker that joins the filter row
|
|
178
|
+
|
|
179
|
+
A complete Custom Code Static block: range picker with the wide-range sentinel, "wait for both ends before reloading", a Clear button, relocation into the filter row, and re-render survival.
|
|
180
|
+
|
|
181
|
+
```html
|
|
182
|
+
<div id="duePickerWrap" style="display:inline-flex; align-items:center; gap:8px; font-family:inherit;">
|
|
183
|
+
<label style="font-size:13px; font-weight:600;">Due between</label>
|
|
184
|
+
<input id="startDate" type="date" style="padding:7px 10px; font-size:13px; border-radius:9999px; border:1px solid rgba(0,0,0,0.1); background:transparent; cursor:pointer;" />
|
|
185
|
+
<span style="font-size:13px; color:#666;">and</span>
|
|
186
|
+
<input id="endDate" type="date" style="padding:7px 10px; font-size:13px; border-radius:9999px; border:1px solid rgba(0,0,0,0.1); background:transparent; cursor:pointer;" />
|
|
187
|
+
<button id="dueClear" type="button" style="padding:7px 12px; font-size:13px; font-weight:600; border-radius:9999px; border:1px solid rgba(0,0,0,0.1); background:transparent; color:#666; cursor:pointer;">Clear</button>
|
|
188
|
+
<span id="dueHint" style="display:none; font-size:12px; color:#999;">Pick both dates to filter</span>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<script>
|
|
192
|
+
(function () {
|
|
193
|
+
var WIDE_START = "2000-01-01", WIDE_END = "2099-12-31";
|
|
194
|
+
var BLOCK_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
|
195
|
+
var FILTER_LABELS = ["Client", "Status", "Owner", "Region"];
|
|
196
|
+
|
|
197
|
+
var wrap = document.getElementById("duePickerWrap");
|
|
198
|
+
var startInput = document.getElementById("startDate");
|
|
199
|
+
var endInput = document.getElementById("endDate");
|
|
200
|
+
var clearBtn = document.getElementById("dueClear");
|
|
201
|
+
var hint = document.getElementById("dueHint");
|
|
202
|
+
var host = wrap ? (wrap.closest("section") || wrap.parentElement) : null;
|
|
203
|
+
var url = new URL(window.location.href);
|
|
204
|
+
|
|
205
|
+
var rawStart = url.searchParams.get("start_date");
|
|
206
|
+
var rawEnd = url.searchParams.get("end_date");
|
|
207
|
+
if (!rawStart && !rawEnd) { // blank load → seed wide → show all
|
|
208
|
+
url.searchParams.set("start_date", WIDE_START);
|
|
209
|
+
url.searchParams.set("end_date", WIDE_END);
|
|
210
|
+
window.history.replaceState({}, "", url);
|
|
211
|
+
window.location.reload();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (rawStart && rawStart !== WIDE_START) startInput.value = rawStart;
|
|
215
|
+
if (rawEnd && rawEnd !== WIDE_END) endInput.value = rawEnd;
|
|
216
|
+
|
|
217
|
+
function apply() {
|
|
218
|
+
url.searchParams.set("start_date", startInput.value ? startInput.value : WIDE_START);
|
|
219
|
+
url.searchParams.set("end_date", endInput.value ? endInput.value : WIDE_END);
|
|
220
|
+
window.history.replaceState({}, "", url);
|
|
221
|
+
window.location.reload();
|
|
222
|
+
}
|
|
223
|
+
function applyWhenComplete() { if (startInput.value && endInput.value) apply(); }
|
|
224
|
+
function updateHint() {
|
|
225
|
+
var one = (startInput.value && !endInput.value) || (!startInput.value && endInput.value);
|
|
226
|
+
if (hint) hint.style.display = one ? "inline" : "none";
|
|
227
|
+
}
|
|
228
|
+
startInput.addEventListener("change", function () { updateHint(); applyWhenComplete(); });
|
|
229
|
+
endInput.addEventListener("change", function () { updateHint(); applyWhenComplete(); });
|
|
230
|
+
clearBtn.addEventListener("click", function () {
|
|
231
|
+
startInput.value = ""; endInput.value = ""; updateHint(); apply();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
function lowestCommonAncestor(els) {
|
|
235
|
+
if (!els.length) return null;
|
|
236
|
+
var anc = els[0];
|
|
237
|
+
for (var i = 1; i < els.length; i++) {
|
|
238
|
+
while (anc && !anc.contains(els[i])) anc = anc.parentElement;
|
|
239
|
+
if (!anc) return null;
|
|
240
|
+
}
|
|
241
|
+
return anc;
|
|
242
|
+
}
|
|
243
|
+
function findFilterRow() {
|
|
244
|
+
var block = document.querySelector('[data-block-id="' + BLOCK_ID + '"]');
|
|
245
|
+
if (!block) return null;
|
|
246
|
+
var btns = block.querySelectorAll("button"), hits = [];
|
|
247
|
+
for (var i = 0; i < btns.length; i++) {
|
|
248
|
+
var t = (btns[i].textContent || "").replace(/\s+/g, " ").trim();
|
|
249
|
+
for (var j = 0; j < FILTER_LABELS.length; j++) {
|
|
250
|
+
if (t === FILTER_LABELS[j]) { hits.push(btns[i]); break; }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (hits.length >= 2) {
|
|
254
|
+
var lca = lowestCommonAncestor(hits);
|
|
255
|
+
if (lca && !lca.querySelector(".softr-list-container")) return lca;
|
|
256
|
+
}
|
|
257
|
+
var chip = block.querySelector(".a0e85ef_wsthdk3");
|
|
258
|
+
if (chip && chip.parentElement) return chip.parentElement;
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
function ensureHomed() {
|
|
262
|
+
try {
|
|
263
|
+
var row = findFilterRow();
|
|
264
|
+
if (!row || !wrap) return;
|
|
265
|
+
if (wrap.parentElement !== row) { wrap.style.marginLeft = "8px"; row.appendChild(wrap); }
|
|
266
|
+
if (host && host.style.display !== "none" && !host.querySelector(".softr-list-container")) {
|
|
267
|
+
host.style.display = "none";
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {}
|
|
270
|
+
}
|
|
271
|
+
ensureHomed();
|
|
272
|
+
setInterval(ensureHomed, 500);
|
|
273
|
+
})();
|
|
274
|
+
</script>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Gotchas (verified June 2026)
|
|
280
|
+
|
|
281
|
+
- **Empty `{URL_PARAM}` = match NOTHING.** A blank/missing param empties a filtered list. Seed a wide sentinel range (and map it back to blank inputs) so "no selection" shows all. §2.
|
|
282
|
+
- **Softr re-renders its list block and discards injected nodes.** One-shot relocation vanishes on the first interaction. Re-home on an interval/observer. §4c.
|
|
283
|
+
- **Page-wide DOM searches hit the native chrome.** Matching by text "Client" also matches a "Clients" nav link → control lands in the sidebar. Always scope to the block's `data-block-id`. §4a.
|
|
284
|
+
- **Filter chips have only hashed classes** (`a0e85ef_…`) that change on deploys. Anchor on your **filter label text + lowest common ancestor**; keep the hash as a fallback only. §4b.
|
|
285
|
+
- **`window.location.reload()` is how Softr re-reads the param.** `replaceState` alone updates the URL but won't re-run the conditional filter — you must reload.
|
|
286
|
+
- **Custom Code renders on the published app, sometimes not in the Studio editor.** Verify on the live/preview app, and **hard-refresh** (Cmd/Ctrl+Shift+R) after editing — a cached empty state can masquerade as "broken."
|
|
287
|
+
- **If the picker AND the records both vanish, the block isn't running.** Quick console probe: `new URL(location.href).searchParams.get("start_date")` (null = the seed never fired → script not executing) and `!!document.querySelector("#duePickerWrap")` (false = the HTML isn't on the page → re-paste/save the block).
|
|
288
|
+
|
|
289
|
+
## Finding the `data-block-id` and filter row in DevTools
|
|
290
|
+
|
|
291
|
+
1. Right-click the List/Grid block → **Inspect**. Walk up to the enclosing `<section data-block-id="…">` and copy that id → `BLOCK_ID`.
|
|
292
|
+
2. Inspect one filter chip; confirm its visible label text matches an entry in `FILTER_LABELS`.
|
|
293
|
+
3. Console sanity check (scoped, deploy-proof):
|
|
294
|
+
```js
|
|
295
|
+
var b = document.querySelector('[data-block-id="BLOCK_ID"]');
|
|
296
|
+
console.log([].filter.call(b.querySelectorAll("button"), function (x) {
|
|
297
|
+
return ["Client","Status","Owner","Region"].indexOf((x.textContent||"").trim()) > -1;
|
|
298
|
+
}).length, "filter buttons found");
|
|
299
|
+
```
|
|
300
|
+
≥2 means the label/LCA path will resolve the row.
|