通常,Svelte 中的数据流是 自上而下,父组件可以在子组件上设置 props,而组件可以在元素上设置属性,但反之则不行。
App.svelte
<script>
let name = 'world';
script>
<input value={name}>
<h1>Hello {name}!h1>
某些情况亟需打破这种规则。以组件中的
元素为例,我们 可以 为其添加 on:input
事件处理程序,在该程序中将 name
的值设置为 event.target.value
,不过这样写法有点拖沓累赘,以后在表单元素的情况愈加苦不堪言。
好在,我们可以使用 bind:value
指令:
<input bind:value={name}>
这意味着更改name的值时会更新 input 的值,反之亦然(双向绑定)。
App.svelte
<script>
let a = 1;
let b = 2;
script>
<label>
<input type=number value={a} min=0 max=10>
<input type=range value={a} min=0 max=10>
label>
<label>
<input type=number value={b} min=0 max=10>
<input type=range value={b} min=0 max=10>
label>
<p>{a} + {b} = {a + b}p>
在 DOM 中的值都是字符串。不能很好地帮助你处理数字输入(type="number"
和 type="range"
),这需要你记得在使用 input.value
之前先将其强制转换一下。
使用 bind:value,Svelte
会为你排忧解难:
<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>
App.svelte
<script>
let yes = false;
script>
<label>
<input type=checkbox checked={yes}>
Yes! Send me regular email spam
label>
{#if yes}
<p>Thank you. We will bombard your inbox and sell your personal details.p>
{:else}
<p>You must opt in to continue. If you're not paying, you're the product.p>
{/if}
<button disabled={!yes}>Subscribebutton>
复选框可用于状态之间的切换,但不是绑定到input.value
,而是input.checked
:
<input type=checkbox bind:checked={yes}>
当你有多个输入框需要关联到同一个值的时候,则可以将bind:group与value属性一起使用。同一组的单选框(Radio)之间是互斥的;而同一组复选框中被选中的值会汇集到一个数组中。
App.svelte
<script>
let scoops = 1;
let flavours = ['Mint choc chip'];
function join(flavours) {
if (flavours.length === 1) return flavours[0];
return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
}
script>
<h2>Sizeh2>
<label><input type=radio group={scoops} value={1}>One scooplabel>
<label><input type=radio group={scoops} value={2}>Two scoopslabel>
<label><input type=radio group={scoops} value={3}>Three scoopslabel>
<h2>Flavoursh2>
<label>
<input type=checkbox group={flavours} value="Cookies and cream">Cookies and cream
label>
<label>
<input type=checkbox group={flavours} value="Mint choc chip">Mint choc chip
label>
<label>
<input type=checkbox group={flavours} value="Raspberry ripple">Raspberry ripple
label>
{#if flavours.length === 0}
<p>Please select at least one flavourp>
{: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}
给每个输入框都加上bind:group
:
<input type=radio bind:group={scoops} value={1}>
在这个例子中,我们可以通过将复选框放在each块中以简化代码,首先,在
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
…然后,替换掉第二部分 Flavours 的代码:
<h2>Flavoursh2>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} value={flavour}>
{flavour}
label>
{/each}
现在,这个冰淇淋菜单可以轻松地向新的令人兴奋的方向扩展了。
我们计划先做一个简易的“MarkDown 在线编辑器”,通过 textarea 输入 markdown 代码,然后直接渲染到页面进行预览:
App.svelte
<script>
import marked from 'marked';
let value = `Some words are *italic*, some are **bold**`;
script>
<style>
textarea { width: 100%; height: 200px; }
style>
<textarea value={value}>textarea>
{@html marked(value)}
在 Svelte 中,元素的做法类似于文本输入框,也是使用bind:value:
<textarea bind:value={value}>textarea>
像下面这个例子属性名都一样,我们可以使用速写形式:
<textarea bind:value>textarea>
这种速写形式适用于所有绑定,而不仅仅是 textarea 标签。
常见的找回密码功能经常使用密码提示问题的方式,下方以“安全提示问题”为例,下拉选择框中选择安全提示问题,并给出正确答案。
App.svelte
<script>
let questions = [
{ id: 1, text: `Where did you go to school?` },
{ id: 2, text: `What is your mother's name?` },
{ id: 3, text: `What is another personal fact that an attacker could easily find with Google?` }
];
let selected;
let answer = '';
function handleSubmit() {
alert(`answered question ${selected.id} (${selected.text}) with "${answer}"`);
}
script>
<style>
input { display: block; width: 500px; max-width: 100%; }
style>
<h2>Insecurity questionsh2>
<form on:submit|preventDefault={handleSubmit}>
<select value={selected} on:change="{() => answer = ''}">
{#each questions as question}
<option value={question}>{question.text}option>
{/each}
select>
<input bind:value={answer}>
<button disabled={!answer} type=submit>Submitbutton>
form>
<p>selected question {selected ? selected.id : '[waiting...]'}p>
bind:value 可以与 <select>元素一起使用。修改 select 元素:
<select bind:value={selected} on:change="{() => answer = ''}">
值得注意的是, 的值是对象,而非字符串,Svelte 是支持这种用法的。
由于尚未初始化 selected 的值,因此绑定会将其自动设置为默认值(列表中的第一个),不过不可大意,因为在绑定初始化之前,selected 实际上仍然是未定义的,我们还是要加以判断,不能盲目地随意引用,比如 39 行中的 selected.id。
当某个选择框使用了multiple属性的时候,那么这种情况下,它的值需要绑定到一个数组而非一个简单的值。
我们再次使用第4节《输入框组 radio & checkbox》的冰淇淋的例子:
App.svelte
<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 `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
}
script>
<h2>Sizeh2>
<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>Flavoursh2>
{#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 flavourp>
{: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}
我们可以使用,我们用来代替复选框:
<h2>Flavoursh2>
<select multiple bind:value={flavours}>
{#each menu as flavour}
<option value={flavour}>{flavour}option>
{/each}
select>
在本章第5节《多行纯文本 textarea》我们实现一个简易的 Markdown 编辑器。
下方我们尝试另一种简易的编辑器实现 —— HTML编辑器,通过给元素加上 contenteditalbe 属性,元素即可获得可编辑能力:
App.svelte
<script>
let html = 'Write some text!
';
script>
<div contenteditable="true">div>
<pre>{html}pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
style>
具有contenteditable="true"属性的元素都支持绑定textContent和innerHTML:
<div
contenteditable="true"
bind:innerHTML={html}
>div>
通过 bind:innerHTML
的绑定,现在编辑 div
的内容,立即会更新 pre
元素中的 HTML。
我们先来实现一个 todo 列表的功能:
App.svelte
<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>
<style>
.done { opacity: 0.4; }
style>
<h1>Todosh1>
{#each todos as todo}
<div class:done={todo.done}>
<input type=checkbox checked={todo.done}>
<input placeholder="What needs to be done?" value={todo.text}>
div>
{/each}
<p>{remaining} remainingp>
<button on:click={add}>Add newbutton>
<button on:click={clear}>Clear completedbutton>
在 Svelte 中,你甚至可以在each块中绑定属性。
<input type=checkbox bind:checked={todo.done}>
<input placeholder="What needs to be done?" bind:value={todo.text}>
请注意,与
元素进行绑定将使数组发生变化。如果你偏好使用不可变数据,则应避免使用这类绑定,而应使用事件处理程序。
和
元素具有一些可以绑定的属性。其中部分可以参看下方示例(代码有点长):
App.svelte
<script>
// 下方这些值都绑定到 video 的属性
let time = 0;
let duration;
let paused = true;
let showControls = true;
let showControlsTimeout;
function handleMousemove(e) {
// 使控件可见,2.5秒内无任何动作则淡出
clearTimeout(showControlsTimeout);
showControlsTimeout = setTimeout(() => showControls = false, 2500);
showControls = true;
if (!(e.buttons & 1)) return; // mouse not down
if (!duration) return; // video not loaded yet
const { left, right } = this.getBoundingClientRect();
time = duration * (e.clientX - left) / (right - left);
}
function handleMousedown(e) {
// 我们不使用内置的click事件而是自行监听,因为它会在拖动后才触发
function handleMouseup() {
if (paused)
e.target.play();
else
e.target.pause();
cancel();
}
function cancel() {
e.target.removeEventListener('mouseup', handleMouseup);
}
e.target.addEventListener('mouseup', handleMouseup);
setTimeout(cancel, 200);
}
function format(seconds) {
if (isNaN(seconds)) return '...';
const minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
if (seconds < 10) seconds = '0' + seconds;
return `${minutes}:${seconds}`;
}
script>
<style>
div { position: relative; }
.controls {
position: absolute;
top: 0;
width: 100%;
transition: opacity 1s;
}
.info {
display: flex;
width: 100%;
justify-content: space-between;
}
span {
padding: 0.2em 0.5em;
color: white;
text-shadow: 0 0 8px black;
font-size: 1.4em;
opacity: 0.7;
}
.time { width: 3em; }
.time:last-child { text-align: right }
progress {
display: block;
width: 100%;
height: 10px;
-webkit-appearance: none;
appearance: none;
}
progress::-webkit-progress-bar { background-color: rgba(0,0,0,0.2); }
progress::-webkit-progress-value { background-color: rgba(255,255,255,0.6); }
video { width: 100%; }
style>
<h1>Caminandes: Llamigosh1>
<p>From <a href="https://cloud.blender.org/open-projects">Blender Open Projectsa>. CC-BYp>
<div>
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousedown={handleMousedown}>
<track kind="captions">
video>
<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
<progress value="{(time / duration) || 0}"/>
<div class="info">
<span class="time">{format(time)}span>
<span>click anywhere to {paused ? 'play' : 'pause'} / drag to seekspan>
<span class="time">{format(duration)}span>
div>
div>
div>
找到 video 元素,添加currentTime={time}、duration和paused绑定:
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMousemove}
on:mousedown={handleMousedown}
bind:currentTime={time}
bind:duration
bind:paused
>video>
bind:duration 等效于 bind:duration={duration},bind:paused 同理。
现在,当你点击视频时,将会自动更新 time、duration 和 paused。这说明我们可以基于它们构建自定义的控件。
通常你应该监听 timeupdate 事件来跟踪
currentTime
。但是这些事件较少触发,导致 UI 不稳定,Svelte 可以做得更好,它通过使用 requestAnimationFrame 来检查currentTime
。
和
的完整可以 只读 绑定的属性如下6个…
…还有5个支持 双向 绑定的属性:
元素的宽高也支持绑定,我们下方的例子是通过 range 输入框的值来指定 span 字体的尺寸,随着字体尺寸的变化,其外部包裹的元素 div 的尺寸也会变化:
App.svelte
<script>
let w;
let h;
let size = 42;
let text = 'edit me';
script>
<style>
input { display: block; }
div { display: inline-block; }
span { word-break: break-all; }
style>
<input type=range bind:value={size}>
<input bind:value={text}>
<p>size: {w}px x {h}pxp>
<div>
<span style="font-size: {size}px">{text}span>
div>
每个块级元素都可以绑定到clientWidth
、clientHeight
、offsetWidth
以及offsetHeight:
{text}
这些绑定都是只读的,修改 w
或 h
的值不会有任何效果。
使用这类测量技术,会涉及到一些开销,因此不建议在大量元素上使用。
display: inline 的内联元素是不能测量的,没有子级的元素也不能(例如 ),这种情况,你需要测量的是包裹它的外层元素。
绑定 this 与 Vue 的 ref 功能相似,都是为了取得当前组件的实际 DOM 对象,例如 canvas:
App.svelte
<script>
import { onMount } from 'svelte';
let canvas;
onMount(() => {
const ctx = canvas.getContext('2d');
let frame = requestAnimationFrame(loop);
function loop(t) {
frame = requestAnimationFrame(loop);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let p = 0; p < imageData.data.length; p += 4) {
const i = p / 4;
const x = i % canvas.width;
const y = i / canvas.height >>> 0;
const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
const b = 128;
imageData.data[p + 0] = r;
imageData.data[p + 1] = g;
imageData.data[p + 2] = b;
imageData.data[p + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
}
return () => { cancelAnimationFrame(frame); };
});
script>
<style>
canvas {
width: 100%;
height: 100%;
background-color: #666;
-webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
mask: url(svelte-logo-mask.svg) 50% 50% no-repeat;
}
style>
<canvas width={32} height={32}>canvas>
this只读绑定适用于任意元素或组件,允许你获取渲染的元素的引用。例如我们可以获取对元素的引用:
<canvas
bind:this={canvas}
width={32}
height={32}
>canvas>
请注意,在组件 mounted 之前,变量canvas的值还是undefined。因此我们需要将逻辑放在onMount这个生命周期函数(下一章将深入介绍)中。
我们尝试实现一个 Pin 码(类似密码)输入键盘组件,每输入一个字符,前面的字符全部使用星号或者实心圆代替,先是键盘组件部分:
Keypad.svelte
<script>
import { createEventDispatcher } from 'svelte';
export let value = '';
const dispatch = createEventDispatcher();
const select = num => () => value += num;
const clear = () => value = '';
const submit = () => dispatch('submit');
script>
<style>
.keypad {
display: grid;
grid-template-columns: repeat(3, 5em);
grid-template-rows: repeat(4, 3em);
grid-gap: 0.5em
}
button { margin: 0 }
style>
<div class="keypad">
<button on:click={select(1)}>1button>
<button on:click={select(2)}>2button>
<button on:click={select(3)}>3button>
<button on:click={select(4)}>4button>
<button on:click={select(5)}>5button>
<button on:click={select(6)}>6button>
<button on:click={select(7)}>7button>
<button on:click={select(8)}>8button>
<button on:click={select(9)}>9button>
<button disabled={!value} on:click={clear}>clearbutton>
<button on:click={select(0)}>0button>
<button disabled={!value} on:click={submit}>submitbutton>
div>
App.svelte
<script>
import Keypad from './Keypad.svelte';
let pin;
$: view = pin ? pin.replace(/\d(?!$)/g, '•') : 'enter your pin';
function handleSubmit() {
alert(`submitted ${pin}`);
}
script>
<h1 style="color: {pin ? '#333' : '#ccc'}">{view}h1>
<Keypad on:submit={handleSubmit}/>
就像可以绑定 DOM 元素的属性一样,你也可以绑定组件的 props
。例如,我们可以绑定
组件的value
属性,就好像它是一个表单元素一样对待:
<Keypad bind:value={pin} on:submit={handleSubmit}/>
绑定之后,当用户与 Keypad 进行交互时,父组件的 pin
值会自动更新。
使用组件绑定需要谨慎小心。如果你的 App 中数据过多,尤其是在没有 “单一数据源” 的情况下,可能难以跟踪 App 相关的数据流。
本章的内容十分长,一匹布那么长……
详尽介绍了与 DOM 元素和自定义组件之间的绑定操作,其中最常用的莫过于表单元素的绑定,如 input
家族就有 text
、number
、range
、radio
、checkbox
,此外还有 选择框 select
、多行文本 textarea
,甚至是 contenteditable
元素都支持绑定其 textContent
和 innerHTML
。
媒体相关的绑定虽然也有强大的绑定支持,毕竟还是小众。
Svelte 当然也支持在 each
块中对上述元素进行绑定,这倒是常见的需求。
难得的是跟尺寸相关的部分属性也纳入了绑定的支持,例如宽高。
对比尺寸的绑定,this
绑定更为实用一些,它类似 Vue 的 ref
,绑定实际渲染的 DOM
对象。对于类似 canvas
这类组件尤其是不可或缺。
Svelte 也支持自定义组件的属性的绑定,这是相当不错的特性,在组件中修改了值,直接影响外部提供的状态,对比 React 的属性不可变性,这种更显实用省事。
一如既往需要吐槽一下这个绑定语法,如果能参考一下 Vue,直接用冒号代表属性绑定,那就最好不过了。