15. Todo - Filter

간단한 Todo App 을 만들어봅니다.

목표

선택된 필터에 타입에 맞게 다른 리스트를 보여준다.

실습

하단에 아래와 같이 아이템의 갯수와 아이템을 필터링 할 수 있는 필터들이 있습니다.

먼저 필터를 구성해보겠습니다.

filter 를 구성하기 위한 state 를 추가합니다. filter 의 종류는 전체, 진행중, 진행완료 세가지로 이뤄집니다.

// components/Footer.vue 

<template>
  <footer class="footer">
    <span class="todo-count">
      <strong>10</strong> items left
    </span>
    <ul class="filters">
      <li v-for="(filter, idx) in filters" :key="idx">
        <a href="javascript:;" class="selected">{{ filter }}</a>
      </li>
    </ul>
  </footer>
</template>

<script>
export default {
  data() {
    return {
      filters: ["All", "Active", "Completed"]
    };
  }
};
</script>

아래와 같이 filter 들이 추가되는 것을 볼 수 있습니다.

selected 라는 class 는 선택된 filter 에만 적용이 되어야 합니다. 선택된 filter 값은 누가 들고 있는것이 좋을까요? filter 의 역할을 생각해보면 간단합니다. 선택된 filter 에 따라서 todo list 에 변화가 일어납니다. todo list 는 App.vue 에서 가지고 있는 state 입니다. 해당 state 에 변화를 주어야되니 App.vue 에 선택된 filter 값을 들고 있어야합니다.

// App.vue

export default {
 // ...,
 data() {
    return {
      todos: [
        {
          id: new Date(),
          text: "Vue 공부하기",
          isDone: true
        },
        {
          id: new Date() + 1,
          text: "치킨 먹기",
          isDone: false
        }
      ],
      filterType: 'All' // 기본 필터는 All 로 지정하겠습니다.
    }
  },
  // ...
}

이제 이 필터값을 Footer.vue 에게 넘겨주면됩니다. Footer.vue 에서는 이 필터 값을 바탕으로 선택된 필터인지 판단합니다.

// App.vue

<template>
  <div id="app">
    <section class="todoapp">
      <Header @insertTodo="insertTodo" />
       <Todo
        :todos="todos"
        @removeTodo="removeTodo"
        @updateDone="updateDone"
        @updateTodo="updateTodo"
      />
      <Footer :filterType="filterType"/>
    </section>
  </div>
</template>

App.vue 에서 넘겨받은 filterType 을 바탕으로 selected class 를 제어합니다.

// components/Footer.vue

<template>
  <footer class="footer">
    <span class="todo-count">
      <strong>10</strong> items left
    </span>
    <ul class="filters">
      <li v-for="(filter, idx) in filters" :key="idx">
        <a href="javascript:;" :class="{selected: filterType === filter}">{{ filter }}</a>
      </li>
    </ul>
  </footer>
</template>

<script>
export default {
  props: {
      filterType: { type: String, default: 'All'}
  },
  data() {
    return {
      filters: ["All", "Active", "Completed"]
    };
  }
};
</script>

이제 눌린 filter 로 filterType 을 변경해보겠습니다. handleFilterType 함수는 type 을 받아 filterType 을 업데이트 합니다.

// App.vue
<template>
  <div id="app">
    <section class="todoapp">
      <Header @insertTodo="insertTodo" />
       <Todo
        :todos="todos"
        @removeTodo="removeTodo"
        @updateDone="updateDone"
        @updateTodo="updateTodo"
      />
      <Footer :filterType="filterType" @onFilterType="handleFilterType"/>
    </section>
  </div>
</template>

export default {
  // ...
  methods: {
    // ...,
    handleFilterType(type) {
      this.filterType = type
    }
  }
};
</script>

Footer.vue 에서는 filter 가 클릭되었을 때 눌린 filter 의 값을 onFilterType 으로 넘겨줍니다.

// components/Footer.vue

<template>
  <footer class="footer">
    <span class="todo-count">
      <strong>10</strong> items left
    </span>
    <ul class="filters">
      <li v-for="(filter, idx) in filters" :key="idx" @click="handleFilterType(filter)">
        <a href="javascript:;" :class="{selected: filterType === filter}">{{ filter }}</a>
      </li>
    </ul>
  </footer>
</template>

<script>
export default {
  props: {
      filterType: { type: String, default: 'All'}
  },
  data() {
    return {
      filters: ["All", "Active", "Completed"]
    };
  },
 methods: {
    handleFilterType (type) {
      this.$emit('onFilterType', type)
    }
 }
};
</script>

이제는 바뀐 filterType 에 따라 다른 리스트만 보여주면 됩니다. computed 를 이용해서 필터링을 할거에요

  • All: 전체 리스트

  • Active: isDone 이 false 인 완료되지 않은 리스트

  • Completed: isDone 이 true 인 완료된 리스트

그리고 Todo.vue 로 넘겨주던 데이터를 필터링된 리스트로 내려주면됩니다. 아이템 갯수 또한 필터링된 리스트의 사이즈로 내려줍니다

// App.vue

<template>
  <div id="app">
    <section class="todoapp">
      <Header @insertTodo="insertTodo" />
       <Todo
        :todos="filteredList"
        @removeTodo="removeTodo"
        @updateDone="updateDone"
        @updateTodo="updateTodo"
      />
      <Footer 
        :filterType="filterType" 
        :size="filteredList.length"
        @onFilterType="handleFilterType"
      />
    </section>
  </div>
</template>

export default {
  // ...
  computed: {
    filteredList () {
      switch(this.filterType) {
        case "All": {
          return this.todos
        }
        case "Active": {
          return this.todos.filter((todo) => !todo.isDone)
        }
        case "Completed": {
          return this.todos.filter((todo) => todo.isDone)
        }
        default: {
          return []
        }
      }
    }
  },
  // ...
}

Footer.vue 에서는 size prop 을 받아서 보여줍니다.

// components/Footer.vue

<template>
  <footer class="footer">
    <span class="todo-count">
      <strong>{{ size }}</strong> items left
    </span>
    <ul class="filters">
      <li v-for="(filter, idx) in filters" :key="idx" @click="handleFilterType(filter)">
        <a href="javascript:;" :class="{selected: filterType === filter}">{{ filter }}</a>
      </li>
    </ul>
  </footer>
</template>

<script>
export default {
  props: {
    filterType: { type: String, default: 'All'},
    size: { type: Number, default: 0 }
  },
  // ... 
};
</script>

Last updated