I would like to propose a new implementation which is slightly different than what I proposed in the OP but it’s more native to Frontity.
My idea is that, instead of using an internal React context, the fills are saved in the state
. The actual match between slots and fills is done by the Slot
component, but accessing the state
, insead of a React context.
Moving configuration to the state
is going to be a common pattern in Frontity.
The slots are added by the theme:
const Header = () => (
<Slot name="Above header" />
<Menu />
<MobileMenu />
{ /* ... */ }
<Slot name="Below header" />
);
And the fills are added to the state, to a common namespace called fills
.
// frontity.settings.js
const settings = {
packages: [
{
name: "smartads",
state: {
fills: {
smartads1: {
slot: "Below Header",
library: "SmartAd",
props: {
networkId: "123",
id: "456"
}
},
smartads2: {
slot: "Below Header",
library: "SmartAd",
priority: 5,
props: {
networkId: "123",
id: "789"
}
}
}
}
}
]
};
The Fill
component is specificed by the property library
and stored in libraries
:
// /packages/smartads/src/index.ts
import HeadScripts from "./components/head-scripts";
import SmartAd from "./components/ad";
export default {
roots: {
smartads: HeadScripts
},
libraries: {
fills: {
SmartAd
}
}
};
With this technique, maybe we can also get rid of the weird name Roots
because we won’t need fills anymore, and rename it to something easier to understand. Also, fills can have priorities.
If fills need to access the state they can do so themselves.
const SmartAd = ({ state, networkId, id }) => {
// ...
return <div smartads-id={id} />;
};
export default connect(SmartAd);
The Slot
component will get the fills it needs from the state
.
const Slot = ({ state, actions, libraries, name, ...slotProps }) => {
const fills = Object.values(state.fills)
.find(fill => fill.name === name)
.sort((a, b) => a.priority > b.priority);
return fills.map(fill => {
const Fill = libraries.fills[fill.library];
return <Fill {...slotProps} {...fill.props} />;
});
};
As you can see, it passes down the props it receives along with the props of fill itself. That means that people can document their slots to instruct people about the props that their fills will receive.
For example, the slots of a Home Carrousel package can pass info about each post.
const HomeCarrousel = ({ state }) => {
// Get first 5 items from the home.
const homePosts = state.source.get("/").items.slice(0, 4);
return (
<>
<Slot name="Before all posts in home carrousel" />
{homePosts.map((item, index) => {
const data = state.source.get(item.link);
return (
<>
<Slot name={`Before post ${index} in home carrousel`} data={data} />
<Post data={data} />
<Slot name={`After post ${index} in home carrousel`} data={data} />
</>
);
})}
<Slot name="After all posts in home carrousel" />
</>
);
};
The Slot
component can support an optional children that is rendered if no fills are present.
const Post = () => (
<>
{/* ... */}
<PostTitle />
<Slot name="Between post title and post meta">
<Seperator />
</Slot>
<PostMeta />
{/* ... */}
</>
);
In the future, once we implement the useConnect
hook, we can expose a useFills
hook to make Slots even more hackable.
const useFills = name =>
useConnect(({ state }) =>
Object.values(state.fills)
// Match only the fills for this name.
.find(fill => fill.name === name)
// Sort fills by priority.
.sort((a, b) => a.priority > b.priority)
// Add real component to the array.
.map(fill => {
const Fill = libraries.fills[fill.library];
return {
...fill,
Fill
};
})
);
And then use that hook instead of the Slot
component.
For example, a theme can use useFills
to render the fills in-between <li>
tags:
const Menu = ({ state }) => {
const menuItems = state.theme.menu;
const menuFills = useFills("Menu items");
return (
<>
<Slot name="Before menu" />
<ul>
{menuItems.map(item => (
<li>
<Link href={item[1]}>{item[0]}</Link>
</li>
))}
{menuFills.map(({ Fill, props }) => (
<li>
<Fill {...props} />
</li>
))}
</ul>
<Slot name="After menu" />
</>
);
};