【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内置属性
相关推荐
大圣编程33 分钟前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang34 分钟前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆1 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜2 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞3 小时前
异步HttpModule的实现方式
java·服务器·前端
丹宇码农6 小时前
把 HLS 字幕玩出花:zwPlayer 如何让 M3U8 视频支持全文搜索、翻译与码率自适应
前端·javascript·音视频·hls·视频播放器
2501_943782356 小时前
【共创季稿事节】猜数字游戏:二分法思维与交互式反馈
前端·游戏·microsoft·harmonyos·鸿蒙·鸿蒙系统
GV191rLvq6 小时前
基于Socket实现的最简单的Web服务器【ASP.NET原理分析】
服务器·前端·asp.net
吠品6 小时前
LangChain 里 tool_call_id 为空?一次 MCP 工具集成的排查记录
前端