Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[step2] 2주차: Web Component 구성 #5

Open
wants to merge 2 commits into
base: pul8219
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion step2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@
- [ ] DOM 사용 최소화화기
- [ ] State(혹은 data)가 변할 때 만 렌더링이 되도록 구성
- [ ] TodoList 리팩토링

8 changes: 8 additions & 0 deletions step2/src/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.todo-list li {
list-style: none;
}

.todo-list li.completed label {
color: gray;
text-decoration: line-through;
}
14 changes: 14 additions & 0 deletions step2/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="module" src="./js/index.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
98 changes: 98 additions & 0 deletions step2/src/js/TodoApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Component from './core/Component.js';
import TodoHeader from './components/TodoHeader.js';
import TodoInput from './components/TodoInput.js';
import TodoList from './components/TodoList.js';
import newGuid from './utils/newGuid.js';

export default class TodoApp extends Component {
init() {
this.state = {
todoItems: [
{
id: newGuid(),
content: '1번 투두',
isComplete: false,
createdAt: Date.now(),
},
{
id: newGuid(),
content: '2번 투두',
isComplete: true,
createdAt: Date.now(),
},
{
id: newGuid(),
content: '3번 투두',
isComplete: false,
createdAt: Date.now(),
},
],
selectedItem: -1,
};
}

template() {
return `
<div class="header-container"></div>
<div class="input-container"></div>
<div class="list-container"></div>
`;
}

mounted() {
const $headerContainer = document.querySelector('.header-container');
const $inputContainer = document.querySelector('.input-container');
const $listContainer = document.querySelector('.list-container');
new TodoHeader($headerContainer);
new TodoInput($inputContainer, {
onAdd: this.onAdd.bind(this),
});
new TodoList($listContainer, {
data: this.state,
onoffEditMode: this.onoffEditMode.bind(this),
onEdit: this.onEdit.bind(this),
onDelete: this.onDelete.bind(this),
onToggle: this.onToggle.bind(this),
});
}

onAdd(content) {
const newItem = {
id: newGuid(),
content: content,
createdAt: Date.now(),
isComplete: false,
};

this.setState({ todoItems: [...this.state.todoItems, newItem] });
}

onoffEditMode(id) {
this.setState({ selectedItem: id });
}

onEdit(id, content) {
const data = JSON.parse(JSON.stringify(this.state));

data.todoItems.find((todo) => todo.id === id).content = content;
data.selectedItem = -1;

this.setState(data);
}

onDelete(id) {
const data = JSON.parse(JSON.stringify(this.state));
const todoIndex = data.todoItems.findIndex((todo) => todo.id === id);
data.todoItems.splice(todoIndex, 1);
this.setState(data);
}

onToggle(id) {
const data = JSON.parse(JSON.stringify(this.state));

const todoData = data.todoItems.find((todo) => todo.id === id);
todoData.isComplete = !todoData.isComplete;

this.setState(data);
}
}
9 changes: 9 additions & 0 deletions step2/src/js/components/TodoHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Component from '../core/Component.js';

export default class TodoHeader extends Component {
template() {
return `
<h1>TODOLIST</h1>
`;
}
}
24 changes: 24 additions & 0 deletions step2/src/js/components/TodoInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Component from '../core/Component.js';
import addEvent from '../utils/addEvent.js';

export default class TodoInput extends Component {
template() {
return `
<input class="new-todo" placeholder="오늘의 할 일" autofocus />
`;
}

setEvent() {
const { onAdd } = this.props;

addEvent('keydown', '.new-todo', this.$target, (event) => {
if (event.key !== 'Enter') return;

const content = event.target.value;

if (!content.trim()) return alert('Todo의 내용을 입력해주세요.');

onAdd(content);
});
}
}
82 changes: 82 additions & 0 deletions step2/src/js/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Component from '../core/Component.js';
import addEvent from '../utils/addEvent.js';

export default class TodoList extends Component {
template() {
const { todoItems, selectedItem } = this.props.data;

return `<ul class="todo-list">${todoItems
.map((todoItem) => {
if (selectedItem === todoItem.id) {
return `
<li class="editing" data-id=${todoItem.id}>
<div class="view">
<form name="modify-form" method="post">
<input class="modify-todo" value='${todoItem.content}' />
<button type="submit">완료</button>
<button class="modify-cancel" type="button">취소</button>
</form>
</div>
</li>
`;
}
return `
<li data-id=${todoItem.id} ${
todoItem.isComplete ? `class="completed"` : ''
}>
<div class="view">
<input class="toggle" type="checkbox" ${
todoItem.isComplete ? 'checked' : ''
}>
<label>${todoItem.content}</label>
<button class="modify" type="button">수정</button>
<button class="destroy" type="button">삭제</button>
</div>
</li>
`;
})
.join('')}</ul>`;
}

setEvent() {
const { onoffEditMode, onEdit, onDelete, onToggle } = this.props;

// 수정 버튼 클릭
addEvent('click', '.modify', this.$target, (event) => {
const id = event.target.closest('li').dataset.id;
onoffEditMode(id);
});

// 수정 취소 버튼 클릭
addEvent('click', '.modify-cancel', this.$target, () => {
onoffEditMode(-1);
});

// 수정 submit
addEvent('submit', `form[name='modify-form']`, this.$target, (event) => {
event.preventDefault();

const content = event.target.querySelector('input').value;

if (!content.trim()) return alert('Todo의 내용을 입력해주세요.');

const id = event.target.closest('li').dataset.id;

onEdit(id, content);
});

// 삭제
addEvent('click', '.destroy', this.$target, (event) => {
const id = event.target.closest('li').dataset.id;

onDelete(id);
});

// 토글
addEvent('click', '.toggle', this.$target, (event) => {
const id = event.target.closest('li').dataset.id;

onToggle(id);
});
}
}
37 changes: 37 additions & 0 deletions step2/src/js/core/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default class Component {
$target; // target element
state;
props;

constructor($target, props) {
this.$target = $target;
this.props = props;
this.setup();
this.render();
this.setEvent();
}

setup() {
this.init();
}

init() {}

template() {
return '';
}

render() {
this.$target.innerHTML = this.template();
this.mounted();
}

setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}

setEvent() {}

mounted() {}
}
5 changes: 5 additions & 0 deletions step2/src/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import TodoApp from './TodoApp.js';

const $app = document.querySelector('#app');

new TodoApp($app);
11 changes: 11 additions & 0 deletions step2/src/js/utils/addEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const addEvent = (eventType, selector, $target, callback) => {
const children = [...$target.querySelectorAll(selector)];
const isTarget = (target) =>
children.includes(target) || target.closest(selector);
$target.addEventListener(eventType, (event) => {
if (!isTarget(event.target)) return false;
callback(event);
});
};

export default addEvent;
53 changes: 53 additions & 0 deletions step2/src/js/utils/newGuid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// export { newGuid };

export default function newGuid() {
var hexValues = [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
];

// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
var oct = '',
tmp;
for (var a = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct +=
hexValues[tmp & 0xf] +
hexValues[(tmp >> 4) & 0xf] +
hexValues[(tmp >> 8) & 0xf] +
hexValues[(tmp >> 12) & 0xf] +
hexValues[(tmp >> 16) & 0xf] +
hexValues[(tmp >> 20) & 0xf] +
hexValues[(tmp >> 24) & 0xf] +
hexValues[(tmp >> 28) & 0xf];
}

// "Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively"
var clockSequenceHi = hexValues[(8 + Math.random() * 4) | 0];
return (
oct.substr(0, 8) +
'-' +
oct.substr(9, 4) +
'-4' +
oct.substr(13, 3) +
'-' +
clockSequenceHi +
oct.substr(16, 3) +
'-' +
oct.substr(19, 12)
);
}