Svelte 官方入门教程(6) - 绑定

svelte cover

上一讲是事件相关的知识,今天我们来看看绑定。先看个最简单的绑定对比来学习。

Svelte

1
2
3
4
5
6
<script>
let text = 'Hello World';
</script>

<p>{text}</p>
<input bind:value={text} />

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react';

export default function InputHello() {
const [text, setText] = useState('Hello world');

function handleChange(event) {
setText(event.target.value);
}

return (
<>
<p>{text}</p>
<input value={text} onChange={handleChange} />
</>
);
}

Vue3

1
2
3
4
5
6
7
8
9
<script setup>
import { ref } from 'vue';
const text = ref('Hello World');
</script>

<template>
<p>{{ text }}</p>
<input v-model="text">
</template>

一、文本输入框

一般情况下,Svelte 中的数据流是自上而下的——父组件可以在子组件上设置 props,组件可以在元素上设置属性,但反过来不行。

1
2
3
4
5
6
7
<script>
let name = 'world';
</script>

<input value={name}>

<h1>Hello {name}!</h1>

有时打破这条规则很有用。 以这个组件中的 <input> 元素为例——我们可以添加一个 on:input 事件处理程序,将 name 的值设置为 event.target.value,但这有点……单调乏味。 如果这样我们将看到其他表单元素会变得更槽糕。

很幸运,我们可以使用 bind:value 指令:(很像angularjs和vue)

1
<input bind:value={name}>

这意味着不仅名称值的更改会更新输入值,而且输入值的更改也会更新名称。(一言以蔽之:双向绑定)

二、数字绑定

DOM 中的值都是字符串。不能很好地帮助你处理数字输入(type="number" 和 type="range"),这意味着你需要记得在使用 input.value 之前先将其强制转换一下。

看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>

<style>
label { display: flex }
input, p { margin: 6px }
</style>

如果你使用 bind:valueSvelte 会为您搞定它。
把上面的代码改成下面的即可。

1
2
<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>

三、复选框

我们不仅可以使用input.value,也可以将复选状态绑定input.checked将复选框的状态绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
let yes = false;
</script>

<label>
<input type=checkbox bind: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}>
Subscribe
</button>

四、输入框组

如果你需要绑定更多值,则可以使用bind:groupvalue 属性放在一起使用。 在bind:group中,同一组的单选框值时互斥的,同一组的复选框会形成一个数组。

添加bind:group 到每一个选择框:

1
<input type=radio bind:group={scoops} value={1}>

在这种情况下,我们可以给复选框标签添加一个 each 块来简化代码。 首先添加一个menu变量到 <script>标签中,然后使用each继续修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<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>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}

现在可以轻松地将我们的冰淇淋菜单扩展到令人兴奋的新方向。

REPL

五、多行纯文本-textarea

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import { marked } from 'marked';
let value = `Some words are *italic*, some are **bold**`;
</script>

{@html marked(value)}

<textarea value={value}></textarea>

<style>
textarea { width: 100%; height: 200px; }
</style>

<textarea> 元素的行为类似于 Svelte 中的文本输入——使用 bind:value

1
<textarea bind:value={value}></textarea>

在这种情况下,如果值与变量名相同,我们也可以使用简写形式:

1
<textarea bind:value></textarea>

这种写法适用于所有绑定,而不仅仅是<textarea>

六、选择框绑定

先看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<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 questions</h2>

<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>
Submit
</button>
</form>

<p>selected question {selected ? selected.id : '[waiting...]'}</p>

我们还可以利用 bind:value 对 <select> 标签进行绑定,更改上面select代码为下面代码:

1
<select bind:value={selected} on:change="{() => answer = ''}">

请注意,<option> 值是对象而不是字符串。 Svelte 也没问题。

因为我们没有设置 selected 的初始值,绑定会自动将其设置为默认值(列表中的第一个)。 不过不可大意,因为在绑定初始化之前,selected 实际上仍然是未定义的,我们还是要加以判断,不能盲目地随意引用,比如 上面例子中的 selected.id

七、选择框多选属性

选择框含有一个名为 multiple 的属性,在这种情况下,它将会被设置为数组而不是单值。
我们将之前的一个例子改造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<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>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>

<select multiple bind:value={flavours}>
{#each menu as flavour}
<option value={flavour}>
{flavour}
</option>
{/each}
</select>

{#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}

把原来的checkbox group缓存select multiple

按住 control 键(或 MacOS 上的 command 键)可选择多个选项。

八、内容可编辑绑定

支持 contenteditable="true"属性的标签,可以使用 textContentinnerHTML 属性的绑定,看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let html = '<p>Write some text!</p>';
</script>

<div contenteditable="true"></div>

<pre>{html}</pre>

<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

修改代码为:

1
2
3
4
<div
contenteditable="true"
bind:innerHTML={html}
></div>

或者

1
2
3
4
<div
contenteditable="true"
bind:textContent={html}
></div>

这个时候你修改div的内容<pre>的内会跟着变化。

九、each块绑定

你甚至可以对 each 块添加绑定,来看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<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>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>

请注意,与 <input> 元素进行绑定将使数组发生变化。如果你偏好使用不可变数据,则应避免使用这类绑定,而应使用事件处理程序。

十、媒体标签绑定

<audio> <video> 元素有几个可以绑定的属性。 这个例子展示了其中的一些。来看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<script>
// These values are bound to properties of the video
let time = 0;
let duration;
let paused = true;

let showControls = true;
let showControlsTimeout;

function handleMousemove(e) {
// Make the controls visible, but fade out after
// 2.5 seconds of inactivity
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) {
// we can't rely on the built-in click event, because it fires
// after a drag — we have to listen for clicks ourselves

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: Llamigos</h1>
<p>From <a href="https://cloud.blender.org/open-projects">Blender Open Projects</a>. CC-BY</p>

<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}
></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 seek</span>
<span class="time">{format(duration)}</span>
</div>
</div>
</div>

在代码第62行, 添加上对 currentTime={time}durationpaused 属性的绑定

1
2
3
4
5
6
7
8
9
<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、durationpaused 属性的值。这意味着我们可以使用它们来创建自定义控件。

通常,在网页中, 你会将currentTime用于对 timeupdate 的事件监听并跟踪。但是这些事件很少触发,导致UI不稳定。 Svelte 使用currentTimerequestAnimationFrame进行查验,进而避免了此问题。

针对 <audio><video> 的 6 个readonly 属性绑定 :

  • duration (readonly) :视频的总时长,以秒为单位。
  • buffered (readonly) :数组{start, end} 的对象。
  • seekable (readonly) :同上。
  • played (readonly) :同上。
  • seeking (readonly) :布尔值。
  • ended (readonly) :布尔值。

…以及 5 个双向绑定:

  • currentTime — 视频中的当前位置,以秒为单位
  • playbackRate — 视频播放倍速,’正常速度’ 为 1
  • paused — 暂停
  • volume — 音量,其值介乎 0 ~ 1 之间
  • muted — 一个布尔值,为 true 则说明静音状态

Videos 还支持绑定只读的 videoWidthvideoHeight 属性。

REPL

十一、尺寸绑定

每个块级标签都可以对 clientWidthclientHeightoffsetWidth 以及 offsetHeight 属性进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
let w;
let h;
let size = 42;
let text = 'edit me';
</script>

<input type=range bind:value={size}>
<input bind:value={text}>

<p>size: {w}px x {h}px</p>

<div bind:clientWidth={w} bind:clientHeight={h}>
<span style="font-size: {size}px">{text}</span>
</div>

<style>
input { display: block; }
div { display: inline-block; }
span { word-break: break-all; }
</style>

这些绑定是只读的,改变 w h 的值不会有任何影响。

使用这类测量技术,会涉及到一些开销,因此不建议在大量元素上使用。
display: inline 的内联元素是不能测量的,没有子级的元素也不能(例如 <canvas>),这种情况,你需要测量的是包裹它的外层元素。

十二、绑定 this

只读的 this 绑定适用于每个元素(和组件),并允许您获取对渲染元素的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<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.width >>> 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>

<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>

<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> 元素的引用:

1
2
3
4
5
<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>

请注意,在组件挂载之前,canvas 的值将是 undefined,因此我们将逻辑放在 onMount 这个生命周期函数中。

十三、组件绑定

正如可以绑定到DOM元素的属性一样,你也可以将组件的属性绑定。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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}/>

例如上例我们能绑定位于组件内的 value 属性,就如同一个表单标签一般:

1
<Keypad bind:value={pin} on:submit={handleSubmit}/>

现在,当用户点击键盘数字时,pin 父组件的数据将会立刻获得更新。

谨慎使用组件绑定。 如果您的应用程序有太多数据流,则可能很难跟踪这些数据流,尤其是在没有“单一事实来源”的情况下。

REPL

十四、绑定到组件实例

正如您可以绑定到 DOM 元素一样,您也可以绑定到组件实例本身。

例如,我们可以将 的实例绑定到一个名为 field 的变量,就像我们在绑定 DOM 元素时所做的那样

App

1
2
3
4
5
6
7
<script>
import InputField from './InputField.svelte';

let field;
</script>

<InputField bind:this={field}/>

InputField

1
2
3
4
5
6
7
8
9
<script>
let input;

export function focus() {
input.focus();
}
</script>

<input bind:this={input} />

现在我们可以使用 field 以编程方式与该组件交互。

1
2
3
4
5
6
7
8
9
10
<script>
import InputField from './InputField.svelte';

let field;
</script>

<InputField bind:this={field}/>

<button on:click={() => field.focus()}>Focus field</button>

请注意,我们不能执行{field.focus},因为在第一次呈现按钮时,字段是未定义的,执行会会抛出错误。

总结

绑定内容太多了,整整14条之多~

  • 把表单元素的绑定都讲了个遍。
  • 非表单的每个块级元素都可以绑定到clientWidth、clientHeight、offsetWidth以及offsetHeight
  • this绑定使用场景很多,类似react的ref
  • 绑定到组件实例让组件之间的交互更简单

Svelte 官方入门教程(6) - 绑定
http://yoursite.com/2022/07/12/svelte-tutorial-6/
作者
昂藏君子
发布于
2022年7月12日
许可协议