就业培训     下载中心     Wiki     联络
登录   注册

Log
  1. 首页
  2. 学习 Web 开发
  3. Tools and testing
  4. 理解客户端侧 JavaScript 框架
  5. Advanced Svelte: Reactivity, lifecycle, accessibility

内容表

  • Code along with us
  • Working on the MoreActions component
  • Reactivity gotchas: updating objects and arrays
  • Finishing our MoreActions component
  • Working with the DOM: focusing on the details
  • Exploring keyboard accessibility issues in our todo app
  • Creating a NewTodo component
  • Working with DOM nodes using the bind:this={dom_node} directive
  • Component lifecycle, and the onMount() function
  • Waiting for the DOM to be updated with the tick() function
  • Adding functionality to HTML elements with the use:action directive
  • Component binding: exposing component methods and variables using the bind:this={component} directive
  • The code so far
  • 摘要
  • In this module

Advanced Svelte: Reactivity, lifecycle, accessibility

  • 上一
  • Overview: Client-side JavaScript frameworks
  • 下一

In the last article we added more features to our To-Do list and started to organize our app into components. In this article we will add the app's final features and further componentize our app. We will learn how to deal with reactivity issues related to updating objects and arrays. To avoid common pitfalls, we'll have to dig a little deeper into Svelte's reactivity system. We'll also look at solving some accessibility focus issues, and more besides.

Prerequisites: At minimum, it is recommended that you are familiar with the core HTML , CSS ,和 JavaScript languages, and have knowledge of the terminal/command line .

You'll need a terminal with node + npm installed to compile and build your app.

Objective: Learn some advanced Svelte techniques involving solving reactivity issues, keyboard accessibility problems to do with component lifecycle, and more.

We'll focus on some accessibility issues involving focus management. To do so, we'll utilize some techniques for accessing DOM nodes and executing methods like focus() and select() . We will also see how to declare and clean-up event listeners on DOM elements.

We also need to learn a bit about component lifecycle, to understand when these DOM nodes get mounted and unmounted from the DOM and how we can access them. We will also learn about the action directive, which will allow us to extend the functionality of HTML elements in a reusable and declarative way.

Finally, we will learn a bit more about components. So far, we've seen how components can share data using props, and communicate with their parents using events and two-way data binding. Now we will see how components can also expose methods and variables.

The following new components will be developed throughout the course of this article:

  • MoreActions : Displays the Check All and Remove Completed buttons, and emits the corresponding events required to handle their functionality.
  • NewTodo : Displays the <input> field and 添加 button for adding a new todo.
  • TodosStatus : Displays the "x out of y items completed" status heading.

Code along with us

Git

Clone the github repo (if you haven't already done it) with:

git clone https://github.com/opensas/mdn-svelte-tutorial.git

								

Then to get to the current app state, run

cd mdn-svelte-tutorial/05-advanced-concepts

								

Or directly download the folder's content:

npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts

								

Remember to run npm install && npm run dev to start your app in development mode.

REPL

To code along with us using the REPL, start at

https://svelte.dev/repl/76cc90c43a37452e8c7f70521f88b698?version=3.23.2

Working on the MoreActions component

Now we'll tackle the Check All and Remove Completed buttons. Let's create a component that will be in charge of displaying the buttons and emitting the corresponding events.

  1. Create a new file — components/MoreActions.svelte .
  2. When the first button is clicked, we'll emit a checkAll event to signal that all the todos should be checked/unchecked. When the second button is clicked, we'll emit a removeCompleted event to signal that all of the completed todos should be removed. Put the following content into your MoreActions.svelte 文件:
    <script>
      import { createEventDispatcher } from 'svelte'
      const dispatch = createEventDispatcher()
      let completed = true
      const checkAll = () => {
        dispatch('checkAll', completed)
        completed = !completed
      }
      const removeCompleted = () => dispatch('removeCompleted')
    </script>
    <div class="btn-group">
      <button type="button" class="btn btn__primary" on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button>
      <button type="button" class="btn btn__primary" on:click={removeCompleted}>Remove completed</button>
    </div>
    
    										
    We've also included a completed variable to toggle between checking and unchecking all tasks.
  3. Back over in Todos.svelte , we are going to import our MoreActions component and create two functions to handle the events emitted by the MoreActions 组件。 Add the following import statement below the existing ones:
    import MoreActions from './MoreActions.svelte'
    
    										
  4. Then add the described functions at the end of the <script> section:
    const checkAllTodos = (completed) => todos.forEach(t => t.completed = completed)
    const removeCompletedTodos = () => todos = todos.filter(t => !t.completed)
    
    										
  5. Now go to the bottom of the Todos.svelte markup section and replace the btn-group <div> that we copied into MoreActions.svelte with a call to the MoreActions component, like so:
    <!-- MoreActions -->
    <MoreActions
      on:checkAll={e => checkAllTodos(e.detail)}
      on:removeCompleted={removeCompletedTodos}
    />
    
    										
  6. OK, let's go back into the app and try it out! You'll find that the Remove Completed button works fine, but the Check All / Uncheck All button just silently fails.

To find out what is happening here, we'll have to dig a little deeper into Svelte reactivity.

Reactivity gotchas: updating objects and arrays

To see what's happening we can log the todos array from the checkAllTodos() function to the console.

  1. Update your existing checkAllTodos() function to the following:
    const checkAllTodos = (completed) => {
      todos.forEach(t => t.completed = completed)
      console.log('todos', todos)
    }
    
    										
  2. Go back to your browser, open your DevTools console, and click Check All / Uncheck All a few times.

You'll notice that the array is successfully updated every time you press the button (the todo objects' completed properties are toggled between true and false ), but Svelte is not aware of that. This also means that in this case, a reactive statement like $: console.log('todos', todos) won't be very useful.

To find out why this is happening, we need to understand how reactivity works in Svelte when updating arrays and objects.

Many web frameworks use the virtual DOM technique to update the page. Basically, the virtual DOM is an in-memory copy of the contents of the web page. The framework updates this virtual representation which is then synced with the "real" DOM. This is much faster than directly updating the DOM and allows the framework to apply many optimization techniques.

These frameworks, by default, basically re-run all our JavaScript on every change against this virtual DOM, and apply different methods to cache expensive calculations and optimize execution. They make little to no attempt to understand what our JavaScript code is doing.

Svelte doesn't use a virtual DOM representation. Instead, it parses and analyzes our code, creates a dependency tree, and then generates the required JavaScript to update only the parts of the DOM that need to be updated. This approach usually generates optimal JavaScript code with minimal overhead, but it also has its limitations.

Sometimes Svelte cannot detect changes to variables being watched. Remember that to tell Svelte that a variable has changed, you have to assign it a new value. A simple rule of thumb is that The name of the updated variable must appear on the left hand side of the assignment.

For example, in the following piece of code:

const foo = obj.foo
foo.bar = 'baz'

								

Svelte won't update references to obj.foo.bar , unless you follow it up with obj = obj . That's because Svelte can't track object references, so we have to explicitly tell it that obj has changed by issuing an assignment.

注意: if foo is a top level variable, you can easily tell Svelte to update obj whenever foo is changed with the following reactive statement: $: foo, obj = obj . With this we are defining foo as a dependency, and whenever it changes Svelte will run obj = obj .

In our checkAllTodos() function, when we run:

todos.forEach(t => t.completed = completed)

								

Svelte will not mark todos as changed because it does not know that when we update our t variable inside the forEach() method, we are also modifying the todos array. And that makes sense, because otherwise Svelte would be aware of the inner workings of the forEach() method; the same would therefore be true for any method attached to any object or array.

Nevertheless, there are different techniques that we can apply to solve this problem, and all of them involve assigning a new value to the variable being watched.

Like we already saw, we could just tell Svelte to update the variable with a self-assignment, like this:

const checkAllTodos = (completed) => {
  todos.forEach(t => t.completed = completed)
  todos = todos
}

								

This will solve the problem. Internally Svelte will flag todos as changed and remove the apparently redundant self-assignment. Apart from the fact that it looks weird, it's perfectly OK to use this technique, and sometimes it's the most concise way to do it.

We could also access the todos array by index, like this:

const checkAllTodos = (completed) => {
  todos.forEach( (t,i) => todos[i].completed = completed)
}

								

Assignments to properties of arrays and objects — e.g. obj.foo += 1 or array[i] = x — work the same way as assignments to the values themselves. When Svelte analyzes this code, it can detect that the todos array is being modified.

Another solution is to assign a new array to todos containing a copy of all the todos with the completed property updated accordingly, like this:

const checkAllTodos = (completed) => {
  todos = todos.map(t => {                  // shorter version: todos = todos.map(t => ({...t, completed}))
    return {...t, completed: completed}
  })
}

								

In this case we are using the map() method, which returns a new array with the results of executing the provided function for each item. The function returns a copy of each todo using spread syntax and overwrites the property of the completed value accordingly. This solution has the added benefit of returning a new array with new objects, completely avoiding mutating the original todos 数组。

注意: Svelte allows us to specify different options that affect how the compiler works. The <svelte:options immutable={true}/> option tells the compiler that you promise not to mutate any objects. This allows it to be less conservative about checking whether values have changed and generate simpler and more performant code. For more information on <svelte:options...> , check the Svelte options documentation .

All of these solutions involve an assignment in which the updated variable is on the left side of the equation. Any of this techniques will allow Svelte to notice that our todos array has been modified.

Choose one, and update your checkAllTodos() function as required. Now you should be able to check and uncheck all your todos at once. Try it!

Finishing our MoreActions component

We will add one usability detail to our component. We'll disable the buttons when there are no tasks to be processed. To create this we'll receive the todos array as a prop, and set the 被禁用 property of each button accordingly.

  1. Update your MoreActions.svelte component like this:
    <script>
      import { createEventDispatcher } from 'svelte'
      const dispatch = createEventDispatcher()
      export let todos
      let completed = true
      const checkAll = () => {
        dispatch('checkAll', completed)
        completed = !completed
      }
      const removeCompleted = () => dispatch('removeCompleted')
      $: completedTodos = todos.filter(t => t.completed).length
    </script>
    <div class="btn-group">
      <button type="button" class="btn btn__primary"
        disabled={todos.length === 0} on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button>
      <button type="button" class="btn btn__primary"
        disabled={completedTodos === 0} on:click={removeCompleted}>Remove completed</button>
    </div>
    
    										
    We've also declared a reactive completedTodos variable to enable or disable the Remove Completed 按钮。
  2. Don't forget to pass the prop into MoreActions from inside Todos.svelte , where the component is called:
    <MoreActions {todos}
        on:checkAll={e => checkAllTodos(e.detail)}
        on:removeCompleted={removeCompletedTodos}
      />
    
    										

Working with the DOM: focusing on the details

Now that we have completed all of the app's required functionality, we'll concentrate on some accessibility features that will improve the usability of our app for both keyboard-only and screenreader users.

In its current state our app has a couple of keyboard accessibility problems involving focus management. Let's have a look at these issues.

Exploring keyboard accessibility issues in our todo app

Right now, keyboard users will find out that the focus flow of our app is not very predictable or coherent.

If you click on the input at the top of our app, you'll see a thick, dashed outline around that input. This outline is your visual indicator that the browser is currently focused on this element.

If you are a mouse user, you might have skipped this visual hint. But if you are working exclusively with the keyboard, knowing which control has focus is of vital importance. It tells us which control is going to receive our keystrokes.

If you press the Tab key repeatedly, you'll see the dashed focus indicator cycling between all the focusable elements on the page. If you move the focus to the 编辑 button and press Enter , suddenly the focus disappears, you can no longer tell which control will receive our keystrokes.

Moreover, if you press the Escape or Enter key, nothing happens. And if you click on 取消 or 保存 , the focus disappears again. For a user working with the keyboard, this behavior will be confusing at best.

We'd also like to add some usability features, like disabling the 保存 button when required fields are empty, giving focus to certain HTML elements or auto-selecting contents when a text input receives focus.

To implement all these features we'll need programmatic access to DOM nodes to run functions like focus() and select() . We will also have to use addEventListener() and removeEventListener() to run specific tasks when the control receives focus.

The problem is that all these DOM nodes are dynamically created by Svelte at runtime. So we'll have to wait for them to be created and added to the DOM in order to use them. To do so, we'll have to learn about the component lifecycle to understand when we can access them — more on this later.

Creating a NewTodo component

Let's begin by extracting our new todo form out to its own component. With what we know so far we can create a new component file and adjust the code to emit an addTodo event, passing the name of the new todo in with the additional details.

  1. Create a new file — components/NewTodo.svelte .
  2. Put the following contents inside it:
    <script>
      import { createEventDispatcher } from 'svelte'
      const dispatch = createEventDispatcher()
      let name = ''
      const addTodo = () => {
        dispatch('addTodo', name)
        name = ''
      }
      const onCancel = () => name = ''
    </script>
    <form on:submit|preventDefault={addTodo} on:keydown={e => e.key === 'Escape' && onCancel()}>
      <h2 class="label-wrapper">
        <label for="todo-0" class="label__lg">What needs to be done?</label>
      </h2>
      <input bind:value={name} type="text" id="todo-0" autoComplete="off" class="input input__lg" />
      <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button>
    </form>
    
    										
    Here we are binding the <input> 到 名称 variable with bind:value={name} and disabling the 添加 button when it is empty (i.e. no text content) using disabled={!name} . We are also taking care of the Escape key with on:keydown={e => e.key === 'Escape' && onCancel()} . Whenever the Escape key is pressed we run onCancel() , which just clears up the 名称 变量。
  3. Now we have to import and use it from inside the Todos component, and update the addTodo() function to receive the name of the new todo. Add the following import statement below the others inside Todos.svelte :
    import NewTodo from './NewTodo.svelte'
    
    										
  4. And update the addTodo() function like so:
    function addTodo(name) {
      todos = [...todos, { id: newTodoId, name, completed: false }]
    }
    
    										
    addTodo() now receives the name of the new todo directly, so we no longer need the newTodoName variable to give it its value. Our NewTodo component takes care of that.

    注意: the { name } syntax is just a shorthand for { name: name } . This one comes from JavaScript itself and has nothing to do with Svelte, besides providing some inspiration for Svelte's own shorthands.

  5. Finally for this section, replace the NewTodo form markup with a call to NewTodo component, like so:
    <!-- NewTodo -->
    <NewTodo on:addTodo={e => addTodo(e.detail)} />
    
    									

Working with DOM nodes using the bind:this={dom_node} directive

Now we want the NewTodo <input> to re-gain focus every time the 添加 button is pressed. For that we'll need a reference to the DOM node of the input. Svelte provides a way to do this with the bind:this={dom_node} directive. When specified, as soon as the component is mounted and the DOM node is created Svelte assigns a reference to the DOM node to the specified variable.

We'll create a nameEl variable and bind it to the input it using bind:this={nameEl} . Then inside addTodo() , after adding the new todo we will call nameEl.focus() to refocus the <input> again. We will do the same when the user presses the Escape key, with the onCancel() 函数。

Update the contents of NewTodo.svelte 像这样:

<script>
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()
  let name = ''
  let nameEl                  // reference to the name input DOM node
  const addTodo = () => {
    dispatch('addTodo', name)
    name = ''
    nameEl.focus()            // give focus to the name input
  }
  const onCancel = () => {
    name = ''
    nameEl.focus()            // give focus to the name input
  }
</script>
<form on:submit|preventDefault={addTodo} on:keydown={e => e.key === 'Escape' && onCancel()}>
  <h2 class="label-wrapper">
    <label for="todo-0" class="label__lg">What needs to be done?</label>
  </h2>
  <input bind:value={name} bind:this={nameEl} type="text" id="todo-0" autoComplete="off" class="input input__lg" />
  <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button>
</form>

							

Try the app out — type a new todo name in to the <input> field, press tab to give focus to the 添加 button, and then hit Enter or Escape to see how the input recovers focus.

Autofocusing our input

The next feature will add to our NewTodo component will be an autofocus prop, which will allow us to specify that we want the <input> field to be focused on page load.

  1. Our first attempt is as follows — let's try adding the autofocus prop and just call nameEl.focus() 从 <script> block. Update the first part of the NewTodo.svelte <script> section (the first four lines) to look like this:
    <script>
      import { createEventDispatcher } from 'svelte'
      const dispatch = createEventDispatcher()
      export let autofocus = false
      let name = ''
      let nameEl                  // reference to the name input DOM node
      if (autofocus) nameEl.focus()
    
    									
  2. Now go back to the Todos component, and pass the autofocus prop into the <NewTodo> component call, like so:
    <!-- NewTodo -->
    <NewTodo autofocus on:addTodo={e => addTodo(e.detail)} />
    
    									
  3. If you try your app out now, you'll see that the page is now blank, and in your DevTools web console you'll see an error along the lines of: TypeError: nameEl is undefined .

To understand what's happening here, let's talk some more about that component lifecycle we mentioned earlier.

Component lifecycle, and the onMount() function

When a component is instantiated, Svelte runs the initialization code (that is, the <script> section of the component). But at that moment, all the nodes that comprise the component are not attached to the DOM, in fact, they don't even exist.

So, how can you know when the component has already been created and mounted on the DOM? The answer is that every component has a lifecycle that starts when it is created, and ends when it is destroyed. There are a handful of functions that allow you to run code at key moments during that lifecycle.

The one you'll use most frequently is onMount() , which lets us run a callback as soon as the component has been mounted on the DOM. Let's give it a try and see what happens to the nameEl 变量。

  1. To start with, add the following line at the beginning of the NewTodo.svelte <script> section:
     import { onMount } from 'svelte'
    
    									
  2. And these lines at the end of it:
    console.log('initializing:', nameEl)
    onMount( () => {
      console.log('mounted:', nameEl)
    })
    
    									
  3. Now remove the if (autofocus) nameEl.focus() line to avoid throwing the error we were seeing before.
  4. The app will now work again, and you'll see the following in your console:
    initializing: undefined
    mounted: <input id="todo-0" class="input input__lg" type="text" autocomplete="off">
    									
    As you can see, while the component is initializing nameEl is undefined, which makes sense because the node input doesn't even exist yet. After the component has been mounted, Svelte assigned a reference to the <input> DOM node to the nameEl variable, thanks to the bind:this={nameEl} directive .
  5. To get the autofocus functionality working, replace the previous console.log() / onMount() block you added with this:
    onMount(() => autofocus && nameEl.focus())    // if autofocus is true, we run nameEl.focus()
    
    									
  6. Go to your app again, and you'll now see the <input> field is focused on page load.

注意: You can have a look at the other lifecycle functions in the Svelte docs , and you can see them in action in the interactive tutorial .

Waiting for the DOM to be updated with the tick() function

Now we will take care of the Todo component's focus management details. First of all, we want a Todo component's edit <input> to receive focus when we enter editing mode by pressing its 编辑 button. In the same fashion as we saw earlier, we'll create a nameEl variable inside Todo.svelte 和调用 nameEl.focus() after setting the editing 变量到 true .

  1. 打开文件 components/Todo.svelte and add a nameEl variable declaration, just below your editing and name declarations:
    let nameEl                              // reference to the name input DOM node
    
    									
  2. Now update your onEdit() function like so:
    function onEdit() {
      editing = true                        // enter editing mode
      nameEl.focus()                        // set focus to name input
    }
    
    									
  3. And finally, bind nameEl 到 <input> field, by updating it like so:
    <input bind:value={name} bind:this={nameEl} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
    
    									
  4. However, when you try the updated app you'll get an error along the lines of "TypeError: nameEl is undefined" in the console when you press a todo's 编辑 按钮。

So, what is happening here? When you update a component's state in Svelte, it doesn't update the DOM immediately. Instead, it waits until the next microtask to see if there are any other changes that need to be applied, including in other components. Doing so avoids unnecessary work and allows the browser to batch things more effectively.

In this case, when editing is false , the edit <input> is not visible because it does not exist in the DOM. Inside the onEdit() function we set editing = true and immediately afterwards try to access the nameEl variable and execute nameEl.focus() . The problem here is that Svelte hasn't yet updated the DOM.

One way to solve this problem is to use setTimeout() to delay the call to nameEl.focus() until the next event cycle, and give Svelte the opportunity to update the DOM.

Try this now:

function onEdit() {
  editing = true                        // enter editing mode
  setTimeout(() => nameEl.focus(), 0)   // asynchronous call to set focus to name input
}

							

The above solution works, but it is rather inelegant. Svelte provides a better way to handle these cases. The tick() function returns a promise that resolves as soon as any pending state changes have been applied to the DOM (or immediately, if there are no pending state changes). Let's try it now.

  1. First of all, import tick at the top of the <script> section alongside your existing import:
    import { tick } from 'svelte'
    
    									
  2. Next, call tick() with await from an async function ; update onEdit() 像这样:
    async function onEdit() {
      editing = true                        // enter editing mode
      await tick()
      nameEl.focus()
    }
    
    									
  3. If you try it now you'll see that everything works as expected.

注意: To see another example using tick() , visit the Svelte tutorial .

Adding functionality to HTML elements with the use:action directive

Next up, we want the name <input> to automatically select all text on focus. Moreover, we want to develop this in such a way that it could be easily reused on any HTML <input> and applied in a declarative way. We will use this requirement as an excuse to show a very powerful feature that Svelte provides us to add functionality to regular HTML elements: actions .

To select the text of a DOM input node we have to call select() . To get this function called whenever the node gets focused, we need an event listener along these lines:

node.addEventListener('focus', event => node.select()).

							

And, in order to avoid memory leaks, we should also call the removeEventListener() function when the node is destroyed.

注意: All this is just standard WebAPI functionality; nothing here is specific to Svelte.

We could achieve all this in our Todo component whenever we add or remove the <input> from the DOM, but we would have to be very careful to add the event listener after the node has been added to the DOM, and remove the listener before the node is removed from the DOM. In addition, our solution would not be very reusable.

That's where Svelte actions come into play. Basically they let us run a function whenever an element has been added to the DOM, and after removal from the DOM.

In our immediate use case, we will define a function called selectOnFocus() that will receive a node as parameter. The function will add an event listener to that node, so that whenever it gets focused it will select the text. Then it will return an object with a destroy property. The destroy property is what Svelte will execute after removing the node from the DOM. Here we will remove the listener to make sure we don't leave any memory leak behind.

  1. Let's create the function selectOnFocus() . Add the following to the bottom of the Todo.svelte <script> section:
    function selectOnFocus(node) {
      if (node && typeof node.select === 'function' ) {               // make sure node is defined and has a select() method
        const onFocus = event => node.select()                        // event handler
        node.addEventListener('focus', onFocus)                       // when node gets focus call onFocus()
        return {
          destroy: () => node.removeEventListener('focus', onFocus)   // this will be executed when the node is removed from the DOM
        }
      }
    }
    
    									
  2. Now we need to tell the <input> to use that function with the use:action directive:
    <input use:selectOnFocus />
    
    									
    With this directive we are telling Svelte to run this function, passing the DOM node of the <input> as a parameter, as soon as the component is mounted on the DOM. It will also be in charge of executing the destroy function when the component is removed from DOM. So, with the 使用 directive, Svelte takes care of the component's lifecycle for us. In our case, our <input> would end up like so — update the component's first label/input pair (inside the edit template) like so:
    <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
    <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text"
    />
    
    									
  3. Let's try it out. Go to your app, press a todo's 编辑 button, then Tab to take focus away from the <input> . Now click on the <input> — you'll see that the entire input text is selected.

Making the action reusable

Now let's make this function truly reusable across components. selectOnFocus() is just a function without any dependency on the Todo.svelte component, so we can just extract it to a file and use it from there.

  1. Create a new file, actions.js , inside the src 文件夹。
  2. Give it the following content:
    export function selectOnFocus(node) {
      if (node && typeof node.select === 'function' ) {               // make sure node is defined and has a select() method
        const onFocus = event => node.select()                        // event handler
        node.addEventListener('focus', onFocus)                       // when node gets focus call onFocus()
        return {
          destroy: () => node.removeEventListener('focus', onFocus)   // this will be executed when the node is removed from the DOM
        }
      }
    }
    
    									
  3. Now import it from inside Todo.svelte ; add the following import statement just below the others:
    import { selectOnFocus } from '../actions.js'
    
    									
  4. And remove the selectOnFocus() definition from Todo.svelte — we no longer need it there.

Reusing our action

To demonstrate our action's reusability, let's make use of it in NewTodo.svelte .

  1. Import selectOnFocus() from actions.js in this file too, as before:
    import { selectOnFocus } from '../actions.js'
    
    									
  2. 添加 use:selectOnFocus directive to the <input> ,像这样:
    <input bind:value={name} bind:this={nameEl} use:selectOnFocus
      type="text" id="todo-0" autoComplete="off" class="input input__lg"
    />
    
    									

With a few lines of code we can add functionality to regular HTML elements, in a very reusable and declarative way. It just takes an import and a short directive like use:selectOnFocus that clearly depicts its purpose. And we can achieve this without the need to create a custom wrapper element like TextInput , MyInput or similar. Moreover, you can add as many use:action directives as you want to an element.

Also, we didn't have to struggle with onMount() , onDestroy() or tick() — the 使用 directive takes care of the component lifecycle for us.

Other actions improvements

In the previous section, while working with the Todo components, we had to deal with bind:this , tick() ,和 async functions just to give focus to our <input> as soon as it was added to the DOM.

  1. This is how we can implement it with actions instead:
    const focusOnInit = (node) => node && typeof node.focus === 'function' && node.focus()
    
    									
  2. And then in our markup we just need to add another use: directive:
    <input bind:value={name} use:selectOnFocus use:focusOnInit ...
    
    									
  3. 我们的 onEdit() function can now be much simpler:
    function onEdit() {
      editing = true                        // enter editing mode
    }
    
    									

As a last example before we move on, let's go back to our Todo.svelte component and give focus to the 编辑 button after the user presses 保存 or 取消 .

We could try just reusing our focusOnInit action again, adding use:focusOnInit 到 编辑 button. But we'd be introducing a subtle bug. When you add a new todo, the focus will be put on the 编辑 button of the recently added todo. That's because the focusOnInit action is running when the component is created.

That's not what we want — we want the 编辑 button to receive focus only when the user has pressed 保存 or 取消 .

  1. So, go back to your Todo.svelte 文件。
  2. First of all we'll create a flag named editButtonPressed and initialize it to false . Add this just below your other variable definitions:
    let editButtonPressed = false           // track if edit button has been pressed, to give focus to it after cancel or save
    
    									
  3. Next, we'll modify the 编辑 button's functionality to save this flag, and create the action for it. Update the onEdit() function like so:
    function onEdit() {
      editButtonPressed = true              // user pressed the Edit button, focus will come back to the Edit button
      editing = true                        // enter editing mode
    }
    
    									
  4. Below it, add the following definition for focusEditButton() :
    const focusEditButton = (node) => editButtonPressed && node.focus()
    
    									
  5. Finally, we 使用 the focusEditButton action on the 编辑 button, like so:
    <button type="button" class="btn" on:click={onEdit} use:focusEditButton>
      Edit<span class="visually-hidden"> {todo.name}</span>
    </button>
    
    									
  6. Go back and try your app again. At this point, every time the 编辑 button is added to the DOM, the focusEditButton action is executed, but it will only give focus to the button if the editButtonPressed flag is true .

注意: We have barely scratched the surface of actions here. Actions can also have reactive parameters, and Svelte lets us detect when any of those parameters change. So we can add functionality that integrates nicely with the Svelte reactive system. Have a look at the relevant Svelte School article for a more detailed introduction to actions . Actions are also very useful for seamlessly integrating with third party libraries .

Component binding: exposing component methods and variables using the bind:this={component} directive

There's still one accessibility annoyance left. When the user presses the 删除 button, the focus vanishes.

So, the last feature we will be looking at in this article involves setting the focus on the status heading after a todo has been deleted.

Why the status heading? In this case, the element that had the focus has been deleted, so there's not a clear candidate to receive focus. We've picked the status heading because it's near the list of todos, and it's a way to give a visual feedback about the removal of the task, as well as indicating what's happened to screenreader users.

First we'll extract the status heading to its own component.

  1. Create a new file — components/TodosStatus.svelte .
  2. Add the following contents to it:
    <script>
      export let todos
      $: totalTodos = todos.length
      $: completedTodos = todos.filter(todo => todo.completed).length
    </script>
    <h2 id="list-heading">{completedTodos} out of {totalTodos} items completed</h2>
    
    									
  3. Import the file at the beginning of Todos.svelte , adding the following import statement below the others:
    import TodosStatus from './TodosStatus.svelte'
    
    									
  4. Replace the <h2> status heading inside Todos.svelte with a call to the TodosStatus component, passing todos to it as a prop, like so:
    <TodosStatus {todos} />
    
    									
  5. You can also do a bit of clean-up, removing the totalTodos and completedTodos variables from Todos.svelte . Just remove the $: totalTodos = ... 和 $: completedTodos = ... lines, and also remove the reference to totalTodos when we calculate newTodoId and use todos.length , instead. To do this, replace the block that begins with  let newTodoId with this:
    $: newTodoId = todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1
    
    									
  6. Everything works as expected — we just extracted the last piece of markup to its own component.

Now we need to find a way to give focus to the <h2> status label after a todo has been removed.

So far we saw how to send information to a component via props, and how a component can communicate with its parent by emitting events or using two-way data binding. The child component could get a reference to the <h2> node using bind:this={dom_node} and expose it to the outside using two-way data binding. But doing so would break the component encapsulation; setting focus on it should be its own responsibility.

So we need the TodosStatus component to expose a method that its parent can call to give focus to it. It's a very common scenario that a component needs to expose some behavior or information to the consumer; let's see how to achieve it with Svelte.

We've already seen that Svelte uses export let var = ... to declare props . But if instead of using let you export a const , class or function , it is read-only outside the component. Function expressions are valid props, however. In the following example, the first three declarations are props, and the rest are exported values:

<script>
  export let bar = 'optional default initial value'       // prop
  export let baz = undefined                              // prop
  export let format = n => n.toFixed(2)                   // prop
  // these are readonly
  export const thisIs = 'readonly'                        // read-only export
  export function greet(name) {                           // read-only export
    alert(`hello ${name}!`)
  }
  export const greet = (name) => alert(`hello ${name}!`)  // read-only export
</script>

							

With this in mind, let's go back to our use case. We'll create a function called focus() that gives focus to the <h2> heading. For that we'll need a headingEl variable to hold the reference to the DOM node and we'll have to bind it to the <h2> element using bind:this={headingEl} . Our focus method will just run headingEl.focus() .

  1. Update the contents of TodosStatus.svelte 像这样:
    <script>
      export let todos
      $: totalTodos = todos.length
      $: completedTodos = todos.filter(todo => todo.completed).length
      let headingEl
      export function focus() {   // shorter version: export const focus = () => headingEl.focus()
        headingEl.focus()
      }
    </script>
    <h2 id="list-heading" bind:this={headingEl} tabindex="-1">{completedTodos} out of {totalTodos} items completed</h2>
    
    									
    Note that we've added a tabindex 属性到 <h2> to allow the element to receive focus programmatically. As we saw earlier, using the bind:this={headingEl} directive gives us a reference to the DOM node in the variable headingEl . Then we use export function focus() to expose a function that gives focus to the <h2> heading. How can we access those exported values from the parent? Just as you can bind to DOM elements with the bind:this={dom_node} directive, you can also bind to component instances themselves with bind:this={component} . So, when you use bind:this on an HTML element, you get a reference to the DOM node, and when you do it on a Svelte component, you get a reference to the instance of that component.
  2. So to bind to the instance of TodosStatus we'll first create a todosStatus variable in Todos.svelte . Add the following line below your import 语句:
    let todosStatus                   // reference to TodosStatus instance
    
    									
  3. Next, add a bind:this={todosStatus} directive to the call, as follows:
    <!-- TodosStatus -->
    <TodosStatus bind:this={todosStatus} {todos} />
    
    									
  4. Now we can call the exported focus() method from our removeTodo() 函数:
    function removeTodo(todo) {
      todos = todos.filter(t => t.id !== todo.id)
      todosStatus.focus()             // give focus to status heading
    }
    
    									
  5. Go back to your app — now if you delete any todo, the status heading will be focussed — this is useful to highlight the change in numbers of todos, both to sighted users and screenreader users.

注意: You might be wondering why we need to declare a new variable for component binding — why can't we just call TodosStatus.focus() ? You might have multiple TodosStatus instances active, so you need a way to reference each particular instance. That's why you have to specify a variable to bind each specific instance to.

The code so far

Git

To see the state of the code as it should be at the end of this article, access your copy of our repo like this:

cd mdn-svelte-tutorial/06-stores

							

Or directly download the folder's content:

npx degit opensas/mdn-svelte-tutorial/06-stores

							

Remember to run npm install && npm run dev to start your app in development mode.

REPL

To see the current state of the code in a REPL, visit:

https://svelte.dev/repl/d1fa84a5a4494366b179c87395940039?version=3.23.2

摘要

In this article we have finished adding all the required functionality to our app, plus we've taken care of a number of accessibility and usability issues. We also finished splitting our app into manageable components, each one with a unique responsibility.

In the meantime, we saw a few advanced Svelte techniques, like:

  • Dealing with reactivity gotchas when updating objects and arrays.
  • Working with DOM nodes using bind:this={dom_node} (binding DOM elements).
  • Using the component lifecycle onMount() 函数。
  • Forcing Svelte to resolve pending state changes with the tick() 函数。
  • Adding functionality to HTML elements in a reusable and declarative way with the use:action 指令。
  • Accessing component methods using bind:this={component} (binding components).

In the next article we will see how to use stores to communicate between components, and add animations to our components.

  • 上一
  • Overview: Client-side JavaScript frameworks
  • 下一

In this module

  • Introduction to client-side frameworks
  • Framework main features
  • React
    • Getting started with React
    • Beginning our React todo list
    • Componentizing our React app
    • React interactivity: Events and state
    • React interactivity: Editing, filtering, conditional rendering
    • Accessibility in React
    • React resources
  • Ember
    • Getting started with Ember
    • Ember app structure and componentization
    • Ember interactivity: Events, classes and state
    • Ember Interactivity: Footer functionality, conditional rendering
    • Routing in Ember
    • Ember resources and troubleshooting
  • Vue
    • Getting started with Vue
    • Creating our first Vue component
    • Rendering a list of Vue components
    • Adding a new todo form: Vue events, methods, and models
    • Styling Vue components with CSS
    • Using Vue computed properties
    • Vue conditional rendering: editing existing todos
    • Focus management with Vue refs
    • Vue resources
  • Svelte
    • Getting started with Svelte
    • Starting our Svelte Todo list app
    • Dynamic behavior in Svelte: working with variables and props
    • Componentizing our Svelte app
    • Advanced Svelte: Reactivity, lifecycle, accessibility
    • Working with Svelte stores
    • TypeScript support in Svelte
    • Deployment and next steps
  • Angular
    • Getting started with Angular
    • Beginning our Angular todo list app
    • Styling our Angular app
    • Creating an item component
    • Filtering our to-do items
    • Building Angular applications and further resources

发现此页面有问题吗?

  • 编辑在 GitHub
  • 源在 GitHub
  • Report a problem with this content on GitHub
  • 想要自己修复问题吗?见 我们的贡献指南 .

最后修改: Jan 22, 2022 , 由 MDN 贡献者

相关话题

  1. Complete beginners start here!
  2. Web 快速入门
    1. Getting started with the Web overview
    2. 安装基本软件
    3. What will your website look like?
    4. 处理文件
    5. HTML 基础
    6. CSS 基础
    7. JavaScript 基础
    8. 发布您的网站
    9. How the Web works
  3. HTML — Structuring the Web
  4. HTML 介绍
    1. Introduction to HTML overview
    2. Getting started with HTML
    3. What's in the head? Metadata in HTML
    4. HTML text fundamentals
    5. Creating hyperlinks
    6. Advanced text formatting
    7. Document and website structure
    8. Debugging HTML
    9. Assessment: Marking up a letter
    10. Assessment: Structuring a page of content
  5. 多媒体和嵌入
    1. Multimedia and embedding overview
    2. Images in HTML
    3. Video and audio content
    4. From object to iframe — other embedding technologies
    5. Adding vector graphics to the Web
    6. Responsive images
    7. Assessment: Mozilla splash page
  6. HTML 表格
    1. HTML tables overview
    2. HTML table basics
    3. HTML Table advanced features and accessibility
    4. Assessment: Structuring planet data
  7. CSS — Styling the Web
  8. CSS 第一步
    1. CSS first steps overview
    2. What is CSS?
    3. Getting started with CSS
    4. How CSS is structured
    5. How CSS works
    6. Using your new knowledge
  9. CSS 构建块
    1. CSS building blocks overview
    2. Cascade and inheritance
    3. CSS 选择器
    4. The box model
    5. Backgrounds and borders
    6. Handling different text directions
    7. Overflowing content
    8. Values and units
    9. Sizing items in CSS
    10. Images, media, and form elements
    11. Styling tables
    12. Debugging CSS
    13. Organizing your CSS
  10. 样式化文本
    1. Styling text overview
    2. Fundamental text and font styling
    3. Styling lists
    4. Styling links
    5. Web fonts
    6. Assessment: Typesetting a community school homepage
  11. CSS 布局
    1. CSS layout overview
    2. Introduction to CSS layout
    3. Normal Flow
    4. Flexbox
    5. Grids
    6. Floats
    7. 位置
    8. Multiple-column Layout
    9. Responsive design
    10. Beginner's guide to media queries
    11. Legacy Layout Methods
    12. Supporting Older Browsers
    13. Fundamental Layout Comprehension
  12. JavaScript — Dynamic client-side scripting
  13. JavaScript 第一步
    1. JavaScript first steps overview
    2. What is JavaScript?
    3. A first splash into JavaScript
    4. What went wrong? Troubleshooting JavaScript
    5. Storing the information you need — Variables
    6. Basic math in JavaScript — Numbers and operators
    7. Handling text — Strings in JavaScript
    8. Useful string methods
    9. 数组
    10. Assessment: Silly story generator
  14. JavaScript 构建块
    1. JavaScript building blocks overview
    2. Making decisions in your code — Conditionals
    3. Looping code
    4. Functions — Reusable blocks of code
    5. Build your own function
    6. Function return values
    7. 事件介绍
    8. Assessment: Image gallery
  15. 引入 JavaScript 对象
    1. Introducing JavaScript objects overview
    2. Object basics
    3. 对象原型
    4. Object-oriented programming concepts
    5. Classes in JavaScript
    6. Working with JSON data
    7. Object building practice
    8. Assessment: Adding features to our bouncing balls demo
  16. 异步 JavaScript
    1. Asynchronous JavaScript overview
    2. General asynchronous programming concepts
    3. Introducing asynchronous JavaScript
    4. Cooperative asynchronous Java​Script: Timeouts and intervals
    5. Graceful asynchronous programming with Promises
    6. Making asynchronous programming easier with async and await
    7. Choosing the right approach
  17. 客户端侧 Web API
    1. 客户端侧 Web API
    2. Introduction to web APIs
    3. Manipulating documents
    4. Fetching data from the server
    5. Third party APIs
    6. Drawing graphics
    7. Video and audio APIs
    8. Client-side storage
  18. Web forms — Working with user data
  19. Core forms learning pathway
    1. Web forms overview
    2. Your first form
    3. How to structure a web form
    4. Basic native form controls
    5. The HTML5 input types
    6. Other form controls
    7. Styling web forms
    8. Advanced form styling
    9. UI pseudo-classes
    10. Client-side form validation
    11. Sending form data
  20. Advanced forms articles
    1. How to build custom form controls
    2. Sending forms through JavaScript
    3. CSS property compatibility table for form controls
  21. Accessibility — Make the web usable by everyone
  22. Accessibility guides
    1. Accessibility overview
    2. What is accessibility?
    3. HTML: A good basis for accessibility
    4. CSS and JavaScript accessibility best practices
    5. WAI-ARIA basics
    6. Accessible multimedia
    7. Mobile accessibility
  23. Accessibility assessment
    1. Assessment: Accessibility troubleshooting
  24. Tools and testing
  25. Client-side web development tools
    1. Client-side web development tools index
    2. Client-side tooling overview
    3. Command line crash course
    4. Package management basics
    5. Introducing a complete toolchain
    6. Deploying our app
  26. Introduction to client-side frameworks
    1. Client-side frameworks overview
    2. Framework main features
  27. React
    1. Getting started with React
    2. Beginning our React todo list
    3. Componentizing our React app
    4. React interactivity: Events and state
    5. React interactivity: Editing, filtering, conditional rendering
    6. Accessibility in React
    7. React resources
  28. Ember
    1. Getting started with Ember
    2. Ember app structure and componentization
    3. Ember interactivity: Events, classes and state
    4. Ember Interactivity: Footer functionality, conditional rendering
    5. Routing in Ember
    6. Ember resources and troubleshooting
  29. Vue
    1. Getting started with Vue
    2. Creating our first Vue component
    3. Rendering a list of Vue components
    4. Adding a new todo form: Vue events, methods, and models
    5. Styling Vue components with CSS
    6. Using Vue computed properties
    7. Vue conditional rendering: editing existing todos
    8. Focus management with Vue refs
    9. Vue resources
  30. Svelte
    1. Getting started with Svelte
    2. Starting our Svelte Todo list app
    3. Dynamic behavior in Svelte: working with variables and props
    4. Componentizing our Svelte app
    5. Advanced Svelte: Reactivity, lifecycle, accessibility
    6. Working with Svelte stores
    7. TypeScript support in Svelte
    8. Deployment and next steps
  31. Angular
    1. Getting started with Angular
    2. Beginning our Angular todo list app
    3. Styling our Angular app
    4. Creating an item component
    5. Filtering our to-do items
    6. Building Angular applications and further resources
  32. Git and GitHub
    1. Git and GitHub overview
    2. Hello World
    3. Git Handbook
    4. Forking Projects
    5. About pull requests
    6. Mastering Issues
  33. Cross browser testing
    1. Cross browser testing overview
    2. Introduction to cross browser testing
    3. Strategies for carrying out testing
    4. Handling common HTML and CSS problems
    5. Handling common JavaScript problems
    6. Handling common accessibility problems
    7. Implementing feature detection
    8. Introduction to automated testing
    9. Setting up your own test automation environment
  34. Server-side website programming
  35. 第一步
    1. First steps overview
    2. Introduction to the server-side
    3. Client-Server overview
    4. Server-side web frameworks
    5. Website security
  36. Django Web 框架 (Python)
    1. Django web framework (Python) overview
    2. 介绍
    3. 设置开发环境
    4. Tutorial: The Local Library website
    5. Tutorial Part 2: Creating a skeleton website
    6. Tutorial Part 3: Using models
    7. Tutorial Part 4: Django admin site
    8. Tutorial Part 5: Creating our home page
    9. Tutorial Part 6: Generic list and detail views
    10. Tutorial Part 7: Sessions framework
    11. Tutorial Part 8: User authentication and permissions
    12. Tutorial Part 9: Working with forms
    13. Tutorial Part 10: Testing a Django web application
    14. Tutorial Part 11: Deploying Django to production
    15. Web application security
    16. Assessment: DIY mini blog
  37. Express Web Framework (node.js/JavaScript)
    1. Express Web Framework (Node.js/JavaScript) overview
    2. Express/Node introduction
    3. Setting up a Node (Express) development environment
    4. Express tutorial: The Local Library website
    5. Express Tutorial Part 2: Creating a skeleton website
    6. Express Tutorial Part 3: Using a database (with Mongoose)
    7. Express Tutorial Part 4: Routes and controllers
    8. Express Tutorial Part 5: Displaying library data
    9. Express Tutorial Part 6: Working with forms
    10. Express Tutorial Part 7: Deploying to production
  38. Further resources
  39. Common questions
    1. HTML questions
    2. CSS questions
    3. JavaScript questions
    4. Web mechanics
    5. Tools and setup
    6. Design and accessibility
  • Web 技术
  • Learn Web Development
  • About MDN
  • Feedback
  • 关于
  • MDN Web Docs Store
  • 联络我们
  • Firefox

MDN

  • MDN on Twitter
  • MDN on Github

Mozilla

  • Mozilla on Twitter
  • Mozilla on Instagram

© 2005- 2022 Mozilla and individual contributors. Content is available under these licenses .

  • Terms
  • Privacy
  • Cookie