웹 구성 요소를 사용하여 탭 컨트롤 만드는 방법

vanilla JS를 사용하여 단계 없이 탭 컨트롤을 구축합니다.이것은 이전 판 시리즈의 일부분이다.따라서 더 많은 템플릿 파일을 건너뛸 수 있습니다.
탭 컨트롤에는 다양한 변형이 있지만 다음과 같이 구성할 컨텐트에 대한 요구 사항이 있습니다.
  • 라벨 한 줄을 원합니다.
  • 각 탭에는 관련 내용이 있습니다.
  • 태그를 클릭하여 내용을 표시하고 나머지 내용을 숨기려고 합니다.
  • 은 모든 항목이어야 합니다.
  • 우리가 하지 않은 것은:
  • 탭을 클릭할 때 라우팅 변경(정적 내용에 추가된 해시 라우팅은 간단하지만 서버 조정에 약간의 작업이 필요할 수 있음)
  • IDE 또는 브라우저의 탭과 같은 탭을 구성할 수 있는
  • 탭에서는 추가, 재배열, 삭제 등을 수행할 수 있습니다.

  • 본보기
    이번에는 더 이상 설명하지 않겠지만, 패널 컨트롤과 같은 샘플을 사용합니다.
    export class WcTabPanel extends HTMLElement {
      static observedAttributes = [];
      constructor(){
        super();
        this.bind(this);
      }
      bind(element){
        element.render = element.render.bind(element);
        element.cacheDom = element.cacheDom.bind(element);
        element.attachEvents = element.attachEvents.bind(element);
      }
      connectedCallback(){
        this.render();
        this.cacheDom();
        this.attachEvents();
      }
      render(){
        this.attachShadow({ mode: "open" });
        this.shadowRoot.innerHTML = ``;
      }
      cacheDom(){
        this.dom = {
        };
      }
      attachEvents(){
    
      }
      attributeChangedCallback(name, oldValue, newValue){
        if(oldValue !== newValue){
          this[name] = newValue;
        }
      }
    }
    
    customElements.define("wc-tab-panel", WcTabPanel);
    
    모든 새로운 방법은bind 방법에 추가되어야 한다는 것을 주의하십시오.간결하게 보기 위해서, 이것은 부분에서 생략될 것이다.

    컨텐트 추가
    너는 내용을 추가하기 위해 다양한 종류의 API를 설계할 수 있다.
    내가 본 몇 개의 시스템에서 사용하는 흔한 방법 중 하나는 외부의'용기'요소(우리는 tab-panel-strip이라고 부르지만 내부에 실제 옵션 카드(예를 들어 tab-label)와 실제 패널(tab-panel)에 사용되는 사용자 정의 요소를 끼워 넣는 것이다.예를 들면 다음과 같습니다.
    <tab-panel-strip>
      <tab-label>Tab A<tab-label>
      <tab-label>Tab B<tab-label>
      <tab-label>Tab C<tab-label>
      <tab-panel>Content A</tab-panel>
      <tab-panel>Content B</tab-panel>
      <tab-panel>Content C</tab-panel>
    </tab-panel-strip>
    
    나는 이런 방법을 좋아하지 않는다.맞춤형 구성 요소를 만들 때, 나의 일반적인 건의는 구성 요소 시스템을 사용하는 것을 피하는 것이다.무엇에 끼워 넣을 수 있는지 기억하기 어렵다.또한 이러한 요소는 사용자 정의 요소이기 때문에 사람들이 당신이 부르는 요소를 잊어버리기 때문에'더'는 더 많은 문서 접근을 의미한다.또한 원소 트리를 검증하지 못하기 때문에 클라이언트는 작용하지 않는 나쁜 물건을 끼워 넣을 수 없습니다. 더 나쁘거나 어느 정도 작용하여 혼란과 오류를 초래할 수 있습니다.설령 검증을 했다 하더라도, 이것은 어느 정도 코드 바이트와 주기를 낭비하는 것이다.반대로 간단함을 유지한다.이미 존재하는 것을 견지해 보아라.
    내 권장 사항은 두 슬롯 사이에 원하는 요소를 하나 탭 및 컨텐트 모두에 중첩시키는 단일 사용자 정의 요소를 사용하는 것입니다.
    <wc-tab-panel>
       <h1 slot="tab">Tab A</h1>
       <p slot="content">Content A</p>
       <h1 slot="tab">Tab B</h1>
       <p slot="content">Content A</p>
       <h1 slot="tab">Tab C</h1>
       <p slot="content">Content A</p>
    </wc-tab-panel>
    
    이것은 그것을 간단하게 유지하지만, 사용자가 단지 <video>을 내용이나 다른 것으로 삽입하려고 한다면, 그것도 더욱 쉬워질 것이다.네스트할 필요가 없습니다!
    내용에 대해 너무 까다롭게 생각하지 마라. 또 다른 멋있는 점은 우리가 더욱 우아하게 뒤로 물러설 수 있다는 것이다.만약 사용자의 브라우저가 사용자 정의 요소를 지원하지 않거나javascript를 지원하지 않는다면, 우리는 유용한 상태로 되돌아갈 수 있습니다.각 태그가 h1이고 각 컨텐츠가 p이라고 가정합니다.위에서 설명한 대로 사용자가 올바르게 인터레이싱하면 장애가 발생한 경우에도 완벽하게 액세스할 수 있도록 잘 표시됩니다.

    그림자 왕국
    섀도우 DOM을 설정합니다.
    render() {
        this.shadow = this.attachShadow({ mode: "open" });
        this.shadow.innerHTML = `
                <style>
                    :host { display: flex; flex-direction: column; }
                    .tabs { display: flex; flex-direction: row; flex-wrap: nowrap; }
                </style>
                <div class="tabs">
                    <slot id="tab-slot" name="tab"></slot>
                </div>
                <div class="tab-contents">
                    <slot id="content-slot" name="content"></slot>
                </div>
            `;
    }
    
    이것은 매우 간단하다.우리는 수평 줄이 옵션 카드에 사용되고, 용기가 옵션 카드의 내용에 사용됩니다.너는 이곳에서 CSS 격자를 사용할 수 있고, 격자도 flexbox가 처리할 수 있는 모든 일을 처리할 수 있다. 나는 단지 잠시 간단함을 유지할 뿐이다.
    다음에 방문할 요소를 선택하기 시작합니다.
    cacheDom() {
        this.dom = {
            tabSlot: this.shadow.querySelector("#tab-slot"),
            contentSlot: this.shadow.querySelector("#content-slot")
        }
        this.dom.tabs = this.dom.tabSlot.assignedElements();
        this.dom.contents = this.dom.contentSlot.assignedElements();
    }
    attachEvents() {
    
    첫 번째 부분은 의미가 있을 수 있습니다. 라벨을 놓는 플러그에 들어가야 하는데 뒤에 두 줄은요?전체 전환 메커니즘은 탭의 인덱스를 기반으로 하기 때문에 탭 목록을 얻어야 합니다.그러나 검색 슬롯의 하위 요소는 기술적으로 원소 자체의 하위 원소이기 때문에 찾을 수 없다.따라서, 그것들을 방문하기 위해, 우리는 assignedElements()개의 플러그인을 조회했다. (assigned Elements에 브리 파라미터가 있는 것을 보았을 수도 있고, 플러그인 요소의 하위 요소를 잡을 수도 있지만, 우리는 그것을 필요로 하지 않는다.)슬롯에도 assignedNodes() 방법이 있습니다.assignedNodes()은 우리가 고려하고 싶지 않은 텍스트와 주석 노드를 포함한 모든 노드를 얻을 것입니다. 따라서 우리는 assignedElements()을 사용합니다.
    여기서 지적해야 할 것은 우리가 기술적으로 라벨과 내용 목록을 미리 계산한다는 것이다.즉, 요소를 초기화하기 전에 존재하는 탭과 내용만 고려합니다.나중에 탭을 삽입하면 노드 목록을 업데이트해야 합니다.

    등록 정보
    이 컨트롤을 만들 때, 나는 selected-index이라는 단일 속성을 사용했다. 이 속성은 색인에 따라 어떤 옵션을 표시하는지 제어한다.내가 더 생각해 본 바와 같이, 보이는 옵션 카드는 사실상 '시각적 상태' 로 볼 수 있고,per Recommensions는 CSS 사용자 정의 속성일 수도 있지만, 사용자 정의 속성만 사용해서 옵션 카드를 바꾸는 방법은 생각해 내지 못했다. 특히 그것들을 관찰하거나 상태를 전환할 수 없기 때문이다.그것이 정말 의미가 있는지, 효과가 있는지 확인하는 것은 여전히 사용자 정의 요소를 만드는 데 어려운 부분 중 하나이다.
    또한 [분할] 패널의 속성과 거의 동일한 방향으로 작동하는 속성이 있으며, 탭이 수직인지 수평인지에 따라 [행]에서 [열]으로 변경됩니다.마찬가지로 이것은 절대적인 시각 상태이다. (미디어 조회에 응답하기 위해 옵션의 방향을 바꾸고 싶다고 상상해 보자.) 그러나 우리는 현재 속성만 사용할 수 있다.
    먼저 관찰된 속성을 추가합니다.
    static observedAttributes = ["selected-index", "direction"];
    
    그런 다음 속성을 속성으로 변환하는 템플릿이 있습니다(이것들은 모두 클래스에 있고 최종 제품의 순서는 다를 수 있습니다. 저는 단지 설명하기 위해 압축할 뿐입니다).
    #selectedIndex = 0;
    #direction = "row";
    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue !== newValue) {
            if(name === "selected-index"){
                this.selectedIndex = newValue;
            } else {
                this[name] = newValue;
            }
        }
    }
    set selectedIndex(value) {
        this.#selectedIndex = value;
    }
    get selectedIndex() {
        return this.#selectedIndex;
    }
    
    분할 패널 컨트롤을 사용한 것처럼 Getter와setter를 만드는 개인 지원 필드를 사용하고 있습니다. 프로그래밍 방식으로 JS를 사용하여 속성을 설정하고 가져올 수 있습니다. 시각 상태나 다른 모든 응답에 대한 형식을 변경할 수 있습니다.attributeChangedCallback에 대해if문장을 사용했습니다. 이것은 새로운 문제를 처리했기 때문에 분할판을 사용하지 않았습니다.HTML에서 여러 단어가 있는 랜드 속성은 HTML이 대소문자를 구분하지 않기 때문에 하이픈 대소문자로 되어 있습니다.따라서 전환할 때 두 가지 선택이 있습니다.
    1) 동일한 양식을 유지하지만 현재 모든 인용은 반드시 색인 문법을 사용해야 한다. 예를 들어 ["selected-index"]2) 속성 이름을 "selectedIndex"와 같이 camelcase로 변환
    관례에 따르면, 두 번째 방법은 틀림없이 더욱 환영을 받을 것이다. 왜냐하면 코드를 작성하는 것이 그리 번거롭지 않기 때문이다. 그러나 나는 일부 구성 요소를 게으름 피우며 구축할 때 이전의 방법을 사용했다.더 복잡한 장면에서, 나는 helper 함수를 만들 것이다. 클래스에 속성을 설정할 때, 모든 속성 이름이 실행되지만, 이것은 단지 하나이기 때문에, 우리는 이 복잡도를 건너뛰고 수동으로 설정할 것이다.

    이벤트
    가장 중요한 내부 이벤트는 탭을 클릭할 때입니다.디스플레이를 업데이트하고 싶습니다.사용자가 탭 슬롯의 아무 곳이나 클릭할 경우 먼저 클릭 이벤트를 등록합니다.
    attachEvents() {
        this.dom.tabSlot.addEventListener("click", this.onTabClick);
    }
    
    그런 다음 프로세서를 설정합니다.
    onTabClick(e) {
        const target = e.target;
        if (target.slot === "tab") {
            const tabIndex = this.dom.tabs.indexOf(target);
            this.selectTabByIndex(tabIndex);
        }
    }
    
    이것은 클릭한 요소에 대한 인용을 가져와서 슬롯에 등록되었는지 확인합니다. (기술적으로 슬롯에 슬롯이 열리지 않은 하위 요소가 있을 수 있습니다. 비록 이것은 영원히 좋은 생각이 아니라고 생각하지만.) 어느 색인에 있는지 찾아내고 (앞에서 설명한 텍스트와 주석 노드를 빼고) 그 색인을 선택하는 방법을 호출합니다.
    selectTabByIndex(index) {
        const tab = this.dom.tabs[index];
        const content = this.dom.contents[index];
        if (!tab || !content) return;
        this.dom.contents.forEach(p => p.classList.remove("selected"));
        this.dom.tabs.forEach(p => p.classList.remove("selected"));
        content.classList.add("selected");
        tab.classList.add("selected");
    }
    
    여기서 우리는 옵션 카드와 상응하는 내용에 대한 인용을 얻었다.우리는 상응하는 내용과 라벨이 있는지 확인하기 위해 신속한 건전성 검사를 한다. 그렇지 않으면 중단할 뿐이다.그리고 우리는 모든 옵션과 내용에서 selected 클래스를 삭제하여 스타일을 리셋한 다음에 우리가 방금 선택한 스타일에 추가합니다.
    필요하면 구성 요소 밖에서 이 사건을 일으킬 수 있지만, 예를 찾기 전에 바이트를 저장하기 위해서 이렇게 하지 않기로 했습니다.
    우리가 주의해야 할 가장 뚜렷하지 않은 사건은 틈새 내용이 변할 때이다.
    attachEvents() {
        this.dom.tabSlot.addEventListener("click", this.onTabClick);
        this.dom.tabSlot.addEventListener("slotchange", this.onTabSlotChange);
        this.dom.contentSlot.addEventListener("slotchange", this.onContentSlotChange);
    }
    onTabSlotChange(){
        this.dom.tabs = this.dom.tabSlot.assignedElements();
    }
    onContentSlotChange(){
        this.dom.contents = this.dom.contentSlot.assignedElements();
    }
    
    slotchange 이벤트는 슬롯별로 제공되며, 항목을 추가하거나 제거할 때 트리거됩니다.이러한 상황이 발생할 때, 우리는 내부 옵션과 내용 목록을 업데이트해서 그것들이 최신이거나, 구성 요소가 초기화된 후에 추가된 옵션이 정상적으로 작동하지 못하도록 해야 한다.
    여기에 균형이 하나 있으니 주의하세요.캐시 목록 대신 {slot}.assignedElements()을 사용하여 업데이트할 수 있습니다.나는 이렇게 하지 않기로 결정했다. 왜냐하면 이것은 옵션을 눌렀을 때, 우리가 실제 옵션을 업데이트할 때가 아니라, 요소를 교체한다는 것을 의미하기 때문이다.코드를 많이 하고 CPU를 절약하세요.사실 당신은 영원히 충분한 라벨이 없을 수도 있습니다. 어쨌든 이것은 매우 중요하지만, 이것은 적어도 slotchange 활동의 좋은 시범입니다.

    콘셉트
    렌더링 방법에서 섀도우 DOM에 다른 스타일을 추가할 수 있습니다.
    :host([direction="column"]) { flex-direction: row; }
    :host([direction="column"]) .tabs { flex-direction: column; }
    .tabs ::slotted(*) { padding: 5px; border: 1px solid #ccc; user-select: none; cursor: pointer; }
    .tabs ::slotted(.selected) { background: #efefef; }
    .tab-contents ::slotted(*) { display: none; }
    .tab-contents ::slotted(.selected) { display: block; padding: 5px; }
    
    여기는 특별한 것이 없습니다. 앞의 두 줄이 수직 탭을 처리하는 경우입니다.나머지는 주로 탭에 테두리와 단추 커서를 추가하고 선택한 탭을 어둡게 해서 시각적으로 탭과 내용을 구분합니다.컨텐트의 경우 모두 숨기고 선택한 컨텐트만 표시합니다.
    사용자가 원하는 내용을 전달할 수 있기 때문에, '옵션 카드에 사용자 정의 요소를 만들지 않기 때문에, 사용자가 싫어하는 내용을 쉽게 덮어쓸 수 있기 때문에, 우리는 정말 여기에 너무 많은 스타일 연결을 추가할 필요가 없다.--tab-gap으로 탭 사이의 간격을 설정하여 보다 간편하게 생활할 수 있는 컨트롤을 추가했습니다.
    .tabs { display: flex; flex-flow: row nowrap; gap: var(--tab-gap, 0px); }
    
    이것은 flexbox에 새로운 gap 속성을 사용했습니다. 용기의 간격을 조절할 수 있기 때문에 사용자는 margin-right 등을 걱정할 필요가 없습니다.

    데모
    다음은 전체 구성 요소의 설명입니다.JS가 실패하거나 다운로드하지 않을 때 무슨 일이 일어날지 시험해 보세요.우리는 여전히 합리적인 결과를 얻어야 한다.방향을 바꾸고 내용을 추가해서 작업 원리를 설명할 수도 있습니다.

    좋은 웹페이지 즐겨찾기