WP Manifestindependent plugin directory
manifest / editor / gateway

Gateway

Second attempt at building a structured data plugin for WordPress with Laravel model integration.

★ 0stars

Install

Download source zip No release zip yet

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.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

A 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/database package -- 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.php when 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 same render.php through the block-renderer REST endpoint. This guarantees the editor preview is never out of sync with what actually ships to the front end. (gateway/datatable itself 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:

  1. gateway/datatable-body's edit.js wraps its <ServerSideRender> output in a ref'd container and passes that ref to useDataTableInit() (hooks/use-datatable-init.js, living in this block now -- see "Header/body/footer" below).
  2. Because <ServerSideRender> fetches and swaps in its markup outside React's normal render cycle, there's no prop change to key a useEffect off of. The hook instead uses a MutationObserver on 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 in shared/datatable.js.
  3. 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 →