Skip to content

Commit cbecebf

Browse files
feat(module:mention): support nzClear (#9377)
Mention component now support nzClear icon --- Co-authored-by: HyperLife1119 <hyperlife1119@qq.com>
1 parent 67d2fc9 commit cbecebf

8 files changed

Lines changed: 224 additions & 12 deletions

File tree

components/mention/demo/clear.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 11
3+
title:
4+
zh-CN: 带移除图标
5+
en-US: With clear icon
6+
---
7+
8+
## zh-CN
9+
10+
自定义清除按钮。
11+
12+
## en-US
13+
14+
Customize clear button.

components/mention/demo/clear.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Component, model } from '@angular/core';
2+
import { FormsModule } from '@angular/forms';
3+
4+
import { NzInputModule } from 'ng-zorro-antd/input';
5+
import { NzMentionModule } from 'ng-zorro-antd/mention';
6+
7+
@Component({
8+
selector: 'nz-demo-mention-clear',
9+
imports: [FormsModule, NzInputModule, NzMentionModule],
10+
template: `
11+
<nz-mention [nzSuggestions]="suggestions" (nzOnSelect)="onSelect($event)" nzAllowClear (nzOnClear)="onClear()">
12+
<textarea rows="1" placeholder="input here" nzMentionTrigger nz-input [(ngModel)]="inputValue"></textarea>
13+
</nz-mention>
14+
<br />
15+
<br />
16+
<nz-mention [nzSuggestions]="suggestions" (nzOnSelect)="onSelect($event)" nzAllowClear (nzOnClear)="onClear()">
17+
<textarea rows="3" placeholder="input here" nzMentionTrigger nz-input [(ngModel)]="inputValue"></textarea>
18+
</nz-mention>
19+
`
20+
})
21+
export class NzDemoMentionClearComponent {
22+
inputValue = model('@afc163');
23+
suggestions = ['afc163', 'benjycui', 'yiminghe', 'RaoHai', '中文', 'にほんご'];
24+
25+
onSelect(e: string): void {
26+
console.log(e);
27+
}
28+
29+
onClear(): void {
30+
console.log('onClear');
31+
}
32+
}

components/mention/doc/index.en-US.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ When need to mention someone or something.
3030
| `[nzPrefix]` | Character which will trigger Mention to show mention list | `string \| string[]` | `'@'` |
3131
| `[nzSuggestions]` | Suggestion content | `any[]` | `[]` |
3232
| `[nzStatus]` | Set validation status | `'error' \| 'warning'` | - |
33+
| `[nzAllowClear]` | If allow to remove mentions content with clear icon | `boolean` | `false` |
34+
| `[nzClearIcon]` | The custom clear icon | `TemplateRef<void>` | - |
3335
| `[nzVariant]` | Variants of Input | `'outlined' \| 'filled' \| 'borderless' \| 'underlined'` | `'outlined'` |
3436
| `[nzValueWith]` | Function that maps an suggestion's value | `(any) => string \| (value: string) => string` |
3537
| `(nzOnSelect)` | Callback function called when select from suggestions | `EventEmitter<any>` | - |
3638
| `(nzOnSearchChange)` | Callback function called when search content changes | `EventEmitter<MentionOnSearchTypes>` | - |
39+
| `(nzOnClear)` | Callback function called when click on clear button | `EventEmitter<void>` | - |
3740

3841
#### Methods
3942

components/mention/doc/index.zh-CN.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ description: 用于在输入中提及某人或某事。
3131
| `[nzPrefix]` | 触发弹出下拉框的字符 | `string \| string[]` | `'@'` |
3232
| `[nzSuggestions]` | 建议内容 | `any[]` | `[]` |
3333
| `[nzStatus]` | 设置校验状态 | `'error' \| 'warning'` | - |
34+
| `[nzAllowClear]` | 支持清除 | `boolean` | `false` |
35+
| `[nzClearIcon]` | 自定义的多选框清空图标 | `TemplateRef<void>` | - |
3436
| `[nzVariant]` | 形态变体 | `'outlined' \| 'filled' \| 'borderless' \| 'underlined'` | `'outlined'` |
35-
| `[nzValueWith]` | 建议选项的取值方法 | `(any) => string \| (value: string) => string` |
37+
| `[nzValueWith]` | 建议选项的取值方法 | `(any) => string \| (value: string) => string` | - |
3638
| `(nzOnSelect)` | 下拉框选择建议时回调 | `EventEmitter<any>` | - |
3739
| `(nzOnSearchChange)` | 输入框中 @ 变化时回调 | `EventEmitter<MentionOnSearchTypes>` | - |
40+
| `(nzOnClear)` | 清空已选项时触发的回调函数 | `EventEmitter<void>` | - |
3841

3942
#### 方法
4043

components/mention/mention-trigger.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export class NzMentionTriggerDirective implements ControlValueAccessor {
6161
@Output() readonly onKeydown = new EventEmitter<KeyboardEvent>();
6262
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
6363
@Output() readonly onClick = new EventEmitter<MouseEvent>();
64-
value?: string;
64+
65+
readonly value = signal<string>('');
6566

6667
readonly disabled = signal(false);
6768

@@ -103,16 +104,19 @@ export class NzMentionTriggerDirective implements ControlValueAccessor {
103104
this.elementRef.nativeElement.value = newValue;
104105
this.focus(mention.startPos + insertValue.length + 1);
105106
this.onChange(newValue);
106-
this.value = newValue;
107+
this.value.set(newValue);
108+
}
109+
110+
clear(): void {
111+
this.value.set('');
112+
this.elementRef.nativeElement.value = '';
113+
this.onChange('');
107114
}
108115

109116
writeValue(value: string): void {
110-
this.value = value;
111-
if (typeof value === 'string') {
112-
this.elementRef.nativeElement.value = value;
113-
} else {
114-
this.elementRef.nativeElement.value = '';
115-
}
117+
const parsedValue = typeof value === 'string' ? value : '';
118+
this.value.set(parsedValue);
119+
this.elementRef.nativeElement.value = parsedValue;
116120
}
117121

118122
onChange: OnChangeType = () => {};

components/mention/mention.component.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { merge, of as observableOf, Subscription } from 'rxjs';
4949
import { distinctUntilChanged, map, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
5050

5151
import { NzFormItemFeedbackIconComponent, NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
52+
import { NzStringTemplateOutletDirective } from 'ng-zorro-antd/core/outlet';
5253
import { DEFAULT_MENTION_BOTTOM_POSITIONS, DEFAULT_MENTION_TOP_POSITIONS } from 'ng-zorro-antd/core/overlay';
5354
import { NgClassInterface, NzSafeAny, NzStatus, NzValidateStatus, NzVariant } from 'ng-zorro-antd/core/types';
5455
import {
@@ -120,6 +121,15 @@ export type MentionPlacement = 'top' | 'bottom';
120121
@if (hasFeedback && !!status) {
121122
<nz-form-item-feedback-icon class="ant-mentions-suffix" [status]="status" />
122123
}
124+
@if (nzAllowClear && hasValue()) {
125+
<span class="ant-mentions-suffix">
126+
<button type="button" tabindex="-1" class="ant-mentions-clear-icon" (click)="clear()">
127+
<ng-template [nzStringTemplateOutlet]="nzClearIcon">
128+
<nz-icon nzType="close-circle" nzTheme="fill" />
129+
</ng-template>
130+
</button>
131+
</span>
132+
}
123133
`,
124134
changeDetection: ChangeDetectionStrategy.OnPush,
125135
host: {
@@ -131,7 +141,13 @@ export type MentionPlacement = 'top' | 'bottom';
131141
'[class.ant-mentions-focused]': `focused()`,
132142
'[class.ant-mentions-disabled]': `disabled()`
133143
},
134-
imports: [NgTemplateOutlet, NzIconModule, NzEmptyModule, NzFormItemFeedbackIconComponent]
144+
imports: [
145+
NgTemplateOutlet,
146+
NzIconModule,
147+
NzEmptyModule,
148+
NzFormItemFeedbackIconComponent,
149+
NzStringTemplateOutletDirective
150+
]
135151
})
136152
export class NzMentionComponent implements OnInit, AfterViewInit, OnChanges {
137153
private ngZone = inject(NgZone);
@@ -150,8 +166,11 @@ export class NzMentionComponent implements OnInit, AfterViewInit, OnChanges {
150166
@Input() nzSuggestions: NzSafeAny[] = [];
151167
@Input() nzStatus: NzStatus = '';
152168
@Input() nzVariant: NzVariant = 'outlined';
169+
@Input({ transform: booleanAttribute }) nzAllowClear = false;
170+
@Input() nzClearIcon: TemplateRef<NzSafeAny> | null = null;
153171
@Output() readonly nzOnSelect = new EventEmitter<NzSafeAny>();
154172
@Output() readonly nzOnSearchChange = new EventEmitter<MentionOnSearchTypes>();
173+
@Output() readonly nzOnClear = new EventEmitter<void>();
155174

156175
@ViewChild(TemplateRef, { static: false }) suggestionsTemp?: TemplateRef<void>;
157176
@ViewChildren('items', { read: ElementRef })
@@ -181,6 +200,9 @@ export class NzMentionComponent implements OnInit, AfterViewInit, OnChanges {
181200
readonly disabled = computed(() => {
182201
return this.trigger().disabled();
183202
});
203+
readonly hasValue = computed(() => {
204+
return !!this.trigger()?.value().trim();
205+
});
184206

185207
private previousValue: string | null = null;
186208
private cursorMention: string | null = null;
@@ -285,7 +307,7 @@ export class NzMentionComponent implements OnInit, AfterViewInit, OnChanges {
285307
}
286308

287309
getMentions(): string[] {
288-
return this.trigger() ? getMentions(this.trigger().value!, this.nzPrefix) : [];
310+
return this.trigger() ? getMentions(this.trigger().value(), this.nzPrefix) : [];
289311
}
290312

291313
selectSuggestion(suggestion: string | {}): void {
@@ -300,10 +322,16 @@ export class NzMentionComponent implements OnInit, AfterViewInit, OnChanges {
300322
this.activeIndex = -1;
301323
}
302324

325+
clear(): void {
326+
this.closeDropdown();
327+
this.trigger().clear();
328+
this.nzOnClear.emit();
329+
}
330+
303331
private handleInput(event: KeyboardEvent): void {
304332
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
305333
this.trigger().onChange(target.value);
306-
this.trigger().value = target.value;
334+
this.trigger().value.set(target.value);
307335
this.resetDropdown();
308336
}
309337

components/mention/mention.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,88 @@ describe('mention', () => {
664664
});
665665
}));
666666
});
667+
668+
describe('clear button', () => {
669+
let fixture: ComponentFixture<NzTestClearMentionComponent>;
670+
let textarea: HTMLTextAreaElement;
671+
672+
beforeEach(fakeAsync(() => {
673+
fixture = TestBed.createComponent(NzTestClearMentionComponent);
674+
fixture.detectChanges();
675+
textarea = fixture.debugElement.query(By.css('textarea')).nativeElement;
676+
tick();
677+
}));
678+
679+
it('should not show clear button when nzAllowClear is false', fakeAsync(() => {
680+
fixture.componentInstance.allowClear = false;
681+
fixture.detectChanges();
682+
typeInElement('test value', textarea);
683+
fixture.detectChanges();
684+
tick();
685+
expect(fixture.debugElement.query(By.css('.ant-mentions-clear-icon'))).toBeNull();
686+
}));
687+
688+
it('should show clear button when nzAllowClear is true and has value', fakeAsync(() => {
689+
fixture.componentInstance.allowClear = true;
690+
fixture.detectChanges();
691+
typeInElement('test value', textarea);
692+
fixture.detectChanges();
693+
tick();
694+
expect(fixture.debugElement.query(By.css('.ant-mentions-clear-icon'))).toBeTruthy();
695+
}));
696+
697+
it('should not show clear button when nzAllowClear is true but has no value', fakeAsync(() => {
698+
fixture.componentInstance.allowClear = true;
699+
fixture.detectChanges();
700+
typeInElement('', textarea);
701+
fixture.detectChanges();
702+
tick();
703+
expect(fixture.debugElement.query(By.css('.ant-mentions-clear-icon'))).toBeNull();
704+
}));
705+
706+
it('should clear input value when clear button is clicked', fakeAsync(() => {
707+
fixture.componentInstance.allowClear = true;
708+
fixture.detectChanges();
709+
typeInElement('test value', textarea);
710+
fixture.detectChanges();
711+
tick();
712+
713+
const clearButton = fixture.debugElement.query(By.css('.ant-mentions-clear-icon')).nativeElement;
714+
clearButton.click();
715+
fixture.detectChanges();
716+
tick();
717+
718+
expect(textarea.value).toBe('');
719+
expect(fixture.componentInstance.inputValue).toBe('');
720+
}));
721+
722+
it('should emit nzOnClear when clear button is clicked', fakeAsync(() => {
723+
const spy = spyOn(fixture.componentInstance, 'onClear');
724+
fixture.componentInstance.allowClear = true;
725+
fixture.detectChanges();
726+
typeInElement('test value', textarea);
727+
fixture.detectChanges();
728+
tick();
729+
730+
const clearButton = fixture.debugElement.query(By.css('.ant-mentions-clear-icon')).nativeElement;
731+
clearButton.click();
732+
fixture.detectChanges();
733+
734+
expect(spy).toHaveBeenCalled();
735+
}));
736+
737+
it('should use custom clear icon when provided', fakeAsync(() => {
738+
fixture.componentInstance.allowClear = true;
739+
fixture.componentInstance.useCustomClearIcon = true;
740+
fixture.detectChanges();
741+
typeInElement('test value', textarea);
742+
fixture.detectChanges();
743+
tick();
744+
745+
const clearIcon = fixture.debugElement.query(By.css('.custom-clear-icon'));
746+
expect(clearIcon).toBeTruthy();
747+
}));
748+
});
667749
});
668750

669751
@Component({
@@ -806,6 +888,33 @@ class NzTestVariantMentionComponent {
806888
@ViewChild(NzMentionComponent, { static: false }) mention!: NzMentionComponent;
807889
}
808890

891+
@Component({
892+
imports: [FormsModule, NzInputModule, NzMentionModule],
893+
template: `
894+
<nz-mention
895+
[nzSuggestions]="suggestions"
896+
[nzAllowClear]="allowClear"
897+
[nzClearIcon]="useCustomClearIcon ? clearIconTemplate : null"
898+
(nzOnClear)="onClear()"
899+
>
900+
<textarea nz-input [nzAutosize]="{ minRows: 4, maxRows: 4 }" [(ngModel)]="inputValue" nzMentionTrigger></textarea>
901+
<ng-template #clearIconTemplate>
902+
<span class="custom-clear-icon">×</span>
903+
</ng-template>
904+
</nz-mention>
905+
`
906+
})
907+
class NzTestClearMentionComponent {
908+
inputValue = '';
909+
suggestions = ['angular', 'ant-design', 'mention'];
910+
allowClear = false;
911+
useCustomClearIcon = false;
912+
913+
@ViewChild(NzMentionComponent, { static: false }) mention!: NzMentionComponent;
914+
915+
onClear(): void {}
916+
}
917+
809918
class MockDirectionality {
810919
value = 'ltr';
811920
change = new Subject();

components/mention/style/index.less

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@
6464
border: none;
6565
outline: none;
6666
resize: none;
67+
6768
& when (@theme = dark) {
6869
background-color: transparent;
6970
}
71+
7072
.placeholder();
7173
}
7274

@@ -175,6 +177,23 @@
175177
align-items: center;
176178
margin: auto;
177179
}
180+
181+
&-clear-icon {
182+
padding: 0;
183+
color: @disabled-color;
184+
font-size: @font-size-sm;
185+
line-height: 0;
186+
vertical-align: -1px;
187+
background-color: transparent;
188+
border: none;
189+
outline: none;
190+
cursor: pointer;
191+
transition: color 0.3s;
192+
193+
&:hover {
194+
color: @text-color-secondary;
195+
}
196+
}
178197
}
179198

180199
@import './rtl';

0 commit comments

Comments
 (0)