在Angular中实现一个级联效果的下拉框

实现一个具有级联效果的下拉搜索框,实现的结果如下图所示


image

我们主要通过这个组件,来学习一些细微的逻辑,比如: 如何计算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;
}
实现相关的逻辑

根据前两个需求,我们需要根据文本框中的”.“进行向后移动,向右移动的最大距离不能超过文本框的宽度。

思路: 我们需要将文本框中的字符串根据”.“来转换成数组,并且要想办法获取文本框中文字的长度。
如何获取文本框中文字的长度呢?
<font color="red">我们可以将文字的内容,重新放到一个display: inline-block的div容器中,然后获取容器的宽度,如下代码所示~</font>

// 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`);
    }
  }
 }

到这里,我们可以完成第一和第二个需求,我们再来看看第三个需求: 主要是根据用户输入的位置以及修改的内容,来决定是否显示搜索和显示下拉框,如果用户输入的不是”.“我们则不显示,如果用户在之前的级联中输入”.“我们就需要进行再次帮用户搜索结果。

思路: 要想完成需求三,我们需要知道用户到底是在哪里操作,<font color='red'>即我们要是可以知道光标的位置就更完美了~</font>

  // 获取光标的位置
  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;
  }

总结

其实这个级联搜索的组件,他的通用性可能并不是很强,但是在实现的过程中,一些细节逻辑的通用性还是比较强的~ 希望这些细节可以给同在开发中的你带来一些帮助~❤

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351