Skip to content

Commit

Permalink
Chapter 2 / 순수함수 렌더링 (#6)
Browse files Browse the repository at this point in the history
* fix: 변수++ 형식을 변수 += 1로 변경

* test: 'innerHTML을 생성하는 유틸리티 함수' 테스트 코드 작성

* feat: innerHTML을 생성하는 유틸리티 함수 구현 완료

* feat: todos.js 구현 완료

* test: counter 테스트 케이스 추가

* feat: counter.js 구현 완료

* feat: filters.js 구현 완료

* chore: .js 확장자를 붙일 수 있도록 eslint 규칙 수정

* fix: 테스트 코드를 제외한 js import 구문에 .js 확장자 추가

* chore(02. 렌더링/02): 닉네임 폴더 아래로 파일 이동
  • Loading branch information
fecapark authored Jan 19, 2024
1 parent 6b7fffe commit ec10308
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 0 deletions.
19 changes: 19 additions & 0 deletions 02. 렌더링/02/fecapark/getTodos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { faker } = window;

const createElement = () => ({
text: faker.random.words(2),
completed: faker.random.boolean(),
});

const repeat = (elementFactory, number) => {
const array = [];
for (let index = 0; index < number; index += 1) {
array.push(elementFactory());
}
return array;
};

export default () => {
const howMany = faker.random.number(10);
return repeat(createElement, howMany);
};
44 changes: 44 additions & 0 deletions 02. 렌더링/02/fecapark/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<html>
<head>
<link rel="shortcut icon" href="../favicon.ico" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.js"></script>
<title>Frameworkless Frontend Development: Rendering</title>
</head>

<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
</section>
<footer class="footer">
<span class="todo-count">1 Item Left</span>
<ul class="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://twitter.com/thestrazz86">Francesco Strazzullo</a></p>
<p>Thanks to <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script type="module" src="index.js"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions 02. 렌더링/02/fecapark/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import getTodos from './getTodos.js';
import appView from './view/app.js';

const state = {
todos: getTodos(),
currentFilter: 'All',
};

const main = document.querySelector('.todoapp');

window.requestAnimationFrame(() => {
const newMain = appView(main, state);
main.replaceWith(newMain);
});
17 changes: 17 additions & 0 deletions 02. 렌더링/02/fecapark/view/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import todosView from './todos.js';
import counterView from './counter.js';
import filtersView from './filters.js';

export default (targetElement, state) => {
const element = targetElement.cloneNode(true);

const list = element.querySelector('.todo-list');
const counter = element.querySelector('.todo-count');
const filters = element.querySelector('.filters');

list.replaceWith(todosView(list, state));
counter.replaceWith(counterView(counter, state));
filters.replaceWith(filtersView(filters, state));

return element;
};
16 changes: 16 additions & 0 deletions 02. 렌더링/02/fecapark/view/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const getNotCompleteCount = (todos) => todos.filter((todo) => !todo.completed).length;
const getNotCompleteTextContent = (todos) => {
const count = getNotCompleteCount(todos);
if (count === 0) return 'No item left';
if (count === 1) return '1 Item left';
return `${count} Items left`;
};

export default (targetElement, state) => {
const { todos } = state;

const newElement = targetElement.cloneNode(true);
newElement.textContent = getNotCompleteTextContent(todos);

return newElement;
};
64 changes: 64 additions & 0 deletions 02. 렌더링/02/fecapark/view/counter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import counterView from './counter';

let targetElement;

describe('counterView', () => {
beforeEach(() => {
targetElement = document.createElement('div');
});

test('새로운 DOM 요소는 완료되지 않은 todo의 수를 가지고 있어야 한다.', () => {
const newCounter = counterView(targetElement, {
todos: [
{
text: 'First',
completed: true,
},
{
text: 'Second',
completed: false,
},
{
text: 'Third',
completed: false,
},
],
});

expect(newCounter.textContent).toBe('2 Items left');
});

test('완료하지 않은 todo가 1개일 경우를 고려해야 한다.', () => {
const newCounter = counterView(targetElement, {
todos: [
{
text: 'First',
completed: true,
},
{
text: 'Third',
completed: false,
},
],
});

expect(newCounter.textContent).toBe('1 Item left');
});

test('전부 완료했을 때의 경우도 고려해야 한다.', () => {
const newCounter = counterView(targetElement, {
todos: [
{
text: 'First',
completed: true,
},
{
text: 'Third',
completed: true,
},
],
});

expect(newCounter.textContent).toBe('No item left');
});
});
20 changes: 20 additions & 0 deletions 02. 렌더링/02/fecapark/view/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const getAllListAnchorsFrom = (element) => [...element.querySelectorAll('li a')];
const setSelectedClassNameToAnchors = (anchors, currentFilter) => {
anchors.forEach((anchor) => {
if (anchor.textContent === currentFilter) {
anchor.classList.add('selected');
} else {
anchor.classList.remove('selected');
}
});
};

export default (targetElement, state) => {
const { currentFilter } = state;

const newElement = targetElement.cloneNode(true);
const anchors = getAllListAnchorsFrom(newElement);
setSelectedClassNameToAnchors(anchors, currentFilter);

return newElement;
};
32 changes: 32 additions & 0 deletions 02. 렌더링/02/fecapark/view/filters.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import filtersView from './filters';

let targetElement;
const TEMPLATE = `<ul class="filters">
<li>
<a href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>`;

describe('filtersView', () => {
beforeEach(() => {
const tempElement = document.createElement('div');
tempElement.innerHTML = TEMPLATE;
[targetElement] = tempElement.childNodes;
});

test('"currentFilter"와 동일한 텍스트를 가지는 anchor 태그에 "selected" 클래스를 추가해야 한다.', () => {
const newCounter = filtersView(targetElement, {
currentFilter: 'Active',
});

const selectedItem = newCounter.querySelector('li a.selected');

expect(selectedItem.textContent).toBe('Active');
});
});
44 changes: 44 additions & 0 deletions 02. 렌더링/02/fecapark/view/todos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createInnerHTML } from './utils.js';

const getTodoItemElement = ({ text, completed }) => {
const toggle = createInnerHTML('input', {
attributes: {
class: 'toggle',
type: 'checkbox',
checked: completed,
},
});
const label = createInnerHTML('label', {
innerHTML: text,
});
const edit = createInnerHTML('input', {
attributes: {
class: 'edit',
value: text,
},
});
const viewBox = createInnerHTML('div', {
attributes: {
class: 'view',
},
innerHTML: toggle + label,
});

return createInnerHTML('li', {
attributes: {
class: completed ? 'completed' : '',
},
innerHTML: `${viewBox}${edit}`,
});
};

export default (targetElement, state) => {
const { todos } = state;

const innerHTML = todos.map((todo) => getTodoItemElement(todo)).join('');

const newElement = targetElement.cloneNode(true);
newElement.innerHTML = innerHTML;

return newElement;
};
58 changes: 58 additions & 0 deletions 02. 렌더링/02/fecapark/view/todos.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import todosView from './todos';

let targetElement;

describe('filtersView', () => {
beforeEach(() => {
targetElement = document.createElement('ul');
});

test('모든 todo 요소에 대해서 li 태그를 생성해야 한다.', () => {
const newCounter = todosView(targetElement, {
todos: [
{
text: 'First',
completed: true,
},
{
text: 'Second',
completed: false,
},
{
text: 'Third',
completed: false,
},
],
});

const items = newCounter.querySelectorAll('li');
expect(items.length).toBe(3);
});

test('"todos"에 따라 모든 li 요소에 올바른 속성을 설정해야 한다.', () => {
const newCounter = todosView(targetElement, {
todos: [
{
text: 'First',
completed: true,
},
{
text: 'Second',
completed: false,
},
],
});

const [firstItem, secondItem] = newCounter.querySelectorAll('li');

expect(firstItem.classList.contains('completed')).toBe(true);
expect(firstItem.querySelector('.toggle').checked).toBe(true);
expect(firstItem.querySelector('label').textContent).toBe('First');
expect(firstItem.querySelector('.edit').value).toBe('First');

expect(secondItem.classList.contains('completed')).toBe(false);
expect(secondItem.querySelector('.toggle').checked).toBe(false);
expect(secondItem.querySelector('label').textContent).toBe('Second');
expect(secondItem.querySelector('.edit').value).toBe('Second');
});
});
13 changes: 13 additions & 0 deletions 02. 렌더링/02/fecapark/view/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const createInnerHTML = (tagName, { attributes = {}, innerHTML = '' } = {}) => {
const arrtibuteString = Object.entries(attributes)
.map(([name, value]) => {
if (typeof value === 'boolean') return value ? name : '';
return `${name}="${value}"`;
})
.join(' ');
const closeTagString = innerHTML !== '' ? `${innerHTML}</${tagName}>` : '';

return `<${tagName} ${arrtibuteString}>${closeTagString}`;
};

export default {};
Loading

0 comments on commit ec10308

Please sign in to comment.