Svelte 官方入门教程(12) - 组件组成(slots)

svelte cover

上一讲是样式,整体上来说算是比较简单的一章节,今天来看下组建的组成slots(插槽)

一、插槽

就像元素可以有子节点一样…

1
2
3
<div>
<p>I'm a child of the div</p>
</div>

例子:
App.svelte

1
2
3
4
5
6
7
<script>
import Box from './Box.svelte';
</script>

<Box>
<!-- put content here -->
</Box>

Box.svelte

1
2
3
<div class="box">
<!-- content should be injected here -->
</div>

组件也是可以拥有子节点。 但是,在组件可以接受子节点之前,它需要知道将它们放在哪里。 我们使用 <slot> 元素来做到这一点。 把它放在 Box.svelte 里面:

1
2
3
<div class="box">
<slot></slot>
</div>

您现在可以将 它 放入框中:

1
2
3
4
<Box>
<h2>Hello!</h2>
<p>This is a box. It can contain anything.</p>
</Box>

学习这个可以通过类比学习,如果你写过原生的webcomponnets 组件那么你对于slot标签一定非常的熟悉,用法是一致的,当然你也可以类比Vue的slot

二、插槽缺省内容

组件可以通过将内容放在 <slot> 元素中来为任何空的插槽指定 缺省内容,还是上面的那个例子:

Box.svelte

1
2
3
4
5
<div class="box">
<slot>
<em>no content was provided</em>
</slot>
</div>

我们现在可以创建没有任何子元素的 <Box> 实例:

1
2
3
4
5
6
<Box>
<h2>Hello!</h2>
<p>This is a box. It can contain anything.</p>
</Box>

<Box/>

REPL

三、命名插槽

上一章节使用了一个默认插槽(default slot),该slot直接在组件内显示,有时候你需要对子级进行细分设置,就如这个<ContactCard>。在这种情况下,我们可以使用 命名插槽(named slots)
看例子:
App

1
2
3
4
5
6
7
<script>
import ContactCard from './ContactCard.svelte';
</script>

<ContactCard>
<!-- contact details go here -->
</ContactCard>

ContactCard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<article class="contact-card">
<h2>
<slot>
<span class="missing">Unknown name</span>
</slot>
</h2>

<div class="address">
<slot>
<span class="missing">Unknown address</span>
</slot>
</div>

<div class="email">
<slot>
<span class="missing">Unknown email</span>
</slot>
</div>
</article>

<style>
.contact-card {
width: 300px;
border: 1px solid #aaa;
border-radius: 2px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
padding: 1em;
}

h2 {
padding: 0 0 0.2em 0;
margin: 0 0 1em 0;
border-bottom: 1px solid #ff3e00
}

.address, .email {
padding: 0 0 0 1.5em;
background: 0 50% no-repeat;
background-size: 1em 1em;
margin: 0 0 0.5em 0;
line-height: 1.2;
}

.address {
background-image: url(/tutorial/icons/map-marker.svg);
}
.email {
background-image: url(/tutorial/icons/email.svg);
}
.missing {
color: #999;
}
</style>

ContactCard.svelte中,给每个标签添加name属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<article class="contact-card">
<h2>
<slot name="name">
<span class="missing">Unknown name</span>
</slot>
</h2>

<div class="address">
<slot name="address">
<span class="missing">Unknown address</span>
</slot>
</div>

<div class="email">
<slot name="email">
<span class="missing">Unknown email</span>
</slot>
</div>
</article>

然后把 slot="..." 添加到 <ContactCard> 标签中:

1
2
3
4
5
6
7
8
9
10
<ContactCard>
<span slot="name">
P. Sherman
</span>

<span slot="address">
42 Wallaby Way<br>
Sydney
</span>
</ContactCard>

命名插槽就像有了对应关系,你可以不用在乎顺序了。

四、检测插槽内容

在某些情况下,您可能希望根据父级是否传入某个插槽的内容来控制组件的某些部分。 也许您在该插槽周围有一个包装器,如果插槽为空,您不想渲染它。 或者您可能只想在插槽存在时才应用 class 名称 。 您可以通过检查特殊 $$slots 变量的属性来做到这一点。

$$slots 是一个对象,其键是父组件传入的插槽名称。 如果父级将插槽留空,则 $$slots 将没有该插槽的条目。

看例子:

App.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<script>
import Project from './Project.svelte'
import Comment from './Comment.svelte'
</script>

<h1>
Projects
</h1>

<ul>
<li>
<Project
title="Add Typescript support"
tasksCompleted={25}
totalTasks={57}
>
<div slot="comments">
<Comment name="Ecma Script" postedAt={new Date('2020-08-17T14:12:23')}>
<p>Those interface tests are now passing.</p>
</Comment>
</div>
</Project>
</li>
<li>
<Project
title="Update documentation"
tasksCompleted={18}
totalTasks={21}
/>
</li>
</ul>

<style>
h1 {
font-weight: 300;
margin: 0 1rem;
}

ul {
list-style: none;
padding: 0;
margin: 0.5rem;
display: flex;
}

@media (max-width: 600px) {
ul {
flex-direction: column;
}
}

li {
padding: 0.5rem;
flex: 1 1 50%;
min-width: 200px;
}
</style>

Project.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<script>
export let title;
export let tasksCompleted = 0;
export let totalTasks = 0;
</script>

<article class:has-discussion={true}>
<div>
<h2>{title}</h2>
<p>{tasksCompleted}/{totalTasks} tasks completed</p>
</div>
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
</article>

<style>
article {
border: 1px #ccc solid;
border-radius: 4px;
position: relative;
}

article > div {
padding: 1.25rem;
}

article.has-discussion::after {
content: '';
background-color: #ff3e00;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
height: 20px;
position: absolute;
right: -10px;
top: -10px;
width: 20px;
}

h2,
h3 {
margin: 0 0 0.5rem;
}

h3 {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}

p {
color: #777;
margin: 0;
}

.discussion {
background-color: #eee;
border-top: 1px #ccc solid;
}
</style>

请注意,此示例中的两个 <Project> 实例都呈现了一个评论容器和一个通知点,即使只有一个有评论内容。 我们希望使用 $$slots 来确保仅在父组件 <App> 传入评论插槽的内容时才渲染这些元素。

Project.svelte 中,更新 <article> 上的 class:has-discussion 指令:

1
<article class:has-discussion={$$slots.comments}>

接下来,将 <div class="discussion"> 包装在一个检查 $$slots 的 if 块中:

1
2
3
4
5
6
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}

现在,当 <App> 将评论槽留空时,评论容器和通知点将不会呈现。
可以通下面的REPL好好体会下

REPL

五、插槽属性

先看示例:
App.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
import Hoverable from './Hoverable.svelte';
</script>

<style>
div {
padding: 1em;
margin: 0 0 1em 0;
background-color: #eee;
}

.active {
background-color: #ff3e00;
color: white;
}
</style>

<Hoverable>
<div class:active={hovering}>
{#if hovering}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>

Hoverable.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let hovering;

function enter() {
hovering = true;
}

function leave() {
hovering = false;
}
</script>

<div on:mouseenter={enter} on:mouseleave={leave}>
<slot></slot>
</div>

在当前程序中,我们有一个名为 <Hoverable>的组件来跟踪鼠标是否放在上面,它需要将数据传回父组件,以便我们可以渲染插入的内容。

为此,我们使用 *插槽属性(slot props)*。将Hoverable.sveltehovering的值传递给它的slot

1
2
3
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot hovering={hovering}></slot>
</div>

请记住,你可以使用{hovering} 这种简短写法。

然后我们使用let来暴露<Hoverable> 组件内的内容:

1
2
3
4
5
6
7
8
9
<Hoverable let:hovering={hovering}>
<div class:active={hovering}>
{#if hovering}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>

如果你想在其父组件中调用active, 你也可以给该变量重命名,

1
2
3
4
5
6
7
8
9
<Hoverable let:hovering={active}>
<div class:active>
{#if active}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>

你可以根据需要拥有任意数量的组件,并在组件内部声明插槽属性。

命名插槽也可以有props(属性),但是let指令需要写在 slot="..." 标签上而不是组件上。

上面这句话我实际测试有一点问题哈,在命名插槽上let指令既可以写在slot="..." 标签上也可以写在组件上。但是插槽是不建议写在内部的标签上的。整体上来说这个使用起来感觉怪怪的,有点绕,大家可以根据下面的REPL体会下我说的问题。

REPL

总结

  • Svelte 的插槽,对比 webcomponentsVue 的插槽而言,近似度很高。可以类比学习。

  • 可以使用语法 <Component slot="name" /> 将组件放置在命名槽中。 为了在不使用包装元素的情况下将内容放置在插槽中,您可以使用特殊元素 <svelte:fragment>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- Widget.svelte -->
    <div>
    <slot name="header">No header was provided</slot>
    <p>Some content between header and footer</p>
    <slot name="footer"></slot>
    </div>

    <!-- App.svelte -->
    <Widget>
    <HeaderComponent slot="header" />
    <svelte:fragment slot="footer">
    <p>All rights reserved.</p>
    <p>Copyright (c) 2019 Svelte Industries</p>
    </svelte:fragment>
    </Widget>
  • 命名插槽也可以有props(属性),但是let指令需要写在 slot="..." 标签上而不是组件上。


Svelte 官方入门教程(12) - 组件组成(slots)
http://yoursite.com/2022/07/20/svelte-tutorial-12/
作者
昂藏君子
发布于
2022年7月20日
许可协议