Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/caseyjmilne/gateway/archive/refs/heads/claude%2Fwordpress-datatable-block-pl580v.zipA WordPress plugin for custom Gutenberg blocks, starting with a Data Table block: a sortable, searchable grid of posts for any registered post type, powered by DataTables.
Requirements
- WordPress 6.3+
- PHP 8.2+ (raised from 7.4 by the vendored
illuminate/databasepackage -- see "Laravel Models (Illuminate/Eloquent)" below) - Node.js 18+ (build tooling only -- not required at runtime)
Getting started
npm install
npm run build
Compiled assets are committed to the repo (blocks/*/build/), so the plugin
works immediately after activation even without running a build. Run
npm run build after changing anything under blocks/*/src/.
Available scripts:
| Script | Purpose |
|---|---|
npm run build |
Clean + production build of every block. |
npm run start |
Watch mode for development. |
npm run clean |
Remove all blocks/*/build output. |
npm run format |
Format block source with wp-scripts format. |
npm run lint:js |
Lint block JS. |
npm run lint:css |
Lint block styles. |
Architecture
gateway.php Plugin bootstrap: constants + boots Block_Loader, Column_Registry, Columns_REST_Controller, Facet_Query
includes/
class-block-loader.php Scans /blocks and register_block_type()'s every block found
class-column-registry.php Discovers columns (core fields + meta/ACF) for a post type; renders cell values
class-columns-rest-controller.php GET /gateway/v1/columns/<post_type> (the block editor's column/facet picker)
class-facet-query.php Applies validated facets to a WP_Query; distinct values for select/checkboxes facets
blocks/
shared/ Cross-block JS, NOT a block itself (no block.json -- webpack's glob skips it)
datatable.js Shared DataTables init/destroy helpers (jQuery + datatables.net-dt)
dom.js Pure DOM helpers (getColumnIndexByKey) -- no jQuery/DataTables dependency
wait-for-datatable.js "Find the sibling table, wait for a DataTable, hide a native widget" (jQuery only)
use-live-datatable-sync.js Editor-only: keeps a live preview wired to the (possibly-replaced) sibling DataTable instance
use-available-columns.js Fetches the field list for a post type (shared by both blocks below)
datatable/
block.json Block metadata, attributes, providesContext, asset + render wiring
render.php PHP render callback: renders the Facets position (a core/group Row, or whatever a site owner put there) then header/body/footer, in that order
src/
index.js Editor registration (editorScript)
edit.js Editor UI: InspectorControls + one self-healing InnerBlocks skeleton
save.js Persists InnerBlocks content only -- render.php doesn't use it
view.js Front-end entry (viewScript): finds tables, inits DataTables
style.scss Shared styles (front end + editor)
controls/
post-type-control.js Reusable "Post Type" SelectControl
limit-control.js "Limit" numeric field
page-size-control.js "Page Size" numeric field
columns-panel.js Renders the two below for the selected columns
facets-panel.js Renders the two below for the selected facets
available-columns-list.js Click-to-toggle field selection (shared by columns + facets)
column-config-table.js Drag-to-reorder + click-to-toggle-sortable table
facet-config-table.js Drag-to-reorder table; "Default" button opens a modal with compare/value
facet-compare-options.js Facet comparison operator list (=, !=, >, ...)
hooks/
use-reconcile-field-list.js Drops selections no longer valid for the current post type
use-required-inner-blocks.js React hook: name-based self-heal for the 4 required children (see below)
build/ Compiled output (generated, do not hand-edit)
datatable-header/
block.json Block metadata, parent restricting it to gateway/datatable
render.php PHP render callback: echoes $content as-is (see "Header/body/footer" below)
src/
index.js Editor registration
edit.js Editor UI: an InnerBlocks area restricted to gateway/datatable-page-size + gateway/datatable-search
save.js Persists InnerBlocks content -- this block's own wrapper + classes
view.js No front-end behavior -- exists only so style.scss gets built
style.scss Layout for the page-size/search children (flex row, spread to opposite ends)
build/ Compiled output (generated, do not hand-edit)
datatable-body/
block.json Block metadata, attributes (mirrored context, SSR-preview fallback only), usesContext
render.php PHP render callback: the actual <table> -- headings + rows -- moved here from datatable/render.php
src/
index.js Editor registration
edit.js Editor UI: mirrors context into attributes, then <ServerSideRender> + DataTables init
save.js Persists nothing (leaf, dynamic block)
hooks/
use-datatable-init.js React hook: (re)inits DataTables against an async-rendered container
build/ Compiled output (generated, do not hand-edit)
datatable-footer/
block.json Block metadata, parent restricting it to gateway/datatable
render.php PHP render callback: echoes $content as-is (see "Header/body/footer" below)
src/
index.js Editor registration
edit.js Editor UI: an InnerBlocks area restricted to gateway/pagination + gateway/datatable-results
save.js Persists InnerBlocks content -- this block's own wrapper + classes
view.js No front-end behavior -- exists only so style.scss gets built
style.scss Layout for the pagination/results children
build/ Compiled output (generated, do not hand-edit)
facet/
block.json Block metadata, ancestor + usesContext restricting it to gateway/datatable
render.php PHP render callback: the input/select/checkboxes control
src/
index.js Editor registration
edit.js Editor UI: pick a facet + a UI type, with validity warnings
view.js Front-end entry: hooks the control into the sibling DataTable instance
style.scss Facet control styles
controls/
facet-key-control.js "Facet" select, from the parent's configured facets (context)
ui-type-control.js "UI Type" select: input / select / checkboxes
compare-control.js "Compare" select (Contains/Equals), Input UI type only
build/ Compiled output (generated, do not hand-edit)
datatable-page-size/
block.json Block metadata, parent restricting it to gateway/datatable-header
render.php PHP render callback: an empty, disabled <select> skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "A live editor preview" below
view.js Front-end entry: finds+waits for the table, hands off to attach-page-size.js
attach-page-size.js Shared option-populating/wiring logic, used by both edit.js and view.js
style.scss Page Size control styles
build/ Compiled output (generated, do not hand-edit)
datatable-search/
block.json Block metadata, parent restricting it to gateway/datatable-header
render.php PHP render callback: a disabled search <input> skeleton
src/
index.js Editor registration
edit.js Editor UI: a static preview (no settings to configure)
view.js Front-end entry: drives the sibling DataTable's search() API
style.scss Search control styles
build/ Compiled output (generated, do not hand-edit)
pagination/
block.json Block metadata, parent restricting it to gateway/datatable-footer
render.php PHP render callback: an empty Prev/Next + page-number skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "Pagination and Results" below
view.js Front-end entry: finds+waits for the table, hands off to attach-pagination.js
attach-pagination.js Shared button-building/wiring logic, used by both edit.js and view.js
style.scss Pagination control styles
build/ Compiled output (generated, do not hand-edit)
datatable-results/
block.json Block metadata, parent restricting it to gateway/datatable-footer
render.php PHP render callback: an empty "Showing X to Y of Z entries" skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "A live editor preview" below
view.js Front-end entry: finds+waits for the table, hands off to attach-results.js
attach-results.js Shared info-text-building logic, used by both edit.js and view.js
style.scss Results text styles
build/ Compiled output (generated, do not hand-edit)
Block discovery / autoloading
Block_Loader::register_blocks() (hooked on init) globs every directory
under /blocks and calls register_block_type() on each one that has a
block.json. Nothing in PHP needs to change to add a new block -- drop a new
blocks/<slug>/ directory in place (with its own block.json, src/, and
optional render.php) and it's picked up automatically. webpack.config.js
does the equivalent for the build: it globs blocks/*/block.json and wires
up index.js/view.js entries per block automatically, compiling each into
that block's own build/ directory.
A dedicated "Gateway" block category, for the four top-level blocks
Block_Loader::register_category() (on block_categories_all, alongside
register_blocks()'s own init hook) registers one new inserter
category, gateway/"Gateway" -- prepended ahead of core's own categories
(Text/Media/Design/etc.), rather than appended after them, so a plugin
whose main purpose IS these blocks gets surfaced first, not buried below
groupings most of this plugin's own users have no reason to browse.
Only the four block.json files a site owner actually starts a layout
with set their own category to "gateway": gateway/data-cards,
gateway/data-display, gateway/datatable, gateway/single-record --
recognizable as exactly the four with neither a parent nor an
ancestor restriction, the only ones ever offered from the top-level
inserter to begin with. Every other block this plugin ships (every
datatable-*/data-cards-* child, gateway/card-field-text/-number/
-image, gateway/related-items, gateway/facet/gateway/card-facet,
gateway/pagination) deliberately keeps its existing "widgets"
category untouched -- none of them are ever reachable from that
top-level list regardless of which category they claim (a parent
-restricted block only ever appears nested inside its one named parent;
an ancestor-restricted one only once already inside a matching
ancestor), so grouping them under "Gateway" too would just be dead
weight in a category no one browses looking for them.
Verified with a new standalone PHP smoke test (register_category()
returns core's own categories untouched plus exactly one new,
well-formed entry, prepended ahead of them) plus a second pass reading
every real blocks/*/block.json on disk and confirming, block by block,
that a top-level block (no parent/ancestor) is in "gateway" and a
child block (either restriction) is not -- alongside the full existing
regression suite. No build step needed: category is plain block.json
metadata register_block_type() reads directly, never something
webpack/@wordpress/scripts compiles into a block's own build/
output.
Dynamic blocks, server-rendered
Every block in this plugin has no meaningful client-side save() markup
(each returns null, or -- for the three InnerBlocks wrappers -- persists
only the child delimiter comments). All markup -- on both the front end and
in the block editor -- comes from each block's own render.php via
block.json's "render" field:
- Front end: WordPress calls
render.phpwhen the block is rendered in a post/page -- normal recursive block rendering, context and all. - Editor: the one block whose editor preview needs to show real,
server-computed content --
gateway/datatable-body, the actual<table>-- renders<ServerSideRender block="gateway/datatable-body" />, which calls that samerender.phpthrough theblock-rendererREST endpoint. This guarantees the editor preview is never out of sync with what actually ships to the front end. (gateway/datatableitself doesn't use<ServerSideRender>at all any more -- see "Header/body/footer" below for why that moved.)
Initializing DataTables inside the Gutenberg editor
This is the part worth calling out explicitly, since it's easy to get wrong:
gateway/datatable-body'sedit.jswraps its<ServerSideRender>output in aref'd container and passes that ref touseDataTableInit()(hooks/use-datatable-init.js, living in this block now -- see "Header/body/footer" below).- Because
<ServerSideRender>fetches and swaps in its markup outside React's normal render cycle, there's no prop change to key auseEffectoff of. The hook instead uses aMutationObserveron the container to detect the moment a<table class="gateway-datatable">actually lands in the DOM (or changes, e.g. after switching Post Type), and (re)initializes DataTables against it via the shared helpers inshared/datatable.js. - Editor iframe: since WP 5.9, the block canvas renders inside an `
This README is longer than the copy stored here. Read the rest on GitHub →