Content and slots
You pass content into another component, such as a title into a Panel, and that content uses Vue data: a v-text, a @click, a v-model. This page shows which component's data the content reads in the browser, and how to keep it working when a component moves content around.
A slot is the place in a component's template, written <c-slot>, where the content goes. A fill is the content you pass, written <c-fill> inside the component's tag. Slots covers both in full.
Understand slot scope
A fill reads Vue values from the template you wrote it in. A slot's fallback, the content inside <c-slot> that shows when nothing fills it, reads values from the component that defines the slot:
<c-Panel>
<c-fill name="title">
<span v-text="pageTitle"></span>
</c-fill>
</c-Panel>
Here pageTitle comes from the outer component, the one whose template has the <c-Panel> tag. Panel's own values are not visible inside the fill. What a fill can read covers the same rule for Python expressions.
v-if around a slot
<c-slot> accepts no Vue syntax, and the template fails when it loads. Its attributes other than name and required become data that Python passes to the fill, so a v-if there could never reach the browser. Put v-if on a <template> around the slot, or v-show on an element around it:
<template v-if="expanded">
<c-slot name="details" />
</template>
<template #name> slots
Vue fills a named slot with <template #name> or <template v-slot:name>. Inside a Citry component tag, the template fails when it loads, on every page, because Citry fills slots only from <c-fill>. Without the check, the content would land in the default slot and the named slot would keep its fallback.
<!-- Fails: Vue slot syntax inside a Citry component tag. -->
<c-Panel>
<template #title>Report</template>
</c-Panel>
<!-- Works: name the slot with c-fill. -->
<c-Panel>
<c-fill name="title">Report</c-fill>
</c-Panel>
The same goes for v-slot and #default, which name the default slot, for slot syntax on another element, such as <div #title>, and for #[expr]; write <c-fill c-name="..."> with a Python expression instead.
The check covers Citry component tags, including <c-component>, and looks through <c-provide> and <c-cache> to the tag around them. <template #name> still works inside a Vue component that a Vue plugin registers, and inside <c-element is="my-widget">, which renders the tag you name, so it can be a plugin's Vue component.
Directly inside a <c-fill>, <template #name> loads, because a transparent component (one with transparent = True, such as <c-provide>) can pass that content on to a Vue component that Citry cannot see when the template loads. Inside an ordinary component's fill it still does nothing: a page with Vue drops the <template> wrapper and shows its content in that fill, and a page without Vue shows nothing.
To pass a slot template through a transparent component of your own, write its default fill out:
<my-widget>
<c-Themed>
<c-fill name="default">
<template #header>Report</template>
</c-fill>
</c-Themed>
</my-widget>
Scoped slots
A component can hand Python data to a fill, but not its Vue values. The Python data is computed at render time, for example each item of a list. Write it as attributes on <c-slot>, plain or with c-:
{# Inside ItemList #}
<c-for each="item in items">
<li><c-slot name="item" c-item="item" /></li>
</c-for>
The fill names the data with data="..." and reads it in Python expressions, such as {{ ... }} or a c-* attribute:
<c-ItemList c-items="items">
<c-fill name="item" data="row">
<span c-title="row.item['name']">
{{ row.item['name'] }}
</span>
</c-fill>
</c-ItemList>
See SlotData for fills for typing and unpacking that data.
A component cannot pass its Vue values to a fill. Vue calls this a scoped slot, and Citry does not support it:
- A
:binding on<c-slot>, such as:selected="selected", fails when the template loads. v-slotand#namefail on a component tag, on<c-fill>, and on a<template>inside a component tag; see<template #name>slots.- A Vue expression inside the fill cannot read the Python
roweither. The editor andcitry checkreport it:citry.vue.python-variableorcitry.vue.unknown-variable. Use ac-*attribute, as inc-titleabove.
When the content needs a value that changes in the browser, use one of these instead:
- Render that part in the component. Move the markup that reads the value into the component's template, or into the slot's fallback, which reads the component's own values. Pass what the page decides as a prop.
- Send the value up with an event. The component emits the value, the page stores it in its own data, and the fill reads it from there. See Listen to child events.
Vue data inside CTabs
Vue data reaches content through slots, which follow where the content is written. Some components move content somewhere else while the server renders the page. When that happens, the Vue data of the component that wrote the content can no longer reach it.
CTabs from the Citry UI library is one of them: it takes the content of each <c-CTab> and <c-CTabPanel> inside it and renders it in its own tab list and panels. So rendering fails when a <c-CTab> whose content reads Vue data is written in a separate component that you place inside <c-CTabs>.
What you write: Page holds the tabs, and TabLabels writes a tab whose label is its own Vue data:
from citry import Component
class Page(Component):
template = """
<c-CTabs
default_value="one"
aria_label="Sections"
>
<c-TabLabels />
<c-CTabPanel value="one">Details</c-CTabPanel>
</c-CTabs>
"""
class TabLabels(Component):
template = """
<c-CTab value="one">
<span v-text="label"></span>
</c-CTab>
"""
js = """
$component({
data() {
return { label: "Overview" };
},
});
"""
What CTabs does: while the page renders on the server, CTabs collects every <c-CTab> and <c-CTabPanel> inside its tag, including those that a component such as TabLabels writes. It moves each tab's content into a tab button in the CTabs template, and each panel's content into a panel. Nothing appears where you wrote <c-CTab>. Simplified, CTabs renders:
<div role="tablist">
<button role="tab">
<!-- moved here from TabLabels -->
<span v-text="label"></span>
</button>
</div>
Why it fails: in the browser, the <span> now sits in the CTabs template, so Vue looks for label in the data of CTabs. But label belongs to TabLabels, which is inside CTabs, because Page wrote <c-TabLabels /> inside <c-CTabs>. Vue data does not pass from a component out to the component around it. Without a check, the label would show nothing, so Citry stops the render with an error instead. The error names the component that wrote the tab (TabLabels), the line, the Vue code it found (v-text="label" on <span>), and CTabs as the component that moves the content.
What works: write the <c-CTab> in the component that holds <c-CTabs>, and define label there:
from citry import Component
class Page(Component):
template = """
<c-CTabs
default_value="one"
aria_label="Sections"
>
<c-CTab value="one">
<span v-text="label"></span>
</c-CTab>
<c-CTabPanel value="one">Details</c-CTabPanel>
</c-CTabs>
"""
js = """
$component({
data() {
return { label: "Overview" };
},
});
"""
This works because Page contains CTabs. Vue can pass content, together with the data it reads, into a component inside the one that wrote it. That is the slot scope rule from the top of this page.
The rule: content inside a <c-CTab> or <c-CTabPanel> that uses Vue data, a Vue event listener, v-model, a ref, or an @c-* binding must be written in the component that holds <c-CTabs>. Python values such as {{ title }} work from any component, because the server fills them in before CTabs moves anything.
To keep writing the tab in TabLabels, set transparent = True on TabLabels. Citry then treats its template as if Page had written it, so define label in Page. If label stays in TabLabels, Citry raises no error, but the tab does not get that value.
Other Citry UI components that collect their item tags this way follow the same rule: CStepper, CTimeline, CTour, CSortable, CSplitter, CTransferList, CVirtualList, and CFormCollection.
See also
- Slots for
<c-slot>,<c-fill>, fallbacks, and slot data in Python. - Props and events for passing browser values between components.
- Server-rendered HTML for slots that Vue builds in the browser.