mirror of
https://github.com/discourse/discourse.git
synced 2026-03-03 20:15:55 +08:00
This commit introduces the Blocks API, a declarative system for composing
validated, conditionally rendered UI layouts.
Blocks are Glimmer components with typed argument schemas, conditional
rendering rules, and outlet restrictions — all validated at registration
time. The system provides centralized registries for blocks, outlets,
and condition types, with a two-phase lifecycle that freezes registries
after initialization. Container blocks enable hierarchical layouts, and
a built-in condition system supports route matching, user state, site
settings, viewport breakpoints, and outlet arg inspection — all
composable with AND/OR/NOT combinators.
## Plugin API
Four methods are added to the plugin API.
`api.registerBlock(BlockClass)` registers a block component for use in
layouts, supporting both direct class registration and lazy loading via
factory functions (`api.registerBlock("name", () => import(...))`).
`api.renderBlocks(outletName, layout)` defines the block layout for an
outlet, where each entry is a `LayoutEntry` describing the block, its
args, class names, children, conditions, and container args.
`api.registerBlockOutlet(name, options)` allows plugins and themes to
define their own named outlets.
`api.registerBlockConditionType(ConditionClass)` registers a custom
condition type.
Registration methods must be called in pre-initializers (before the
registry freeze), while `renderBlocks` must be called in
api-initializers (after the freeze).
## The `@block` Decorator
The `@block(name, options)` decorator transforms a Glimmer component
into a block. It supports typed argument schemas with validation
constraints (`min`, `max`, `minLength`, `maxLength`, `pattern`, `enum`,
`itemEnum`, `integer`) for types `string`, `number`, `boolean`, `array`,
`object`, and `any`. Cross-argument constraints like `atLeastOne`,
`exactlyOne`, `allOrNone`, `atMostOne`, and `requires` enforce
relationships between arguments. A `validate(args)` function enables
custom validation logic.
Blocks can restrict which outlets they render in using `allowedOutlets`
and `deniedOutlets` with glob patterns.
Container blocks (`container: true`) can nest children and define a
`childArgs` schema for args passed from children to the parent.
Namespacing is enforced: core blocks use `block-name`, plugins use
`plugin-name:block-name`, and themes use `theme:theme-name:block-name`.
Blocks can only render inside `<BlockOutlet>` components or as
authorized children of container blocks — direct template usage throws
an error.
## `<BlockOutlet>` Component
`<BlockOutlet>` is the root rendering component and is itself a
container block. It accepts `@name` (the outlet identifier),
`@outletArgs` (arguments passed to all blocks in the outlet), and
`@deprecatedArgs` (args that trigger deprecation warnings on access). It
provides `<:before>`, `<:after>`, and `<:error>` named block slots.
Four core outlets are defined: `hero-blocks`, `homepage-blocks`,
`main-outlet-blocks`, and `sidebar-blocks`. The application template
places `<BlockOutlet>` components for `hero-blocks` and
`main-outlet-blocks`, both hidden on admin routes.
## Conditions
Five built-in condition types handle the most common rendering
scenarios.
The `route` condition matches URL patterns using glob syntax, semantic
page types (`CATEGORY_PAGES`, `TAG_PAGES`, `DISCOVERY_PAGES`,
`HOMEPAGE`, `TOPIC_PAGES`, `USER_PAGES`, `ADMIN_PAGES`, `GROUP_PAGES`,
`TOP_MENU`), route params, and query params.
The `user` condition checks login status, trust level,
admin/moderator/staff status, and group membership.
The `setting` condition evaluates site settings with operators like
`enabled`, `equals`, `includes`, `contains`, and `containsAny`.
The `viewport` condition matches breakpoints (`sm`, `md`, `lg`, `xl`,
`2xl`) and touch capability.
The `outlet-arg` condition matches outlet argument values using
dot-notation paths with value matchers supporting primitives, arrays,
regex, `not`, and `any`.
Conditions are composable through combinators: arrays represent AND
logic, `{ any: [...] }` represents OR logic, and `{ not: {...} }`
represents NOT logic. Parameters within conditions (`params`,
`queryParams`) support the same combinator syntax.
## The `@blockCondition` Decorator
Custom conditions extend `BlockCondition` and use the
`@blockCondition(config)` decorator. The config accepts a `type` name, a
`sourceType` for resolving external data (`"none"`, `"outletArgs"`, or
`"object"` for sources like theme settings), an argument schema with the
same validation as block args, cross-arg constraints, and a custom
validation function.
## Built-in Container Blocks
Two container blocks are provided. `group` renders all its children in
sequence, acting as a simple grouping mechanism. `head` renders only the
first child whose conditions pass, implementing an if/else-if/else
fallback pattern.
## Two-Phase Initialization
Registration happens in pre-initializers. The `freeze-block-registry`
initializer then registers built-in blocks and core condition types, and
freezes all three registries (blocks, outlets, conditions) to prevent
further modifications. Layout rendering via `api.renderBlocks()` happens
in api-initializers after the freeze, ensuring all blocks and conditions
are available when layouts are validated.
## Developer Tooling
The dev tools integration provides a visual overlay that renders block
boundaries with hover tooltips showing the block name, conditions, and
arguments. Ghost blocks appear as dashed placeholders for hidden blocks,
showing why they're hidden (failed conditions, no visible children, or
hidden by `head`). Console logging outputs hierarchical condition
evaluation with pass/fail icons, color coding, resolved values, and type
mismatch hints. Outlet info badges display the outlet name, block count,
and arguments.
## Testing Utilities
`withTestBlockRegistration(callback)` and
`withTestConditionRegistration(callback)` temporarily unfreeze their
respective registries for test registration. Block query helpers
(`hasBlock`, `getBlockEntry`, `resolveBlock`, `tryResolveBlock`,
`isBlockResolved`) enable assertions on registry state.
`validateConditions(spec)` validates condition specs in tests.
`setupGhostCapture()` captures ghost block data for assertions on which
blocks were skipped and why.
## Test Coverage
Unit tests cover all five condition types, argument validation,
constraint validation, layout validation, the block/outlet/condition
registries, string similarity, outlet args, value matching, URL
matching, page definitions, debug logging, and console formatting.
Integration tests cover `<BlockOutlet>` rendering, container behavior
(`group`, `head`), condition evaluation, and layout wrappers. System
tests validate conditional rendering across SPA navigation and the dev
tools overlay.
---------
Co-authored-by: David Taylor <david@taylorhq.com>
12 lines
574 B
Text
12 lines
574 B
Text
# Dev Experience (Block API, Dev Tools, Registry, Plugin API)
|
|
/frontend/discourse/app/blocks/ @discourse/dev-xp
|
|
/frontend/discourse/app/lib/blocks/ @discourse/dev-xp
|
|
/frontend/discourse/app/services/blocks.js @discourse/dev-xp
|
|
/frontend/discourse/app/static/dev-tools/ @discourse/dev-xp
|
|
/frontend/discourse/app/lib/registry/ @discourse/dev-xp
|
|
/frontend/discourse/app/lib/plugin-api.gjs @discourse/dev-xp
|
|
|
|
# Migrations tooling
|
|
/migrations/ @discourse/migrations-tooling
|
|
/script/bulk_import/ @discourse/migrations-tooling
|
|
/script/import_scripts/ @discourse/migrations-tooling
|