【Svelte从入门到精通】实战篇——TodoList之数据交互

327 阅读2分钟

状态管理

安装tailwind-merge

npm install tailwind-merge -D

tailwind-merge是一个类似于classnames的工具。

新建store.js,用来管理我们的数据

import { writable } from 'svelte/store';

export const tab = writable('todo'); // todo | done | all

我们修改Tabs.svelte

<script>
  import { twMerge } from "tailwind-merge";
  import { tab } from "./store";
  let tabsClass = "grid grid-cols-3 gap-4 h-12 my-4";
  let tabClass =
    "h-12 flex items-center justify-center hover:cursor-pointer rounded-lg";
  let tabActiveClass = "bg-color2 text-white"

  const changeTab = (t) => tab.set(t);
</script>

<div class={tabsClass}>
  <button
    class={twMerge(tabClass, $tab === "todo" && tabActiveClass)}
    on:click={() => changeTab("todo")}>Todo</button
  >
  <button
    class={twMerge(tabClass, $tab === "done" && tabActiveClass)}
    on:click={() => changeTab("done")}>Done</button
  >
  <button
    class={twMerge(tabClass, $tab === "all" && tabActiveClass)}
    on:click={() => changeTab("all")}>All</button
  >
</div>
<div class="flex-1 overflow-auto">
  <slot></slot>
</div>

主要的逻辑修改就是,判断当前激活的是哪个tab,然后添加active样式,页面此时如下:

active style

当点击其中一个tab时,能够看到tab样式的变化。

在store.js中添加待办项列表的数据:

import { writable, derived } from 'svelte/store';

export const tab = writable('todo'); // todo | done | all
export const list = writable([]);

export const todoList = derived([list], ([$list]) => {
  return $list.filter(item => !item.done);
});

export const doneList = derived([list], ([$list]) => {
  return $list.filter(item => item.done);
});

修改List.svelte

<script>
  import Item from './Item.svelte';
  import { tab, list, todoList, doneList } from './store';

  let currentList = [];

  $: {
    switch($tab) {
      case 'all': currentList = $list;
        break;
      case 'done': currentList = $doneList;
        break;
      default: 
        currentList = $todoList;
    }
  };
  
</script>

{#each currentList as {text, done}, i}
  <Item index={i} name={text} done={done} />
  {:else}
    No data
{/each}

然后我们开始为输入框添加事件,当点击按钮时,添加待办项:

<script>
  import { list } from './store';

  let inputClass = "flex-1 h-full border rounded-lg mr-4 px-4 caret-color2 focus:outline-color2";
  let buttonClass = "w-[100px] h-full rounded-lg flex items-center justify-center bg-color2 text-white flex-shrink-0 hover:cursor-pointer";

  let task = ''
  const addItem = () => {
    if (!task) {
      return;
    }
    const obj = {
      id: new Date().valueOf(),
      text: task,
      done: false,
    };
    $list = [...$list, obj];
  };
</script>

<div class="flex h-12">
  <input
    type="text"
    bind:value={task}
    class={inputClass}
  />
  <button
    class={buttonClass} on:click={addItem}>Add</button
  >
</div>

Add Todo Item

在我们点击勾选后,已完成的待办项仍留在Todo栏里。 完善Item的逻辑,从外部接收tab参数,用来修饰index的样式,然后是为checkbox绑定更新事件:

<script>
  import { createEventDispatcher } from 'svelte';
  import { twMerge } from 'tailwind-merge';
  
  export let tab = 'todo';
  
  const dispatch = createEventDispatcher();

  let indexWrapClass = "w-12 h-12 flex items-center justify-center"
  $: indexClass = twMerge(
    "w-8 h-8 text-left flex-shrink-0 rounded-full text-color1 flex items-center justify-center",
    tab === 'todo' && 'bg-color3',
    tab === 'done' && 'bg-color2',
    tab === 'all' && 'bg-color4',
  );

  ...

  const changeDone = (e) => {
    dispatch("change", { checked: e.target.checked });
  };
</script>

<div class={divClass}>
  <span class={indexWrapClass}>
    <span class={indexClass}>{index}</span>
  </span>
  <span class={nameClass}>{name}</span>
  <span class={checkboxWrapClass}>
    <input
      type="checkbox"
      checked={done}
      on:change={changeDone}
    />
  </span>
</div>

<style>
...
</style>

接着是外层的list,为Item绑定事件,用于更新store里的数据。剩余的是一些边角料工作,将index设置成从1开始index={i + 1},传递tab参数,数据为空时的empty居中样式等。

<script>
  import Item from "./Item.svelte";
  import { tab, list, todoList, doneList } from "./store";

  ...

  const updateItem = (id, value) => {
    const newArr = $list.map((item) => {
      if (item.id === id) {
        return {
          ...item,
          done: value,
        };
      }
      return item;
    });
    $list = newArr;
  };
</script>

{#each currentList as { id, text, done }, i}
  <Item
    index={i + 1}
    name={text}
    {done}
    tab={$tab}
    on:change={({ detail: { checked } }) => updateItem(id, checked)}
  />
{:else}
  <div class="mt-20 text-center">No data</div>
{/each}

写到这里,读者们应该能正常地添加任务,设置任务状态,然而细心的你一定能够发现,当我们设置完成时,出现了一点小问题:

当我们在设置第一项已完成后,第一项能够正常移除,然后剩余的第二项变成第一项后,勾选状态却是已完成!这个时候就轮到key出场了。

{#each currentList as { id, text, done }, i (id)}

接着我们将All栏的展示改成禁止点击,现在Item.svelte中:

<script>
  ...
  
  export let disabled = false;

  ...
  
  $: nameClass = twMerge(
    "flex-1 overflow-hidden text-ellipsis",
    disabled && "opacity-65"
  );
</script>

<div class={divClass}>
  ...
  <input
    type="checkbox"
    checked={done}
    on:change={changeDone}
    {disabled}
  />
  ...
</div>

<style>
  ...

  input[type="checkbox"]:disabled::before {
    box-shadow: inset 1em 1em #FFADA8;
  }
</style>

然后在List.svelte中

<Item
  ...
  disabled={$tab === 'all'}
/>

动画

在之前我们mock数据时,如果读者们点击过勾选框,应该能注意到有动画效果。然而经过上述的一系列改动,我们发现,当我们点击勾选后,已完成的数据马上被移到Done的Tab栏。

为了能够观察到动画效果,在List.svelte中,我们将数据更新做一个延迟

const updateItem = (id, value) => {
  const newArr = $list.map((item) => {
    if (item.id === id) {
      return {
        ...item,
        done: value,
      };
    }
    return item;
  });
  setTimeout(() => {
    $list = newArr;
  }, 300);
};

接着我们为Item.svelte添加一些Svelte自带的动画效果

<script>
  import { slide } from "svelte/transition";

  ...
</script>

<div class={divClass} transition:slide>
  ...
</div>

<style>
  ...
</style>

至此,我们已基本完成了一个功能完整的TodoList项目。

小结

本章我们完成了:

  • 使用svelte/store进行状态管理
  • 循环列表中key值的使用,防止不正常的渲染
  • 为待办项的增加和减少添加动画