-
-
Notifications
You must be signed in to change notification settings - Fork 614
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
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a88ad2f
Add initial documentation
sneridagh a7aa0a8
Fist implementation for the slots renderers
sneridagh 40c70aa
Define potential Slots names and where to insert them
sneridagh 1388887
Add exact property to enable/disable slot inheritance
sneridagh de20f81
Small amendments
sneridagh f16f430
Add content container for left-right slots
sneridagh b6cc3f2
Merge branch 'master' into slots
sneridagh 2cb85ed
Add missing key
sneridagh d156cbe
Merge remote-tracking branch 'origin/master' into slots
tiberiuichim eab2317
Fix bug
sneridagh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, in `props` property) | ||
{ path: '/', component: ExtraComponent, props: {}, exact: true }, | ||
], | ||
}; | ||
``` | ||
|
||
Slots are inherited by default on all children routes, but you can block inheritance by | ||
defining `exact` property to `true`. | ||
|
||
## Slots | ||
|
||
- aboveContentTitle | ||
- belowContentTitle | ||
- aboveContentBody | ||
- belowContentBody | ||
- footer | ||
|
||
- asideLeftSlot | ||
- asideRightSlot | ||
|
||
- afterApp | ||
- afterToolbar | ||
|
||
- htmlHead | ||
- htmlBeforeBody | ||
- htmlAfterBody | ||
|
||
### 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}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
src/components/theme/ContentContainer/ContentContainer.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from 'react'; | ||
import { Container, Grid } from 'semantic-ui-react'; | ||
import { SlotRenderer } from '@plone/volto/components'; | ||
import { matchPath, useLocation } from 'react-router-dom'; | ||
import { slots } from '~/config'; | ||
|
||
const ContentContainer = ({ children, content }) => { | ||
const pathname = useLocation().pathname; | ||
const hasSlot = (name) => { | ||
if (!slots[name]) { | ||
return null; | ||
} | ||
return slots[name].filter((slot) => | ||
matchPath(pathname, { path: slot.path, exact: slot.exact }), | ||
); | ||
}; | ||
const hasLeftSlot = hasSlot('asideLeftSlot'); | ||
const hasRightSlot = hasSlot('asideRightSlot'); | ||
|
||
const contentWidth = () => { | ||
if (hasLeftSlot && hasRightSlot) { | ||
return 6; | ||
} else if (hasLeftSlot || hasRightSlot) { | ||
return 9; | ||
} else { | ||
return 12; | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
{hasLeftSlot || hasRightSlot ? ( | ||
<Grid stackable as={Container}> | ||
{hasLeftSlot && ( | ||
<Grid.Column as="aside" className="aside-left-slot" width={3}> | ||
<SlotRenderer name="asideLeftSlot" /> | ||
</Grid.Column> | ||
)} | ||
<Grid.Column className="content-body" width={contentWidth()}> | ||
{children} | ||
</Grid.Column> | ||
{hasRightSlot && ( | ||
<Grid.Column as="aside" className="aside-right-slot" width={3}> | ||
<SlotRenderer name="asideRightSlot" /> | ||
</Grid.Column> | ||
)} | ||
</Grid> | ||
) : ( | ||
<>{children}</> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default ContentContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React from 'react'; | ||
import { matchPath, useLocation } from 'react-router-dom'; | ||
import { v4 as uuid } from 'uuid'; | ||
import { slots } from '~/config'; | ||
|
||
const SlotRenderer = ({ name }) => { | ||
const pathname = useLocation().pathname; | ||
|
||
if (!slots[name]) { | ||
return null; | ||
} | ||
|
||
const currentSlot = slots[name]; | ||
const active = currentSlot.filter((slot) => | ||
matchPath(pathname, { path: slot.path, exact: slot.exact }), | ||
); | ||
|
||
return active.map(({ component, props }) => { | ||
const id = uuid(); | ||
const Slot = component; | ||
return <Slot {...props} key={id} id={id} />; | ||
}); | ||
}; | ||
|
||
export default SlotRenderer; |
144 changes: 144 additions & 0 deletions
144
src/components/theme/SlotRenderer/SlotRenderer.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
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); | ||
}); | ||
test('renders a SlotRenderer component for the aboveContentTitle with inheritance', () => { | ||
slots.aboveContentTitle = [ | ||
{ | ||
path: '/other-place', | ||
component: (props) => <div {...props} />, | ||
props: { className: 'slot-component-aboveContentTitle' }, | ||
}, | ||
]; | ||
|
||
const { container } = render( | ||
<MemoryRouter initialEntries={[{ pathname: '/other-place/other-dir' }]}> | ||
<SlotRenderer name="aboveContentTitle" /> | ||
</MemoryRouter>, | ||
); | ||
const divSlot = container.querySelector('div'); | ||
expect(divSlot).toHaveClass('slot-component-aboveContentTitle'); | ||
}); | ||
test('renders a SlotRenderer component for the aboveContentTitle disable inheritance', () => { | ||
slots.aboveContentTitle = [ | ||
{ | ||
path: '/other-place', | ||
component: (props) => <div {...props} />, | ||
props: { className: 'slot-component-aboveContentTitle' }, | ||
exact: true, | ||
}, | ||
]; | ||
|
||
const { container } = render( | ||
<MemoryRouter initialEntries={[{ pathname: '/other-place/other-dir' }]}> | ||
<SlotRenderer name="aboveContentTitle" /> | ||
</MemoryRouter>, | ||
); | ||
const divSlot = container.querySelector('div'); | ||
expect(divSlot).toBe(null); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.