-
Notifications
You must be signed in to change notification settings - Fork 0
[서인] Web Component #4
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
base: seoin_dev
Are you sure you want to change the base?
Changes from 8 commits
fee228a
1ceffea
3cd9ce1
3aa2d8f
5e441cf
5d355f5
a5f8155
d1a0a63
5ae0651
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import Component from "./core/Component"; | ||
| import Items from "./components/Items"; | ||
| import ItemAppender from "./components/ItemAppender"; | ||
| 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-appender"></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, addItem, deleteItem, toggleItem, filterItem } = this; | ||
| const $itemAppender = this.$target.querySelector('[data-component="item-appender"]') | ||
| const $items = this.$target.querySelector('[data-component="items"]') | ||
| const $itemFilter = this.$target.querySelector('[data-component="item-filter"]') | ||
|
|
||
| new ItemAppender(($itemAppender as HTMLElement), { | ||
| addItem: addItem.bind(this) | ||
| }) | ||
|
|
||
| new Items(($items as HTMLElement), { | ||
| filteredItems, | ||
| deleteItem: deleteItem.bind(this), | ||
| toggleItem: toggleItem.bind(this) | ||
| }) | ||
|
|
||
| new ItemFilter(($itemFilter as HTMLElement), { | ||
| filterItem: filterItem.bind(this) | ||
| }) | ||
| } | ||
|
|
||
| get filteredItems(): IItem[] { | ||
| const { isFilter, items } = this.state; | ||
| return items.filter(({ active }: { active: boolean }) => (isFilter === 1 && active) || (isFilter === 2 && !active) || (isFilter === 0) ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. items가 타입이 지정되어있으면 매개변수에 타입을 지정할 필요가 없어요. state에 타입을 지정해주는게 좋아보입니다! 제네릭도 사용할만하겠네요
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. state에 Record<string, any>를 지정하면 초기값을 빈 객체로 지정할 때 에러가 떠 매개변수 타입을 IItem으로 지정했습니다 |
||
| } | ||
|
|
||
| addItem(content: string) { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
메서드의 이름을 보면서 네이밍 유추가 잘되었지만, 코드레벨에서도 고민해보면 좋을것 같습니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 한 번에 1개만 삭제할 수 있기 때문에 findIndex 메서드를 쓰는 게 효율적이라고 생각합니다.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
취향차이지만, 매개변수로 id를 받고, id는 고유하기 때문에 1개만 필터링 되지않을까? 생각이 드네요! |
||
| items.splice(index, 1) | ||
| this.setState({ items }) | ||
| } | ||
|
|
||
| toggleItem(id: number) { | ||
| const items = [...this.state.items]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`] }) | ||
| // }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import Component from "../core/Component"; | ||
|
|
||
| export default class ItemAppender extends Component { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| template() { | ||
| return ` | ||
| <input type="text" class="appender" placeholder="아이템 내용 입력"> | ||
| ` | ||
| } | ||
|
|
||
| setEvent() { | ||
| const { addItem } = this.props | ||
| this.addEvent<KeyboardEvent>('keyup', '.appender', (event) => { | ||
| const { key, target } = event | ||
| if (key !== 'Enter') return; | ||
| addItem((target as HTMLInputElement)?.value) | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import Component from "../core/Component"; | ||
|
|
||
| interface IItemFilter { | ||
| filterItem: (isFilter: number ) => void; | ||
| } | ||
|
|
||
| export default class ItemFilter extends Component { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| props: IItemFilter; | ||
|
|
||
| constructor($target: HTMLElement, props: IItemFilter) { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| super($target, props); | ||
| this.props = props; | ||
| } | ||
|
|
||
| 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)) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import Component from "../core/Component"; | ||
|
|
||
| interface IItem { | ||
| id: number; | ||
| content: string; | ||
| active: boolean; | ||
| } | ||
|
|
||
| export default class Items extends Component { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| 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)) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| interface IState { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| [key: string]: any; | ||
| } | ||
|
|
||
| interface IProps { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| [key: string]: any; | ||
| } | ||
|
|
||
| export default class Component { | ||
| $target: HTMLElement; | ||
| props: IProps; | ||
| state: IState; | ||
| isMounted: boolean; | ||
|
|
||
| constructor($target: HTMLElement, props: IProps = {}) { | ||
| this.$target = $target; | ||
| this.props = props; | ||
| this.state = {}; | ||
| this.isMounted = false; | ||
| this.setEvent(); | ||
| this.setup(); | ||
| this.render(); | ||
| } | ||
| setup() { }; | ||
| mounted() { | ||
| if (!this.isMounted) { | ||
| this.isMounted = true; | ||
| } | ||
| }; | ||
|
Comment on lines
+25
to
+24
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드는 불필요해 보여요. 왜냐하면 App에서 mounted로 실행을 하게되면, 즉 의도하신건 "불필요한 렌더링을 줄이자" 인것 같은데, 의도하것처럼 동작하지가 않아요. ItemAppender에 하지만 이런걸 개선하려고 했던 건 너무나도 좋은것 같습니다. 저는 그런 생각 못했어요! |
||
| template() { return ''; } | ||
| render() { | ||
| this.$target.innerHTML = this.template(); | ||
| !this.isMounted && this.mounted() | ||
| } | ||
| setEvent() { } | ||
| setState(newState: IState) { | ||
| this.state = { ...this.state, ...newState }; | ||
| this.render() | ||
| } | ||
|
|
||
| addEvent<T extends Event>(eventType: string, selector: string, callback: (event: T) => void ) { | ||
|
Seoin02 marked this conversation as resolved.
Outdated
|
||
| this.$target.addEventListener(eventType, event => { | ||
| if (event && event.target instanceof HTMLElement && !event.target.closest(selector)) return false; | ||
| callback(event as T) | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,14 @@ | ||
| // your Code | ||
| import App from './App' | ||
|
|
||
| class Main { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
Uh oh!
There was an error while loading. Please reload this page.