Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slots in core #5775

Merged
merged 75 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
d8b1eb5
API support for slots in registry
sneridagh Feb 16, 2024
fa39782
WIP SlotRenderer
sneridagh Feb 16, 2024
24babd7
First working implementation
sneridagh Feb 20, 2024
b58a977
Improve SlotRenderer
sneridagh Feb 20, 2024
9f5e56c
Changelog
sneridagh Feb 20, 2024
c2bd017
Merge branch 'main' into slots2024
sneridagh Feb 20, 2024
81dfe61
Improve and test as TS
sneridagh Feb 20, 2024
c2d859f
Better implementation, improve tests
sneridagh Feb 20, 2024
3a5d56b
Add coresandbox example, adjust here and there
sneridagh Feb 21, 2024
efc4805
Add ignore
sneridagh Feb 21, 2024
6bf883e
Use latest @types/react and @types/react-dom
sneridagh Feb 21, 2024
70c93f3
Temptative docs
sneridagh Feb 21, 2024
4ad1dd0
Merge branch 'main' into slots2024
sneridagh Feb 21, 2024
3f6ed67
Apply suggestions from code review
sneridagh Feb 22, 2024
829bea1
Update docs/source/configuration/slots.md
sneridagh Feb 22, 2024
4aba432
F*ck ts-jest, cannot wait to say bye to Jest
sneridagh Feb 22, 2024
3fbe33c
Change name of the
sneridagh Feb 22, 2024
0f013fc
Remove Semantic Container from coresandbox example
sneridagh Feb 22, 2024
3241e76
Add slots to toctree
stevepiercy Feb 22, 2024
8662ee4
Add https://stackoverflow.com to ignore in linkcheck, and sort entries
stevepiercy Feb 22, 2024
c4cfe86
Expound upon the slot tree structure and how it works with an ASCII tree
stevepiercy Feb 22, 2024
7e30c9d
Put the pending stuff into a todo, so that it sticks out like a sore …
stevepiercy Feb 22, 2024
520fc60
Put the predicates into a definition list (I changed my mind from a g…
stevepiercy Feb 22, 2024
1a9cb43
Create a new parent subsection for predicate helpers
stevepiercy Feb 22, 2024
a8f6de3
Tidy up `RouteCondition`
stevepiercy Feb 22, 2024
ffced49
Final pass, nitpicky stuff
stevepiercy Feb 22, 2024
f17ecbe
Fix TS tests
sneridagh Feb 22, 2024
6b98d6c
push locks
sneridagh Feb 22, 2024
675e0af
Finish the missing docs
sneridagh Feb 22, 2024
187e21e
Make ts-jest happy
sneridagh Feb 22, 2024
f189d85
Use a pattern of Type, required/optional, description for signatures.
stevepiercy Feb 22, 2024
e234d63
Grammar, no empty headings, tidy up
stevepiercy Feb 22, 2024
c9d6f9e
Merge remote-tracking branch 'origin/slots2024' into slots2024
stevepiercy Feb 22, 2024
b23bd9a
zero-indexed
stevepiercy Feb 22, 2024
2ad12dc
Add Cypress test
sneridagh Feb 22, 2024
6598127
Update docs/source/configuration/slots.md
sneridagh Feb 22, 2024
9bec47f
Force fresh build always
sneridagh Feb 22, 2024
5fde359
Force build on coresandbox
sneridagh Feb 22, 2024
c93d146
Revert "Force build on coresandbox"
sneridagh Feb 22, 2024
37e6b92
Remove methods that are not 18.x compatible :( (toReversed, toSplice)
sneridagh Feb 22, 2024
26d4dda
Add a small comment
sneridagh Feb 22, 2024
ac09167
Remove comments
sneridagh Feb 22, 2024
33b96c9
Merge branch 'main' into slots2024
sneridagh Feb 22, 2024
adec91d
Update docs/source/configuration/slots.md
stevepiercy Feb 24, 2024
049e16a
Link to API
stevepiercy Feb 24, 2024
547a677
Enhance intro sentence
stevepiercy Feb 24, 2024
3496942
s/inherited from/inspired by
stevepiercy Feb 24, 2024
594c5f9
Merge branch 'main' into slots2024
stevepiercy Feb 24, 2024
394622d
David's suggestions
sneridagh Feb 26, 2024
829ea91
Fix tests
sneridagh Feb 26, 2024
d73cb36
Add unit test
sneridagh Feb 26, 2024
ba356c3
Add navRoot to the equation, this enables Add form correct checks, an…
sneridagh Feb 26, 2024
b66b832
Add which slots are added by default in Volto
sneridagh Feb 26, 2024
589ed69
Refactor reorderSlotComponent to accommodate actions and targets
sneridagh Feb 26, 2024
2bb38c0
Fix tests
sneridagh Feb 26, 2024
cc40798
Jumpstart Volto glossary to include `predicates`. See #3344.
stevepiercy Feb 26, 2024
bcb5572
Revert. `make docs-livehtml` does not use Intersphinx, but `make docs…
stevepiercy Feb 26, 2024
c44c6a9
Fix tree diagram text and description of concepts
stevepiercy Feb 26, 2024
23e18a4
MyST syntax, move narrative text from heading, grammar fix.
stevepiercy Feb 26, 2024
70708ec
Revise descriptions of `reorderSlotComponent` and how its arguments w…
stevepiercy Feb 26, 2024
7030258
Merge branch 'main' into slots2024
stevepiercy Feb 26, 2024
fa44ecb
Steve better condition proposal
sneridagh Feb 27, 2024
8b37c10
Other better condition
sneridagh Feb 27, 2024
8ddb708
Tidy up code samples
stevepiercy Feb 28, 2024
cdfb28f
Consistent code samples
stevepiercy Feb 28, 2024
c8685f7
Overhaul Anatomy
stevepiercy Feb 28, 2024
18333ee
Merge branch 'main' into slots2024
stevepiercy Feb 28, 2024
40c1dfd
Rename slot name, and slot component's name and component
stevepiercy Feb 28, 2024
275b2c5
Rename other slot component's component
stevepiercy Feb 28, 2024
a35f373
Add a name for component in example
stevepiercy Feb 28, 2024
59a53fd
Enhance part about ordering slot components, and add targets to headings
stevepiercy Feb 28, 2024
6eea733
Restructure API docs to be more cohesive and contextual, specifically…
stevepiercy Feb 28, 2024
0942929
Align `reorderSlotComponent` description with earlier narrative text
stevepiercy Feb 28, 2024
2eae4b2
Reorder API methods so that they are in order of CRUD: create, get (r…
stevepiercy Feb 28, 2024
64c8c11
Revise per conference with Victor
stevepiercy Feb 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@
# Ignore github.com pages with anchors
r"https://github.com/.*#.*",
# Ignore other specific anchors
r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue",
r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0",
r"https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects".
r"https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects".
r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue",
r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0",
r"https://stackoverflow.com", # volto and documentation # TODO retest with latest Sphinx.
]
linkcheck_anchors = True
linkcheck_timeout = 10
Expand Down
1 change: 1 addition & 0 deletions docs/source/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ workingcopy
environmentvariables
expanders
locking
slots
```
379 changes: 379 additions & 0 deletions docs/source/configuration/slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
---
myst:
html_meta:
"description": "Slots are insertion points in the Volto rendering tree structure."
"property=og:description": "Slots are insertion points in the Volto rendering tree structure."
"property=og:title": "Slots"
"keywords": "Volto, Plone, frontend, React, configuration, slots, viewlets"
---

# Slots
sneridagh marked this conversation as resolved.
Show resolved Hide resolved

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 {doc}`plone:classic-ui/viewlets`.
```


## Anatomy

Slots have a name, and they contain a list of slot components.

Volto renders slots using the `SlotRenderer` component.
You can add slot insertion points in your code, as shown in the following example.

```ts
<SlotRenderer name="toolbar" content={content} />
```

Slot components consist of a data structure with key/value pairs, where the keys are the parent's slot name, the slot component's name, the component to render in the slot, and optional predicates.
They are registered in the {ref}`configuration registry using a specific API for slots <configuration-registry-for-slot-components>`.

The renderer of a slot component is controlled by the presence or absence of a list of conditions called {term}`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 `@sneridagh what goes here?`, and the predicate matches a route that begins with `/de/about`.

```ts
config.registerSlotComponent({
slot: 'toolbar',
name: 'save',
component: '@sneridagh what goes here?',
predicates: [RouteCondition('/de/about')],
stevepiercy marked this conversation as resolved.
Show resolved Hide resolved
});
```

The following tree structure diagram illustrates the resultant registration.

```text
Slot (`name`=`toolbar`)
└── SlotComponent
├── `slot`=`toolbar`
├── `name`=`save`
├── `component`=`@sneridagh what goes here?`
└── 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`.

```ts
config.registerSlotComponent({
slot: 'toolbar',
name: 'save',
component: '@sneridagh what goes here?',
stevepiercy marked this conversation as resolved.
Show resolved Hide resolved
predicates: [ContentTypeCondition(['Document', 'News Item'])],
});
```

The following tree structure diagram illustrates the result of the second registration.

```text
Slot (`name`=`toolbar`)
├── SlotComponent
│ ├── `slot`=`toolbar`
│ ├── `name`=`save`
│ ├── `component`=`@sneridagh what goes here?`
│ └── predicate of "only appear under `/de/about`"
└── SlotComponent
├── `slot`=`toolbar`
├── `name`=`save`
├── `component`=`@sneridagh what goes here?`
└── 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.

```ts
config.registerSlotComponent({
slot: 'toolbar',
name: 'save',
component: '@sneridagh what goes here as a different component?',
stevepiercy marked this conversation as resolved.
Show resolved Hide resolved
});
```

The following tree structure diagram illustrates the result of the third registration.

```text
Slot (`name`=`toolbar`)
├── SlotComponent
│ ├── `slot`=`toolbar`
│ ├── `name`=`save`
│ ├── `component`=`@sneridagh what goes here?`
│ └── predicate of "only appear under `/de/about`"
├── SlotComponent
│ ├── `slot`=`toolbar`
│ ├── `name`=`save`
│ ├── `component`=`@sneridagh what goes here?`
│ └── predicate of "only appear when the content type is either a Document or News Item"
└── SlotComponent
├── `slot`=`toolbar`
├── `name`=`save`
└── `component`=`@sneridagh what goes here as a different component?`
```

When the slot components with the same name and component under a given slot have all of their predicates return `true`, then that component will render in the slot.
Else, if there are slot components with the same name, but with a different component, and without predicates, then that different component will render.
Copy link
Member Author

@sneridagh sneridagh Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not accurate, at least how I understand it by reading it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The below example is cocrrect though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the component doesn't have any effect on which SlotComponent is chosen; it's only used for rendering after the right SlotComponent is chosen.

For each name, the algorithm picks the first SlotComponent with predicates that return true (or no predicates), and its component will be rendered. There's nothing looking for all predicates from SlotComponents with the same component.


Thus the example slot renderer will have the following behavior.

- When both a user visits the route beginning with `/de/about`, and the content type is either a Document or News Item, then the component `@sneridagh what goes here?` will render in the `toolbar` slot.
- When one or both of the predicates are false, then the component `@sneridagh what goes here as a different component?` will render in the `toolbar` slot.

```{tip}
In our example, if we had not registered the third slot component—the one without predicates—and when either of the first two slot components' predicates return `false`, then no component would render.
```

```{todo}
The order in which the slot components render is governed by the order in which they were registered.

You can change the order of the defined slot components for a different slot using the API.
You can even delete the rendering of a registered slot component using the API.

[@sneridagh, this section needs to be explicit.
From our conversation yesterday, I think you said that the last slot component registered *per slot* wins.
Is that correct?
This has further implications, such as whether this rule applies per slot component's name, component, and predicates. --@stevepiercy]
```
sneridagh marked this conversation as resolved.
Show resolved Hide resolved


## Default slots

Volto comes with the following default slots.

- `aboveContent`
- `belowContent`


(configuration-registry-for-slot-components)=

## Configuration registry for slot components

You register a slot component using the configuration registry, as shown.

```ts
config.registerSlotComponent({
slot: 'toolbar',
name: 'save',
component: '@sneridagh this needs a component',
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.

```ts
export type SlotPredicate = (args: any) => boolean;
```


## Predicate helpers

There are two predicate helpers available in the Volto helpers.


### `RouteCondition`

```ts
export function RouteCondition(path: string, exact?: boolean) {
return ({ pathname }: { pathname: string }) =>
Boolean(matchPath(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 from `path`.


### `ContentTypeCondition`

```ts
export function ContentTypeCondition(contentType: string[]) {
return ({ content }: { content: Content }) =>
contentType.includes(content['@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.


### 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` and the `pathname` into your custom predicate helper.
You can also tailor your own `SlotRenderer`s, or shadow the original `SlotRenderer`, to satisfy your requirements.


## Manage registered slots and slot components

You can manage registered slots and slot components through the slots API.


### `getSlotComponents`

`getSlotComponents` returns the list of 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:

```ts
config.getSlotComponents(slot: string): string[]
```

`slot`
: String.
Required.
The name of the slot, where the slot components are stored.


### `reorderSlotComponent`

`reorderSlotComponent` reorders the list of 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:

```ts
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` or `action` is required.
The destination position in the registered list of slot components.
The position is zero-indexed.
davisagli marked this conversation as resolved.
Show resolved Hide resolved

`action`
: Enum: `"after"` | `"before"` | `"first"` | `"last"` | undefined.
Exactly one of `position` or `action` is required.
The action to perform on `name`.

When using either the `"after"` or `"before"` values, a `target` is required.
The slot component will be repositioned relative to the `target`.

When using either the `"first"` and `"last"` values, a `target` 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 given `action`.


(slots-getSlotComponent-label)=

### `getSlotComponent`

`getSlotComponent` returns the list of registered components under the given slot component name.
This is useful to debug what is registered and in what order, and later remove a component's registration, if needed.
This is the signature:

```ts
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.


### `unregisterSlotComponent`

It removes a registration for a specific component, given its registration position.
This is the signature:

```ts
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 {ref}`slots-getSlotComponent-label` to find the zero-indexed position of the registered component to remove.


### `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:

```ts
config.getSlot<T>(name: string, args: T): SlotComponent['component'][] | undefined
```

It has the following parameters.

`name`
: String.
Required.
The name of the slot we want to render.

`options`
: Object.
Required.
An object containing the arguments to pass to the predicates.
Loading
Loading