✅ todo list에 todoItem을 키보드로 입력하여 추가하기
✅ todo list의 체크박스를 클릭하여 complete 상태로 변경. (li tag 에 completed class 추가)
✅ todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
✅ todo list를 더블클릭했을 때 input 모드로 변경. (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
✅ todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
✅ todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기
Redux 창시자 Dan Abramov-Todo list 예제를 따라서 실습을 진행했다.
최상위 컴포넌트(TodoApp)에 TodoItems 라는 상태를 정의하고 해당 상태를 하위 컴포넌트에서 내려 받아 사용하는 방식이다.
이렇게 한 화면에서 상태를 공유할 수 있는 관리방법으로 setState
와 render
함수를 사용한 패턴이 많이 사용된다.
각 요구사항에 따라 핵심 기능을 추가하면서 진행해보자
가장 먼저 TodoItems
라는 이름의 배열 상태를 갖는 최상위 컴포넌트를 정의한다.
function TodoApp() {
const $todoList = document.querySelector('#todo-list')
this.todoItems = [] // todoItems 를 배열의 상태로 갖고 있다.
this.setState = updatedItems => {
this.todoItems = updatedItems
}
}
입력을 위한 TodoInput
함수 컴포넌트를 추가한다.
function TodoInput({onAdd}) {
const $todoInput = document.querySelector('#new-todo-title')
$todoInput.addEventListener('keyup', event =>
this.addTodoItem(event))
this.addTodoItem = event => {
$target = event.target
if (this.isValid(event, $target.value)) {
event.preventDefault()
onAdd($target.value)
$target.value = ""
}
}
this.isValid(event, value) {
return event.key === 'Enter' && value.trim() !== ""
}
}
최상위 앱에서 onAdd() 함수에 TodoItems를 채워넣는 로직을 구현해보자.
function TodoApp() {
const todoItems = []
let id = 0
new TodoInput({
onAdd: contents => {
const todoItem = new TodoItem(id++, contents, 'active')
this.todoItems.push(todoItem)
this.setState(this.todoItems)
}
})
}
새로운 todoItem을 받아서 추가했다면, 추가된 항목들을 화면에 띄우는 작업을 해보자.
TodoList라는 컴포넌트를 만들어서 render 함수를 구현하면 된다.
export function TodoList() {
const $todoList = document.querySet('#todo-list')
this.setState = updatedItems => {
this.render(updatedItems)
}
this.render(todoItems) {
const template = todoItems.map(TodoListTemplate)
this.$todoList.innerHTML = template.join('')
}
}
로직은 단순하다. setState함수의 인자로 들어온 upatedItems 데이터를 화면에 뿌려주는 역할을 한다. 이때 상위 앱에서 TodoList 인스턴스를 만들고 setState 함수를 호출해줘야 한다.
function TodoApp() {
const $todoList = document.querySelector('#todo-list')
const todoItems = []
this.setState = updatedItems => {
this.todoItems = updatedItems
$todoList.setState(updatedItems)
}
new TodoInput({
onAdd: contents => {
const todoItem = new TodoItem(id++, contents, 'active')
this.todoItems.push(todoItem)
this.setState(this.todoItems)
}
})
}
이렇게 하면 input 창에 새로운 항목을 입력하고 enter를 입력하면 list에 항목이 추가된 것을 볼 수 있다.
체크박스를 클릭하면 css를 변경함과 동시에 클릭된 TodoItem
의 상태를 completed로 변경하도록 구현해보자. 여기서는 이벤트 위임을 사용해야 한다.
먼저 checkbox 컴포넌트를 만들고 엘리먼트에 이벤트 리스너를 등록하자.
export function TodoCheckBox({onCheck}) {
const $todoList = document.querySelector('#todo-list')
$todoList.addEventListener('click', event =>
this.clickCheckbox(event))
this.clickCheckbox = event => {
const $target = event.target
if ($target.type !== 'checkbox') {
return;
}
onCheck($target.closest('li').id)
}
}
TodoCheckBox를 인스턴스를 생성하고 onCheck메서드를 외부에 정의해보자.
function TodoApp() {
const $todoList = document.querySelector('#todo-list')
const todoItems = []
this.setState = updatedItems => {
this.todoItems = updatedItems
$todoList.setState(updatedItems)
}
new TodoInput({
onAdd: contents => {
const todoItem = new TodoItem(id++, contents, 'active')
this.todoItems.push(todoItem)
this.setState(this.todoItems)
}
})
new TodoCheckBox({
onCheck: id => {
const updatedItems = this.todoItems.map(
item => item.swapCheckStatus(parseInt(id))
)
this.setState(updatedItems)
}
})
}
// TodoItem 에 swapCheckStatus() 함수 추가
export class TodoItem {
constructor(id, content, status) {
this.id = id
this.content = content
this.status = status
}
swapCheckStatus(id) {
if (this.id === id && this.status === 'active') {
this.status = 'completed'
} else if (this.id === id && this.status === 'completed') {
this.status = 'active'
}
return this
}
}
자 이렇게 하면 checkBox를 클릭하면 원하는대로 css가 토글이 되는 것을 확인할 수 있을 것이다.
이번에는 Todo항목을 삭제하는 기능을 추가해보자. checkbox를 클릭하면 특정 이벤트가 수행되는 것과 아주 비슷한 원리이므로 자세한 설명은 생략한다.
이번에는 x (destroy)버튼에 이벤트를 바인딩하기 위해서 해당 컴포넌트를 만들어보자.
export function TodoDestroy({onDestroy}) {
const $todoList = document.querySelector('#todo-list')
$todoList.addEventListener('click', event =>
this.onDestroy(event))
this.onDestroy = event => {
const $target = event.target
if ($target.classList.contains('destroy') {
event.preventDefault()
if (confirm("삭제하시겠습니까?")) {
onDestroy($target.closest('li').id)
}
}
}
}
// TodoApp.js
// ... 추가
new TodoDestroy({
onDestroy: id => {
const updatedItems = this.todoItems.filter(
item => item.id !== parseInt(id))
this.setState(updatedItems)
}
})
(단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀)
특정 리스트를 더블클릭했을 때, editing className을 토글할 수 있는 기능을 추가해보자.
li를 감싸고 있는 ul태그에 해당하는 #todo-list
에서 조작이 일어나야하므로 TodoList안에 이벤트를 바인딩하자.
// TodoList.js
export function TodoList({}) {
const $todoList = document.querySelector('#todo-list')
this.setState(updatedItems) {
return render(updatedItems)
}
this.render(items) {
const template = items.map(TodoListTemplate)
$todoList.innerHTML = template.join('')
}
this.onChangeToEditMode = event => {
event.preventDefault()
$target = event.target
$todoItem = $target.closest('li')
$todoItem.classList.toggle('editing')
}
$todoList.addEventListener('dblclick', event =>
onChangeToEditMode(event))
}
이렇게 하면 'editing'이라는 클래스 네임이 토글되는 것까지 성공이다. 실제로 수정이 가능하게 만들고 esc를 누르면 다시 view 모드로 돌아가게 만들어보자.
이를 위해서 TodoList에 'keyup'에 해당하는 이벤트를 추가하고 함수를 추가한다. 전체 앱 state에도 반영해야하기 때문에 state를 변경하는 로직은 외부로 뺀다.
// TodoList.js
export function TodoList({onEditMode, onUpdate}) {
const $todoList = document.querySelector("#todo-list");
this.setState = updatedTodoItems => {
this.render(updatedTodoItems);
};
this.render = items => {
const template = items.map(TodoItemTemplate);
$todoList.innerHTML = template.join("");
}
this.onChangeEditMode = event => {
event.preventDefault()
const $target = event.target
const $todoItem = $target.closest('li')
$todoItem.classList.toggle('editing')
onEditMode($todoItem.id)
}
this.onFinishEditMode = event => {
event.preventDefault()
const $target = event.target
if ($target && event.key === 'Escape') {
document.getSelection().anchorNode.classList.remove('editing')
} else if ($target && event.key === 'Enter') {
const id = $target.closest('li').id
onUpdate(id, $target.value)
}
}
$todoList.addEventListener('dblclick', event => this.onChangeEditMode(event))
$todoList.addEventListener('keyup', event => this.onFinishEditMode(event))
}
// TodoApp.js 에 TodoList 부분 변경
.....
const todoList = new TodoList({
onEditMode: id => {
const updateItems = this.todoItems.map(item => {
if (item.id === parseInt(id) && item.status === 'active') {
return new TodoItem(item.id, item.content, 'editing')
}
return item
})
this.setState(updateItems)
},
onUpdate: (id, value) => {
const updateItems = this.todoItems.map(item => {
if (item.id === parseInt(id) && item.status === 'editing') {
return new TodoItem(item.id, value, 'active')
}
return item
})
this.setState(updateItems)
}
}) // TodoList 함수 인스턴스 생성
....
이제 item의 총 개수를 계산하는 함수를 만들어보자.
이는 TodoCount라는 컴포넌트를 만들어서 어플리케이션 실행 시 init에서 state를 전달받는 코드를 추가해보자.
// TodoApp.js
... 추가
const todoCount = new TodoCount({
selectedTodoItems: selectedTodoItems => {
todoList.setState(selectedTodoItems)
}
})
this.init = () => {
todoCount.init()
todoCount.setState(this.todoItems)
todoList.setState(this.todoItems)
}
...
}
// TodoCount.js
export function TodoCount({onSelectedGroup}) {
const $count = document.querySelector('.todo-count')
const $all = document.querySelector('.all')
const $active = document.querySelector('.active')
const $completed = document.querySelector('.completed-job')
this.todoItems = []
this.setState = items => {
this.todoItems = [...items]
this.render(this.todoItems)
}
this.removeSelected = () => {
$all.classList.remove('selected')
$active.classList.remove('selected')
$completed.classList.remove('selected')
}
this.onShowSelectedItems = (event, element, status) => {
event.preventDefault()
this.removeSelected()
element.classList.toggle('selected')
let selectedItems = this.todoItems
if (status === 'completed' || status === 'active') {
selectecItems = this.todoItems.filter(
item => item.status === status)
}
this.render(selectedItems)
onSelectedGroup(selectedItems)
}
this.init() {
$all.addEventListener('click', event => onShowAllItems(event, $all, ''))
$active.addEventListener('click', event => onShowActiveItems(event, $active, 'active'))
$completed.addEventListener('click', event => onShowCompletedItems(event, $completed, 'completed'))
}
this.render(items) {
$count.innerHTML = TodoCountTemplate(items)
}
}
이렇게 하면 처음 작성한 요구사항을 모두 만족하는 TodoList를 만들 수 있다.
setState와 render를 통해서 상위 컴포넌트에 공통자원을 저장해놓고 필요할 때마다 가져다 쓰는 방식으로 구현할 수 있다.
이전 글에서는 store 객체를 따로 만들고 상태를 별도로 관리 했었는데 이번 글에서는 상위 컴포넌트에서 관리하는 방법을 사용해보았다.
다음 포스팅에서는 todo list에 Ajax 통신을 추가해보겠다.