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

Chapter 2 / 순수함수 렌더링 #6

Merged
merged 11 commits into from
Jan 19, 2024
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {},
rules: {
'import/extensions': 'off',
},
};
2 changes: 1 addition & 1 deletion 02. 렌더링/02/getTodos.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const createElement = () => ({

const repeat = (elementFactory, number) => {
const array = [];
for (let index = 0; index < number; index++) {
for (let index = 0; index < number; index += 1) {
array.push(elementFactory());
}
return array;
Expand Down
4 changes: 2 additions & 2 deletions 02. 렌더링/02/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import getTodos from './getTodos';
import appView from './view/app';
import getTodos from './getTodos.js';
import appView from './view/app.js';

const state = {
todos: getTodos(),
Expand Down
6 changes: 3 additions & 3 deletions 02. 렌더링/02/view/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import todosView from './todos';
import counterView from './counter';
import filtersView from './filters';
import todosView from './todos.js';
import counterView from './counter.js';
import filtersView from './filters.js';

export default (targetElement, state) => {
const element = targetElement.cloneNode(true);
Expand Down
16 changes: 16 additions & 0 deletions 02. 렌더링/02/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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수는 모조리 분리하시네요 역시 대상혁

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;
};
17 changes: 17 additions & 0 deletions 02. 렌더링/02/view/counter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,21 @@ describe('counterView', () => {

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/view/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const getAllListAnchorsFrom = (element) => [...element.querySelectorAll('li a')];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

책에서는 Array.from()으로 nodeList를 배열로 바꿔서 spread 구문이랑 뭔 차이가 있을까 궁금해서 검색했는데

array-from-vs-spread-syntax

spread 구문은 iterable 객체만 배열로 바꿀 수 있고 Array.from()은 유사 배열 객체까지 배열로 바꿀 수 있어서 Array.from()이 더 범용성이 좋다고 하네요

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;
};
44 changes: 44 additions & 0 deletions 02. 렌더링/02/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}`,
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성 측면에서는 템플릿 스트링 사용하는게 더 좋아보이는데 어떻게 생각하시나요?


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;
};
13 changes: 13 additions & 0 deletions 02. 렌더링/02/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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 R키와 T키를 바꾸셨나요🤔

.map(([name, value]) => {
if (typeof value === 'boolean') return value ? name : '';
return `${name}="${value}"`;
})
.join(' ');
const closeTagString = innerHTML !== '' ? `${innerHTML}</${tagName}>` : '';

return `<${tagName} ${arrtibuteString}>${closeTagString}`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return `<${tagName} ${arrtibuteString}>${closeTagString}`;
return `<${tagName} ${arrtibuteString}>${innerHTML}</${tagName}>`;

innerHTML 매개변수 기본값으로 빈 문자열 넣어주니까 closeTagString 변수 따로 만들 필요 없이 이렇게 작성해도 되지 않을까요?

};

export default {};
67 changes: 67 additions & 0 deletions 02. 렌더링/02/view/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createInnerHTML } from './utils';

describe('유틸리티 함수 테스트', () => {
let container;

beforeEach(() => {
container = document.createElement('div');
});

test('createInnerHTML: 빈 태그를 잘 생성하는가?', () => {
const tagNames = ['div', 'input', 'span', 'a', 'ul', 'li'];

tagNames.forEach((tagName) => {
container.innerHTML = createInnerHTML(tagName);
const testTarget = container.querySelector(tagName);

expect(testTarget.tagName).toBe(tagName.toUpperCase());
expect(testTarget.innerHTML).toBe('');
});
});

test('createInnerHTML: 속성을 잘 할당하는가?', () => {
container.innerHTML = createInnerHTML('input', {
attributes: {
class: 'test-input',
type: 'checkbox',
checked: true,
},
});
const testTarget = container.querySelector('input');

expect(testTarget.className).toBe('test-input');
expect(testTarget.type).toBe('checkbox');
expect(testTarget.checked).toBe(true);
});

test('createInnerHTML: innerHTML을 잘 할당하는가?', () => {
const html = [
createInnerHTML('div', {
attributes: {
class: 'inner-div',
},
innerHTML: 'hello',
}),
createInnerHTML('input', {
attributes: {
class: 'inner-input',
type: 'number',
value: '1',
},
}),
].join('');

container.innerHTML = createInnerHTML('div', {
attributes: {
class: 'test-container',
},
innerHTML: html,
});
const testTarget = container.querySelector('.test-container');
const innerDiv = testTarget.querySelector('.inner-div');
const innerInput = testTarget.querySelector('.inner-input');

expect(innerDiv.textContent).toBe('hello');
expect(innerInput.value).toBe('1');
});
});
Loading