Skip to content

fix(table): make the category filter work#2024

Merged
markdumay merged 20 commits into
mainfrom
fix/table-category-filter
Jul 15, 2026
Merged

fix(table): make the category filter work#2024
markdumay merged 20 commits into
mainfrom
fix/table-category-filter

Conversation

@markdumay

Copy link
Copy Markdown
Collaborator

The table shortcode's filter argument — a button group that narrows a table's rows to a chosen category — has never worked from Markdown. data/structures/table.yml declares filter and filter-col, so {{< table filter="widget,gadget" >}} validated cleanly and then did nothing at all.

Fixing it turned up five separate defects, plus three more found along the way.

The filter

It was unreachable. layouts/_shortcodes/table.html never forwarded filter / filter-col to the partial.

Forwarding alone would not have worked. filter's declared type is [string, slice], a shortcode named parameter is always a string, and mod-utils validates an argument's kind against accepts without casting between kinds — so the partial would have received the unsplit string "widget,gadget" and reached {{ range $filter }}, which Hugo cannot iterate over a string. The partial now normalizes: a slice is used as-is, a string is split on commas with each value trimmed and blanks dropped.

filter now implies a data table. Setting it alone adds the data-table class even with sortable, paginate and searchable all off. That is safe — in simple-datatables v10.2.0 options.searchable gates only the search input and a wrapper class, not the public search() method. Filtering therefore always runs through the library's row model, where it composes with sorting, paging and free-text search, and an author never has to switch on sorting they don't want just to get filtering.

That makes the module's DOM-fallback branch unreachable, so gethinode/mod-simple-datatables#278 deletes it — along with the bug it carried on wrapped tables.

A build warning replaces a silent failure. A data table needs the module's JavaScript and its per-page stylesheet, and Hinode loads an optional module's assets only for pages listing it in frontmatter. Omit modules: ["simple-datatables"] and the table silently did nothing — true of sortable / paginate / searchable all along. The shortcode now says so at build time. It lives in the shortcode rather than the partial because only there is .Page reliably the content page; a Bookshop block resolves it to the rendering template's page, which would warn about pages holding no table at all.

The i18n keys did not exist. tableFilterLabel and tableFilterAll were absent from every language file — the partial only rendered because it passes a | default to T. Added across all eight languages.

Bugs found along the way

utilities/AddModule.html destroyed the dependency list it was meant to append to. It used complement, which returns the elements of the last collection absent from the others, and then set the scratch to that result — replacing the list instead of appending, and wiping it entirely when re-adding an entry already present. It also ignored the page argument callers hand it, using Hugo's bare global page; mod-blocks' list component and mod-mermaid both pass one and were silently broken by this.

data-filter-id changed on every build. It was md5(delimit (slice . now) "-") — the now meant any page with a filtered table emitted different HTML each time, defeating output diffing and CDN caching. It is now a deterministic per-page counter in page.Store. (page.Store was empirically confirmed to survive an embedded RenderString, which is what {{< example >}} does; .Ordinal was confirmed not to.)

filter-col="0" was silently coerced to 1. A redundant | default 1 met Hugo's default, which treats 0 as empty — so column 0 could never be filtered.

Verification

Driven in a real browser, with zero uncaught console exceptions: clicking a category filters the rows; the pager recounts to the filtered set (2 pages → 1), proving the filter reaches the row model rather than merely hiding DOM rows; sorting survives a filter; and on a wrapped table below the breakpoint the filtered-out records vanish entirely while every remaining record keeps both its data row and its description row — the old fallback bug is gone. exampleSite/content/en/table-demo.md carries the fixture tables.

Follow-up, not in this PR

.Ordinal-derived DOM ids collide inside {{< example >}}: example.html re-renders its inner content with .Page.RenderString, and .Ordinal restarts at 0 in that sub-render. Reproduced — a page with two example blocks each holding an accordion emits accordion-0 for both. It affects 12 shortcodes across hinode, mod-lottie and mod-leaflet, and hits the component docs hardest. The page.Store counter landed here is the ready-made replacement.

🤖 Generated with Claude Code

markdumay and others added 11 commits July 14, 2026 13:45
The filter has never worked from Markdown: the shortcode does not forward its
arguments, a string value is never split into a slice, its behaviour lives in an
optional module, the DOM fallback breaks on wrapped tables, and its i18n keys do
not exist.

Makes the filter a data-table feature: filter implies a data table, so it works
standalone, and the DOM fallback is deleted along with its bug.
Six tasks across hinode and mod-simple-datatables, each ending in an
independently verifiable deliverable.
tableFilterLabel and tableFilterAll were called by the table partial but existed
in no language file, so they were untranslatable and emitted i18n warnings. The
partial only rendered because it passes a default to T.

Also records in the table structure that filtering, like sorting, paging and
searching, needs the simple-datatables module.
AddModule used complement to add a module to the page's dependencies scratch,
but complement returns the elements of the LAST collection absent from the
others - so it replaced the list with the single module being added, and wiped
it entirely when re-adding one already present. Append and deduplicate instead,
matching how head.html and scripts.html merge the same scratch.
The shortcode never forwarded filter or filter-col, so a filter declared in
Markdown validated cleanly and then did nothing. Forward both, and split a
comma-separated string into a slice: a shortcode parameter is always a string,
InitArgs validates a kind without casting, and range cannot iterate a string.

A filtered table is now always a data table, even with sorting, paging and
search all off - filtering runs through simple-datatables' row model, so it
composes with them, and searchable gates only the search input, not the search
method. An author no longer has to enable sorting merely to get filtering.

Drops data-filter-container, which only the DOM fallback ever read.
Replace the md5(page + now)-based filter id with a sequential counter
held in page.Store. now() was evaluated per call, so a filtered
table's data-filter-id changed on every build even when the page
content was unchanged, defeating output diffing and CDN caching. The
id only needs to be unique within a page, so a page-scoped ordinal
(table-filter-1, table-filter-2, ...) is sufficient and stable.
A sortable, paginated, searchable or filtered table needs simple-datatables'
JavaScript and its per-page stylesheet, and Hinode loads an optional module's
assets only for pages listing it in frontmatter. Omitting the key produced a
table that silently did nothing.

The opt-in cannot be added at render time: head.html emits the stylesheet before
the content renders, so a dependency declared by a shortcode would attach the
script without the styles. Warn at build time instead.
AddModule.html ignored its `page` argument and always wrote dependencies to
Hugo's bare global `page`. In a Bookshop-block render that global can resolve
to the wrong page, so the dependency landed on the wrong page's Scratch. Now
an explicitly-passed page is used, falling back to the global `page` when the
caller omits it, matching how mod-blocks' list component already calls it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Read `filter-col` as-is in assets/table.html: mod-utils already defaults it to 1, and piping it
  through Hugo's `default` rewrote an explicit `0` (an empty value to `default`) back to 1, so
  column 0 could never be filtered
- Move the missing-module warning from the partial to the shortcode: `.Page` is the content page
  only in a shortcode, while a Bookshop block resolves the partial's `page` argument to the
  rendering template's page - which warned about pages holding no table at all
- Pass `details` as a slice, so LogMsg no longer appends a stray `%!s(<nil>)` line
- Correct the rationale comment: a render-time dependency is unreliable because it is
  indeterminate (see assets/lightbox.html), not because head.html runs before the content

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Treat `filter` as present only if it has a non-comma, non-whitespace
  character, matching how assets/table.html normalizes it (split on
  commas, trim, drop blanks). A separator-only value like `filter=","`
  no longer triggers a spurious missing-module warning.
- Correct the rationale comment for warning instead of auto-declaring
  the module dependency: state the actual reason (auto-declaring would
  tie the shortcode to head.html's eager evaluation of `.Page.Content`,
  an implementation detail, not a contract) instead of the disproven
  claim about Page.Store timing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy Preview for gethinode-demo ready!

Name Link
🔨 Latest commit b1e299b
🔍 Latest deploy log https://app.netlify.com/projects/gethinode-demo/deploys/6a57079072686c0008b31012
😎 Deploy Preview https://deploy-preview-2024--gethinode-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

markdumay and others added 7 commits July 14, 2026 17:46
Four components in mod-blocks build a DOM id from md5(... now ...), so the id
changes on every build and any page carrying one emits different HTML each time.
The table shortcode had the same bug; its fix introduced a deterministic
page.Store counter. Extract that into utilities/UniqueID.html and apply it to
all four remaining sites.

A spike confirmed page.Store is stable and unique in a Bookshop block render,
which was the design's key unknown.
No gethinode module declares a dependency on Hinode, so a partial there is
reachable only because the site merges Hinode's layouts - an undeclared inverted
dependency that breaks each module's own exampleSite build and would block the
.Ordinal follow-up, which must reach mod-lottie and mod-leaflet.

Moves AddModule.html to mod-utils for the same reason: mod-blocks calls it
without declaring Hinode. Callers are unaffected; Hugo resolves partials by path.
Hinode's exampleSite pins mod-blocks, so merging Hinode first would deploy a demo
site still emitting dropdown-panel-<md5>. Sequence is mod-utils, then mod-blocks,
then Hinode.
Six tasks across mod-utils, mod-blocks and hinode. Merge order is strictly
sequential: mod-utils, then mod-blocks, then hinode.
The table carried its own page.Store counter, duplicating what mod-utils'
UniqueID now provides. Same rendered id shape, one implementation.

Also drops Hinode's copy of AddModule.html, which has moved to mod-utils:
mod-blocks calls it without declaring a dependency on Hinode, so it belongs
where every module can legitimately reach it.
Reflects deterministic ids for panel, faq, nav, preview, and
testimonial-carousel once built against the unreleased mod-utils
UniqueID helper and mod-blocks call-site migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Regenerate hugo_stats.json against the UID-shaped ids produced by the
updated UniqueID.html helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@markdumay

Copy link
Copy Markdown
Collaborator Author

Added: adopt the shared UniqueID helper

This branch now also carries Hinode's part of the deterministic-element-id work (design + plan committed under docs/superpowers/):

  • The table's filter id moves from an inline page.Store counter to mod-utils' new UniqueID helper (feat(utilities): add UniqueID helper, adopt AddModule mod-utils#345) — same table-filter-<n> shape, one implementation instead of two.
  • Hinode's layouts/_partials/utilities/AddModule.html is deleted; it moved to mod-utils. No gethinode module declares a dependency on Hinode, yet mod-blocks' list component calls AddModule — so it belongs where every module can reach it. The only real caller in Hinode was that block; lightbox.html and the table shortcode merely mention it in comments.

Merge order — this PR goes last. It deletes its own AddModule.html, so it cannot build until go.mod points at a mod-utils release that provides the replacement. Sequence:

  1. feat(utilities): add UniqueID helper, adopt AddModule mod-utils#345 merges and releases (minor).
  2. fix(blocks): stop element ids changing on every build mod-blocks#159 bumps its mod-utils pin, merges and releases (patch).
  3. This PR bumps go.mod (mod-utils) and exampleSite/go.mod (mod-blocks), then merges.
  4. gethinode/gethinode.com#158 merges after its vendor bump.

Companion PRs: gethinode/mod-utils#345, gethinode/mod-blocks#159, gethinode/gethinode.com#158.

mod-utils v6.5.0 provides the UniqueID helper the table now calls and the
AddModule partial this branch deleted from Hinode, so the build resolves both
from a release rather than a local override. mod-blocks v2.1.3 carries the four
components' conversion to the helper, so the deployed exampleSite renders their
ids in the deterministic -UID- form instead of a per-build hash.
…lter

# Conflicts:
#	exampleSite/hugo_stats.json
@markdumay markdumay enabled auto-merge July 15, 2026 04:11
@markdumay markdumay merged commit 2e33181 into main Jul 15, 2026
16 checks passed
@markdumay

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 3.2.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@markdumay markdumay deleted the fix/table-category-filter branch July 15, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant