Skip to content

Commit

Permalink
feat(clipboard): add the ability to specify number of attempts in cli…
Browse files Browse the repository at this point in the history
…pboard directive

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.
  • Loading branch information
crisbeto committed Nov 12, 2019
1 parent 320f387 commit e158d47
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 29 deletions.
7 changes: 7 additions & 0 deletions src/cdk/clipboard/clipboard.md
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
@@ -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
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
@@ -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

0 comments on commit e158d47

Please sign in to comment.