Skip to content

Commit e3ca181

Browse files
author
Rich Harris
committed
better component binding example
1 parent 1feefbc commit e3ca181

File tree

9 files changed

+553
-48
lines changed

9 files changed

+553
-48
lines changed

content/tutorial/02-advanced-svelte/04-actions/01-actions/app-a/src/lib/App.svelte

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,30 @@
4343
</div>
4444
{/if}
4545

46-
<button class="show-menu" on:click={() => showMenu = !showMenu}>
47-
{showMenu ? 'close' : 'menu'}
48-
</button>
46+
<div class="controls">
47+
<button class="show-menu" on:click={() => showMenu = !showMenu}>
48+
{showMenu ? 'close' : 'menu'}
49+
</button>
50+
</div>
4951
</div>
5052

5153
<style>
5254
.container {
53-
display: flex;
54-
justify-content: center;
55-
align-items: center;
5655
position: fixed;
5756
left: 0;
5857
top: 0;
5958
width: 100%;
6059
height: 100%;
6160
}
6261
63-
.show-menu {
62+
.controls {
6463
position: absolute;
65-
left: 1em;
66-
top: 1em;
64+
left: 0;
65+
top: 0;
66+
padding: 1em;
67+
}
68+
69+
.show-menu {
6770
width: 5em;
6871
}
6972

content/tutorial/02-advanced-svelte/04-actions/01-actions/app-b/src/lib/App.svelte

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,30 @@
4444
</div>
4545
{/if}
4646

47-
<button class="show-menu" on:click={() => showMenu = !showMenu}>
48-
{showMenu ? 'close' : 'menu'}
49-
</button>
47+
<div class="controls">
48+
<button class="show-menu" on:click={() => showMenu = !showMenu}>
49+
{showMenu ? 'close' : 'menu'}
50+
</button>
51+
</div>
5052
</div>
5153

5254
<style>
5355
.container {
54-
display: flex;
55-
justify-content: center;
56-
align-items: center;
5756
position: fixed;
5857
left: 0;
5958
top: 0;
6059
width: 100%;
6160
height: 100%;
6261
}
6362
64-
.show-menu {
63+
.controls {
6564
position: absolute;
66-
left: 1em;
67-
top: 1em;
65+
left: 0;
66+
top: 0;
67+
padding: 1em;
68+
}
69+
70+
.show-menu {
6871
width: 5em;
6972
}
7073

content/tutorial/02-advanced-svelte/05-bindings/07-component-this/README.md

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,53 @@
22
title: Binding to component instances
33
---
44

5-
Just as you can bind to DOM elements, you can bind to component instances themselves. For example, we can bind the instance of `<InputField>` to a variable named `field` in the same way we did when binding DOM Elements
5+
Just as you can bind to DOM elements, you can bind to component instances themselves with `bind:this`.
6+
7+
This is useful in the rare cases that you need to interact with a component programmatically (rather than by providing it with updated props). Revisiting our canvas app from [a few exercises ago](actions), it would be nice to add a button to clear the screen.
8+
9+
First, let's export a function from `Canvas.svelte`:
10+
11+
```svelte
12+
/// file: Canvas.svelte
13+
export let color;
14+
export let size;
15+
16+
+++export function clear() {
17+
context.clearRect(0, 0, canvas.width, canvas.height);
18+
}+++
19+
```
20+
21+
Then, create a reference to the component instance:
622

723
```svelte
824
/// file: App.svelte
925
<script>
10-
import InputField from './InputField.svelte';
26+
import Canvas from './Canvas.svelte';
27+
import { trapFocus } from './actions.js';
1128
12-
+++let field;+++
29+
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
30+
let selected = colors[0];
31+
let size = 10;
32+
33+
let showMenu = true;
34+
+++let canvas;+++
1335
</script>
1436
15-
<InputField +++bind:this={field}+++ />
37+
<div class="container">
38+
<Canvas +++bind:this={canvas}+++ color={selected} size={size} />
1639
```
1740

18-
Now we can programmatically interact with this component using `field`.
41+
Finally, add a button that calls the `clear` function:
1942

2043
```svelte
2144
/// file: App.svelte
22-
<button +++on:click={() => field.focus()}+++>
23-
Focus field
24-
</button>
25-
```
45+
<div class="controls">
46+
<button class="show-menu" on:click={() => showMenu = !showMenu}>
47+
{showMenu ? 'close' : 'menu'}
48+
</button>
2649
27-
> Note that we can't do `{field.focus}` since `field` is undefined when the button is first rendered.
50+
+++ <button on:click={() => canvas.clear()}>
51+
clear
52+
</button>+++
53+
</div>
54+
```
Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,128 @@
11
<script>
2-
import InputField from './InputField.svelte';
2+
import Canvas from './Canvas.svelte';
3+
import { trapFocus } from './actions.js';
4+
5+
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
6+
let selected = colors[0];
7+
let size = 10;
8+
9+
let showMenu = true;
310
</script>
411

5-
<InputField />
12+
<div class="container">
13+
<Canvas color={selected} size={size} />
14+
15+
{#if showMenu}
16+
<div
17+
class="modal-background"
18+
on:click|self={() => showMenu = false}
19+
on:keydown={(e) => {
20+
if (e.key === 'Escape') showMenu = false;
21+
}}
22+
>
23+
<div class="menu" use:trapFocus>
24+
<div class="colors">
25+
{#each colors as color}
26+
<button
27+
class="color"
28+
aria-label={color}
29+
aria-current={selected === color}
30+
style="--color: {color}"
31+
on:click={() => {
32+
selected = color;
33+
}}
34+
/>
35+
{/each}
36+
</div>
37+
38+
<label>
39+
small
40+
<input type="range" bind:value={size} min="1" max="50" />
41+
large
42+
</label>
43+
</div>
44+
</div>
45+
{/if}
46+
47+
<div class="controls">
48+
<button class="show-menu" on:click={() => showMenu = !showMenu}>
49+
{showMenu ? 'close' : 'menu'}
50+
</button>
51+
</div>
52+
</div>
53+
54+
<style>
55+
.container {
56+
position: fixed;
57+
left: 0;
58+
top: 0;
59+
width: 100%;
60+
height: 100%;
61+
}
62+
63+
.controls {
64+
position: absolute;
65+
left: 0;
66+
top: 0;
67+
padding: 1em;
68+
}
69+
70+
.show-menu {
71+
width: 5em;
72+
}
73+
74+
.modal-background {
75+
position: fixed;
76+
display: flex;
77+
justify-content: center;
78+
align-items: center;
79+
left: 0;
80+
top: 0;
81+
width: 100%;
82+
height: 100%;
83+
backdrop-filter: blur(20px);
84+
}
85+
86+
.menu {
87+
position: relative;
88+
background: var(--bg-2);
89+
width: calc(100% - 2em);
90+
max-width: 28em;
91+
padding: 1em 1em 0.5em 1em;
92+
border-radius: 1em;
93+
box-sizing: border-box;
94+
user-select: none;
95+
}
96+
97+
.colors {
98+
display: grid;
99+
align-items: center;
100+
grid-template-columns: repeat(9, 1fr);
101+
grid-gap: 0.5em;
102+
}
103+
104+
.color {
105+
aspect-ratio: 1;
106+
border-radius: 50%;
107+
background: var(--color, #fff);
108+
transform: none;
109+
filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
110+
transition: all 0.1s;
111+
}
112+
113+
.color[aria-current="true"] {
114+
transform: translate(1px, 1px);
115+
filter: none;
116+
box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
117+
}
6118
7-
<button>
8-
Focus field
9-
</button>
119+
.menu label {
120+
display: flex;
121+
width: 100%;
122+
margin: 1em 0 0 0;
123+
}
10124
125+
.menu input {
126+
flex: 1;
127+
}
128+
</style>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script>
2+
import { onMount } from "svelte";
3+
4+
export let color;
5+
export let size;
6+
7+
let canvas;
8+
let context;
9+
let previous;
10+
11+
function get_coords(e) {
12+
const { clientX, clientY } = e;
13+
const { left, top } = canvas.getBoundingClientRect();
14+
const x = clientX - left;
15+
const y = clientY - top;
16+
return { x, y };
17+
}
18+
19+
onMount(() => {
20+
context = canvas.getContext('2d');
21+
22+
function resize() {
23+
canvas.width = window.innerWidth;
24+
canvas.height = window.innerHeight;
25+
}
26+
27+
window.addEventListener('resize', resize);
28+
resize();
29+
30+
return () => {
31+
window.removeEventListener('resize', resize);
32+
};
33+
});
34+
</script>
35+
36+
37+
38+
<canvas
39+
bind:this={canvas}
40+
on:pointerdown={(e) => {
41+
const coords = get_coords(e);
42+
context.fillStyle = color;
43+
context.beginPath();
44+
context.arc(coords.x, coords.y, size / 2, 0, 2 * Math.PI);
45+
context.fill();
46+
47+
previous = coords;
48+
}}
49+
on:pointerleave={() => {
50+
previous = null;
51+
}}
52+
on:pointermove={(e) => {
53+
const coords = get_coords(e);
54+
55+
if (e.buttons === 1) {
56+
e.preventDefault();
57+
58+
context.strokeStyle = color;
59+
context.lineWidth = size;
60+
context.lineCap = 'round';
61+
context.beginPath();
62+
context.moveTo(previous.x, previous.y);
63+
context.lineTo(coords.x, coords.y);
64+
context.stroke();
65+
}
66+
67+
previous = coords;
68+
}}
69+
/>
70+
71+
{#if previous}
72+
<div
73+
class="preview"
74+
style="--color: {color}; --size: {size}px; --x: {previous.x}px; --y: {previous.y}px"
75+
/>
76+
{/if}
77+
78+
<style>
79+
canvas {
80+
position: absolute;
81+
left: 0;
82+
top: 0;
83+
width: 100%;
84+
height: 100%;
85+
}
86+
87+
.preview {
88+
position: absolute;
89+
left: var(--x);
90+
top: var(--y);
91+
width: var(--size);
92+
height: var(--size);
93+
transform: translate(-50%, -50%);
94+
background: var(--color);
95+
border-radius: 50%;
96+
opacity: 0.5;
97+
pointer-events: none;
98+
}
99+
</style>

content/tutorial/02-advanced-svelte/05-bindings/07-component-this/app-a/src/lib/InputField.svelte

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)