Slots
Slots allows a component to treat the JSX children of the component as a form of input and project these children into the component's DOM tree.
This concept has different names in different frameworks:
- In Angular is called Content Projection
- In React, it's the
children
of the props - In Vue, and web components it's called slots as well
Slots in Qwik are symbolic, allowing Qwik to render parents and children in perfect isolation, allowing to render the parent component without ever rerender the childrens and vice versa.
Note: Because slots are symbolic, the children can NOT be read, or transformed by the components, like it's possible in React.
Usage
The main API to achieve this is the <Slot>
component, exported in @builder.io/qwik
:
import { Slot } from '@builder.io/qwik';
export const Button = component$(() => {
return (
<button>
<Slot />
</button>
);
});
The <Slot>
component is a placeholder for the children of the component. The <Slot>
component will be replaced by the children of the component, when rendering the app.
Let's see the usage of this component:
<Button>
{... this will be placed where the <Slot> is used inside the Button component ...}
</Button>
Named slots
The Slot
component can be used multiple times in the same component, as long as it has a different name
property:
import { Slot } from '@builder.io/qwik';
export const Button = component$(() => {
return (
<button>
<div>
<Slot name="start" /> {/* "start" slot */}
</div>
<Slot /> {/* default slot */}
<div>
<Slot name="end" /> {/* "end" slot */}
</div>
</button>
);
});
Now, when consuming the <Button>
component, we can pass children and specify in which slot they should be placed, using the q:slot
attribute:
<Button>
<div q:slot="start">Start</div>
<div>Default</div>
<div q:slot="end">End</div>
<icon q:slot="end"></icon>
</Button>
The result will be:
<button>
<div>
<!-- Slot name=start -->
<div q:slot="start">Start</div>
</div>
<!-- Slot (default) -->
<div>Default</div>
<div>
<!-- Slot name=end -->
<div q:slot="end">End</div>
<icon q:slot="end"></icon>
</div>
</button>
Notice that:
- If
q:slot
is not specified or it's an empty string, the content will be projected into the default<Slot>
, ie. the<Slot>
without aname
property. - Multiple
q:slot="end"
attributes coalesce items together in the content projection.
Not projecting content
Qwik keeps all content around, even if not projected. This is because the content could be projected in the future.
export const Project = component$(() => {
// Notice, this component does not have a <Slot> component
return <div />;
});
export const MyApp = component$(() => {
return <Project>unwrapped text</Project>;
});
Results in:
<my-app>
<q:template q:slot="">unwrapped text</q:template>
<div></div>
</my-app>
Notice that the un-projected content is moved into an inert <q:template>
. This is done just in case the Project
component re-renders and inserts a <Slot>
. In that case, we don't want to have to re-render the parent component just to generate the projected content. By persisting the un-projected content when the parent is initially rendered, the rendering of the two components can stay independent.
Invalid projection
The q:slot
attribute must be a direct child of a component.
export const Project = component$(() => { ... })
export const MyApp = component$(() => {
return (
<Project>
<span q:slot="title">ok, direct child of Project</span>
<div>
<span q:slot="title">Error, not a direct child of Project</span>
</div>
</Project>
);
});
children
Projection vs All frameworks need a way for a component to wrap its complex content in a conditional way. This problem is solved in many different ways, but there are two predominant approaches:
- projection: Projection is a declarative way of describing how the content gets from the parent template to where it needs to be projected.
children
:children
refers to vDOM approaches that treat content just like another input.
The two approaches can best be described as declarative vs imperative. They both come with their set of advantages and disadvantages.
Qwik uses the declarative projection approach. The reason for this is that Qwik needs to be able to render parent/children components independently from each other. With an imperative (children
) approach, the child component can modify the children
in countless ways. If a child component relied on children
, it would be forced to re-render whenever a parent component would re-render to reapply the imperative transformation to the children
. The extra rendering goes explicitly against the goals of Qwik components rendering in isolation.
For example, let's go back to our Collapsible
example from above:
- The parent needs to be able to change the title and the text without forcing the
Collapsible
component to re-render. Qwik needs to be able to redistribute the changes to theMyApp
template without affecting theCollapsible
component. - The child component needs to change what is projected without having the parent component re-render. In our case,
Collapsible
should be able to show/hide the defaultq:slot
without downloading and re-rendering theMyApp
component.
In order for the two components to have an independent lifecycle, the projection needs to be declarative. In this way, either the parent or child can change what is projected or how it is projected without re-rendering the other.
With children
approach, the component can imperatively modify the children
in endless ways. This would make it extremely difficult to build a framework that would not force re-rendering both parent and children.
Advanced Example
An example of a collapsible component which conditionally projects its content.
export const Collapsible = component$(() => {
const store = useStore({ isOpen: true });
return (
<div class="collapsible">
<div class="title" onClick$={() => (store.isOpen = !store.isOpen)}>
<Slot name="title"></Slot>
</div>
{store.isOpen ? <Slot /> : null}
</div>
);
});
The above component can be used from a parent component like so:
export const MyApp = component$(() => {
return (
<Collapsible>
<span q:slot="title">Title text</span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate accumsan pretium.
</Collapsible>
);
});
The Collapsible
component will always display the title, but the body of the text will only display if store.isOpen
is true
.
Rendered output
The above example would render into this HTML if isOpen===true
:
<my-app>
<div class="collapsible">
<div class="title">
<span q:slot="title" has-content>Title text</span>
</div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate accumsan pretium.
</div>
</my-app>