Bindings
Text inputs
일반적으로, Svelte에서의 데이터 흐름은 탑-다운 형식이다. 부모 컴포넌트에서 자식 컴포넌트에 props를 전달할 수 있고, 컴포넌트는 보유한 요소들에 대해 어트리뷰트를 설정할 수 있다.
가끔, 이러한 규칙을 깨야하는 경우가 있는데, 대표적인 경우가 컴포넌트 내에 <input> 태그를 보유한 경우다. 우리는 on:input에 대한 이벤트 핸들러를 추가하여 event.target.name에 따라 별도의 상태값을 변경하도록 구성할 수 있다. 다만, 이러한 과정 자체를 매번 반복하게 되면 너무 번거롭다.
대신에, 이러한 상황에 bind:value 명령을 사용할 수 있다.
<script>
let name = 'world';
</script>
<input bind:value={name}>
<h1>Hello {name}!</h1>
bind를 사용하는 경우, input의 value값 변경에 따라 name의 값을 변경할 뿐만 아니라, name의 값이 변경됨에 따라 value값 역시 변경된다.
Numeric inputs
DOM 상에서, 모든 값들은 string 으로 다루어진다. 이런 경우, type="number" 혹은 type="range"인 상황에서, 값을 다루기에 다소 까다로워지며, 별도로 데이터 타입을 변환해주어야 한다.
Svelte에서는 bind:value를 사용하면, 알아서 이러한 과정을 처리해준다.
<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>
Checkbox inputs
체크박스들은 상태 값들을 토글링(toggling)하기 위해 사용된다. 이 경우, 이들의 상태값은 value가 아닌 checked가 되므로, 다음과 같이 사용해야 한다.
<input type=checkbox bind:checked={yes}>
Group inputs
동일한 값에 대한 여러 input들이 존재한다면, bind:group을 사용할 수 있다. 대표적으로 radio 혹은 checkbox 태그가 있다.
<script>
let scoops = 1;
let flavours = ['Mint choc chip'];
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
function join(flavours) {
if (flavours.length === 1) return flavours[0];
return `<span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.001892em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal">rs</span><span class="mord">.</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">ce</span><span class="mopen">(</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">−</span><span class="mord">1</span><span class="mclose">)</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.05724em;">j</span><span class="mord mathnormal">o</span><span class="mord mathnormal">in</span><span class="mopen"><span class="mopen">(</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.751892em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mpunct"><span class="mpunct">,</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.751892em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mclose">)</span></span><span class="mord mathnormal">an</span><span class="mord mathnormal">d</span></span></span></span>{flavours[flavours.length - 1]}`;
}
</script>
<h2>Size</h2>
<label>
<input type=radio bind:group={scoops} value={1}>
One scoop
</label>
<label>
<input type=radio bind:group={scoops} value={2}>
Two scoops
</label>
<label>
<input type=radio bind:group={scoops} value={3}>
Three scoops
</label>
<h2>Flavours</h2>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} value={flavour}>
{flavour}
</label>
{/each}
{#if flavours.length === 0}
<p>Please select at least one flavour</p>
{:else if flavours.length > scoops}
<p>Can't order more flavours than scoops!</p>
{:else}
<p>
You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'}
of {join(flavours)}
</p>
{/if}
Textarea inputs
<textarea> 요소는 <text>와 거의 동일하게 동작하며, bind:value를 사용하면 된다.
<textarea bind:value={value}></textarea>
만약, 변경할 상태의 변수명이 똑같이 value로 일치한다면, 아래와 같이 약식으로 작성할 수 있다.
<textarea bind:value></textarea>
Select bindings
<select> 요소에도 bind:value를 사용할 수 있다.
<select bind:value={selected} on:change="{() => answer = ''}">
<select> 하위에 있는 <option> 값들이 string이 아닌 object임을 주의하자.
selected에 대한 기본값을 설정하지 않았기 때문에, binding 시에 기본적으로 select는 리스트의 첫번째 값을 selected로 설정해준다.
Select multiple
<select>는 multiple 어트리뷰트를 사용할 수 있으며, 이 경우 하나 이상의 값들을 Array 형태로 받아올 수 있게 된다.
<h2>Flavours</h2>
<select multiple bind:value={flavours}>
{#each menu as flavour}
<option value={flavour}>
{flavour}
</option>
{/each}
</select>
Contenteditable bindings
contenteditable="true" 어트리뷰트를 보유한 요소들은 textContent와 innerHTML에 대해서도 바인딩을 할 수 있다.
<div
contenteditable="true"
bind:innerHTML={html}
></div>
Each block bindings
each 문 내에서도 바인딩을 할 수 있다.
<script>
let todos = [
{ done: false, text: 'finish Svelte tutorial' },
{ done: false, text: 'build an app' },
{ done: false, text: 'world domination' }
];
function add() {
todos = todos.concat({ done: false, text: '' });
}
function clear() {
todos = todos.filter(t => !t.done);
}
: remaining = todos.filter(t => !t.done).length;
</script>
<h1>Todos</h1>
{#each todos as todo}
<div class:done={todo.done}>
<input
type=checkbox
bind:checked={todo.done}
>
<input
placeholder="What needs to be done?"
bind:value={todo.text}
>
</div>
{/each}
<p>{remaining} remaining</p>
<button on:click={add}>
Add new
</button>
<button on:click={clear}>
Clear completed
</button>
<style>
.done {
opacity: 0.4;
}
</style>
이 경우 바인딩은 todos Array를 직접 변경하게 된다. 본인이 불변성을 유지하는 형태를 선호한다면, 이 경우엔 바인딩을 사용하지 말고 이벤트 핸들러를 직접 작성하는 편이 좋다.
Media elements
<audio>와 <video> 요소는 바인딩 할 수 있는 수많은 프로퍼티들이 존재한다.
아래는 바인딩 가능한 읽기 전용 프로퍼티다.
duration(readonly) — 비디오 및 오디오의 전체 시간, 초 단위buffered(readonly) — {start, end} 객체 Arrayseekable(readonly) — {start, end} 객체 Arrayplayed(readonly) — {start, end} 객체 Arrayseeking(readonly) — booleanended(readonly) — boolean
비디오의 경우는 videoWidth 및 videoHeight 읽기 전용 속성이 추가적으로 존재한다.
아래는 쌍방향으로 바인딩 가능한 프로퍼티들이다.
currentTime- 비디오 및 오디오의 현재 위치playbackRate- 비디오 및 오디오의 재생 속도,1이 기본.paused- 정지 여부volume-0부터1사이의 값muted- 음소거 여부, boolean
Dimensions
모든 블록 요소들은 clientWidth, clientHeight, offsetWidth, 그리고 offsetHeight에 대한 바인딩을 할 수 있다.
<div bind:clientWidth={w} bind:clientHeight={h}>
<span style="font-size: {size}px">{text}</span>
</div>
이들 바인딩은 읽기 전용이며, 이들 값을 직접 변경하는 것은 아무 의미가 없다.
주의 : 요소의 크기 등을 측정하기 위해서는 내부적으로 이런 테크닉을 사용한다. 이 경우, 오버헤드에 대한 우려 때문에 너무 많은 수에 대해 해당 바인딩을 사용하지 않는 것을 추천한다.
This
this에 대한 바인딩은 읽기 전용이며, 모든 요소 및 컴포넌트에 사용할 수 있다.
이는 React에서의 Ref와 유사하며, 요소 및 컴포넌트에 대한 참조값을 얻을 수 있다.
<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>
위 예시에서 canvas는 컴포넌트가 마운트되기 전까지는 undefined임에 유의하자. 때문에, 마운트 시에 해당 컴포넌트 및 요소에 특정 로직을 적용하려면 onMount 등의 라이프사이클 메서드를 사용해야 한다. 이에 대해선 추후 살펴본다.
Component bindings
DOM 요소들의 프로퍼티에 대해 바인딩을 할 수 있는 것처럼, 컴포넌트의 props에 대해서도 바인딩을 할 수 있다.
<Keypad bind:value={pin} on:submit={handleSubmit}/>
이 경우, 하위 컴포넌트에서 상태 변화가 일어나면, 바인딩을 적용한 부모 요소에 대해서도 이에 따른 변경이 일어난다.
주의 : 가능하다면 컴포넌트 바인딩은 사용하지 않는 편이 좋다. 애플리케이션 규모가 커지면서, 데이터 흐름이 너무 많아지는 경우 이에 대한 추적이 어려울 수 있다. 이러한 문제는 single souce of truth가 없는 경우에 더욱 심각해진다.