Slots
Slots allow 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 Web components it's
<slot>
as well
The main API to achieve this is the <Slot>
component, exported in @builder.io/qwik
:
import { Slot, component$ } from '@builder.io/qwik';
const Button = component$(() => {
return (
<button>
Content: <Slot />
</button>
);
});
export default component$(() => {
return (
<Button>
This goes inside {'<Button>'} component marked by{`<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 during rendering.
Note: Slots in Qwik are declarative, allowing Qwik to render parents and children in isolation. Because slots are declarative, the children can NOT be read, or transformed by the components.
Why? Because Qwik needs to be able to render parent/children components independently from each other. With an imperative (
children
) approach, the child component can modify thechildren
in countless ways. If a child component relied onchildren
, it would be forced to re-render whenever a parent component would re-render to reapply the imperative transformation to thechildren
. The extra rendering goes explicitly against the goals of Qwik components rendering in isolation.
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, component$, useStylesScoped$ } from '@builder.io/qwik';
import CSS from './index.css?inline';
const Tab = component$(() => {
useStylesScoped$(CSS);
return (
<section>
<h2>
<Slot name="title" />
</h2>
<div>
<Slot /> {/* default slot */}
<div>
<Slot name="footer" />
</div>
</div>
</section>
);
});
export default component$(() => {
return (
<Tab>
<div q:slot="title">Qwik</div>
<div>A resumable framework for building instant web applications</div>
<span q:slot="footer">made with ❤️ by </span>
<a q:slot="footer" href="https://builder.io">
builder.io
</a>
</Tab>
);
});
Now, when consuming the <Tab>
component, we can pass children and specify in which slot they should be placed, using the q:slot
attribute:
Notice that:
- If
q:slot
is not specified or it's an empty string, the content will be projected into the default<Slot>
, i.e. the<Slot>
without aname
property. - Multiple
q:slot="footer"
attributes coalesce items together in the content projection.
Unprojected content
Qwik keeps all content around, even if not projected. This is because the content could be projected in the future. When projected content does not match any <Slot>
component, the content is moved into an inert <q:template>
element.
import { Slot, component$, useSignal } from '@builder.io/qwik';
const Accordion = component$(() => {
const isOpen = useSignal(false);
return (
<div>
<h1 onClick$={() => (isOpen.value = !isOpen.value)}>
{isOpen.value ? '▼' : '▶︎'}
</h1>
{isOpen.value && <Slot />}
</div>
);
});
export default component$(() => {
return (
<Accordion>
I am pre-rendered on the Server and hidden until needed.
</Accordion>
);
});
Results in:
<div>
<h1>▶︎</h1>
</div>
<q:template q:slot hidden aria-hidden="true">
I am pre-rendered on the Server and hidden until needed.
</q:template>
Notice that the un-projected content is moved into an inert <q:template>
. This is done just in case the Accordion
component re-renders and inserts the <Slot>
. In that case, we avoid having 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.
import { component$ } from '@builder.io/qwik';
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>
);
});
Advanced Example
An example of a collapsible component that conditionally projects editable content.
import { Slot, component$, useSignal } from '@builder.io/qwik';
export const Collapsible = component$(() => {
const isOpen = useSignal(true);
return (
<div>
<h1 onClick$={() => (isOpen.value = !isOpen.value)}>
{isOpen.value ? '▼' : '▶︎'}
<Slot name="title" />
</h1>
{isOpen.value && <Slot />}
</div>
);
});
export default component$(() => {
const title = useSignal('Qwik');
const description = useSignal(
'A resumable framework for building instant web applications'
);
return (
<>
<label>Title</label>
<input bind:value={title} type="text" />
<label>Description</label>
<textarea bind:value={description} cols={50} />
<hr />
<Collapsible>
<span q:slot="title">{title}</span>
{description}
</Collapsible>
</>
);
});
The Collapsible
component will always display the title, but the body of the text will only display if store.isOpen
is true
.
Additionally, the title
and description
of the projected content are editable.
- The parent component needs to be able to change content without forcing the
Collapsible
component to re-render. - 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 the parent 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 forcing the re-rendering of the other.
children
Projection vs All frameworks need a way for a component to wrap its complex content conditionally. 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.