Skip to content

feat(clipboard): add the ability to specify number of attempts in clipboard directive #17547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/cdk/clipboard/clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ class HeroProfile {
}
}
```

If you're using the `cdkCopyToClipboard` you can pass in the `cdkCopyToClipboardAttempts` input
to automatically attempt to copy some text a certain number of times.

```html
<button [cdkCopyToClipboard]="longText" [cdkCopyToClipboardAttempts]="5">Copy text</button>
```
82 changes: 61 additions & 21 deletions src/cdk/clipboard/copy-to-clipboard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Component} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';

import {Clipboard} from './clipboard';
import {ClipboardModule} from './clipboard-module';
import {PendingCopy} from './pending-copy';

const COPY_CONTENT = 'copy content';

Expand All @@ -11,17 +12,18 @@ const COPY_CONTENT = 'copy content';
template: `
<button
[cdkCopyToClipboard]="content"
(cdkCopyToClipboardCopied)="copied.emit($event)"></button>`,
[cdkCopyToClipboardAttempts]="attempts"
(cdkCopyToClipboardCopied)="copied($event)"></button>`,
})
class CopyToClipboardHost {
@Input() content = '';
@Output() copied = new EventEmitter<boolean>();
content = '';
attempts = 1;
copied = jasmine.createSpy('copied spy');
}

describe('CdkCopyToClipboard', () => {
let fixture: ComponentFixture<CopyToClipboardHost>;
let mockCopy: jasmine.Spy;
let copiedOutput: jasmine.Spy;
let clipboard: Clipboard;

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

const host = fixture.componentInstance;
host.content = COPY_CONTENT;
copiedOutput = jasmine.createSpy('copied');
host.copied.subscribe(copiedOutput);
mockCopy = spyOn(TestBed.get(Clipboard), 'copy');

clipboard = TestBed.get(Clipboard);
fixture.detectChanges();
});

it('copies content to clipboard upon click', () => {
spyOn(clipboard, 'copy');
fixture.nativeElement.querySelector('button')!.click();

expect(mockCopy).toHaveBeenCalledWith(COPY_CONTENT);
expect(clipboard.copy).toHaveBeenCalledWith(COPY_CONTENT);
});

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

expect(copiedOutput).toHaveBeenCalledWith(true);
}));
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
}));

it('emits copied event false when copy fails', fakeAsync(() => {
mockCopy.and.returnValue(false);
fixture.nativeElement.querySelector('button')!.click();
tick();
spyOn(clipboard, 'copy').and.returnValue(false);
fixture.nativeElement.querySelector('button')!.click();
tick();

expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
}));

it('should be able to attempt multiple times before succeeding', fakeAsync(() => {
const maxAttempts = 3;
let attempts = 0;
spyOn(clipboard, 'beginCopy').and.returnValue({
copy: () => ++attempts >= maxAttempts,
destroy: () => {}
} as PendingCopy);
fixture.componentInstance.attempts = maxAttempts;
fixture.detectChanges();

fixture.nativeElement.querySelector('button')!.click();
fixture.detectChanges();
tick();

expect(attempts).toBe(maxAttempts);
expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
}));

it('should be able to attempt multiple times before failing', fakeAsync(() => {
const maxAttempts = 3;
let attempts = 0;
spyOn(clipboard, 'beginCopy').and.returnValue({
copy: () => {
attempts++;
return false;
},
destroy: () => {}
} as PendingCopy);
fixture.componentInstance.attempts = maxAttempts;
fixture.detectChanges();

expect(copiedOutput).toHaveBeenCalledWith(false);
}));
fixture.nativeElement.querySelector('button')!.click();
fixture.detectChanges();
tick();

expect(attempts).toBe(maxAttempts);
expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
}));
});
66 changes: 61 additions & 5 deletions src/cdk/clipboard/copy-to-clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, EventEmitter, Input, Output} from '@angular/core';

import {
Directive,
EventEmitter,
Input,
Output,
NgZone,
InjectionToken,
Inject,
Optional,
} from '@angular/core';
import {Clipboard} from './clipboard';

/** Object that can be used to configure the default options for `CdkCopyToClipboard`. */
export interface CdkCopyToClipboardConfig {
/** Default number of attempts to make when copying text to the clipboard. */
attempts?: number;
}

/** Injection token that can be used to provide the default options to `CdkCopyToClipboard`. */
export const CKD_COPY_TO_CLIPBOARD_CONFIG =
new InjectionToken<CdkCopyToClipboardConfig>('CKD_COPY_TO_CLIPBOARD_CONFIG');

/**
* Provides behavior for a button that when clicked copies content into user's
* clipboard.
Expand All @@ -24,6 +42,12 @@ export class CdkCopyToClipboard {
/** Content to be copied. */
@Input('cdkCopyToClipboard') text: string = '';

/**
* How many times to attempt to copy the text. This may be necessary for longer text, because
* the browser needs time to fill an intermediate textarea element and copy the content.
*/
@Input('cdkCopyToClipboardAttempts') attempts: number = 1;

/**
* Emits when some text is copied to the clipboard. The
* emitted value indicates whether copying was successful.
Expand All @@ -38,10 +62,42 @@ export class CdkCopyToClipboard {
*/
@Output('copied') _deprecatedCopied = this.copied;

constructor(private readonly _clipboard: Clipboard) {}
constructor(
private _clipboard: Clipboard,
/**
* @deprecated _ngZone parameter to become required.
* @breaking-change 10.0.0
*/
private _ngZone?: NgZone,
@Optional() @Inject(CKD_COPY_TO_CLIPBOARD_CONFIG) config?: CdkCopyToClipboardConfig) {

if (config && config.attempts != null) {
this.attempts = config.attempts;
}
}

/** Copies the current text to the clipboard. */
copy() {
this.copied.emit(this._clipboard.copy(this.text));
copy(attempts: number = this.attempts): void {
if (attempts > 1) {
let remainingAttempts = attempts;
const pending = this._clipboard.beginCopy(this.text);
const attempt = () => {
const successful = pending.copy();
if (!successful && --remainingAttempts) {
// @breaking-change 10.0.0 Remove null check for `_ngZone`.
if (this._ngZone) {
this._ngZone.runOutsideAngular(() => setTimeout(attempt));
} else {
setTimeout(attempt);
}
} else {
pending.destroy();
this.copied.emit(successful);
}
};
attempt();
} else {
this.copied.emit(this._clipboard.copy(this.text));
}
}
}
14 changes: 11 additions & 3 deletions tools/public_api_guard/cdk/clipboard.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
export declare class CdkCopyToClipboard {
_deprecatedCopied: EventEmitter<boolean>;
attempts: number;
copied: EventEmitter<boolean>;
text: string;
constructor(_clipboard: Clipboard);
copy(): void;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkCopyToClipboard, "[cdkCopyToClipboard]", never, { 'text': "cdkCopyToClipboard" }, { 'copied': "cdkCopyToClipboardCopied", '_deprecatedCopied': "copied" }, never>;
constructor(_clipboard: Clipboard,
_ngZone?: NgZone | undefined, config?: CdkCopyToClipboardConfig);
copy(attempts?: number): void;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkCopyToClipboard, "[cdkCopyToClipboard]", never, { 'text': "cdkCopyToClipboard", 'attempts': "cdkCopyToClipboardAttempts" }, { 'copied': "cdkCopyToClipboardCopied", '_deprecatedCopied': "copied" }, never>;
static ɵfac: i0.ɵɵFactoryDef<CdkCopyToClipboard>;
}

export interface CdkCopyToClipboardConfig {
attempts?: number;
}

export declare const CKD_COPY_TO_CLIPBOARD_CONFIG: InjectionToken<CdkCopyToClipboardConfig>;

export declare class Clipboard {
constructor(document: any);
beginCopy(text: string): PendingCopy;
Expand Down