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

Initial Slots implementation #1563

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ nav:
- Anatomy: 'blocks/anatomy.md'
- Settings: 'blocks/settings.md'
- Edit components: 'blocks/editcomponent.md'
- Slots:
- Anatomy: 'slots/anatomy.md'
- Recipes:
- App component insertion point: 'recipes/appextras.md'
- Lazy loading and code splitting: 'recipes/lazyload.md'
Expand Down
58 changes: 58 additions & 0 deletions docs/source/slots/anatomy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Slots anatomy

The slots are insertion points in the Volto rendering tree structure. You can add a
component, along with its configuration, if any and it will be rendered in that
insertion point. You can control in which route do you want that element appear as well
as the order (position) of the items that you add in the in the slots. Slots are named,
so you can add in the configuration object:

```js
export const slots = {
aboveContentTitle: [
// List of components (might have config too, maybe in `data` property)
{ path: '/', component: 'Component', data: {} },
// It can include blocks too (makes sense when we will be able to save them)
Copy link
Member Author

Choose a reason for hiding this comment

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

It's an initial thought about the future of slots, first iteration can have only "viewlet-ish", no sved config. In a second iteration we can provide a full backend support for saving those persistently, and have the ability to save slots state so we can even use blocks inside slots.

{ path: '/', '@type': 'text' },
{ path: '/', '@type': 'image' },
],
};
```

## Slots

- aboveContentTitle
- belowContentTitle
- aboveContentBody
- belowContentBody
- footer

- leftAsideSlot
- rightAsideSlot

- afterApp
- afterToolbar

- htmlHead
- htmlBeforeBody
- htmlAfterBody

Copy link
Member Author

Choose a reason for hiding this comment

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

I initially identified these slots (and their insertion positions), did I miss something? We can add more later, for sure. @nzambello the ones referring to your "Macros" PR are there as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think Sidebar is a good candidate, as well.

Though I can definitely see Martjin's point for the toolbar: if extra content is needed there, then maybe we need a system of definable actions. For the Toolbar, my wishlist include new buttons in the main toolbar, but also now components available for the "More" menu.

### Slots definition

You can define new slots anywhere in the tree, then define them in the configuraion
object. This is how you define them in JSX:

```jsx
import {SlotRenderer} from '@plone/volto/components';
...

<SlotRenderer name="aboveContentTitle" />

```

### Slots in addons

You can define slots also in addons:

```js
config.slots.aboveContentTitle.push({path:'/', component: ExtraComponent})
```
21 changes: 21 additions & 0 deletions src/components/theme/SlotRenderer/SlotRenderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { v4 as uuid } from 'uuid';
import { slots } from '~/config';

export default ({ name }) => {
const pathname = useLocation().pathname;

if (!slots[name]) {
return null;
}

const currentSlot = slots[name];
const active = currentSlot.filter((slot) => matchPath(pathname, slot.path));

return active.map(({ component, props }) => {
const id = uuid();
const Slot = component;
return <Slot {...props} key={id} id={id} />;
});
};
109 changes: 109 additions & 0 deletions src/components/theme/SlotRenderer/SlotRenderer.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import SlotRenderer from './SlotRenderer';
import { slots } from '~/config';

describe('SlotRenderer Component', () => {
test('renders a SlotRenderer component for the aboveContentTitle with two slots in the root', () => {
slots.aboveContentTitle = [
{
path: '/',
component: (props) => <div {...props} />,
props: { className: 'slot-component' },
},
{
path: '/',
component: (props) => <aside {...props} />,
props: { className: 'slot-component' },
},
];

const { container } = render(
<MemoryRouter initialEntries={[{ pathname: '/' }]}>
<SlotRenderer name="aboveContentTitle" />
</MemoryRouter>,
);
const divSlot = container.querySelector('div');
expect(divSlot).toHaveClass('slot-component');
const asideSlot = container.querySelector('aside');
expect(asideSlot).toHaveClass('slot-component');
});
test('renders a SlotRenderer component for the aboveContentTitle with one slots in the root and other in other place', () => {
slots.aboveContentTitle = [
{
path: '/',
component: (props) => <div {...props} />,
props: { className: 'slot-component' },
},
{
path: '/other-place',
component: (props) => <aside {...props} />,
props: { className: 'slot-component' },
},
];

const { container } = render(
<MemoryRouter initialEntries={[{ pathname: '/' }]}>
<SlotRenderer name="aboveContentTitle" />
</MemoryRouter>,
);
const divSlot = container.querySelector('div');
expect(divSlot).toHaveClass('slot-component');
const asideSlot = container.querySelector('aside');
expect(asideSlot).toBe(null);
});
test('renders a SlotRenderer component for the aboveContentTitle and belowContentTitle, only renders the appropiate one', () => {
slots.aboveContentTitle = [
{
path: '/',
component: (props) => <div {...props} />,
props: { className: 'slot-component-aboveContentTitle' },
},
];
slots.belowContentTitle = [
{
path: '/',
component: (props) => <aside {...props} />,
props: { className: 'slot-component-belowContentTitle' },
},
];

const { container } = render(
<MemoryRouter initialEntries={[{ pathname: '/' }]}>
<SlotRenderer name="aboveContentTitle" />
</MemoryRouter>,
);
const divSlot = container.querySelector('div');
expect(divSlot).toHaveClass('slot-component-aboveContentTitle');
const asideSlot = container.querySelector('aside');
expect(asideSlot).toBe(null);
});
test('renders a SlotRenderer component for the aboveContentTitle and belowContentTitle with different paths, only renders the appropiate one', () => {
slots.aboveContentTitle = [
{
path: '/other-place',
component: (props) => <div {...props} />,
props: { className: 'slot-component-aboveContentTitle' },
},
];
slots.belowContentTitle = [
{
path: '/',
component: (props) => <aside {...props} />,
props: { className: 'slot-component-belowContentTitle' },
},
];

const { container } = render(
<MemoryRouter initialEntries={[{ pathname: '/other-place' }]}>
<SlotRenderer name="aboveContentTitle" />
</MemoryRouter>,
);
const divSlot = container.querySelector('div');
expect(divSlot).toHaveClass('slot-component-aboveContentTitle');
const asideSlot = container.querySelector('aside');
expect(asideSlot).toBe(null);
});
});
10 changes: 10 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,13 @@ export const blocks = {
};

export const addonReducers = {};

export const slots = {
aboveDocumentTitle: [
// List of components (might have config too, maybe in `data` property)
{ path: '/', component: 'Component', data: {} },
// It can include blocks too (makes sense when we will be able to save them)
{ path: '/', '@type': 'text' },
{ path: '/', '@type': 'image' },
],
};