Svelte基础
Svelte高阶
上下文API
特殊元素
接下来
SvelteKit基础
Shared modules
API routes
$app/state
Errors and redirects
Advanced SvelteKit
Page options
Link options
Advanced routing
Advanced loading
Environment variables
Conclusion
嘿小主,恭喜你突破了💕! 接下来我们修炼些动作(Actions)。如果你先前接触过React可能感觉跟它的自定义hook有点像。 动作(Actions)其实就是一些跟Dom元素同生共死的函数,在解决某些问题时还是很有用滴,比如:
- 跟第三方库交互
- 懒加载图片
- 提示信息(tooltips)
- 添加自定义事件处理器
Actions are essentially element-level lifecycle functions. They’re useful for things like:
- interfacing with third-party libraries
- lazy-loaded images
- tooltips
- adding custom event handlers
举个例子🌰:这有个画布<canvas>
, 你可以在目录里调整刷子的颜色和大小。随便画吧,直抒胸臆,把你一腔豪情或者郁闷倾注到刷子上。一画开天地,二画分阴阳,三画可以先停下,先完成练习吧,小主。
当你打开目录时(默认应该已经打开了,如果没有打开自行点击memu
按钮),不要用鼠标去选择颜色,尝试使用Tab键。有没有发现个问题?按Tab键无效,直到用鼠标点击下目录面板让它获得鼠标焦点. 做为一个贴心的开发者是不是可以让目录面板打开时自动获取焦点?
我们可以这样做:
首先从actions.svelte.js
引入trapFocus
In this app, you can scribble on the
<canvas>
, and change colours and brush size via the menu. But if you open the menu and cycle through the options with the Tab key, you’ll soon find that the focus isn’t trapped inside the modal. We can fix that with an action. ImporttrapFocus
fromactions.svelte.js
...
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
let selected = $state(colors[0]);
let size = $state(10);
let showMenu = $state(true);
</script>
<script lang="ts">
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
let selected = $state(colors[0]);
let size = $state(10);
let showMenu = $state(true);
</script>
然后使用use
指令把trapFocus
添加到目录面板上:
...then add it to the menu with the
use:
directive:
<div class="menu" use:trapFocus>
接下来我们还得一起看下trapFocus
函数呢。
这就是个动作 —— 所谓动作与Dom元素(这里就是<div class=""menu>
)同生共死————当元素被挂载到Dom上时被调用, 当元素被卸载时记得自行销毁。
在动作trapFocus
中我们使用了效应(effect)
在效应(effect)中注册了个处理Tab键按下操作的事件监听:
Let’s take a look at the
trapFocus
function inactions.svelte.js
. An action function is called with anode
— the<div class="menu">
in our case — when the node is mounted to the DOM. Inside the action, we have an effect. First, we need to add an event listener that intercepts Tab key presses:
$effect(() => {
focusable()[0]?.focus();
node.addEventListener('keydown', handleKeydown);
});
当Dom元素被卸载时,我们需要清理下事件监听,然后把聚焦还原:
Second, we need to do some cleanup when the node is unmounted — removing the event listener, and restoring focus to where it was before the element mounted:
$effect(() => {
focusable()[0]?.focus();
node.addEventListener('keydown', handleKeydown);
return () => {
node.removeEventListener('keydown', handleKeydown);
previous?.focus();
};
});
问题解决了,小主可以再试下,记得快点回来哦。
Now, when you open the menu, you can cycle through the options with the Tab key.
<script>
import Canvas from './Canvas.svelte';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
let selected = $state(colors[0]);
let size = $state(10);
let showMenu = $state(true);
</script>
<div class="container">
<Canvas color={selected} size={size} />
{#if showMenu}
<div
role="presentation"
class="modal-background"
onclick={(event) => {
if (event.target === event.currentTarget) {
showMenu = false;
}
}}
onkeydown={(e) => {
if (e.key === 'Escape') {
showMenu = false;
}
}}
>
<div class="menu">
<div class="colors">
{#each colors as color}
<button
class="color"
aria-label={color}
aria-current={selected === color}
style="--color: {color}"
onclick={() => {
selected = color;
}}
></button>
{/each}
</div>
<label>
small
<input type="range" bind:value={size} min="1" max="50" />
large
</label>
</div>
</div>
{/if}
<div class="controls">
<button class="show-menu" onclick={() => showMenu = !showMenu}>
{showMenu ? 'close' : 'menu'}
</button>
</div>
</div>
<style>
.container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.controls {
position: absolute;
left: 0;
top: 0;
padding: 1em;
}
.show-menu {
width: 5em;
}
.modal-background {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
left: 0;
top: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(20px);
}
.menu {
position: relative;
background: var(--bg-2);
width: calc(100% - 2em);
max-width: 28em;
padding: 1em 1em 0.5em 1em;
border-radius: 1em;
box-sizing: border-box;
user-select: none;
}
.colors {
display: grid;
align-items: center;
grid-template-columns: repeat(9, 1fr);
grid-gap: 0.5em;
}
.color {
aspect-ratio: 1;
border-radius: 50%;
background: var(--color, #fff);
transform: none;
filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
transition: all 0.1s;
}
.color[aria-current="true"] {
transform: translate(1px, 1px);
filter: none;
box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
}
.menu label {
display: flex;
width: 100%;
margin: 1em 0 0 0;
}
.menu input {
flex: 1;
}
</style>