diff --git a/PROGRESS.md b/PROGRESS.md index 08ea7a23c50..e6228e277dd 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -41,7 +41,7 @@ | timepicker | x | x | x | trotyl | - | | calendar | x | x | x | trotyl | - | | affix | x | x | x | cipchk | - | -| transfer | x | x | x | cipchk | - | +| transfer | √ | 100% | 100% | cipchk | x | | avatar | √ | 100% | 100% | cipchk | x | | list | √ | 100% | 100% | cipchk | x | | upload | x | x | x | cipchk | - | diff --git a/components/index.ts b/components/index.ts index 2a7fc8d2f3b..45280740984 100644 --- a/components/index.ts +++ b/components/index.ts @@ -49,6 +49,7 @@ export * from './grid'; export * from './layout'; export * from './dropdown'; export * from './menu'; +export * from './transfer'; export * from './i18n'; export * from './locale/index'; export * from './list/index'; diff --git a/components/transfer/demo/advanced.ts b/components/transfer/demo/advanced.ts index cbb06ee1501..90ceb82871e 100644 --- a/components/transfer/demo/advanced.ts +++ b/components/transfer/demo/advanced.ts @@ -23,11 +23,11 @@ import { NzMessageService } from 'ng-zorro-antd'; export class NzDemoTransferAdvancedComponent implements OnInit { list = []; - ngOnInit() { + ngOnInit(): void { this.getData(); } - getData() { + getData(): void { const ret = []; for (let i = 0; i < 20; i++) { ret.push({ @@ -40,16 +40,16 @@ export class NzDemoTransferAdvancedComponent implements OnInit { this.list = ret; } - reload(direction: string) { + reload(direction: string): void { this.getData(); this.msg.success(`your clicked ${direction}!`); } - select(ret: {}) { + select(ret: {}): void { console.log('nzSelectChange', ret); } - change(ret: {}) { + change(ret: {}): void { console.log('nzChange', ret); } diff --git a/components/transfer/demo/basic.ts b/components/transfer/demo/basic.ts index a8a7bec6434..411a03b3586 100644 --- a/components/transfer/demo/basic.ts +++ b/components/transfer/demo/basic.ts @@ -13,9 +13,10 @@ import { NzMessageService } from 'ng-zorro-antd'; ` }) export class NzDemoTransferBasicComponent implements OnInit { + // tslint:disable-next-line:no-any list: any[] = []; - ngOnInit() { + ngOnInit(): void { for (let i = 0; i < 20; i++) { this.list.push({ key : i.toString(), @@ -27,11 +28,11 @@ export class NzDemoTransferBasicComponent implements OnInit { [ 2, 3 ].forEach(idx => this.list[ idx ].direction = 'right'); } - select(ret: any) { + select(ret: {}): void { console.log('nzSelectChange', ret); } - change(ret: any) { + change(ret: {}): void { console.log('nzChange', ret); } } diff --git a/components/transfer/demo/can-move.ts b/components/transfer/demo/can-move.ts index 036678b462b..10e35dda635 100644 --- a/components/transfer/demo/can-move.ts +++ b/components/transfer/demo/can-move.ts @@ -1,15 +1,14 @@ import { Component, OnInit } from '@angular/core'; -import { NzMessageService } from 'ng-zorro-antd'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { delay } from 'rxjs/operators'; +import { TransferCanMove, TransferItem, NzMessageService } from 'ng-zorro-antd'; @Component({ selector: 'nz-demo-transfer-can-move', template: ` @@ -31,7 +30,7 @@ export class NzDemoTransferCanMoveComponent implements OnInit { [ 2, 3 ].forEach(idx => this.list[ idx ].direction = 'right'); } - canMove(arg) { + canMove(arg: TransferCanMove): Observable { if (arg.direction === 'right' && arg.list.length > 0) arg.list.splice(0, 1); // or // if (arg.direction === 'right' && arg.list.length > 0) delete arg.list[0]; diff --git a/components/transfer/demo/custom-item.ts b/components/transfer/demo/custom-item.ts index 3796cd63229..28df6556b99 100644 --- a/components/transfer/demo/custom-item.ts +++ b/components/transfer/demo/custom-item.ts @@ -16,13 +16,14 @@ import { NzMessageService } from 'ng-zorro-antd'; ` }) export class NzDemoTransferCustomItemComponent implements OnInit { - list: {}[] = []; + // tslint:disable-next-line:no-any + list: any[] = []; - ngOnInit() { + ngOnInit(): void { this.getData(); } - getData() { + getData(): void { const ret = []; for (let i = 0; i < 20; i++) { ret.push({ @@ -36,11 +37,11 @@ export class NzDemoTransferCustomItemComponent implements OnInit { this.list = ret; } - select(ret: {}) { + select(ret: {}): void { console.log('nzSelectChange', ret); } - change(ret: {}) { + change(ret: {}): void { console.log('nzChange', ret); } diff --git a/components/transfer/demo/search.ts b/components/transfer/demo/search.ts index 5166bcefbf3..3694fa94ec9 100644 --- a/components/transfer/demo/search.ts +++ b/components/transfer/demo/search.ts @@ -15,9 +15,10 @@ import { NzMessageService } from 'ng-zorro-antd'; ` }) export class NzDemoTransferSearchComponent implements OnInit { + // tslint:disable-next-line:no-any list: any[] = []; - ngOnInit() { + ngOnInit(): void { for (let i = 0; i < 20; i++) { this.list.push({ key : i.toString(), @@ -28,19 +29,20 @@ export class NzDemoTransferSearchComponent implements OnInit { } } - filterOption(inputValue, option) { - return option.description.indexOf(inputValue) > -1; + // tslint:disable-next-line:no-any + filterOption(inputValue: string, item: any): boolean { + return item.description.indexOf(inputValue) > -1; } - search(ret: any) { + search(ret: {}): void { console.log('nzSearchChange', ret); } - select(ret: any) { + select(ret: {}): void { console.log('nzSelectChange', ret); } - change(ret: any) { + change(ret: {}): void { console.log('nzChange', ret); } } diff --git a/components/transfer/doc/index.en-US.md b/components/transfer/doc/index.en-US.md index b97bb681988..f0efce2b7d5 100644 --- a/components/transfer/doc/index.en-US.md +++ b/components/transfer/doc/index.en-US.md @@ -17,32 +17,50 @@ One or more elements can be selected from either column, one click on the proper | Property | Description | Type | Default | | -------- | ----------- | ---- | ------- | -| className | A custom CSS class. | string | ['', ''] | -| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop. | [TransferItem](https://git.io/vMM64)\[] | \[] | -| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | | -| footer | A function used for rendering the footer. | (props): ReactNode | | -| lazy | property of [react-lazy-load](https://github.com/loktar00/react-lazy-load) for lazy rendering items. Turn off it by set to `false`. | object|boolean | `{ height: 32, offset: 32 }` | -| listStyle | A custom CSS style used for rendering the transfer columns. | object | | -| notFoundContent | Text to display when a column is empty. | string|ReactNode | 'The list is empty' | -| operations | A set of operations that are sorted from bottom to top. | string\[] | ['>', '<'] | -| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a React element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a React element and `value` is for title | Function(record) | | -| searchPlaceholder | The hint text of the search box. | string | 'Search here' | -| selectedKeys | A set of keys of selected items. | string\[] | \[] | -| showSearch | If included, a search box is shown on each column. | boolean | false | -| targetKeys | A set of keys of elements that are listed on the right column. | string\[] | \[] | -| titles | A set of titles that are sorted from left to right. | string\[] | - | -| onChange | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | | -| onScroll | A callback function which is executed when scroll options list | (direction, event): void | | -| onSearchChange | A callback function which is executed when search field are changed | (direction: 'left'|'right', event: Event): void | - | -| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | | - -## Warning - -According the [standard](http://facebook.github.io/react/docs/lists-and-keys.html#keys) of React, the key should always be supplied directly to the elements in the array. In Transfer, the keys should be set on the elements included in `dataSource` array. By default, `key` property is used as an unique identifier. - -If there's no `key` in your data, you should use `rowKey` to specify the key that will be used for uniquely identify each element. - -```jsx -// eg. your primary key is `uid` -return record.uid} />; -``` +| nzDataSource | Used for setting the source data. Except the elements whose keys are `direction: 'right'` prop. | TransferItem[] | [] | +| nzTitles | A set of titles that are sorted from left to right. | string[] | ['', ''] | +| nzOperations | A set of operations that are sorted from bottom to top. | string[] | ['', ''] | +| nzListStyle | A custom CSS style used for rendering the transfer columns. equal `ngStyle` | object | | +| nzItemUnit | single unit | string | item | +| nzItemsUnit | multiple unit | string | items | +| #render | The function to generate the item shown on a column. please refer to the case. | `TemplateRef` | - | +| #footer | A function used for rendering the footer. please refer to the case. | `TemplateRef` | - | +| nzShowSearch | If included, a search box is shown on each column. | boolean | false | +| nzFilterOption | A function to determine whether an item should show in search result list | `(inputValue: string, item: TransferItem) => boolean` | +| nzSearchPlaceholder | The hint text of the search box. | string | 'Search here' | +| nzNotFoundContent | Text to display when a column is empty. | string | 'The list is empty' | +| canMove | Two verification when transfer choice box. please refer to the case. | `(arg: TransferCanMove) => Observable` | - | +| (nzChange) | A callback function that is executed when the transfer between columns is complete. | `EventEmitter` | - | +| (nzSearchChange) | A callback function which is executed when search field are changed | `EventEmitter` | - | +| (nzSelectChange) | A callback function which is executed when selected items are changed. | `EventEmitter` | - | + +### TransferItem + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| title | Used to display and search keyword | string | - | +| direction | Used for setting the source data. Except the elements whose keys are `direction: 'right'` prop. | `left,right` | - | +| disabled | specifies whether the checkbox is disabled | boolean | false | +| checked | specifies whether the checkbox is selected | boolean | false | + +### TransferCanMove + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| direction | data direction | `left,right` | - | +| list | Used for setting the source data. | TransferItem[] | [] | + +### TransferChange + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| from | data direction | `left,right` | - | +| to | data direction | `left,right` | - | +| list | Used for setting the source data. | TransferItem[] | [] | + +### TransferSearchChange + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| direction | data direction | `left,right` | - | +| value | Search keyword | string | - | diff --git a/components/transfer/doc/index.zh-CN.md b/components/transfer/doc/index.zh-CN.md index c9e5a576f13..d5481eb5be5 100644 --- a/components/transfer/doc/index.zh-CN.md +++ b/components/transfer/doc/index.zh-CN.md @@ -19,32 +19,50 @@ title: Transfer | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| className | 自定义类 | string | | -| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] | -| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | | -| footer | 底部渲染函数 | (props): ReactNode | | -| lazy | Transfer 使用了 [react-lazy-load](https://github.com/loktar00/react-lazy-load) 优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object|boolean | `{ height: 32, offset: 32 }` | -| listStyle | 两个穿梭框的自定义样式 | object | | -| notFoundContent | 当列表为空时显示的内容 | string|ReactNode | '列表为空' | -| operations | 操作文案集合,顺序从下至上 | string\[] | ['>', '<'] | -| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | Function(record) | | -| searchPlaceholder | 搜索框的默认值 | string | '请输入搜索内容' | -| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | -| showSearch | 是否显示搜索框 | boolean | false | -| targetKeys | 显示在右侧框数据的key集合 | string\[] | \[] | -| titles | 标题集合,顺序从左至右 | string\[] | ['', ''] | -| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | | -| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | | -| onSearchChange | 搜索框内容时改变时的回调函数 | (direction: 'left'|'right', event: Event): void | - | -| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | | - -## 注意 - -按照 React 的[规范](http://facebook.github.io/react/docs/lists-and-keys.html#keys),所有的组件数组必须绑定 key。在 Transfer 中,`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。 - -如果你的数据没有这个属性,务必使用 `rowKey` 来指定数据列的主键。 - -```jsx -// 比如你的数据主键是 uid -return record.uid} />; -``` +| nzDataSource | 数据源,其中若数据属性 `direction: 'right'` 将会被渲染到右边一栏中 | TransferItem[] | [] | +| nzTitles | 标题集合,顺序从左至右 | string[] | ['', ''] | +| nzOperations | 操作文案集合,顺序从下至上 | string[] | ['', ''] | +| nzListStyle | 两个穿梭框的自定义样式,等同 `ngStyle` | object | | +| nzItemUnit | 单数单位 | string | 项目 | +| nzItemsUnit | 复数单位 | string | 项目 | +| #render | 每行数据渲染模板,见示例 | `TemplateRef` | - | +| #footer | 底部渲染模板,见示例 | `TemplateRef` | - | +| nzShowSearch | 是否显示搜索框 | boolean | false | +| nzFilterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | `(inputValue: string, item: TransferItem) => boolean` | +| nzSearchPlaceholder | 搜索框的默认值 | string | '请输入搜索内容' | +| nzNotFoundContent | 当列表为空时显示的内容 | string | '列表为空' | +| canMove | 穿梭时二次校验。**注意:** 穿梭组件内部始终只保留一份数据,二次校验过程中需取消穿梭项则直接删除该项;具体用法见示例。 | `(arg: TransferCanMove) => Observable` | - | +| (nzChange) | 选项在两栏之间转移时的回调函数 | `EventEmitter` | - | +| (nzSearchChange) | 搜索框内容时改变时的回调函数 | `EventEmitter` | - | +| (nzSelectChange) | 选中项发生改变时的回调函数 | `EventEmitter` | - | + +### TransferItem + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 标题,用于显示及搜索关键字判断 | string | - | +| direction | 指定数据方向,若指定 `right` 为右栏,其他情况为左栏 | `left,right` | - | +| disabled | 指定checkbox为不可用状态 | boolean | false | +| checked | 指定checkbox为选中状态 | boolean | false | + +### TransferCanMove + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| direction | 数据方向 | `left,right` | - | +| list | 数据源 | TransferItem[] | [] | + +### TransferChange + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| from | 数据方向 | `left,right` | - | +| to | 数据方向 | `left,right` | - | +| list | 数据源 | TransferItem[] | [] | + +### TransferSearchChange + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| direction | 数据方向 | `left,right` | - | +| value | 搜索关键词 | string | - | diff --git a/components/transfer/index.ts b/components/transfer/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/components/transfer/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/components/transfer/interface.ts b/components/transfer/interface.ts new file mode 100644 index 00000000000..a8c3a9ca596 --- /dev/null +++ b/components/transfer/interface.ts @@ -0,0 +1,31 @@ +export interface TransferItem { + title: string; + direction?: 'left' | 'right'; + disabled?: boolean; + checked?: boolean; + _hiden?: boolean; + [key: string]: {}; +} + +export interface TransferCanMove { + direction: string; + list: TransferItem[]; +} + +export interface TransferChange { + from: string; + to: string; + list: TransferItem[]; +} + +export interface TransferSearchChange { + direction: string; + value: string; +} + +export interface TransferSelectChange { + direction: string; + checked: boolean; + list: TransferItem[]; + item: TransferItem; +} diff --git a/components/transfer/item.ts b/components/transfer/item.ts deleted file mode 100644 index 6a2f75b7065..00000000000 --- a/components/transfer/item.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface TransferItem { - title: string; - direction?: 'left' | 'right'; - disabled?: boolean; - checked?: boolean; - _hiden?: boolean; - /* tslint:disable-next-line:no-any */ - [key: string]: any; -} diff --git a/components/transfer/nz-transfer-list.component.ts b/components/transfer/nz-transfer-list.component.ts index 27b414b4083..cf4bed21f33 100644 --- a/components/transfer/nz-transfer-list.component.ts +++ b/components/transfer/nz-transfer-list.component.ts @@ -9,25 +9,28 @@ import { OnChanges, OnInit, Output, - Renderer2, SimpleChanges, TemplateRef } from '@angular/core'; +import { NzUpdateHostClassService } from '../core/services/update-host-class.service'; import { toBoolean } from '../core/util/convert'; -import { TransferItem } from './item'; +import { TransferItem } from './interface'; @Component({ selector : 'nz-transfer-list', preserveWhitespaces: false, + providers : [ NzUpdateHostClassService ], template : `
- {{ (stat.checkCount > 0 ? stat.checkCount + '/' : '') + stat.shownCount }} {{ dataSource.length > 1 ? itemsUnit : itemUnit }} - {{ titleText }} - + [nzIndeterminate]="stat.checkHalf"> + + {{ (stat.checkCount > 0 ? stat.checkCount + '/' : '') + stat.shownCount }} {{ dataSource.length > 1 ? itemsUnit : itemUnit }} + {{ titleText }} + +
@@ -97,18 +100,14 @@ export class NzTransferListComponent implements OnChanges, OnInit, DoCheck { // region: styles - _prefixCls = 'ant-transfer-list'; - _classList: string[] = []; + prefixCls = 'ant-transfer-list'; - _setClassMap(): void { - this._classList.forEach(cls => this._renderer.removeClass(this._el.nativeElement, cls)); - - this._classList = [ - this._prefixCls, - !!this.footer && `${this._prefixCls}-with-footer` - ].filter(item => !!item); - - this._classList.forEach(cls => this._renderer.addClass(this._el.nativeElement, cls)); + setClassMap(): void { + const classMap = { + [ this.prefixCls ]: true, + [ `${this.prefixCls}-with-footer` ]: !!this.footer + }; + this.updateHostClassService.updateHostClass(this.el.nativeElement, classMap); } // endregion @@ -129,8 +128,6 @@ export class NzTransferListComponent implements OnChanges, OnInit, DoCheck { } }); - // // ngModelChange 事件内对状态的变更会无效,因此使用延迟改变执行顺序 - // setTimeout(() => this.updateCheckStatus()); this.updateCheckStatus(); this.handleSelectAll.emit(status); } @@ -169,24 +166,24 @@ export class NzTransferListComponent implements OnChanges, OnInit, DoCheck { // endregion - _listDiffer: IterableDiffer<{}>; + listDiffer: IterableDiffer<{}>; - constructor(private _el: ElementRef, private _renderer: Renderer2, differs: IterableDiffers) { - this._listDiffer = differs.find([]).create(null); + constructor(private el: ElementRef, private updateHostClassService: NzUpdateHostClassService, differs: IterableDiffers) { + this.listDiffer = differs.find([]).create(null); } ngOnChanges(changes: SimpleChanges): void { if ('footer' in changes) { - this._setClassMap(); + this.setClassMap(); } } ngOnInit(): void { - this._setClassMap(); + this.setClassMap(); } ngDoCheck(): void { - const change = this._listDiffer.diff(this.dataSource); + const change = this.listDiffer.diff(this.dataSource); if (change) { this.updateCheckStatus(); } diff --git a/components/transfer/nz-transfer-search.component.ts b/components/transfer/nz-transfer-search.component.ts index 7118daa5d56..20c9f3edd4f 100644 --- a/components/transfer/nz-transfer-search.component.ts +++ b/components/transfer/nz-transfer-search.component.ts @@ -9,7 +9,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; - + + ` }) diff --git a/components/transfer/nz-transfer.component.ts b/components/transfer/nz-transfer.component.ts index 5552a34933a..d3bd1a43370 100644 --- a/components/transfer/nz-transfer.component.ts +++ b/components/transfer/nz-transfer.component.ts @@ -1,4 +1,3 @@ -// tslint:disable:member-ordering import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -16,32 +15,9 @@ import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { toBoolean } from '../core/util/convert'; -import { NzLocaleService } from '../locale/index'; +import { NzI18nService } from '../i18n/nz-i18n.service'; -import { TransferItem } from './item'; - -export interface TransferCanMove { - direction: string; - list: TransferItem[]; -} - -export interface TransferChange { - from: string; - to: string; - list: TransferItem[]; -} - -export interface TransferSearchChange { - direction: string; - value: string; -} - -export interface TransferSelectChange { - direction: string; - checked: boolean; - list: TransferItem[]; - item: TransferItem; -} +import { TransferCanMove, TransferChange, TransferItem, TransferSearchChange, TransferSelectChange } from './interface'; @Component({ selector : 'nz-transfer', @@ -86,7 +62,6 @@ export interface TransferSelectChange { (handleSelect)="handleRightSelect($event)" (handleSelectAll)="handleRightSelectAll($event)"> `, - // tslint:disable-next-line:use-host-property-decorator host : { '[class.ant-transfer]': 'true' }, @@ -101,11 +76,11 @@ export class NzTransferComponent implements OnChanges { // region: fields @Input() nzDataSource: TransferItem[] = []; - @Input() nzTitles: string[] = this._locale.translate('Transfer.titles').split(','); + @Input() nzTitles: string[] = ['', '']; @Input() nzOperations: string[] = []; @Input() nzListStyle: object; - @Input() nzItemUnit = this._locale.translate('Transfer.itemUnit'); - @Input() nzItemsUnit = this._locale.translate('Transfer.itemsUnit'); + @Input() nzItemUnit = this.i18n.translate('Transfer.itemUnit'); + @Input() nzItemsUnit = this.i18n.translate('Transfer.itemsUnit'); @Input() canMove: (arg: TransferCanMove) => Observable = (arg: TransferCanMove) => of(arg.list); @ContentChild('render') render: TemplateRef; @ContentChild('footer') footer: TemplateRef; @@ -121,8 +96,8 @@ export class NzTransferComponent implements OnChanges { } @Input() nzFilterOption: (inputValue: string, item: TransferItem) => boolean; - @Input() nzSearchPlaceholder = this._locale.translate('Transfer.searchPlaceholder'); - @Input() nzNotFoundContent = this._locale.translate('Transfer.notFoundContent'); + @Input() nzSearchPlaceholder = this.i18n.translate('Transfer.searchPlaceholder'); + @Input() nzNotFoundContent = this.i18n.translate('Transfer.notFoundContent'); // events @Output() nzChange: EventEmitter = new EventEmitter(); @@ -204,11 +179,9 @@ export class NzTransferComponent implements OnChanges { const datasource = direction === 'left' ? this.rightDataSource : this.leftDataSource; const targetDatasource = direction === 'left' ? this.leftDataSource : this.rightDataSource; for (const item of list) { - const idx = datasource.indexOf(item); - if (idx === -1) continue; item.checked = false; targetDatasource.push(item); - datasource.splice(idx, 1); + datasource.splice(datasource.indexOf(item), 1); } this.updateOperationStatus(oppositeDirection); this.nzChange.emit({ @@ -216,16 +189,15 @@ export class NzTransferComponent implements OnChanges { to : direction, list }); - // this.nzSelectChange.emit({ direction: oppositeDirection, list: [] }); } // endregion - constructor(private _locale: NzLocaleService, private el: ElementRef, private cd: ChangeDetectorRef) { + constructor(private i18n: NzI18nService, private el: ElementRef, private cd: ChangeDetectorRef) { } ngOnChanges(changes: SimpleChanges): void { - if ('nzDataSource' in changes || 'nzTargetKeys' in changes) { + if ('nzDataSource' in changes) { this.splitDataSource(); this.updateOperationStatus('left'); this.updateOperationStatus('right'); diff --git a/components/transfer/nz-transfer.module.ts b/components/transfer/nz-transfer.module.ts index 68b1147ad7b..42f37cd529a 100644 --- a/components/transfer/nz-transfer.module.ts +++ b/components/transfer/nz-transfer.module.ts @@ -4,15 +4,15 @@ import { FormsModule } from '@angular/forms'; import { NzButtonModule } from '../button/nz-button.module'; import { NzCheckboxModule } from '../checkbox/nz-checkbox.module'; +import { NzI18nModule } from '../i18n'; import { NzInputModule } from '../input/nz-input.module'; -import { NzLocaleModule } from '../locale/index'; import { NzTransferListComponent } from './nz-transfer-list.component'; import { NzTransferSearchComponent } from './nz-transfer-search.component'; import { NzTransferComponent } from './nz-transfer.component'; @NgModule({ - imports: [CommonModule, FormsModule, NzCheckboxModule, NzButtonModule, NzInputModule, NzLocaleModule], + imports: [CommonModule, FormsModule, NzCheckboxModule, NzButtonModule, NzInputModule, NzI18nModule], declarations: [NzTransferComponent, NzTransferListComponent, NzTransferSearchComponent], exports: [NzTransferComponent] }) diff --git a/components/transfer/public-api.ts b/components/transfer/public-api.ts new file mode 100644 index 00000000000..a0651daa01a --- /dev/null +++ b/components/transfer/public-api.ts @@ -0,0 +1,5 @@ +export * from './interface'; +export { NzTransferListComponent } from './nz-transfer-list.component'; +export { NzTransferSearchComponent } from './nz-transfer-search.component'; +export { NzTransferComponent } from './nz-transfer.component'; +export { NzTransferModule } from './nz-transfer.module'; diff --git a/components/transfer/style/index.less b/components/transfer/style/index.less index d54e289621a..6da1eae2b6c 100644 --- a/components/transfer/style/index.less +++ b/components/transfer/style/index.less @@ -167,4 +167,4 @@ 100% { background: transparent; } -} +} \ No newline at end of file diff --git a/components/transfer/transfer.spec.ts b/components/transfer/transfer.spec.ts new file mode 100644 index 00000000000..11c8f50c96a --- /dev/null +++ b/components/transfer/transfer.spec.ts @@ -0,0 +1,321 @@ +// tslint:disable:no-any no-parameter-reassignment +import { Component, DebugElement, Injector, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Observable } from 'rxjs/Observable'; +import { of } from 'rxjs/observable/of'; +import { delay, map } from 'rxjs/operators'; + +import { NzTransferComponent, NzTransferModule } from './index'; +import { TransferCanMove, TransferItem } from './interface'; + +const COUNT = 20; +const LEFTCOUNT = 2; + +describe('transfer', () => { + let injector: Injector; + let fixture: ComponentFixture; + let dl: DebugElement; + let instance: TestTransferComponent; + let pageObject: TransferPageObject; + beforeEach(() => { + injector = TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, NzTransferModule], + declarations: [TestTransferComponent, TestTransferCustomRenderComponent] + }); + fixture = TestBed.createComponent(TestTransferComponent); + dl = fixture.debugElement; + instance = dl.componentInstance; + pageObject = new TransferPageObject(); + fixture.detectChanges(); + }); + + describe('[default]', () => { + it('should be from left to right', () => { + pageObject.expectLeft(LEFTCOUNT) + .transfer('right', 0) + .expectLeft(LEFTCOUNT - 1) + .expectRight(COUNT - LEFTCOUNT + 1); + }); + + it('should be from right to left', () => { + pageObject.expectRight(COUNT - LEFTCOUNT) + .transfer('left', [0, 1]) + .expectRight(COUNT - LEFTCOUNT - 2) + .expectLeft(LEFTCOUNT + 2); + }); + + it('should be from left to right when via search found items', () => { + pageObject.expectLeft(LEFTCOUNT) + .search('left', '1') + .transfer('right', 0) + .expectLeft(LEFTCOUNT - 1) + .expectRight(COUNT - LEFTCOUNT + 1); + expect(pageObject.leftList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(0); + }); + + it('should be from right to left when via search found items', () => { + pageObject.expectRight(COUNT - LEFTCOUNT) + .search('right', '2') + .transfer('left', [0, 1]) + .expectLeft(LEFTCOUNT + 2) + .expectRight(COUNT - LEFTCOUNT - 2); + expect(pageObject.rightList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(0); + }); + + it('should be custom filter option', () => { + instance.nzFilterOption = (inputValue: string, item: any): boolean => { + return item.description.indexOf(inputValue) > -1; + }; + fixture.detectChanges(); + pageObject.expectLeft(LEFTCOUNT).search('left', 'description of content1'); + expect(pageObject.leftList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(1); + (pageObject.leftList.querySelector('.ant-transfer-list-search-action') as HTMLElement).click(); + expect(pageObject.leftList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(LEFTCOUNT); + }); + + it('should be clear search keywords', () => { + pageObject.expectLeft(LEFTCOUNT).search('left', '1'); + expect(pageObject.leftList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(1); + (pageObject.leftList.querySelector('.ant-transfer-list-search-action') as HTMLElement).click(); + expect(pageObject.leftList.querySelectorAll('.ant-transfer-list-content-item').length).toBe(LEFTCOUNT); + }); + + it('should be checkbox is toggle select', () => { + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + pageObject.checkItem('left', 0); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(1); + pageObject.checkItem('left', 0); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + }); + + it('should be checkbox is disabled toggle select when setting disabled prop', () => { + instance.nzDataSource = [ { title : `content`, disabled: true } ]; + fixture.detectChanges(); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + pageObject.checkItem('left', 0); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + pageObject.checkItem('left', 0); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + }); + + it('should be checkbox is toggle select via checkbox all in left', () => { + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + const btn = (pageObject.leftList.querySelector('.ant-transfer-list-header .ant-checkbox') as HTMLElement); + btn.click(); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(LEFTCOUNT); + btn.click(); + expect(instance.comp.leftDataSource.filter(w => w.checked).length).toBe(0); + }); + + it('should be checkbox is toggle select via checkbox all in right', () => { + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(0); + const btn = (pageObject.rightList.querySelector('.ant-transfer-list-header .ant-checkbox') as HTMLElement); + btn.click(); + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(COUNT - LEFTCOUNT); + btn.click(); + expect(instance.comp.rightDataSource.filter(w => w.checked).length).toBe(0); + }); + + it('should be uncheck all when two verification error', () => { + instance.canMove = (arg: TransferCanMove): Observable => { + return of(arg.list).pipe(map(() => { + throw new Error('error'); + })); + }; + fixture.detectChanges(); + pageObject.expectLeft(LEFTCOUNT) + .transfer('right', [ 0, 1 ]) + .expectLeft(LEFTCOUNT) + .expectRight(COUNT - LEFTCOUNT); + }); + + it('should be custom render item', () => { + const tempFixture = TestBed.createComponent(TestTransferCustomRenderComponent); + tempFixture.detectChanges(); + const leftList = tempFixture.debugElement.query(By.css('[data-direction="left"]')).nativeElement as HTMLElement; + expect(leftList.querySelectorAll('.anticon-frown-o').length).toBe(LEFTCOUNT); + }); + + it('should be custom footer', () => { + expect(pageObject.leftList.querySelector('#transfer-footer') != null).toBe(true); + }); + }); + + describe('#canMove', () => { + it('default', () => { + fixture = TestBed.createComponent(TestTransferCustomRenderComponent); + dl = fixture.debugElement; + instance = dl.componentInstance; + pageObject = new TransferPageObject(); + fixture.detectChanges(); + pageObject.expectLeft(LEFTCOUNT) + .transfer('right', 0) + .expectLeft(LEFTCOUNT - 1) + .expectRight(COUNT - LEFTCOUNT + 1); + }); + it('should be from left to right when two verification', () => { + instance.canMove = (arg: TransferCanMove): Observable => { + if (arg.direction === 'right' && arg.list.length > 0) arg.list.splice(0, 1); + return of(arg.list); + }; + fixture.detectChanges(); + pageObject.expectLeft(LEFTCOUNT) + .transfer('right', [ 0, 1 ]) + .expectLeft(LEFTCOUNT - 1) + .expectRight(COUNT - LEFTCOUNT + 1); + }); + }); + + xdescribe('#issues', () => { + xit('#996', () => { + const tempFixture = TestBed.createComponent(TestTransferCustomRenderComponent); + tempFixture.detectChanges(); + }); + }); + + class TransferPageObject { + [key: string]: any; + get leftBtn(): HTMLButtonElement { + return dl.query(By.css('.ant-transfer-operation .anticon-left')).nativeElement as HTMLButtonElement; + } + get rightBtn(): HTMLButtonElement { + return dl.query(By.css('.ant-transfer-operation .anticon-right')).nativeElement as HTMLButtonElement; + } + get leftList(): HTMLElement { + return dl.query(By.css('[data-direction="left"]')).nativeElement as HTMLElement; + } + get rightList(): HTMLElement { + return dl.query(By.css('[data-direction="right"]')).nativeElement as HTMLElement; + } + transfer(direction: 'left' | 'right', index: number | number[]): this { + if (!Array.isArray(index)) index = [ index ]; + this.checkItem(direction === 'left' ? 'right' : 'left', index); + (direction === 'left' ? this.leftBtn : this.rightBtn).click(); + fixture.detectChanges(); + return this; + } + checkItem(direction: 'left' | 'right', index: number | number[]): this { + if (!Array.isArray(index)) index = [ index ]; + const items = (direction === 'left' ? this.leftList : this.rightList).querySelectorAll('.ant-transfer-list-content-item'); + for (const idx of index) { + (items[idx] as HTMLElement).click(); + fixture.detectChanges(); + } + fixture.detectChanges(); + return this; + } + search(direction: 'left' | 'right', value: string): this { + const ipt = ((direction === 'left' ? this.leftList : this.rightList).querySelector('.ant-transfer-list-search') as HTMLInputElement); + ipt.value = value; + ipt.dispatchEvent(new Event('input')); + fixture.detectChanges(); + return this; + } + expectLeft(count: number): this { + expect(instance.comp.leftDataSource.length).toBe(count); + return this; + } + expectRight(count: number): this { + expect(instance.comp.rightDataSource.length).toBe(count); + return this; + } + } +}); + +@Component({ + template: ` + + + + + + `, + styleUrls: [ './style/index.less' ], + encapsulation: ViewEncapsulation.None +}) +class TestTransferComponent implements OnInit { + @ViewChild('comp') comp: NzTransferComponent; + nzDataSource: any[] = []; + nzTitles = ['Source', 'Target']; + nzOperations = ['to right', 'to left']; + nzItemUnit = 'item'; + nzItemsUnit = 'items'; + nzListStyle = {'width.px': 300, 'height.px': 300}; + nzShowSearch = true; + nzFilterOption = null; + nzSearchPlaceholder = '请输入搜索内容'; + nzNotFoundContent = '列表为空'; + canMove(arg: TransferCanMove): Observable { + // if (arg.direction === 'right' && arg.list.length > 0) arg.list.splice(0, 1); + // or + // if (arg.direction === 'right' && arg.list.length > 0) delete arg.list[0]; + return of(arg.list); + } + + ngOnInit(): void { + const ret = []; + for (let i = 0; i < COUNT; i++) { + ret.push({ + key : i.toString(), + title : `content${i + 1}`, + description: `description of content${i + 1}`, + direction : i >= LEFTCOUNT ? 'right' : '', + icon : `frown-o` + }); + } + this.nzDataSource = ret; + } + + search(ret: {}): void { + } + + select(ret: {}): void { + } + + change(ret: {}): void { + } +} + +@Component({ + template: ` + + + {{ item.title }} + + + ` +}) +class TestTransferCustomRenderComponent implements OnInit { + @ViewChild('comp') comp: NzTransferComponent; + nzDataSource: any[] = []; + ngOnInit(): void { + const ret = []; + for (let i = 0; i < COUNT; i++) { + ret.push({ + key : i.toString(), + title : `content${i + 1}`, + description: `description of content${i + 1}`, + direction : i >= LEFTCOUNT ? 'right' : '', + icon : `frown-o` + }); + } + this.nzDataSource = ret; + } +}