Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slots #88

Merged
merged 3 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmark/Real.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<Layout title="Hello">
<Card>
<Greeting :message="message" />
<Greeting message={{ message }} />
<button type="button">Close</button>
</Card>
</Layout>
4 changes: 2 additions & 2 deletions docs/components/Home.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@
<div>
<Card div="So clean">
{% for product in products %}
<Product :product="product" />
<Product product={{ product }} />
{% endfor %}
</Card>
</div>
</div>
<Paginator :items="products" />
<Paginator items={{ products }} />
</Layout>
```
{% endraw %}{% endfilter %}
Expand Down
4 changes: 2 additions & 2 deletions docs/components/SocialCardIndex.jinja
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{#def page #}
<Layout
:title="page.title"
:description="page.description"
title={{ page.title }}
description={{ page.description }}
>
<style>
body {
Expand Down
7 changes: 7 additions & 0 deletions docs/components/guide/SlotsModal.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% filter markdown %}{% raw %}
```html+jinja
<dialog class="modal">
{{ content }}
</dialog>
```
{% endraw %}{% endfilter %}
7 changes: 7 additions & 0 deletions docs/components/guide/SlotsModalBody.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% filter markdown %}{% raw %}
```html+jinja
<div class="modal-body">
{{ content }}
</div>
```
{% endraw %}{% endfilter %}
7 changes: 7 additions & 0 deletions docs/components/guide/SlotsModalFooter.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% filter markdown %}{% raw %}
```html+jinja
<footer class="modal-footer">
{{ content }}
</footer>
```
{% endraw %}{% endfilter %}
10 changes: 10 additions & 0 deletions docs/components/guide/SlotsModalHeader.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% filter markdown %}{% raw %}
```html+jinja
<header class="modal-header>
<h2 class="modal-title">
{{ content }}
</h2>
<CloseButton />
</header>
```
{% endraw %}{% endfilter %}
17 changes: 9 additions & 8 deletions docs/content/guide/components.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
title: Components
description: All about declaring and using components.
description: Declaring and using components.
---

<Header title="Components">
</Header>

## Declaring and Using Components

The components are simple text files that look like regular Jinja templates, with three requirements:
Expand Down Expand Up @@ -181,18 +182,18 @@ A great use case of the `content` is to make layout components:

<ExampleTabs
prefix="comp-layouts"
:panels="{
panels={{ {
'ArchivePage.jinja': 'guide.CompArchive',
'Layout.jinja': 'guide.CompLayout',
}"
} }}
/>

Everything between the open and close tags of the components will be rendered and passed to the `Layout` component as an implicit `content` variable.

To test a component in isolation, you can also manually send a content argument using the special `__content` argument:
To test a component in isolation, you can also manually send a content argument using the special `_content` argument:

```python
catalog.render("PageLayout", title="Hello world", __content="TEST")
catalog.render("PageLayout", title="Hello world", _content="TEST")
```

## Extra Arguments
Expand Down Expand Up @@ -288,15 +289,15 @@ are sorted by name and rendered like this:
<Callout type="warning">
Using `<Component {{ attrs.render() }}>` to pass the extra arguments to other components **WILL NOT WORK**. That is because the components are translated to macros before the page render.

You must pass them as the special argument `__attrs`.
You must pass them as the special argument `_attrs`.

```html+jinja
{#--- WRONG 😵 ---#}
<MyButton {{ attrs.render() }} />

{#--- GOOD 👍 ---#}
<MyButton __attrs={{ attrs }} />
<MyButton :__attrs="attrs" />
<MyButton _attrs={{ attrs }} />
<MyButton :_attrs="attrs" />
```
</Callout>

Expand Down
156 changes: 156 additions & 0 deletions docs/content/guide/slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: Slots / Content
description: Working with content in components.
---

<Header title="Slots / Content">
Besides attributes, components can also accept content to render inside them.
</Header>

This is a very common pattern, and it is called a **_slot_**. A slot is a placeholder for content that can be provided by the user of the component. For example, we may have a `<FancyButton>` component that supports usage like this:

```html+jinja
<FancyButton>
<i class="icon></i> Click me!
</FancyButton>
```

The template of `<FancyButton>` looks like this:

```html+jinja
<button class="fancy-btn">
{{ content }}
</button>
```

![slot diagram](/static/img/slots-diagram.png)


The `<FancyButton>` is responsible for rendering the outer `<button>` (and its fancy styling), while the inner content is provided by the parent component.

<Callout type="info">
The `content` variable is available in the template of the component automatically, you don't need to declare it.
</Callout>


## Fallback Content

There are cases when it's useful to specify fallback (i.e. default) content for a slot, to be rendered only when no content is provided. For example, in a `<SubmitButton>` component:

```html+jinja
<button type="submit">
{{ content }}
</button>
```

We might want the text "Submit" to be rendered inside the `<button>` if the parent didn't provide any slot content. The special "content" variable is just a string like any other, so we can test if it's empty to make "Submit" the fallback content:

```html+jinja
<button type="submit">
{% if content %}
{{ content }}
{% else %}
Submit <!-- fallback content -->
{% endif %}
</button>
```

Now when we use `<SubmitButton>` in a parent component, providing no content for the slot:

```html+jinja
<SubmitButton />
```

This will render the fallback content, "Submit":

```html
<button type="submit">Submit</button>
```

But if we provide content:

```html+jinja
<SubmitButton>Save</SubmitButton>
```

Then the provided content will be rendered instead:

```html
<button type="submit">Save</button>
```

.


## Multiple content slots (a.k.a. "named slots")

There are cases when a component is complex enough to need multiple content slots. For example, a `<Modal>` component might need a `header`, a `body`, and a `footer` content.

One way to implement it is using multiple content slots. To do so, instead of rendering `content` as a string, you can also _call_ it with name. Then, the parent component can provide a content _for_ that name.

![_slot variable](/static/img/slots-_slot.png)

Note the `_slot` special variable. This is automatically available in the content in the parent component and contains the named the component has used to call request its content.

The `_slot` variable is scoped to the content of that component, so it's not available outside of it:

```html+jinja hl_lines="2 7 11"
<FancyButton>
{% if _slot == "hi" %} {# <--- _slot #}
Hello{% endif %}
</FancyButton>

<FancyButton2>
{% if _slot == "hi" %} {# <--- This _slot is a different one #}
Sup?{% endif %}
</FancyButton2>

{{ _slot }} {# <--- Undefined variable #}
```

.


## Composability: and alternative to named slots

Named slots are a quick way to have multiple content slots, but are a bit messy beyond some simple cases.

Composability offers a more flexible and idiomatic approach when multiple content slots are needed. The idea is to have separated components for each content slot, and then compose them together.

This pattern allows allows for more reusable components. Let's explore this concept using the same example as above.

Consider a `Modal` component that requires three distinct sections: a header, a body, and a footer. Instead of using named slots, we can create separate components for each section and composing them within a `Modal` component wrapper.

```html+jinja hl_lines="3-4 7 10-11"
<Modal>
<ModalHeader>
<i class="icon-rocket"></i>
Hello World!
</ModalHeader>
<ModalBody>
<p>The modal body.</p>
</ModalBody>
<ModalFooter>
<button>Cancel</button>
<button>Save</button>
</ModalFooter>
</Modal>
```

Now, the `Modal` component is responsible for rendering the outer `<dialog>` and its styling, while the inner content is provided by the child components.

<ExampleTabs
prefix="demo"
:panels="{
'Modal.jinja': 'guide.SlotsModal',
'ModalHeader.jinja': 'guide.SlotsModalHeader',
'ModalBody.jinja': 'guide.SlotsModalBody',
'ModalFooter.jinja': 'guide.SlotsModalFooter',
}"
/>

### Advantages of Composability

- **Flexibility**: You can easily rearrange, omit, or add new sections without modifying the core `Modal` component.
- **Reusability**: Each section (`ModalHeader`, `ModalBody`, `ModalFooter`) can be used independently or within other components.
- **Maintainability**: It's easier to update or style individual sections without affecting the others.
12 changes: 6 additions & 6 deletions docs/content/ui/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ description: Easily create accessible, fully customizable tab interfaces, with r

<ExampleTabs
prefix="demo"
:panels="{
panels={{ {
'Result': 'ui.Tabs.DemoResult',
'HTML': 'ui.Tabs.DemoHTML',
'CSS': 'ui.Tabs.DemoCSS',
}"
} }}
/>

Tabs are built using the `TabGroup`, `TabList`, `Tab`, and `TabPanel` components. Clicking on any tab or selecting it with the keyboard will activate the corresponding panel.
Expand Down Expand Up @@ -47,10 +47,10 @@ Remember to add styles to the `:focus` state of the tab so is clear to the user

<ExampleTabs
prefix="manual"
:panels="{
panels={{{
'HTML': 'ui.Tabs.ManualHTML',
'Result': 'ui.Tabs.ManualResult',
}"
} }}
/>

The manual prop has no impact on mouse interactions — tabs will still be selected as soon as they are clicked.
Expand All @@ -62,10 +62,10 @@ If you've styled your `TabList` to appear vertically, use the `vertical` attribu

<ExampleTabs
prefix="vertical"
:panels="{
panels={{ {
'HTML': 'ui.Tabs.VerticalHTML',
'Result': 'ui.Tabs.VerticalResult',
}"
} }}
/>


Expand Down
1 change: 1 addition & 0 deletions docs/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
[
"guide/index.md",
"guide/components.md",
"guide/slots.md",
"guide/css_and_js.md",
# "guide/integrations.md",
# "guide/performance.md",
Expand Down
Binary file added docs/static/img/slots-_slot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/static/img/slots-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion docs/static/prose.css
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,11 @@ pre a {
color: rgb(82 82 91);
font-size: 0.75rem;
}
.highlight .hll {
background-color: #333;
display: block;
}

.highlight .hll { background-color: rgba(0, 0, 0, 0.9) }
.highlight .c { color: hsl(31, 76%, 64%) } /* Comment */
.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
.highlight .k { color: #66d9ef } /* Keyword */
Expand Down
9 changes: 4 additions & 5 deletions docs/theme/ExampleTabs.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@
{%- for text in panels.keys() %}
<Tab
class="example-tab"
:target="'ex-%s-%s' % (prefix, loop.index)""
:selected="loop.index == 1"
);"
target={{ "ex-%s-%s" % (prefix, loop.index) }}
selected={{ loop.index == 1 }}
>{{ text }}</Tab>
{%- endfor %}
</TabList>
{%- for name in panels.values() %}
<TabPanel
class="example-tabpanel"
:id="'ex-%s-%s' % (prefix, loop.index)"
:hidden="loop.index != 1"
id={{ "ex-%s-%s" % (prefix, loop.index) }}
hidden={{ loop.index != 1 }}
>
{{ catalog.irender(name) }}
</TabPanel>
Expand Down
2 changes: 1 addition & 1 deletion docs/theme/Layout.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<html lang="{{ page.lang }}" class="light">
<head>
<meta charset="utf-8">
<MetaTags :page="page" />
<MetaTags page={{ page }} />
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="apple-touch-icon" sizes="120x120" href="/static/img/apple-touch-icon.png">
<link rel="stylesheet" href="/static/theme.css?v={{ utils.timestamp }}">
Expand Down
2 changes: 1 addition & 1 deletion docs/theme/NavGlobal.jinja
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div {{ attrs.render(class="cd-nav-global scrollbar-thin", data_component="NavGlobal") }}>
<Toc :toc="nav.toc" :page="page" />
<Toc toc={{ nav.toc }} page={{ page}} />
</div>
Loading
Loading