Web과 프로그래밍 언어/JavaScript

[SVELTE] Component composition - Slots, Slot fallbacks, Named slots, Checking for slot content, Slot props

cosmicgy 2023. 3. 17. 09:27

 

Component composition

 

1. Slots

 

HTML 요소가 자식 요소를 가질 수 있듯이 컴포넌트 slot을 사용하면 자식요소를 가질 수 있다.

<!-- App.svelte -->
<script>
    import Box from './Box.svelte';
</script>

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

<!-- Box.svelte -->
<div class="box">
    <slot></slot>
</div>

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

App.svelte의 <Box> 의 자식 요소들이 Box.svelte의 <slot> 위치에 나타난다.

 

2. Slot fallbacks

 

<!-- App.svelte -->
<script>
    import Box from './Box.svelte';
</script>

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

<Box/>

<!-- Box.svelte -->
<div class="box">
    <slot>
        <em>no content was provided</em>
    </slot>
</div>

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

App.svelte 의 <Box> 의 자식요소가 없는 경우, Box.svelte 의 <slot> 안의 요소가 화면에 출력된다

  • fallback : 정확한 경로 선택 방식을 사용해서는 적절한 경로를 선택할 수 없을 때 최소 수준의 속성에 일치하는 경로를 찾아내기 위해 ATM 네트워크가 사용하는 메커니즘

 

3. Named slots

 

슬롯에 이름을 붙여 사용하면 원하는 위치에 요소를 배치할 수 있다.

<!-- App.svelte -->
<script>
    import ContactCard from './ContactCard.svelte';
</script>

<ContactCard>
    <span slot="name">
        P. Sherman
    </span>

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

<!-- ContactCard.svelte -->
<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>

<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 0 no-repeat;
        background-size: 20px 20px;
        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>

App.svelte에 slot="email"을 사용하지 않았기 때문에 ContactCard.svelte의 <slot name="email">은 기본 요소가 나타나게 된다.

 

4. Checking for slot content

 

<!-- App.svelte -->
<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>

<!-- Comment.svelte -->
<script>
    export let name;
    export let postedAt;

    $: avatar = `https://ui-avatars.com/api/?name=${name.replace(/ /g, '+')}&rounded=true&background=ff3e00&color=fff&bold=true`;
</script>

<article>
    <div class="header">
        <img src={avatar} alt="" height="32" width="32">
        <div class="details">
            <h4>{name}</h4>
            <time datetime={postedAt.toISOString()}>{postedAt.toLocaleDateString()}</time>
        </div>
    </div>
    <div class="body">
        <slot></slot>
    </div>
</article>

<style>
    article {
        background-color: #fff;
        border: 1px #ccc solid;
        border-radius: 4px;
        padding: 1rem;
    }

    .header {
        align-items: center;
        display: flex;
    }

    .details {
        flex: 1 1 auto;
        margin-left: 0.5rem
    }

    h4 {
        margin: 0;
    }

    time {
        color: #777;
        font-size: 0.75rem;
        text-decoration: underline;
    }

    .body {
        margin-top: 0.5rem;
    }

    .body :global(p) {
        margin: 0;
    }
</style>

<!-- Project.svelte -->
<script>
    export let title;
    export let tasksCompleted = 0;
    export let totalTasks = 0;
</script>

<article class:has-discussion={$$slots.comments}>
<!-- <article class:has-discussion={true}> 와 $$slots.comments 의 if 조건문 X -->
    <div>
        <h2>{title}</h2>
        <p>{tasksCompleted}/{totalTasks} tasks completed</p>
    </div>
    {#if $$slots.comments}
        <div class="discussion">
            <h3>Comments</h3>
            <slot name="comments"></slot>
        </div>
    {/if}
</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>

$$slots 에 대한 검사(조건)가 가능하다

 

5. Slot props

 

<!-- App.svelte -->
<script>
    import Hoverable from './Hoverable.svelte';
</script>

<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>

<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>

<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>

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

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

<!-- Hoverable.svelte -->
<script>
    let hovering;

    function enter() {
        hovering = true;
    }

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

<div on:mouseenter={enter} on:mouseleave={leave}>
    <slot hovering={hovering}></slot>
</div>
  • <slot hovering={hovering}></slot> 와 같이 상위 컴포넌트로 전달할 값을 선언해준다. hovering은 상위로 전달할 변수 명이고, {hovering}은 상위로 전달할 변수이다. 두 개의 이름이 hovering으로 동일하기 때문에 <slot {hovering}></slot>와 같은 약어로 작성할 수 있다.
  • <Hoverable let:hovering={active}> 와 같이 let 디렉티브를 사용해서 값을 전달받을 수 있다.
  • 이렇게 전달된 값은 로컬 방식으로 동작하기 때문에 별도의 변수를 정의하지 않아도된다.