这篇文章要仿照ng.ant.design里面的穿梭框组件,手写一个TransferPanel(就是图片里面红色框的部分),这个TransferPanel要实现以下功能:
- 选中高亮
- 默认选中高亮
- 上边的选中个数随着选中或者取消选中动态变化
- 根据输入框中输入的关键字进行搜索
页面布局
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>
到这里的话基本的布局就已经完成了,我们来看一下效果:
动态渲染
但是目前的话,数据都是写死的,我们现在要通过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)
}
}
现在的话已经可以达到这种效果了:
接下来我们对他做一些优化,因为我们封装组件是给别人用的,所以我们应该用@ 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数组中。
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)
}
}
根据输入框中输入的关键字进行搜索
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只是半个,后续完整的会在之后的文章中进行介绍~