在Angular中實現一個級聯效果的下拉框

實現一個具備級聯效果的下拉搜索框,實現的結果以下圖所示 html

咱們主要經過這個組件,來學習一些細微的邏輯,好比: 如何計算input框內文字的長度; 如何獲取光標的位置;如何實現滾動條隨着上下鍵盤的按動進行移動......

具體需求以下

  • 級聯搜索最多不超過三級,以」.「做爲級聯搜索的鏈接符
  • 搜索框跟着文本框中的」.「進行向後移動,向右移動的最大距離不能超過文本框的寬度
  • 當用戶修改以前的級聯內容,則不進行搜索,並隱藏搜索框;若用戶在以前輸入的是」.「, 則將此」.「以後的內容所有刪除並搜索當前的相關內容

接下來咱們根據需求,來寫咱們的邏輯

首先咱們搭建html頁面
<input
       #targetInput
        autocomplete="off"
        nz-input
        [(ngModel)]="searchValue"
        (keydown)="handlePress($event)"
        (input)="handleSearchList()"/>
      
      <div #searchList class="search-popup" [hidden]="!visible" (keyDown)="onKeydown($event)">
          <nz-spin [nzSpinning]="searchLoading" [class.spinning-height]="searchLoading">
            <div class="data-box"  *ngIf="searchData && searchData.length !== 0">
              <ul>
              // 這裏在上篇文章中已經講解過,如何實現讓匹配的文字高亮顯示~
                <li
                  id="item"
                  *ngFor="let item of searchData;let i = index;"
                  [class.item-selected]="curIndex === i"
                  (mouseover)='hoverDataItem(i)'
                  (click)="onSelectClick(item)">
                  <span [innerHTML]="item | highlightSearchResult:searchValue | safe: 'html'"></span>
                </li>
              </ul>
            </div>
          </nz-spin>
     </div>
複製代碼
.search-popup {
  height: 376px;
  width: 246px;
  overflow-y: auto;
  box-shadow: 0 2px 8px rgba(0,0,0,.15);
  border-radius: 4px;
  position: absolute;
  background-color: #fff;
  z-index: 999;
  top: 92px;
  right: 61px;

  .data-box {
    margin: 0 10px;

    &:not(:last-child) {
      border-bottom: 1px solid #E4E5E7;
    }

    .no-search-data {
      display: inline-block;
      width: 100%;
      text-align: center;
      color: #C3C9D3;
      line-height: 40px;
    }
  }

  & ul {
    margin: 0 -10px;
    margin-bottom: 0;
    text-align: left;
  }

  & li {
    padding: 3px 10px;
    position: relative;
    list-style: none;
    height: 32px;
    line-height: 26px;
    &:hover {
      cursor: pointer;
      background-color: #e6f7ff;
    }
    &.item-selected {
      background-color: #E6F7FF;
    }
  }

  &.item-selected {
    background-color: #E6F7FF;

  }

.hidden-box {
  display: inline-block;
  border: 1px solid #ddd;
  visibility: hidden;
}
複製代碼
實現相關的邏輯

根據前兩個需求,咱們須要根據文本框中的」.「進行向後移動,向右移動的最大距離不能超過文本框的寬度。數組

思路: 咱們須要將文本框中的字符串根據」.「來轉換成數組,而且要想辦法獲取文本框中文字的長度。
如何獲取文本框中文字的長度呢?
咱們能夠將文字的內容,從新放到一個display: inline-block的div容器中,而後獲取容器的寬度,以下代碼所示~bash

// html
  <!-- 用於測量input框的文字寬度 -->
  <div class="hidden-box" #firstLevel></div> // 以」.「轉化的數組,下標爲0的內容的寬度
  <div class="hidden-box" #secondLevel></div> // 以」.「轉化的數組,下標爲1的內容的寬度
  <div class="hidden-box" #allLevel></div> // 整個文本框的文字的寬度
 
 // ts
 import { ElementRef, Renderer2 } from '@angular/core';
 
 export class SearchListComponent {
    @ViewChild('searchList', { static: true }) public searchList: ElementRef;
    @ViewChild('firstLevel', { static: true }) public firstLevel: ElementRef;
    @ViewChild('secondLevel', { static: true }) public secondLevel: ElementRef;
    @ViewChild('allLevel', { static: true }) public allLevel: ElementRef;
    constructor(private _renderer: Renderer2) {}
         
    public setSearchPosition(rightValue: string): void {
        this._renderer.setStyle(
          this.searchList.nativeElement,
          'right',
          rightValue);
      } 
      
    public setSearchListPosition(targetValue: string): void {
    const inputWidth = 217;
    const defaultRightPosition = 60;
    const maxRightPosition = -148;
    const firstLevel = this.firstLevel.nativeElement;
    const secondLevel = this.secondLevel.nativeElement;
    const allLevel = this.allLevel.nativeElement;
    const targetValueArr = targetValue ? targetValue.split('.') : [];

    // 將input中的內容,根據」.「轉換成數組以後,將相關的內容賦值到新的div容器中,爲了便於獲取文字的寬度
    allLevel.innerHTML = targetValue;
    firstLevel.innerHTML =  targetValueArr && targetValueArr[0];
    secondLevel.innerHTML = targetValueArr && targetValueArr.length > 1 ? targetValueArr[1] : '';

    // 獲得相關寬度以後,實現下拉框移動的邏輯
    if (firstLevel.offsetWidth >= inputWidth
      ||  (firstLevel.offsetWidth + secondLevel.offsetWidth) >= inputWidth
      || allLevel.offsetWidth >= inputWidth) {
        this.setSearchPosition(this._renderer, this.searchList, `${maxRightPosition}px`);
     } else if (targetValueArr.length <= 1) {
      this.setSearchPosition(this.renderer, this.searchList, '61px');
    } else if (targetValueArr.length <= 2) {
      this.setSearchPosition(this.renderer, this.searchList, `${defaultRightPosition - firstLevel.offsetWidth}px`);
    } else if (targetValueArr.length <= 3) {
      this.setSearchPosition(renderer,
                              this.searchList,
                              `${defaultRightPosition - firstLevel.offsetWidth - secondLevel.offsetWidth}px`);
    }
  }
 }
複製代碼

到這裏,咱們能夠完成第一和第二個需求,咱們再來看看第三個需求: 主要是根據用戶輸入的位置以及修改的內容,來決定是否顯示搜索和顯示下拉框,若是用戶輸入的不是」.「咱們則不顯示,若是用戶在以前的級聯中輸入」.「咱們就須要進行再次幫用戶搜索結果。ide

思路: 要想完成需求三,咱們須要知道用戶究竟是在哪裏操做,即咱們要是能夠知道光標的位置就更完美了~學習

// 獲取光標的位置
  public getCursorPosition(element: HTMLInputElement): number {
    let cursorPosition = 0;
    if (element.selectionStart || element.selectionStart === 0) {
      cursorPosition = element.selectionStart;
    }
    return cursorPosition;
  }
  
  // 用來獲取用戶輸入的內容是什麼
  public handlePress(event: KeyboardEvent): void {
     this.curPressKey = event.key;
   }

  // 用戶input的時候調用的核心方法
  public handleSearchList(value: string): void {
    this.curIndex = 0;
    const cursorPosition = this.getCursorPosition(this.targetInput.nativeElement); // 獲取光標位置
    let targetValue = value;
    const targetValueArr = targetValue ? targetValue.split('.') : [];
    const valueArrLength = targetValueArr.length;
    this.setSearchListPosition(targetValue); // 調整位置
    // 判斷那些狀況下應該搜索並顯示下拉框
    if (valueArrLength === 1
      || valueArrLength === 2 && cursorPosition >= targetValueArr[0].length + 1
      || valueArrLength === 3 && cursorPosition >= targetValueArr[0].length + targetValueArr[1].length + 2) {
        this.searchLoading = true;
        this.visible = true;
        ...獲取下拉框中的數據
    } else {
      this.hidePopup();
    }
  }
複製代碼
最後爲了更好的提升用的體驗,咱們還須要讓下拉框支持鍵盤事件哦~方法也很簡單,以下所示
public onKeydown(keyDownInfo: {index: number, code: number, e: KeyboardEvent}): void {
    const { code, e } = keyDownInfo;
    e.stopPropagation();
    if (code === 38) { // 鍵盤上
      e.preventDefault(); // 防止光標由最後邊移動到前邊,只是在開發中遇到的一點體驗上小問題
      if (this.curIndex > 0) {
        this.curIndex--;
      }
    } else if (code === 40) { // 鍵盤下
      if (this.curIndex < this.searchData.length - 1) {
        this.curIndex++;
      }
    } else if (code === 13) {   // 回車,即至關於用戶點擊
      this.ruleModal.showModal();
      const curData = this.searchData[this.curIndex];
      if (curData) {
       this.onSelectClick(curData);
      }
    }
    // 實現下拉框的滾動條隨着鍵盤的上下鍵按動時一塊兒移動
    const lis = document.querySelectorAll('#item');
    const curLiEle = lis[this.curIndex] as HTMLElement;
    const searchList = this.searchList.nativeElement;
    const liEleHeight = 32;
    //(當前選中li標籤的offsetTop + li標籤自身的高度 - 下拉框的高度)
    searchList.scrollTop = curLiEle.offsetTop + liEleHeight -  searchList.clientHeight;
  }
複製代碼

總結

其實這個級聯搜索的組件,他的通用性可能並非很強,可是在實現的過程當中,一些細節邏輯的通用性仍是比較強的~ 但願這些細節能夠給同在開發中的你帶來一些幫助~❤ui

相關文章
相關標籤/搜索