Skip to content

Commit a67cef6

Browse files
crisbetommalerba
authored andcommitted
feat(clipboard): add the ability to specify number of attempts in clipboard directive (#17547)
Adds a new input to the `cdkCopyToClipboard` directive which allows consumers to set the number of attempts to try and copy their text. We currently have an example of how to implement attempts in the readme, but this feature makes it more convenient so that consumers don't have to do it on a case-by-case basis.
1 parent 49f2ed1 commit a67cef6

File tree

4 files changed

+140
-29
lines changed

4 files changed

+140
-29
lines changed

src/cdk/clipboard/clipboard.md

+7
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,10 @@ class HeroProfile {
5555
}
5656
}
5757
```
58+
59+
If you're using the `cdkCopyToClipboard` you can pass in the `cdkCopyToClipboardAttempts` input
60+
to automatically attempt to copy some text a certain number of times.
61+
62+
```html
63+
<button [cdkCopyToClipboard]="longText" [cdkCopyToClipboardAttempts]="5">Copy text</button>
64+
```
+61-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {Component, EventEmitter, Input, Output} from '@angular/core';
1+
import {Component} from '@angular/core';
22
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
33

44
import {Clipboard} from './clipboard';
55
import {ClipboardModule} from './clipboard-module';
6+
import {PendingCopy} from './pending-copy';
67

78
const COPY_CONTENT = 'copy content';
89

@@ -11,17 +12,18 @@ const COPY_CONTENT = 'copy content';
1112
template: `
1213
<button
1314
[cdkCopyToClipboard]="content"
14-
(cdkCopyToClipboardCopied)="copied.emit($event)"></button>`,
15+
[cdkCopyToClipboardAttempts]="attempts"
16+
(cdkCopyToClipboardCopied)="copied($event)"></button>`,
1517
})
1618
class CopyToClipboardHost {
17-
@Input() content = '';
18-
@Output() copied = new EventEmitter<boolean>();
19+
content = '';
20+
attempts = 1;
21+
copied = jasmine.createSpy('copied spy');
1922
}
2023

2124
describe('CdkCopyToClipboard', () => {
2225
let fixture: ComponentFixture<CopyToClipboardHost>;
23-
let mockCopy: jasmine.Spy;
24-
let copiedOutput: jasmine.Spy;
26+
let clipboard: Clipboard;
2527

2628
beforeEach(fakeAsync(() => {
2729
TestBed.configureTestingModule({
@@ -37,31 +39,69 @@ describe('CdkCopyToClipboard', () => {
3739

3840
const host = fixture.componentInstance;
3941
host.content = COPY_CONTENT;
40-
copiedOutput = jasmine.createSpy('copied');
41-
host.copied.subscribe(copiedOutput);
42-
mockCopy = spyOn(TestBed.get(Clipboard), 'copy');
43-
42+
clipboard = TestBed.get(Clipboard);
4443
fixture.detectChanges();
4544
});
4645

4746
it('copies content to clipboard upon click', () => {
47+
spyOn(clipboard, 'copy');
4848
fixture.nativeElement.querySelector('button')!.click();
49-
50-
expect(mockCopy).toHaveBeenCalledWith(COPY_CONTENT);
49+
expect(clipboard.copy).toHaveBeenCalledWith(COPY_CONTENT);
5150
});
5251

5352
it('emits copied event true when copy succeeds', fakeAsync(() => {
54-
mockCopy.and.returnValue(true);
55-
fixture.nativeElement.querySelector('button')!.click();
53+
spyOn(clipboard, 'copy').and.returnValue(true);
54+
fixture.nativeElement.querySelector('button')!.click();
5655

57-
expect(copiedOutput).toHaveBeenCalledWith(true);
58-
}));
56+
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
57+
}));
5958

6059
it('emits copied event false when copy fails', fakeAsync(() => {
61-
mockCopy.and.returnValue(false);
62-
fixture.nativeElement.querySelector('button')!.click();
63-
tick();
60+
spyOn(clipboard, 'copy').and.returnValue(false);
61+
fixture.nativeElement.querySelector('button')!.click();
62+
tick();
63+
64+
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
65+
}));
66+
67+
it('should be able to attempt multiple times before succeeding', fakeAsync(() => {
68+
const maxAttempts = 3;
69+
let attempts = 0;
70+
spyOn(clipboard, 'beginCopy').and.returnValue({
71+
copy: () => ++attempts >= maxAttempts,
72+
destroy: () => {}
73+
} as PendingCopy);
74+
fixture.componentInstance.attempts = maxAttempts;
75+
fixture.detectChanges();
76+
77+
fixture.nativeElement.querySelector('button')!.click();
78+
fixture.detectChanges();
79+
tick();
80+
81+
expect(attempts).toBe(maxAttempts);
82+
expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
83+
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
84+
}));
85+
86+
it('should be able to attempt multiple times before failing', fakeAsync(() => {
87+
const maxAttempts = 3;
88+
let attempts = 0;
89+
spyOn(clipboard, 'beginCopy').and.returnValue({
90+
copy: () => {
91+
attempts++;
92+
return false;
93+
},
94+
destroy: () => {}
95+
} as PendingCopy);
96+
fixture.componentInstance.attempts = maxAttempts;
97+
fixture.detectChanges();
6498

65-
expect(copiedOutput).toHaveBeenCalledWith(false);
66-
}));
99+
fixture.nativeElement.querySelector('button')!.click();
100+
fixture.detectChanges();
101+
tick();
102+
103+
expect(attempts).toBe(maxAttempts);
104+
expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
105+
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
106+
}));
67107
});

src/cdk/clipboard/copy-to-clipboard.ts

+61-5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,28 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, EventEmitter, Input, Output} from '@angular/core';
10-
9+
import {
10+
Directive,
11+
EventEmitter,
12+
Input,
13+
Output,
14+
NgZone,
15+
InjectionToken,
16+
Inject,
17+
Optional,
18+
} from '@angular/core';
1119
import {Clipboard} from './clipboard';
1220

21+
/** Object that can be used to configure the default options for `CdkCopyToClipboard`. */
22+
export interface CdkCopyToClipboardConfig {
23+
/** Default number of attempts to make when copying text to the clipboard. */
24+
attempts?: number;
25+
}
26+
27+
/** Injection token that can be used to provide the default options to `CdkCopyToClipboard`. */
28+
export const CKD_COPY_TO_CLIPBOARD_CONFIG =
29+
new InjectionToken<CdkCopyToClipboardConfig>('CKD_COPY_TO_CLIPBOARD_CONFIG');
30+
1331
/**
1432
* Provides behavior for a button that when clicked copies content into user's
1533
* clipboard.
@@ -24,6 +42,12 @@ export class CdkCopyToClipboard {
2442
/** Content to be copied. */
2543
@Input('cdkCopyToClipboard') text: string = '';
2644

45+
/**
46+
* How many times to attempt to copy the text. This may be necessary for longer text, because
47+
* the browser needs time to fill an intermediate textarea element and copy the content.
48+
*/
49+
@Input('cdkCopyToClipboardAttempts') attempts: number = 1;
50+
2751
/**
2852
* Emits when some text is copied to the clipboard. The
2953
* emitted value indicates whether copying was successful.
@@ -38,10 +62,42 @@ export class CdkCopyToClipboard {
3862
*/
3963
@Output('copied') _deprecatedCopied = this.copied;
4064

41-
constructor(private readonly _clipboard: Clipboard) {}
65+
constructor(
66+
private _clipboard: Clipboard,
67+
/**
68+
* @deprecated _ngZone parameter to become required.
69+
* @breaking-change 10.0.0
70+
*/
71+
private _ngZone?: NgZone,
72+
@Optional() @Inject(CKD_COPY_TO_CLIPBOARD_CONFIG) config?: CdkCopyToClipboardConfig) {
73+
74+
if (config && config.attempts != null) {
75+
this.attempts = config.attempts;
76+
}
77+
}
4278

4379
/** Copies the current text to the clipboard. */
44-
copy() {
45-
this.copied.emit(this._clipboard.copy(this.text));
80+
copy(attempts: number = this.attempts): void {
81+
if (attempts > 1) {
82+
let remainingAttempts = attempts;
83+
const pending = this._clipboard.beginCopy(this.text);
84+
const attempt = () => {
85+
const successful = pending.copy();
86+
if (!successful && --remainingAttempts) {
87+
// @breaking-change 10.0.0 Remove null check for `_ngZone`.
88+
if (this._ngZone) {
89+
this._ngZone.runOutsideAngular(() => setTimeout(attempt));
90+
} else {
91+
setTimeout(attempt);
92+
}
93+
} else {
94+
pending.destroy();
95+
this.copied.emit(successful);
96+
}
97+
};
98+
attempt();
99+
} else {
100+
this.copied.emit(this._clipboard.copy(this.text));
101+
}
46102
}
47103
}

tools/public_api_guard/cdk/clipboard.d.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
export declare class CdkCopyToClipboard {
22
_deprecatedCopied: EventEmitter<boolean>;
3+
attempts: number;
34
copied: EventEmitter<boolean>;
45
text: string;
5-
constructor(_clipboard: Clipboard);
6-
copy(): void;
7-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkCopyToClipboard, "[cdkCopyToClipboard]", never, { 'text': "cdkCopyToClipboard" }, { 'copied': "cdkCopyToClipboardCopied", '_deprecatedCopied': "copied" }, never>;
6+
constructor(_clipboard: Clipboard,
7+
_ngZone?: NgZone | undefined, config?: CdkCopyToClipboardConfig);
8+
copy(attempts?: number): void;
9+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkCopyToClipboard, "[cdkCopyToClipboard]", never, { 'text': "cdkCopyToClipboard", 'attempts': "cdkCopyToClipboardAttempts" }, { 'copied': "cdkCopyToClipboardCopied", '_deprecatedCopied': "copied" }, never>;
810
static ɵfac: i0.ɵɵFactoryDef<CdkCopyToClipboard>;
911
}
1012

13+
export interface CdkCopyToClipboardConfig {
14+
attempts?: number;
15+
}
16+
17+
export declare const CKD_COPY_TO_CLIPBOARD_CONFIG: InjectionToken<CdkCopyToClipboardConfig>;
18+
1119
export declare class Clipboard {
1220
constructor(document: any);
1321
beginCopy(text: string): PendingCopy;

0 commit comments

Comments
 (0)