These pages document @evanion/astro-widget 0.3.4, the version on npm.
API reference
@evanion/astro-widget publishes one component, Widgets.astro, and re-exports
nine names from the widget core, @evanion/widget, which it pins to one exact
version. Each re-exported name links to the widget core’s entry for it.
Widgets.astro
Widgets.astro renders a list of items, each through the component its type
names in the registry, in the order the list gives them. It is published as
.astro source under @evanion/astro-widget/components/Widgets.astro, because
only Astro’s own compiler, running in your project, can compile it. Two items
from a Baize listing page render through a registry of two widgets:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import Widgets from '@evanion/astro-widget/components/Widgets.astro';
import { registry } from '../examples/src/registry';
const items = [
{
id: 'header',
type: 'listing-header',
props: { title: 'Brass: Birmingham', players: '2-4' },
},
{ id: 'price', type: 'price-box', props: { price: '649 kr' } },
];
const container = await AstroContainer.create();
const html = await container.renderToString(Widgets, {
props: { items, registry },
});
html; // -> '<header><h1>Brass: Birmingham</h1><p>2-4 players</p></header><p class="price">649 kr</p>'| Prop | Type | Meaning |
|---|---|---|
items | AnyWidgetItem[] | the items to render |
registry | WidgetRegistry | each item type mapped to the .astro component for it |
ctx | Record<string, unknown> | optional; page-level data passed to every widget |
chrome | { item?: unknown } | optional; item is a component wrapped around every widget |
Each widget receives the item’s props spread as its own props, plus id,
ctx and children, which are set after the spread and win over a prop of the
same name. It never receives meta. An items that is present and is not an
array renders nothing and warns in development.
ctx
ctx reaches every widget as a prop of that name, so page-level data such as
the store is written once on the page. An Astro component has no render-time
context to read from, so ctx is a prop and there is no provider.
@evanion/react-widget passes the same prop under the same name.
Page data, wrappers and nested items
renders a widget that reads it.
chrome
chrome.item wraps every widget. It receives the item’s type, id and
meta, never its props, and it has to render <slot />, because an Astro
component that renders no slot drops the widget with no error and no warning.
@evanion/react-widget accepts a chrome.wrapper around the whole list.
astro-widget has none, so put <Widgets> inside the element you want around
the list.
Page data, wrappers and nested items
renders a chrome around a widget.
Nested items
Widgets.astro does not render an item’s children. It passes them to the
widget as a prop named children that holds the items as data, because an Astro
component takes rendered child content through <slot />. A widget that nests
renders its own Widgets over them, through a registry in a module of its own.
Page data, wrappers and nested items
renders a grid of game cards and says why that registry has its own module.
Unknown types
An item whose type is not an own key of the registry renders nothing, and the
rest of the list renders. Unless NODE_ENV is production, console.warn
prints ERROR_MESSAGES.UNKNOWN_WIDGET for the item. It
prints each distinct message once for the life of the process. Below, the
registry holds no answer-wall, so only the price renders:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import Widgets from '@evanion/astro-widget/components/Widgets.astro';
import { ERROR_MESSAGES } from '@evanion/astro-widget';
import { registry } from '../examples/src/registry';
const items = [
{ id: 'questions', type: 'answer-wall', props: {} },
{ id: 'price', type: 'price-box', props: { price: '649 kr' } },
];
const container = await AstroContainer.create();
const html = await container.renderToString(Widgets, {
props: { items, registry },
});
html; // -> '<p class="price">649 kr</p>'
ERROR_MESSAGES.UNKNOWN_WIDGET('answer-wall', 'questions'); // -> 'Unknown widget type "answer-wall" for widget ID "questions". Skipping render.'The lookup uses Object.prototype.hasOwnProperty, so an item typed
constructor, toString or __proto__ is an unknown type and never resolves
to a function on Object.prototype.
defineWidgets
function defineWidgets<R extends WidgetRegistry>(registry: R): R;defineWidgets returns the registry it was handed, typed as the literal object
passed in, so a code editor completes the type names. Annotating the same object
as WidgetRegistry widens its keys to string.
The widget core’s entry carries the rest.
import { defineWidgets } from '@evanion/astro-widget';
import ListingHeader from './widgets/ListingHeader.astro';
import PriceBox from './widgets/PriceBox.astro';
export const registry = defineWidgets({
'listing-header': ListingHeader,
'price-box': PriceBox,
});validateItems
function validateItems(
items: unknown,
known: KnownWidgetTypes,
required?: Record<string, string[]>,
): WidgetProblem[];validateItems walks items and their children, and returns every problem it
finds. It never throws and never stops at the first problem, so one call reports
a whole list. It is the only check an astro-widget item’s props get. Below,
answer-wall is not in the list of known types, and the price-box item has no
price:
import { } from '@evanion/astro-widget';
const = [
{
: 'header',
: 'listing-header',
: { : 'Brass: Birmingham' },
},
{ : 'price', : 'price-box', : {} },
{ : 'questions', : 'answer-wall', : {} },
];
const = { 'listing-header': ['title'], 'price-box': ['price'] };
const = (
,
['listing-header', 'price-box'],
,
);
; // -> [{ index: 1, id: 'price', type: 'price-box', message: 'missing field price' }, { index: 2, id: 'questions', type: 'answer-wall', message: 'unknown widget type' }]Check CMS items before Astro renders them runs it
before astro build, and
the widget core’s entry carries the parameters.
AnyWidgetItem
interface AnyWidgetItem<Type extends string = string, Props = object> {
id: string;
type: Type;
props: Props;
meta?: WidgetMeta;
children?: AnyWidgetItem[];
}AnyWidgetItem is one item as the CMS saved it. Widgets.astro spreads props
into the widget, hands meta to the chrome, and passes children to the widget
as data. The Baize listing page assigns the JSON its CMS saved to
AnyWidgetItem[], so the compiler checks each entry’s id, type and props:
import type { AnyWidgetItem } from '@evanion/astro-widget';
import saved from './brass-birmingham.json';
export const items: AnyWidgetItem[] = saved;@evanion/react-widget offers WidgetItem<C>, a checked counterpart inferred
from its components. astro-widget has none, for the reason under
WidgetRegistry.
The widget core’s entry carries the fields.
WidgetRegistry
type WidgetRegistry<T = unknown> = Record<string, T>;WidgetRegistry maps a widget type to the component that renders it.
astro-widget leaves T at unknown. An .astro module’s default export is an
AstroComponentFactory, and its props live in the file’s frontmatter Props
interface, out of the factory type’s reach, so a narrower T would check
nothing. The widget core’s entry carries the rest.
WidgetMeta
type WidgetMeta = Record<string, unknown>;WidgetMeta is the meta an item carries for its chrome, such as a background,
a column or a span. Widgets.astro hands it to chrome.item and never to the
widget, and a chrome that reads it narrows it itself, as the layout page’s
Section.astro does with
meta?: { background?: string }.
The widget core’s entry carries the rest.
WidgetProblem
interface WidgetProblem {
index: number;
id: string;
type: string;
message: string;
}WidgetProblem is one line of the report validateItems returns. index is
the item’s position in its own sibling list, and -1 when items is not an
array. id and type are the item’s own, or - when that field is not a
string. A problem carries no depth, so id tells apart two problems at
index: 0 on different levels only when the ids are unique across the tree.
The widget core’s entry carries the rest.
KnownWidgetTypes
type KnownWidgetTypes = WidgetRegistry | readonly string[];KnownWidgetTypes is the set of widget types a list may use: a registry, or a
plain list of type names. validateItems reads only a registry’s keys, so both
report the same problems. A script run under plain Node, which cannot import
.astro modules, passes the names. The example passes both:
import { validateItems } from '@evanion/astro-widget';
import { items } from '../examples/src/data/brass-birmingham';
import { registry } from '../examples/src/registry';
Object.keys(registry); // -> ['listing-header', 'price-box']
validateItems(items, registry); // -> []
validateItems(items, ['listing-header', 'price-box']); // -> []The widget core’s entry carries the rest.
ERROR_MESSAGES
ERROR_MESSAGES holds the text Widgets.astro and @evanion/react-widget
warn with, so a stale type reads the same way whether React or Astro drew the
page. Widgets.astro uses three of the five keys:
| Key | Widgets.astro uses it |
|---|---|
UNKNOWN_WIDGET | to warn that an item’s type is not an own key of the registry |
MALFORMED_ITEMS | to warn that items is present and is not an array |
UNKNOWN | in the UNKNOWN_WIDGET warning, for a type or id that is not a string |
Only @evanion/react-widget warns with MALFORMED_ITEM and
MALFORMED_CHILDREN. The Unknown types example prints
UNKNOWN_WIDGET, and the widget core’s entry
lists all five.
VALIDATION_MESSAGES
VALIDATION_MESSAGES holds every message validateItems reports, so a caller
sorts a report without matching on prose. MISSING_FIELD is a function of the
field name, and the other eight keys are strings.
The widget core’s entry carries the rest.
| Key | Message |
|---|---|
NOT_A_LIST | items is not a list |
NOT_AN_OBJECT | item is not an object |
INVALID_ID | item id is not a string |
INVALID_TYPE | item type is not a string |
UNKNOWN_TYPE | unknown widget type |
INVALID_PROPS | props is not an object |
INVALID_CHILDREN | children is not a list |
DUPLICATE_ID | duplicate sibling id |
MISSING_FIELD | missing field <field> |
A CMS webhook splits one report by message, unknown types for the developers and blank fields for the editor:
import { , } from '@evanion/astro-widget';
const = [
{ : 'header', : 'listing-header', : { : '' } },
{ : 'questions', : 'answer-wall', : {} },
];
const = (, ['listing-header'], {
'listing-header': ['title'],
});
const = .(
() => . === .,
);
const = .(
() => . === .('title'),
);
.(() => .); // -> ['questions']
.(() => .); // -> ['header']What validateItems reports gives the list that produces each message.