Follow-up of #262 and #263. A debate first, before anyone implements it.
Today
WidgetPropsMap in @plone/types is augmented per app, like UtilityTypeMap. It holds one props type for every widget slot. @plone/cmsui declares props: FormWidgetProps<any>, so every registered widget is checked against the widget contract, whatever value it reads and writes.
That catches widgets that don't follow the contract (#262). It can't catch a widget registered for the wrong kind of field. For example, NumberWidget (FormWidgetProps<number | null>) registered for type: 'boolean' type-checks.
Idea
Type each slot by its key, the way UtilityTypeMap types each utility by its type. Keys the map doesn't list fall back to the app-wide props, the way UtilityMethodFor falls back.
declare module '@plone/types' {
interface WidgetKeyPropsMap {
type: {
boolean: FormWidgetProps<boolean>;
number: FormWidgetProps<number | null>;
integer: FormWidgetProps<number | null>;
array: FormWidgetProps<TermValue[] | null>;
};
widget: {
date: FormWidgetProps<string | null>;
datetime: FormWidgetProps<string | null>;
};
factory: {
File: FormWidgetProps<FileValue | null>;
Image: FormWidgetProps<FileValue | null>;
};
}
}
registerWidget({ key: 'type', definition: { boolean: NumberWidget } }) would then fail the type check. getWidget('boolean', 'type') could return a typed component.
Open questions
- What does it buy? It only checks registration. The form picks the widget from the schema at runtime, so
SchemaField still only sees FormWidgetProps<unknown>. Is checking registrations worth a map to maintain?
- Where does the map live? In
@plone/cmsui, next to the widgets, augmenting @plone/types like WidgetPropsMap does? Volto must stay unaffected, with no augmentation and any props.
- Variance.
onChange: (value: T) => void makes FormWidgetProps<T> invariant in T, so an add-on that overrides a slot must match the declared value type exactly. Is that what we want, or should a slot declare the value it reads and accept wider widgets?
- Field values from the API. Some values come as terms (
{ token, title }) or as another type than their token (allow_discussion). The declared value type has to say what the widget reads, not only what it writes.
Related: #243, #244, #262, #263.
Follow-up of #262 and #263. A debate first, before anyone implements it.
Today
WidgetPropsMapin@plone/typesis augmented per app, likeUtilityTypeMap. It holds one props type for every widget slot.@plone/cmsuideclaresprops: FormWidgetProps<any>, so every registered widget is checked against the widget contract, whatever value it reads and writes.That catches widgets that don't follow the contract (#262). It can't catch a widget registered for the wrong kind of field. For example,
NumberWidget(FormWidgetProps<number | null>) registered fortype: 'boolean'type-checks.Idea
Type each slot by its key, the way
UtilityTypeMaptypes each utility by itstype. Keys the map doesn't list fall back to the app-wide props, the wayUtilityMethodForfalls back.registerWidget({ key: 'type', definition: { boolean: NumberWidget } })would then fail the type check.getWidget('boolean', 'type')could return a typed component.Open questions
SchemaFieldstill only seesFormWidgetProps<unknown>. Is checking registrations worth a map to maintain?@plone/cmsui, next to the widgets, augmenting@plone/typeslikeWidgetPropsMapdoes? Volto must stay unaffected, with no augmentation and any props.onChange: (value: T) => voidmakesFormWidgetProps<T>invariant inT, so an add-on that overrides a slot must match the declared value type exactly. Is that what we want, or should a slot declare the value it reads and accept wider widgets?{ token, title }) or as another type than their token (allow_discussion). The declared value type has to say what the widget reads, not only what it writes.Related: #243, #244, #262, #263.