Skip to content

Widgets: let widget.json declare example - #85240

Draft
retrofox wants to merge 7 commits into
trunkfrom
update/widget-json-example
Draft

retrofox wants to merge 7 commits into
trunkfrom
update/widget-json-example

Conversation

@retrofox

@retrofox retrofox commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What

widget.json can now declare example, with the shape of the runtime field ({ "attributes": { … } }), and it travels through the build manifest, the server registry, the REST record and useWidgetTypes. Hello World moves its example there and drops its metadata module.

Closes #85170

Why

example is what hosts preview a widget type with and what a new instance starts from, and it was the last field only a widget.ts could declare. Most examples are plain data, so widgets kept a module for one JSON literal.

How

packages/wp-build/lib/build.mjs toPhpExampleLiteral() copies example into build/widgets/registry.php next to attributes, as null unless it carries a non-empty attributes object.

lib/experimental/dashboard-widgets/widget-types.php gutenberg_sanitize_widget_example() gates registration: attributes stays a map of attribute names to JSON data (checked recursively by gutenberg_is_widget_json_value()), other keys and non-serializable attributes drop, and an empty result becomes null. Example values are data, so nothing is translated.

lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php Exposes example and declares it in the schema as object|null. A nested empty object still reaches the client as [], as with block.json examples; the top-level map always encodes as an object.

packages/widget-primitives/src/hooks/use-widget-types.ts A record without a metadata module takes example from the record. With a module, the module's example wins and the record's stands in only when the module declares none. WidgetModuleRecord.example carries the wire form.

widgets/hello-world example moves into widget.json and widget.ts goes away.

docs/explanations/architecture/dashboard-widgets.md, packages/wp-build/README.md Document the field and the module-wins rule. The README also catches up with drift unrelated to example: icon and actions in the field list, the source extensions the build accepts, style.module.css, and name living in widget.json. The architecture doc feeds the System Architecture story.

Testing

  1. Run npm run build and check build/widgets/registry.php: core/hello-world carries 'example' => array( 'attributes' => array( 'message' => 'Hello World' ) ) and 'has_widget' => false.
  2. With the gutenberg-dashboard-widgets experiment on, GET /wp/v2/widget-modules/core/hello-world returns "example": { "attributes": { "message": "Hello World" } } and "widget_module": null.
  3. In the dashboard, add Hello World from the widget picker and open its settings: the Message field is prefilled with "Hello World". The render falls back to the same text when the attribute is empty, so the field is where the example shows.

Follow-ups

@retrofox retrofox added [Type] Enhancement A suggestion for improvement. [Package] wp-build /packages/wp-build [Feature] Dashboard WordPress admin dashboard, widget framework, and layout customization [Package] Widget primitives /packages/widget-primitives labels Oct 20, 2026
@retrofox retrofox self-assigned this Oct 20, 2026
wp-build copies it into the registry; the registration gate keeps JSON attributes
the module example wins; the record stands in when it declares none
the widget ships without a metadata module
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

📦 Bundle size

Size Change: +22 B (0%)

Total Size: 9.05 MB

📦 View Changed
Filename Size Change
build/modules/widget-primitives/index.min.js 2.36 kB +22 B (+0.94%)

40475fa Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 3539af2 trunk % Change
timeToFirstByte 42.2 ms +12.32% -5.21% 41.1 ms +12.53% -2.31% 2.68%
largestContentfulPaint 74 ms +2.7% -8.11% 74 ms +8.11% -8.11% 0%
lcpMinusTtfb 28.8 ms +13.37% -4.17% 28.65 ms +18.67% -4.19% 0.52%
wpBeforeTemplate 21.78 ms +19.01% -1.42% 21.52 ms +23% -0.88% 1.21%
wpTemplate 16.1 ms +5.09% -5.53% 16.11 ms +3.79% -5.46% -0.06%
wpTotal 38.92 ms +11.64% -5.22% 37.86 ms +11.6% -2.32% 2.8%
wpMemoryUsage 7.65 MB +0% -0% 7.61 MB +0% -0% 0.53%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 3539af2 trunk % Change
timeToFirstByte 31.45 ms +8.74% -2.23% 34.45 ms +6.68% -1.74% -8.71%
largestContentfulPaint 70 ms +5.71% -2.86% 80 ms +0% -5% -12.5%
lcpMinusTtfb 37.75 ms +7.28% -1.32% 42.55 ms +6.35% -1.18% -11.28%
wpBeforeTemplate 19.26 ms +8.93% -1.25% 18.98 ms +5.48% -1% 1.48%
wpTemplate 9.42 ms +5.41% -1.59% 12.42 ms +5.88% -1.61% -24.15%
wpTotal 28.99 ms +8.87% -1.79% 31.72 ms +7.35% -1.77% -8.61%
wpMemoryUsage 6.23 MB +0% -0% 6.30 MB +0% -0% -1.11%
wpDbQueries 9 +0% -0% 13 +0% -0% -30.77%

media-processing

Metric 3539af2 trunk % Change
mediaProcessingJpeg 322.44 ms +0.78% -0.84% 320.78 ms +1.18% -0.51% 0.52%
mediaProcessingAvif 4809.58 ms +0.18% -0.15% 4833.13 ms +0.32% -0.08% -0.49%
mediaProcessingJpegToAvif 3361.85 ms +0.32% -0.36% 3367.48 ms +0.19% -0.19% -0.17%

media-upload

Metric 3539af2 trunk % Change
jpegUploadProcessing 1407.53 ms +0.91% -1.05% 1396.01 ms +0.96% -0.64% 0.83%
pngUploadProcessing 175.96 ms +3.92% -1.84% 185 ms +2.23% -3.23% -4.89%
largeJpegUploadProcessing 1401.84 ms +0.69% -1.07% 1395.18 ms +1.7% -0.58% 0.48%
multipleImageUploadProcessing 1475.99 ms +1.61% -1.31% 1478.46 ms +0.31% -1.56% -0.17%

post-editor

Metric 3539af2 trunk % Change
serverResponse 327.61 ms +2.89% -9.03% 336.85 ms +0.77% -1.33% -2.74%
firstPaint 327.29 ms +28.3% -47.36% 415.16 ms +2.39% -13.86% -21.17%
domContentLoaded 933.46 ms +2.27% -3.57% 916.71 ms +2.83% -0.27% 1.83%
loaded 934.59 ms +2.26% -3.57% 917.68 ms +2.84% -0.25% 1.84%
firstContentfulPaint 412.31 ms +4.22% -10.21% 416.31 ms +2.34% -2.94% -0.96%
firstBlock 2745.97 ms +4.13% -1.54% 2715.17 ms +2.65% -0.47% 1.13%
type 17.2 ms +2.85% -5.17% 16.61 ms +4.46% -2.23% 3.55%
typeWithoutInspector 16.35 ms +1.16% -5.2% 16.69 ms +8.93% -4.37% -2.04%
typeWithTopToolbar 20.72 ms +5.36% -3.43% 22.03 ms +4.27% -4.81% -5.95%
typeContainer 7.6 ms +1.58% -4.87% 7.39 ms +2.71% -4.87% 2.84%
focus 101.15 ms +7.18% -3.91% 93.67 ms +4.13% -5.47% 7.99%
firstFocus 228.01 ms +0% -0% 180.28 ms +0% -0% 26.48%
selectAll 481.82 ms +0.48% -0.95% 450.53 ms +2.23% -9.03% 6.95%
listViewOpen 64.73 ms +7.34% -2.98% 63.05 ms +9.94% -8.34% 2.66%
inserterOpen 36.48 ms +4.47% -8.53% 33.9 ms +6.31% -7.05% 7.61%
inserterHover 10.74 ms +2.98% -7.54% 10.29 ms +11.56% -7.29% 4.37%
inserterSearch 7.46 ms +12.47% -9.12% 7.27 ms +7.98% -4.13% 2.61%
loadPatterns 521.59 ms +2.92% -4.36% 500.54 ms +3.81% -2.68% 4.21%
wpTotal 319.16 ms +2.97% -9.31% 327.98 ms +0.9% -1.26% -2.69%
wpMemoryUsage 14.69 MB +0% -0% 14.65 MB +0% -0% 0.29%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 3539af2 trunk % Change
serverResponse 475.66 ms +10.56% -3.08% 519.58 ms +0.54% -8.01% -8.45%
firstPaint 280.16 ms +5.6% -2.02% 298.35 ms +12.14% -24.99% -6.1%
domContentLoaded 1299.2 ms +3.32% -2.01% 1277.61 ms +1.03% -0.64% 1.69%
loaded 1300.45 ms +3.34% -2.02% 1278.74 ms +1.02% -0.64% 1.7%
firstContentfulPaint 536.63 ms +1.68% -3.04% 525.73 ms +1.2% -1.32% 2.07%
firstBlock 4817.58 ms +0.69% -0.89% 4737.08 ms +0.82% -0.56% 1.7%
type 18.77 ms +5.43% -2.77% 18.67 ms +7.39% -0.91% 0.54%
navigate 114.76 ms +9.65% -6.41% 114.92 ms +25.41% -3.61% -0.14%
loadPatterns 1223.27 ms +2.62% -5.5% 1229.49 ms +1.62% -0.22% -0.51%
loadPages 1010.56 ms +4.81% -1.46% 993.99 ms +4.3% -3.85% 1.67%
wpTotal 466.2 ms +10.88% -3.08% 510.11 ms +0.57% -8.17% -8.61%
wpMemoryUsage 12.71 MB +0% -0% 12.67 MB +0% -0% 0.33%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

40475fa Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
drags a multi-selection from anywhere inside it in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:844:3
refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 1
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=223"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/223", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/222", "id": 222, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-223", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-21T16:06:27", "date_gmt": "2026-10-21T16:06:27", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/b047fa9c-0b93-4565-acdb-77b504bdd490.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/b047fa9c-0b93-4565-acdb-77b504bdd490.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "b047fa9c-0b93-4565-acdb-77b504bdd490.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/b047fa9c-0b93-4565-acdb-77b504bdd490.png"}, "id": 223, "link": "http://localhost:8889/?attachment_id=223", "media_details": {"file": "2026/10/b047fa9c-0b93-4565-acdb-77b504bdd490.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-21T16:06:27", "modified_gmt": "2026-10-21T16:06:27", "ping_status": "closed", "post": 222, "slug": "b047fa9c-0b93-4565-acdb-77b504bdd490", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/b047fa9c-0b93-4565-acdb-77b504bdd490.png", "status": "inherit", "template": "", "title": {"rendered": "b047fa9c-0b93-4565-acdb-77b504bdd490"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45
should update the URL from the last navigation if only varies in the URL fragment in /test/e2e/specs/interactivity/router-navigate.spec.ts, passed after 1 failed attempt.
Error: expect(locator).toHaveText(expected) failed

Locator:  getByTestId('title')
Expected: "Link 1"
Received: "Main"
Timeout:  5000ms

Call log:
  - Expect "toHaveText" getByTestId('title') with timeout 5000ms
  - waiting for getByTestId('title')
    14 × locator resolved to <h2 data-testid="title">Main</h2>
       - unexpected value "Main"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-navigate.spec.ts:160:25

40475fa Run

@retrofox
retrofox force-pushed the update/widget-json-example branch from efe2597 to e2c3df7 Compare October 20, 2026 22:52
icon and actions fields, source extensions, style module, name in widget.json, module-less records
the type carries the manifest's declarative metadata, not only name and module handles

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Dashboard WordPress admin dashboard, widget framework, and layout customization [Package] Widget primitives /packages/widget-primitives [Package] wp-build /packages/wp-build [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widgets: let widget.json declare example

1 participant