【Svelte从入门到精通】入门篇——插槽

当我们将功能封装成一个组件并引用时,通常的写法如下:

html 复制代码
<Component />

然而一些情况下,我们希望我们的组件可以支持如下功能:

html 复制代码
<Component>我的自定义内容</Component>

一个最常见的场景便是UI组件中的Modal弹窗组件。弹窗的弹出隐藏是通用的,但是弹窗内的内容则是五花八门,完全由用户自己定义,而这种能够往组件内部插入自定义内容的模块,我们称之为"插槽"。

默认插槽

在Vue中,使用 slot标签来表示这是一个插槽。在React当中,由于使用JSX语法,万物都可当成js来写,所以没有像Vue一样的特定标签,可以直接从 props.children拿到组件的插值。而Svelte和Vue一样,提供了特定的标签slot来放置插槽内容。

写法如下:

html 复制代码
<slot><!-- 可选回调 --></slot>

我们声明两个组件Child.svelte和Father.svelte

html 复制代码
<!-- Child.svelte -->
<slot>child content</slot>
html 复制代码
<!-- Father.svelte -->
<script>
  import Child from './Child.svelte';
</script>

<Child></Child>

此时页面上我们看见的是"child content"这个字符串内容。因为如果<slot>内部有内容,当我们没有在外部没有使用到slot时,这部分内容便会作为默认值展示。

如果我们往Child组件内添加一下内容:

html 复制代码
<Child>
  father content
</Child>

那么我们会看到,原来slot内提供的默认内容已被替换成了外部组件的内容"father content"。

具名插槽

有时候我们只希望用户能够替换组件功能的某一部分内容,比如一个input输入框,我们允许用户替换prefix和suffix:

亦或者是一个Modal模态框,我们允许用户使用组件写好的header和footer,也允许用户自定义头部和底部,那么这时候,我们便需要使用到带名字的插槽,即具名插槽。

html 复制代码
<slot name="x"><!-- 可选回调 --></slot>

举个例子:

html 复制代码
<script>
  // Child.svelte
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  export let show;

  let dialogRef;

  $: if (dialogRef && show) {
    dialogRef.showModal();
  }
  
  $: if (dialogRef && !show) {
    dialogRef.close();
  }
  
  const onClose = () => {
    dialogRef.close();
    show = false;
  };
</script>

<dialog bind:this={dialogRef}>
  <header>
    <slot name="header">
      header
      <span on:click|stopPropagation={onClose}>close</span>
    </slot>
  </header>
  <main>
    <slot>body</slot>
  </main>

  <footer><slot name="footer">footer</slot></footer>
</dialog>

<style>
  dialog {
    width: 300px;
    height: 250px;
    border: 1px solid black;
  }

  header {
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  main {
    height: 150px;
  }

  footer {
    height: 50px;
  }

  dialog::backdrop {
    background-image: linear-gradient(
      45deg,
      magenta,
      rebeccapurple,
      dodgerblue,
      green
    );
    opacity: 1;
  }
</style>
html 复制代码
<script>
  import Modal from './Child.svelte';
  let show = false;

  const onOpen = () => {
    show = true;
  }
</script>

<Modal bind:show={show}>
</Modal>
<button on:click={onOpen} >open</button>

页面展示效果如下:

当我们自定义slot内容时:

html 复制代码
<script>
  import Modal from './Child.svelte';
  let show = false;

  const onOpen = () => {
    show = true;
  }

  const onClose = () => {
    show = false;
  }
</script>

<Modal bind:show={show}>
  <div on:click={onClose} slot="header"> custom header </div>
  <div slot="footer">custom footer</div>
  custom body content
</Modal>
<button on:click={onOpen} >open</button>

那么我们可以得到如下效果:

传值

通过prop属性将值传回父级,父级使用 let:指令将值暴露到slot模板。

html 复制代码
<slot prop={value}></slot>

一个非常经典的例子:在表格单元格中,获取到对应的数据项的值。

由于在Svelte5中方能支持动态slot,因此实现表格的传值,我们只能如下写:

html 复制代码
<script>
  // Child.svelte
  export let columns = [];
  export let data = [];
</script>

<table >
  <thead>
    <tr>
    {#each columns as column}
      <th>{column.name}</th>
    {/each}
    </tr>
  </thead>
  <tbody>
    {#each data as row}
      <tr>
        <slot {row}></slot>
      </tr>
    {/each}
  </tbody>
</table>

<style>
  table, th {
    border: 1px solid black;
  }
  :global(td) {
    border: 1px solid black;
  }
</style>
html 复制代码
<script>
  // Father.svelte
  import Table from "./Child.svelte";

  let columns = [
    {
      key: "id",
      name: "id",
    },
    {
      key: "name",
      name: "姓名",
    },
    {
      key: "age",
      name: "年龄",
    },
  ];

  let data = [
    {
      id: 1,
      name: "张三",
      age: 18,
    },
    {
      id: 2,
      name: "李四",
      age: 19,
    },
  ];
</script>

<Table {data} {columns}>
  <svelte:fragment let:row>
    <td>{row.id}</td>
    <td>{row.name}</td>
    <td>{row.age}</td>
  </svelte:fragment>
</Table>

通常适用的速记规则:let:item 等效于let:item={item},并且<slot {item}>等效于<slot item={item}>

$$slots

$$slots是一个对象,它的键是父级传递到组件中的插槽的名称。如果父级没有传入具有特定名称的插槽,那么该名称不会出现在$$slots中。 我们可以使用这一特性来判断父级是否有传入特定名称的插槽内容:

html 复制代码
{#if $$slots.header}
  <slot name="header"></slot>
{/if}

继续以上面的Modal组件为例,我们在组件内试着打印出$$slotsconsole.log('$$slots', $$slots, $$props);:

我们可以看到,$$slots是一个对象,default可以表示默认的slot,其他key表示其他具名插槽。如果使用了对应的slot,对应的key的值便是true,反之则为false。因此,如果我们想像React和Vue那样劫持slots的内容并重新定义,那是不可能了。

比如Antd的Space组件,作用是把被其包裹的子元素间隔开。其实现核心就是对slot的内容添加子元素样式,这些样式能够添加间距。

Ant-Design的Space组件的源码内容:

javascript 复制代码
const childNodes = toArray(children, { keepEmpty: true });

const nodes = childNodes.map((child, i) => {
  if (child !== null && child !== undefined) {
    latestIndex = i;
  }

  const key = (child && child.key) || `${itemClassName}-${i}`;

  return (
    <Item
      className={itemClassName}
      key={key}
      direction={direction}
      index={i}
      marginDirection={marginDirection}
      split={split}
      wrap={wrap}
    >
      {child}
    </Item>
  );
});

Ant-Design-Vue的Space组件的源码内容:

javascript 复制代码
const children = slots.default?.();
const items = filterEmpty(children);
const len = items.length;

const itemClassName = `${prefixCls.value}-item`;
const horizontalSizeVal = horizontalSize.value;
const latestIndex = len - 1;
return (
  <div>
    {items.map((child, index) => {}
  </div>
)

那如果Svelte想要实现此类功能应该如何操作?我们留到后文《指令》中进行讲解。

小结

本章我们学习了:

  • 插槽的概念。通过<slot></slot>可以接收外部传递的页面内容。
  • 指定名称的插槽。在定义时通过<slot name="x">来指定具体名称的插槽,在使用时通过在html标签内添加slot属性(<div slot="x">)的形式来调用。
  • slot的传值。通过prop属性将值传回父级,父级使用 let:指令将值暴露到slot模板。
  • Svelte提供的$$slots内置属性
相关推荐
迷雾漫步者35 分钟前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试