Skip to content
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
188 changes: 188 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import Component from "./core/Component";
import ListItems from "./components/ListItems";
import ItemAdd from "./components/ItemAdd";
import ItemFilter from "./components/ItemFilter";

interface IItem {
id: number;
content: string;
active: boolean;
}

export default class App extends Component {
setup() {
this.state = { isFilter: 0, items: [{ id: 1, content: 'item1', active: false }, { id: 2, content: 'item2', active: true }] }
}

template() {
return `
<header data-component="item-add"></header>
<main data-component="items"></main>
<footer data-component="item-filter"></footer>
`
// 컴포넌트 분할 전
// return `
// <header>
// <input type="text" class="appender" placeholder="아이템 내용 입력">
// </header>
// <main>
// <ul>
// ${this.filteredItems.map(({ id, content, active }) =>
// `<li data-id="${id}">
// ${content}
// <button class="toggleButton" data-id="${id}" style="color: ${active ? '#09F' : '#F09'}">
// ${active ? '활성' : '비활성' }
// </button>
// <button class="deleteButton" data-id="${id}">삭제</button>
// </li>`).join('')}
// </ul>
// </main>
// <footer>
// <button class="filterButton" data-is-filter="0">전체 보기</button>
// <button class="filterButton" data-is-filter="1">활성 보기</button>
// <button class="filterButton" data-is-filter="2">비활성 보기</button>
// </footer>
// `
}

mounted() {
const { filteredItems, addListItem, deleteItem, toggleItem, filterItem } = this;
const $itemAdd = this.$target.querySelector('[data-component="item-add"]') as HTMLInputElement;
const $listItems = this.$target.querySelector('[data-component="items"]') as HTMLUListElement;
const $itemFilter = this.$target.querySelector('[data-component="item-filter"]') as HTMLButtonElement;

new ItemAdd(($itemAdd), {
addListItem: addListItem.bind(this)
})

new ListItems(($listItems), {
filteredItems,
deleteItem: deleteItem.bind(this),
toggleItem: toggleItem.bind(this)
})

new ItemFilter(($itemFilter), {
filterItem: filterItem.bind(this)
})
}

get filteredItems(): IItem[] {
const { isFilter, items } = this.state;
return items.filter(({ active }: IItem) => (isFilter === 1 && active) || (isFilter === 2 && !active) || (isFilter === 0) )
}

addListItem(content: string) {
const { items } = this.state;
const id = Math.max(0, ...items.map((v: IItem) => v.id)) + 1;
const active = false;
this.setState({
items: [
...items,
{id, content, active}
]
})
}

deleteItem(id: number) {
const items = [...this.state.items];
const index = items.findIndex(item => item.id === id)
Copy link
Contributor

Choose a reason for hiding this comment

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

filter를 사용하는게 더 직관적일 것 같아요

메서드의 이름을 보면서 네이밍 유추가 잘되었지만, 코드레벨에서도 고민해보면 좋을것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

한 번에 1개만 삭제할 수 있기 때문에 findIndex 메서드를 쓰는 게 효율적이라고 생각합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

한 번에 1개만 삭제할 수 있기 때문에 findIndex 메서드를 쓰는 게 효율적이라고 생각합니다.

취향차이지만, 매개변수로 id를 받고, id는 고유하기 때문에 1개만 필터링 되지않을까? 생각이 드네요!

items.splice(index, 1)
this.setState({ items })
}

toggleItem(id: number) {
const items = [...this.state.items];
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분도 map을 사용하는게 조금 더 좋지 않을까? 라는 생각이 듭니다. map을 사용한다면 코드가 한줄로 표현도되지만, 선언적으로 작성하는 웹 개발 시대에는 지금처럼 부수효과를 일으키는 것보다 나은것 같아요!

물론! 깊은 복사를 하셔서 큰 상관은없지만 선언적이지 않다라고 느껴지네요

const index = items.findIndex(v => v.id === id);
items[index].active = !items[index].active;
this.setState({ items })
}

filterItem(isFilter: number) {
this.setState({ isFilter })
}

// 컴포넌트 분할 전
// setEvent() {
// this.addEvent<KeyboardEvent>('keyup', '.appender', (event) => {
// const { key, target } = event
// if (key !== 'Enter') return;
// const { items } = this.state;
// const id = Math.max(0, ...items.map((v: IItem) => v.id)) + 1;
// const content = (target as HTMLInputElement)?.value;
// const active = false;
// this.setState({
// items: [
// ...items,
// {id, content, active}
// ]
// })
// })

// this.addEvent('click', '.deleteButton', ({ target }) => {
// const items = [...this.state.items];
// if (target instanceof HTMLElement && target.dataset.id) {
// const id = Number(target?.dataset?.id)
// const index = items.findIndex(item => item.id === id)
// items.splice(index, 1)
// this.setState({ items })
// }
// })

// this.addEvent('click', '.toggleButton', ({ target }) => {
// const items = [...this.state.items];
// if (target instanceof HTMLElement && target.dataset.id) {
// const id = Number(target?.dataset?.id)
// const index = items.findIndex(v => v.id === id);
// items[index].active = !items[index].active;
// }
// this.setState({ items })
// })

// this.addEvent('click', '.filterButton', ({ target }) => {
// if (target instanceof HTMLElement && target.dataset.isFilter) {
// this.setState({ isFilter: Number(target.dataset.isFilter) })
// }
// })

// 이벤트 버블링 추상화 적용한 코드
// this.addEvent('click', '.addButton', ({ target }) => {
// const items = [...this.state.items];
// this.setState({ items: [...items, `item${items.length + 1}`] })
// })
// this.addEvent('click', '.deleteButton', ({ target }) => {
// const items = [...this.state.items];
// if (target instanceof HTMLElement && target.dataset.index) {
// items.splice(Number(target?.dataset?.index), 1)
// }
// this.setState({ items })
// })
// 이벤트 버블링 적용한 코드
// this.$target.addEventListener('click', ({ target }) => {
// if (!target || !(target instanceof HTMLElement)) return;
// const items = [...this.state.items];

// if (target && target.classList && target.classList.contains('addButton')) {
// this.setState({ items: [...items, `item${items.length + 1}`] })
// }

// if (target && target.classList && target.classList.contains('deleteButton')) {
// if (target instanceof HTMLElement && target.dataset.index) {
// items.splice(Number(target?.dataset?.index), 1)
// }
// this.setState({ items })
// }
// })

// 이벤트 버블링 적용 전 코드
// this.$target.querySelectorAll('.deleteButton')?.forEach(deleteButton => deleteButton?.addEventListener('click', ({target}) => {
// const items = [...this.state.items];
// if (target instanceof HTMLElement && target.dataset.index) {
// items.splice(Number(target?.dataset?.index), 1)
// }
// this.setState({ items })
// }))
// this.$target.querySelector('.addButton')?.addEventListener('click', () => {
// const { items } = this.state;
// this.setState({ items: [...items, `item${items.length + 1}`] })
// })
}
22 changes: 22 additions & 0 deletions src/components/ItemAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Component from "../core/Component";

interface IItemAdd {
addListItem: (content: string) => void;
}

export default class ItemAdd extends Component<IItemAdd> {
template() {
return `
<input type="text" class="addListItem" placeholder="항목 입력">
`
}

setEvent() {
const { addListItem } = this.props
this.addEvent<KeyboardEvent>('keyup', '.addListItem', (event) => {
const { key, target } = event
if (key !== 'Enter') return;
addListItem((target as HTMLInputElement)?.value)
})
}
}
24 changes: 24 additions & 0 deletions src/components/ItemFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Component from "../core/Component";

interface IItemFilter {
filterItem: (isFilter: number ) => void;
}

export default class ItemFilter extends Component<IItemFilter> {
template() {
return `
<button class="filterButton" data-is-filter="0">전체 보기</button>
<button class="filterButton" data-is-filter="1">활성 보기</button>
<button class="filterButton" data-is-filter="2">비활성 보기</button>
`
}

setEvent() {
const { filterItem } = this.props
this.addEvent('click', '.filterButton', ({ target }) => {
if (target instanceof HTMLElement && target.dataset.isFilter) {
filterItem(Number(target.dataset.isFilter))
}
})
}
}
46 changes: 46 additions & 0 deletions src/components/ListItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from "../core/Component";

interface IItem {
id: number;
content: string;
active: boolean;
}

interface IListItems {
filteredItems: IItem[];
deleteItem: (id: number) => void;
toggleItem: (id: number) => void;
}

export default class ListItems extends Component<IListItems> {
template() {
const { filteredItems } = this.props
return `
<ul>
${filteredItems.map(({ id, content, active }: IItem) =>
`<li data-id="${id}">
${content}
<button class="toggleButton" data-id="${id}" style="color: ${active ? '#09F' : '#F09'}">
${active ? '활성' : '비활성' }
</button>
<button class="deleteButton" data-id="${id}">삭제</button>
</li>`).join('')}
</ul>
`
}

setEvent() {
const { deleteItem, toggleItem } = this.props
this.addEvent('click', '.deleteButton', ({ target }) => {
if (target instanceof HTMLElement && target.dataset.id) {
deleteItem(Number(target?.dataset?.id))
}
})

this.addEvent('click', '.toggleButton', ({ target }) => {
if (target instanceof HTMLElement && target.dataset.id) {
toggleItem(Number(target?.dataset?.id))
}
})
}
}
42 changes: 42 additions & 0 deletions src/core/Component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
type TComponentData = Record<string, any>

export default class Component<Props = TComponentData, State = TComponentData> {
$target: HTMLElement;
props: Props;
state: State;
isMounted: boolean;

constructor($target: HTMLElement, props: Props) {
this.$target = $target;
this.props = props;
this.state = {} as State;
this.isMounted = false;
this.setEvent();
this.setup();
this.render();
}
setup() { };
mounted() {
if (!this.isMounted) {
console.log(this.mounted)
this.isMounted = true;
}
};
template() { return ''; }
render() {
this.$target.innerHTML = this.template();
!this.isMounted && this.mounted()
}
setEvent() { }
setState(newState: State) {
this.state = { ...this.state, ...newState };
this.render()
}

addEvent<T extends Event>(eventType: keyof HTMLElementEventMap, selector: keyof HTMLElementTagNameMap | string, callback: (event: T) => void ) {
this.$target.addEventListener(eventType, event => {
if (event && event.target instanceof HTMLElement && !event.target.closest(selector)) return false;
callback(event as T)
})
}
}
15 changes: 14 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
// your Code
import App from './App'

class Main {
Copy link
Contributor

Choose a reason for hiding this comment

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

Main 클래스를 여러개의 인스턴스를 만들어도 이미 생성된 인스턴스를 가리키도록 싱글턴 패턴을 활용하면 좋지않을까 ? 라는 생각이드네요

constructor(){
const $app: Element | null = document.querySelector('#app');
if ($app instanceof HTMLElement) {
new App($app, {});
} else {
console.error('Element with ID "app" not found.');
}
}
}

new Main()
Loading