본문 바로가기
JavaScript/Vanila

[Vanila js] 탭 구현하기

by D.O.T 2024. 8. 1.

Result

tab 기능


View

export default class TabView extends View {

    constructor() {
        super(qs('#tab-view'));
     
        this.template = new Template();
    }
    
    show() {}
    bindEvents() {}
    
}

 

정적 페이지의 tab-view id를 가진 태그에 작업을 수행한다.

Result와 같이 탭을 보여줄 기능과, 탭을 누르는 행위를 하는 기능이 필요


Controller

export default class Controller {
  constructor(store, {searchFormView, searchResultView, tabView}) {
    this.store = store;
    
    this.searchFormView = searchFormView;
    this.searchResultView = searchResultView;
    this.tabView = tabView;

    this.subscribeViewEvents();
    this.render();
  }
}

 

Controller에는 View와 Model의 제어를 위해 당연히 tabView가 추가 되어야한다.

추가적으로 View에서 구현한 tab을 보여주는 기능과 클릭과 같은 이벤트들을 처리하는 작업을 구현해야한다.

  subscribeViewEvents() {
    this.tabView
    .on('@select', (event) => this.select(event.detail.target));
  }

예를 들어, tabView에서 select라는 이벤트가 발생했을 때, chaining 된 on method로 이벤트를 캐치하고 처리한다.


Model

export default class Store {
  constructor(storage) {
    if (!storage) throw "no storage";

    this.storage = storage;
    this.searchResult = [];
    this.searchKeyword = "";
    this.selectedTab = TabType.KEYWORD;
  }
}

 

Store Model 에서는 어떤 키워드를 선택했는지에 대한 정보를 담기 위한 selectedTab 변수가 추가된다.

맨 처음, 렌더링 할 때 default로 추천검색어를 선택한다.


Quest

Q. 각 탭을 클릭하면 탭 아래 내용이 변경되도록 한다.

우선, 강의의 챕터 상 탭 아래 내용은 tab-content로 다른 책임을 가지고 있으므로 잠시 비워둔다.

    bindEvents() {
        on(this.element, "click", event => this.handleClick(event));
    }

    handleClick(event) {
        const { target } = event;
        
        this.emit("@select", { target });
    }

나는 TabView에서 클릭 이벤트가 발생하면 클릭한 주체를 @select 이벤트로 발행했다.

 

그리고 앞서 Controller에서 작성한, subscribeEvents 메소드에서 tabView로 부터 @select 이벤트가 발생하면 처리한다.

Controller 입장에서, 어떤 태그를 클릭했는지 명확히 알 수 있다. (TabView에서 알려줌)

  select(target) {
    const tab = target.dataset.tab;

    this.store.select(tab);
    this.tabView.show(this.store.selectedTab);
  }

 

그리고 target의 tab 정보를 가져와서 store의 selectedTab 변수로 지정한다.

그리고 tabView를 다시 렌더링 한다.


강사님께서는 utility(helper.js)의 delegate를 사용하고 있다.

export function delegate(target, eventName, selector, handler) {
  const emitEvent = (event) => {
    const potentialElements = qsAll(selector, target);

    for (const potentialElement of potentialElements) {
      if (potentialElement === event.target) {
        return handler.call(event.target, event);
      }
    }
  };

  on(target, eventName, emitEvent);
}

1. 상위 tag의 모든 하위 요소를 다 가져온다.

2. 상위 tag의 모든 하위 요소 중 브라우저에서 event가 발생한 요소와 같은 것을 반환한다.

3. 상위 tag의 이벤트로 추가한다.

 

내 코드의 문제점은 li 태그 외에 다른 태그가 추가되었을 때도 이벤트가 발행된다는 점이다.

위에 작성한 코드에서 bindEvents만 delegate로 수정하면 된다.

handleClick(event) {
    const tab = event.target.dataset.tab;
    this.emit("@tabchange", { tab });
}
 
this.tabView
.on('@tabchange', (event) => {this.select(event.detail.tab);});
    
select(tab) {
    this.store.select(tab);
    this.tabView.show(this.store.selectedTab);
}

그리고 추가로 명확히 해주었다. 

TabView에서 click event가 tab change만 존재하지 않고, 추가적인 event가 발생할 수 있다.

-> event 발행 시 명확한 이벤트 명과 필요한 데이터를 가지고 발행한다.


Tip

class Template {
    getTabList() {
        return `
            <ul class="tabs">
                ${Object.values(TabType)
                .map(tabType => ({ tabType, tabLabel: TabLabel[tabType] }))
                .map(this._getTab)
                .join("")}
            </ul>
        `;
    }

    _getTab({tabType, tabLabel}) {
        return `
            <li data-tab=${tabType}>
                ${tabLabel}
            </li>
        `;
    }
}

 

렌더링을 위한 Template 클래스에서 _getTap method로 map 함수를 돌리는 과정에서 $[tabLabel] 백틱 부분이 계속해서 undefined로 출력됐다.

이유는 js에서 중괄호, {}를 사용하면 파라미터 전달이 아니고 mapping이 되는 것이다. 즉, getTabList() 메소드에서 첫번 째 map 메소드에서 { tabType, tabLabel } 로 바꾸었고, 그 다음 map 메소드에서 _getTab으로 변환하는 중 tabLabel이 tabLable로 오타가 발생해서 매핑이 되지 않는 문제였다.

 

이 팁은 handleClick 메소드와 같이 작성할 때도 신경써야하는 부분이다.