Angular之手写组件库--TransferPanel

519 阅读4分钟

这篇文章要仿照ng.ant.design里面的穿梭框组件,手写一个TransferPanel(就是图片里面红色框的部分),这个TransferPanel要实现以下功能:

  • 选中高亮
  • 默认选中高亮
  • 上边的选中个数随着选中或者取消选中动态变化
  • 根据输入框中输入的关键字进行搜索 image.png

页面布局

TransferPanel.component.html

<div class="transfer-panel border rounded">
    <div class="head pl-3">
      <p>
        <span>12 /</span> 133 项
      </p>
    </div>
    <div class="search-box p-2">
      <input type="text" class="form-control" placeholder="请输入关键字" />
    </div>
    <ul class="list mb-0 overflow-auto">
        <li class="list-item text-truncate list-group-item-action">content</li>
        <li class="list-item text-truncate list-group-item-action">content</li>
        <li class="list-item text-truncate list-group-item-action">content</li>
        <li class="list-item text-truncate list-group-item-action">content</li>
        <li class="list-item text-truncate list-group-item-action">content</li>
    </ul>
  </div>

TransferPanel.component.scss

.transfer-panel {
    user-select: none;
    width: 200px;
    .head {
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid #f0f0f0;
    }
    .list {
      height: 176px;
      margin-left: -20px;
      .list-item {
        cursor: pointer;
        padding: 10px;
        transition: all .2s;
      }
      .active {
        color: #fff;
        background-color: #007bff;
      }
    }
  }
  
  

然后把它挂到app.component.html

<app-transfer-panel-left></app-transfer-panel-left>

到这里的话基本的布局就已经完成了,我们来看一下效果:

image.png

动态渲染

但是目前的话,数据都是写死的,我们现在要通过ng-for来渲染li的数据,首先我们在TransferPanel.component.ts里面生成我们需要的数据

import { Component, OnInit } from '@angular/core';
import { TransferPanelLeft } from './types';

@Component({
  selector: 'app-transfer-panel-left',
  templateUrl: './transfer-panel-left.component.html',
  styleUrls: ['./transfer-panel-left.component.scss']
})
export class TransferPanelLeftComponent implements OnInit {

//我们写ts的时候,最好每一个变量都给他指定一个类型
  list:TransferPanelLeft[] = []
  constructor() { 
  this.setList()
   }

  ngOnInit(): void {
  
  }
  setList() {
    this.list = [];
    const prefix = 'item' + Date.now().toString().slice(-3);
    for (let i = 0; i < 20; i++) {
      this.list.push({
        key: prefix + '_' + i,
        value: `${prefix}${i + 1}`,
        checked: i % 6 === 0
      });
    }
}

}

types.ts

export interface TransferPanelLeft {
    checked:boolean
    key:string
    value:string
}

高亮显示

接下来我们要实现的是,点击每一个li时进行高亮显示,并且上边的选中项随着点击选中和取消进行动态增减: TransferPanel.component.html

<div class="transfer-panel border rounded">
  <div class="head pl-3">
    <p><span *ngIf="selected.length"> {{selected.length}} /</span> {{list.length}} 项</p>
  </div>
  <div class="search-box p-2">
    <input type="text" class="form-control" placeholder="请输入关键字" />
  </div>
  <ul class="list mb-0 overflow-auto">
    <li
      class="list-item text-truncate list-group-item-action"
      [class.active]="targetIndex(item.key)>-1"
      *ngFor="let item of list"
      (click)="itemClick(item)"
    >
      {{ item.value }}
    </li>
  </ul>
</div>

TransferPanel.component.ts

import { Component, OnInit } from '@angular/core';
import { TransferPanelLeft } from './types';

@Component({
  selector: 'app-transfer-panel-left',
  templateUrl: './transfer-panel-left.component.html',
  styleUrls: ['./transfer-panel-left.component.scss']
})
export class TransferPanelLeftComponent implements OnInit {
  // list数组用来渲染数据
  list:TransferPanelLeft[] = []
  // selected数组用来记录选中的项
  selected:TransferPanelLeft[] = []
  constructor() {
    this.setList()
    console.log(this.list,'========>showList');
   }

  ngOnInit(): void {
    // 组件加载之后就调用这个方法
  
    
  }
  setList() {
    this.list = [];
    const prefix = 'item' + Date.now().toString().slice(-3);
    for (let i = 0; i < 20; i++) {
      this.list.push({
        key: prefix + '_' + i,
        value: `${prefix}${i + 1}`,
        checked: i % 6 === 0
      });
    }
}
itemClick(target:TransferPanelLeft){
  // 这里需要判断一下,我们点的这个元素在数组中有没有,有的话应该删除没有的话应该加进去
  // const index = this.selected.findIndex(item => item.key===target.key)
  const index = this.targetIndex(target.key)
  if(index>-1){
    // index>-1说明在里面已经有了
    this.selected.splice(index,1)
  }else{
    this.selected.push(target)
  }

}
// 这里把判断索引的方法进行一个封装,方便之后动态绑定active
targetIndex(key:string):number{
  return this.selected.findIndex(item => item.key===key)
}

}

现在的话已经可以达到这种效果了:

image.png 接下来我们对他做一些优化,因为我们封装组件是给别人用的,所以我们应该用@ input来修饰这个数组,之前我们是在组件内部赋值现在改成在组件外部赋值。 TransferPanel.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { TransferPanelLeft } from './types';

@Component({
  selector: 'app-transfer-panel-left',
  templateUrl: './transfer-panel-left.component.html',
  styleUrls: ['./transfer-panel-left.component.scss']
})
export class TransferPanelLeftComponent implements OnInit {
  // list数组用来渲染数据
  @Input() list:TransferPanelLeft[] = []
  // selected数组用来记录选中的项
  @Input() selected:TransferPanelLeft[] = []
  constructor() {
    // this.setList()
    // console.log('constructor');
   }

  ngOnInit(): void {
    // 组件加载之后就调用这个方法
    // console.log('ngOnInit');
    
    // this.setList()
    // console.log(this.list,'========>showList');
    
  }
  
itemClick(target:TransferPanelLeft){
  // 这里需要判断一下,我们点的这个元素在数组中有没有,有的话应该删除没有的话应该加进去
  // const index = this.selected.findIndex(item => item.key===target.key)
  const index = this.targetIndex(target.key)
  if(index>-1){
    // index>-1说明在里面已经有了
    this.selected.splice(index,1)
  }else{
    this.selected.push(target)
  }

}
// 这里把判断索引的方法进行一个封装,方便之后动态绑定active
targetIndex(key:string):number{
  return this.selected.findIndex(item => item.key===key)
}

}

app.component.html

<button class="btn btn-primary" (click)="setList()">reset list</button>
<app-transfer-panel-left [list]="list"></app-transfer-panel-left>

app.component.ts

import { Component } from '@angular/core';
import { TransferPanelLeft } from './components/transfer-panel-left/types';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']

})
export class AppComponent {
  list:TransferPanelLeft[] = []
  constructor(){
    this.setList()
  }
  setList() {
    this.list = [];
    const prefix = 'item' + Date.now().toString().slice(-3);
    for (let i = 0; i < 20; i++) {
      this.list.push({
        key: prefix + '_' + i,
        value: `${prefix}${i + 1}`,
        checked: i % 6 === 0
      });
    }
}
}

其实到这里的话基本就快完成了,但是当我们点击reset list按钮的时候我们会发现虽然面板上选中项被重置了,但是header部分的数字并没有更新,还是原来选中时的数字,所以我们这里需要对输入属性进行一个监听,监听的话一般有两个方法,这里我们通过 onChange() 生命周期函数来进行监听。TransferPanel.component.ts

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { TransferPanelLeft } from './types';

@Component({
  selector: 'app-transfer-panel-left',
  templateUrl: './transfer-panel-left.component.html',
  styleUrls: ['./transfer-panel-left.component.scss']
})
export class TransferPanelLeftComponent implements OnInit,OnChanges {
  // list数组用来渲染数据
  @Input() list:TransferPanelLeft[] = []
  // selected数组用来记录选中的项
  @Input() selected:TransferPanelLeft[] = []
  constructor() {
    // this.setList()
    // console.log('constructor');
   }
  ngOnChanges(changes: SimpleChanges): void {
    console.log(changes,'-------------gogogo');
    // 只有变化了的输入属性才会有list属性,没有变化的话时没有得
    const {list} = changes
    if(list){
      // 所以这里就是当你发生变化的时候我就要把你重置
      this.selected = []
    }
    
  }

  ngOnInit(): void {
    // 组件加载之后就调用这个方法
    // console.log('ngOnInit');
    
    // this.setList()
    // console.log(this.list,'========>showList');
    
  }
  
itemClick(target:TransferPanelLeft){
  // 这里需要判断一下,我们点的这个元素在数组中有没有,有的话应该删除没有的话应该加进去
  // const index = this.selected.findIndex(item => item.key===target.key)
  const index = this.targetIndex(target.key)
  if(index>-1){
    // index>-1说明在里面已经有了
    this.selected.splice(index,1)
  }else{
    this.selected.push(target)
  }

}
// 这里把判断索引的方法进行一个封装,方便之后动态绑定active
targetIndex(key:string):number{
  return this.selected.findIndex(item => item.key===key)
}

}

我们可以打印一下看看changes里面有什么,我们可以发现有list,不过只有变化了的输入属性才会有list属性,没有变化的话是没有的。这样的话我们就可以根据是否有list来判断是不是发生了变化,进而对selected进行操作。另外我们可以看到list.currentValue里面有一个checked属性,这个就是默认选中项,所以这里就是当你发生变化的时候我就选择checked为true的项保存到selected数组中。

image.png

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { TransferPanelLeft } from './types';

@Component({
  selector: 'app-transfer-panel-left',
  templateUrl: './transfer-panel-left.component.html',
  styleUrls: ['./transfer-panel-left.component.scss']
})
export class TransferPanelLeftComponent implements OnInit,OnChanges {
  // list数组用来渲染数据
  @Input() list:TransferPanelLeft[] = []
  // selected数组用来记录选中的项
  @Input() selected:TransferPanelLeft[] = []
  constructor() {
    // this.setList()
    // console.log('constructor');
   }
  ngOnChanges(changes: SimpleChanges): void {
    console.log(changes,'-------------gogogo');
    // 只有变化了的输入属性才会有list属性,没有变化的话是没有的

    const {list} = changes
    if(list){
      console.log(list.currentValue,'-------list.currentValue');
      // 所以这里就是当你发生变化的时候我就选择checked为true的项保存到selected数组中
      this.selected = this.list.filter(item => item.checked)
    }
    
  }

  ngOnInit(): void {
    // 组件加载之后就调用这个方法
    // console.log('ngOnInit');
    
    // this.setList()
    // console.log(this.list,'========>showList');
    
  }
  
itemClick(target:TransferPanelLeft){
  // 这里需要判断一下,我们点的这个元素在数组中有没有,有的话应该删除没有的话应该加进去
  // const index = this.selected.findIndex(item => item.key===target.key)
  const index = this.targetIndex(target.key)
  if(index>-1){
    // index>-1说明在里面已经有了
    this.selected.splice(index,1)
  }else{
    this.selected.push(target)
  }

}
// 这里把判断索引的方法进行一个封装,方便之后动态绑定active
targetIndex(key:string):number{
  return this.selected.findIndex(item => item.key===key)
}

}

image.png

根据输入框中输入的关键字进行搜索

TransferPanel.component.html

<div class="transfer-panel border rounded">
  <div class="head pl-3">
    <p><span *ngIf="showList.length"> {{selected.length}} /</span> {{list.length}} 项</p>
  </div>
  <div class="search-box p-2">
    <input #getValue (input)="onInput(getValue.value)" class="form-control" placeholder="请输入关键字" type="text"/>
  </div>
  <ul class="list mb-0 overflow-auto">
    <li
      class="list-item text-truncate list-group-item-action"
      [class.active]="targetIndex(item.key)>-1"
      *ngFor="let item of showList"
      (click)="itemClick(item)"
    >
      {{ item.value }}
    </li>
  </ul>
</div>

TransferPanel.component.ts

import { Component } from '@angular/core';
import { TransferPanelLeft } from './components/transfer-panel-left/types';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']

})
export class AppComponent {
  list:TransferPanelLeft[] = []
  constructor(){
    this.setList()
  }
  setList() {
    this.list = [];
    const prefix = 'item' + Date.now().toString().slice(-3);
    for (let i = 0; i < 20; i++) {
      this.list.push({
        key: prefix + '_' + i,
        value: `${prefix}${i + 1}`,
        checked: i % 6 === 0
      });
    }
}
onChanged(selected:TransferPanelLeft[]){
  console.log(selected,'selected');
  
}
}

到这里这个组件就封装完了,不过我们封装的这个组件还不是一个完整的transferpanel只是半个,后续完整的会在之后的文章中进行介绍~