Slots#
Slots provide a way for Volto add-ons to insert their own components at predefined locations in the rendered page.
Note
This concept is inspired by the Plone Classic UI Viewlets.
Anatomy#
Slots have a name, and they contain a list of named slot components.
Volto renders slots using the SlotRenderer
component.
You can add slot insertion points in your code, as shown in the following example.
<SlotRenderer name="aboveContent" content={content} />
Slot components are registered in the configuration registry using a specific API for slots.
The rendering of a slot component is controlled by the presence or absence of a list of conditions called predicates.
You can register multiple slot components with the same name under the same slot, as long as they have different predicates or components.
To illustrate how slots are structured and work, let's register a slot component, where the component is PageHeader
, and the predicate matches a route that begins with /de/about
.
config.registerSlotComponent({
slot: 'aboveContent',
name: 'header',
component: PageHeader,
predicates: [RouteCondition('/de/about')],
});
The following tree structure diagram illustrates the resultant registration.
Slot (`name`=`aboveContent`)
└── SlotComponent
├── `slot`=`aboveContent`
├── `name`=`header`
├── `component`=PageHeader
└── predicate of "only appear under `/de/about`"
Next, let's register another slot component in the same slot, with the same name and component, but with a different predicate where the content type matches either Document
or News Item
.
config.registerSlotComponent({
slot: 'aboveContent',
name: 'header',
component: PageHeader,
predicates: [ContentTypeCondition(['Document', 'News Item'])],
});
The following tree structure diagram illustrates the result of the second registration.
Slot (`name`=`aboveContent`)
├── SlotComponent
│ ├── `slot`=`aboveContent`
│ ├── `name`=`header`
│ ├── `component`=PageHeader
│ └── predicate of "only appear under `/de/about`"
└── SlotComponent
├── `slot`=`aboveContent`
├── `name`=`header`
├── `component`=PageHeader
└── predicate of "only appear when the content type is either a Document or News Item"
Finally, let's register another slot component in the same slot, with the same name, but with a different component and without a predicate.
config.registerSlotComponent({
slot: 'aboveContent',
name: 'header',
component: 'DefaultHeader',
});
The following tree structure diagram illustrates the result of the third registration.
Slot (`name`=`aboveContent`)
├── SlotComponent
│ ├── `slot`=`aboveContent`
│ ├── `name`=`header`
│ ├── `component`=PageHeader
│ └── predicate of "only appear under `/de/about`"
├── SlotComponent
│ ├── `slot`=`aboveContent`
│ ├── `name`=`header`
│ ├── `component`=PageHeader
│ └── predicate of "only appear when the content type is either a Document or News Item"
└── SlotComponent
├── `slot`=`aboveContent`
├── `name`=`header`
└── `component`=`DefaultHeader`
The rendering of slot components follows an algorithm:
The last registered slot component is evaluated first.
The first evaluated slot component's predicates to return
true
has its component rendered in the slot.A slot component without predicates becomes the fallback for other slot components with predicates.
Working through the above diagram from bottom to top, let's assume a visitor goes to the route /de/about
and views an Event content type.
The algorithm looks for the third slot component's predicates. Because it has no predicates to be evaluated, and therefore cannot return
true
, its component is a fallback to other slot components.Moving upward, the second slot component's predicates are evaluated. If they are
true
, then its component is rendered in the slot, and evaluation stops. But in this case, the content type is an Event, thus it returnsfalse
, and evaluation continues upward.The first slot component's predicates are evaluated. In this case, they are true because the visitor is on the route
/de/about
. Evaluation stops, and its component is rendered in the slot.
Within a slot, slot components are grouped by their name. The order in which the grouped slot components are evaluated is governed by the order in which they are registered.
Extending our previous example, let's register another slot component with a different name.
config.registerSlotComponent({
slot: 'aboveContent',
name: 'subheader',
component: PageSubHeader,
predicates: [ContentTypeCondition(['Document', 'News Item'])],
});
Thus the order of evaluation of the named slot components would be header
, subheader
.
As each group of slot components is evaluated, their predicates will determine what is rendered in their position.
You can change the order of the named slot components for a different slot using the reorderSlotComponent API.
In our example, you can reorder the subheading
before the heading
, although it would probably look strange.
config.reorderSlotComponent({
slot: 'aboveContent',
name: 'subheader',
action: 'before',
target: 'header',
});
You can even delete the rendering of a registered slot component using the unregisterSlotComponent API.
Default slots#
Volto comes with the following default slots.
aboveContent
belowContent
Configuration registry for slot components#
You can manage slot components using the configuration registry for slot components and its API.
registerSlotComponent
#
registerSlotComponent
registers a slot component as shown.
config.registerSlotComponent({
slot: 'aboveContent',
name: 'header',
component: PageHeader,
predicates: [
RouteCondition('/de/about'),
ContentTypeCondition(['Document', 'News Item'])
],
});
A slot component must have the following parameters.
slot
The name of the slot, where the slot components are stored.
name
The name of the slot component that we are registering.
component
The component that we want to render in the slot.
predicates
A list of functions that return a function with this signature.
export type SlotPredicate = (args: any) => boolean;
Predicate helpers#
There are two predicate helpers available in the Volto helpers. You can also create custom predicate helpers.
RouteCondition
#
export function RouteCondition(path: string, exact?: boolean) {
return ({ location }: { location: Location }) =>
Boolean(matchPath(location.pathname, { path, exact }));
}
The RouteCondition
predicate helper renders a slot if the specified route matches.
It accepts the following parameters.
path
String. Required. The route.
exact
Boolean. Optional. If
true
, then the match will be exact, else matches "begins with", for the given string frompath
.
ContentTypeCondition
#
export function ContentTypeCondition(contentType: string[]) {
return ({ content, location }: { content: Content; location: Location }) => {
return (
contentType.includes(content?.['@type']) ||
contentType.some((type) => {
return location.search.includes(`type=${encodeURIComponent(type)}`);
})
);
};
}
The ContentTypeCondition
helper predicate allows you to render a slot when the given content type matches the current content type.
It accepts a list of possible content types.
It supports the Add
form and can detect which content type you add.
Custom predicates#
You can create your own predicate helpers to determine whether your slot component should render.
The SlotRenderer
will pass down the current content
, the location
object, and the current navRoot
object into your custom predicate helper.
You can also tailor your own SlotRenderer
s, or shadow the original SlotRenderer
, to satisfy your requirements.
Changed in version 18.0.0-alpha.33: Now config.getSlots
in the configuration registry takes the argument location
instead of pathname
.
getSlot
#
getSlot
returns the components to be rendered for the given named slot.
You should use this method while building you own slot renderer or customizing the existing SlotRenderer
.
You can use the implementation of SlotRenderer
as a template.
This is the signature:
config.getSlot(name: string, args: GetSlotArgs): GetSlotReturn
It has the following parameters.
name
String. Required. The name of the slot we want to render.
args
Object. Required. An object containing the arguments to pass to the predicates.
getSlotComponents
#
getSlotComponents
returns the list of named slot components registered in a given slot.
This is useful to debug what is registered and in what order, informing you whether you need to change their order.
This is the signature:
config.getSlotComponents(slot: string): string[]
slot
String. Required. The name of the slot where the slot components are stored.
getSlotComponent
#
getSlotComponent
returns the registered named component's data for the given slot component name.
This is the signature:
config.getSlotComponent(slot: string, name: string): SlotComponent[]
slot
String. Required. The name of the slot where the slot components are stored.
name
String. Required. The name of the slot component to retrieve.
reorderSlotComponent
#
reorderSlotComponent
reorders the list of named slot components registered per slot.
Given a slot
and the name
of a slot component, you must either specify the desired position
or perform an action
to reposition the slot component in the given slot, but not both.
The available actions are "after"
, "before"
, "first"
, and "last"
.
"first"
and "last"
do not accept a target
.
This is the signature:
config.reorderSlotComponent({ slot, name, position, action, target }: {
slot: string;
name: string;
position?: number | undefined;
action?: "after" | "before" | "first" | "last" | undefined;
target?: string | undefined;
}): void
slot
String. Required. The name of the slot where the slot components are stored.
name
String. Required. The name of the slot component to reposition in the list of slot components.
position
Number. Exactly one of
position
oraction
is required. The destination position in the registered list of slot components. The position is zero-indexed.action
Enum:
"after"
|"before"
|"first"
|"last"
| undefined. Exactly one ofposition
oraction
is required. The action to perform onname
.When using either the
"after"
or"before"
values, atarget
is required. The slot component will be repositioned relative to thetarget
.When using either the
"first"
and"last"
values, atarget
must not be used. The slot component will be repositioned to either the first or last position.target
String. Required when
action
is either"after"
or"before"
, else must not be provided. The name of the slot component targeted for the givenaction
.
unregisterSlotComponent
#
unregisterSlotComponent
removes a registration for a named slot component, given its registration position.
This is the signature:
config.unRegisterSlotComponent(slot: string, name: string, position: number): void
slot
String. Required. The name of the slot that contains the slot component to unregister.
name
String. Required. The name of the slot component to unregister inside the component.
position
Number. Required. The component position to remove in the slot component registration. Use getSlotComponent to find the zero-indexed position of the registered component to remove.