rfcs/2019-v3-slots.md
Sometimes we need to make a container component that can have multiple complex layouts to solve a variety of use cases. By default we want it to be easy to do the “right thing”. Children to a component should be laid out in the right places regardless of the components used or what order they appear in the DOMs.
For instance:
<Card>
<Image />
<Avatar />
</Card>
should produce the same design as
<Card>
<Avatar />
<Image />
</Card>
We want to separate design from implementation. It's been historically hard to solve the problem of where to place child components. Some approaches that have been tried.
We would like to have our designs not be dependent on the underlaying dom structure. This should make it easier to swap out designs (responsive) or update the design.
We'd like for design to give us a wireframe that has named regions and a clear underlying grid where elements should be placed. Potentially something like this.
To accomplish this we can leverage CSS Grid. Children of a grid can specify the areas in the grid that they take up.
For the CSS, we might get something like this.
.spectrum-Card {
border: 1px solid lightgrey;
border-radius: 4px;
background: white;
}
/* example grid css https://css-tricks.com/snippets/css/complete-guide-grid/ */
.container {
display: grid;
grid-template-columns: 14px auto 1fr 1fr 14px;
grid-template-rows: auto 32px 16px minmax(30px, auto) auto auto 10px;
grid-template-areas:
"preview preview preview preview preview"
". avatar . . ."
". avatar . . ."
". title title title ."
". body body body ."
". divider divider divider ."
". footer footer footer ."
". . . . .";
}
.preview {
grid-area: preview-start / preview-start / span 2 /preview-end;
height: 200px; /* build into grid template? minmax? */
}
.avatar {
grid-area: avatar;
z-index: 1;
height: 48px; /* this would ideally be off in avatar land */
width: 48px;
}
.title {
grid-area: title;
}
.body {
grid-area: body;
}
.divider {
grid-area: divider;
}
.footer {
grid-area: footer; /* this might be an issue because footer is a reserved word */
}
We would introduce a new component, 'Grid', that would accept a object for its slots prop.
This object would be expected to provide a mapping of Slot Names to an object containing props grid-area: props.
export const Card = (props) => {
let defaults = {slots: {
container: {UNSAFE_className: classNames(styles, 'container')},
preview: {UNSAFE_className: classNames(styles, 'preview')},
avatar: {UNSAFE_className: classNames(styles, 'avatar')},
title: {UNSAFE_className: classNames(styles, 'title')},
footer: {UNSAFE_className: classNames(styles, 'footer')},
divider: {UNSAFE_className: classNames(styles, 'divider')},
buttonGroup: {UNSAFE_className: classNames(styles, 'buttonGroup')}
}};
let {slots} = {...defaults, ...props};
return (
<div className={classNames(styles, 'spectrum-Card')}>
<Grid slots={slots}>
<Image slot="preview" />
<Avatar slot="avatar" />
<Flex slot="title">
<Title>Title</Title>
<Button>More</Button>
</Flex>
<Description slot="description">Description</Description>
<ButtonGroup slot="footer">Final remarks</ButtonGroup>
</Grid>
</div>
);
};
Or to make a more general container.
export const Card = (props) => {
let defaults = {slots: {
container: {UNSAFE_className: classNames(styles, 'container')},
preview: {UNSAFE_className: classNames(styles, 'preview')},
avatar: {UNSAFE_className: classNames(styles, 'avatar')},
title: {UNSAFE_className: classNames(styles, 'title')},
footer: {UNSAFE_className: classNames(styles, 'footer')},
divider: {UNSAFE_className: classNames(styles, 'divider')},
buttonGroup: {UNSAFE_className: classNames(styles, 'buttonGroup')}
}};
let {slots} = {...defaults, ...props};
return (
<div className={classNames(styles, 'spectrum-Card')}>
<Grid slots={slots}>
{props.children}
</Grid>
</div>
);
};
The reason for slots passing props is because sometimes we need specific variants of components. For example, in Dialogs there can be a Divider between the header and the content.
<Dialog>
<Header><Heading>The Heading</Heading></Header>
<Divider />
<Content>Some content</Content>
</Dialog>
This Divider should be a 'M' medium sized Divider so that ours will just work and look right. Anyone else creating their own Divider going into that slot can choose to use/ignore the slot props with the exception of className since that includes the positioning information.
An end user could just use our Cards, but they may also want to specify their own grid. Components that implement grid layouts should expose their slots to override the grid.
I've included a special Slot component, but it's not strictly necessary depending on the implementation. What is necessary is that all slots are direct descendants in the DOM of the container displaying grid. Items that aren't children of the grid do not participate in grid layout. MDN
import styles from './CustomCardStyles.css';
<Card slots={styles}>
<Image slot="preview" />
<Avatar slot="avatar" />
<Flex slot="title">
<Title>Title</Title>
<Button>More</Button>
</Flex>
<Description slot="description">Description</Description>
<Footer slot="footer">Final remarks</Footer>
</Card>
These next examples show how easily we can achieve vastly different layouts without changing anything in React Spectrum or in products. Only the things that need changing have been included.
It's all CSS.
Both of these examples use the same JSX from the previous example
Design
Spectrum CSS
.container {
display: grid;
grid-template-columns: auto 5px 200px;
grid-template-rows: auto 1fr;
grid-template-areas:
"preview . title"
"preview . body";
}
.preview {
grid-area: preview;
}
.title {
grid-area: title;
margin: 5px 5px 5px 0; /* ideally off in heading land */
}
.body {
grid-area: body;
}
/* these have no place in the grid, hide them :) */
.avatar, .divider, .footer {
display: none;
}
Design
Custom CSS
.container {
display: grid;
grid-template-columns: 14px auto 1fr 1fr 14px;
grid-template-rows: 5px auto auto auto auto 5px;
grid-template-areas:
". . . . ."
". avatar title title ."
". body body body ."
"preview preview preview preview preview"
". footer footer footer ."
". . . . .";
}
.avatar {
grid-area: avatar;
align-self: center;
height: 48px;
width: 48px;
}
.title {
grid-area: title;
align-self: center;
}
.body {
grid-area: body;
}
.preview {
grid-area: preview;
height: 200px;
}
.footer {
grid-area: footer;
}
We have a couple options here:
renderAs prop and the name of the dom element it needs to render, this Box component would replace the top level node of every Component we create. All components would still need to accept a 'slot' prop and pass it through to <Box>.