Skip to content

Commit fe441bd

Browse files
authoredFeb 3, 2025··
feat(material-angular-io): allow module imports to be copied from API tab (#30389)
adds icon button next to module import text allowing users to copy it directly fixes #16127
1 parent f9ca76c commit fe441bd

File tree

6 files changed

+126
-6
lines changed

6 files changed

+126
-6
lines changed
 

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer-module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
1010
import {HeaderLink} from './header-link';
1111
import {CodeSnippet} from '../example-viewer/code-snippet';
1212
import {DeprecatedFieldComponent} from './deprecated-tooltip';
13+
import {ModuleImportCopyButton} from './module-import-copy-button';
1314

1415
// ExampleViewer is included in the DocViewerModule because they have a circular dependency.
1516
@NgModule({
@@ -25,7 +26,8 @@ import {DeprecatedFieldComponent} from './deprecated-tooltip';
2526
HeaderLink,
2627
CodeSnippet,
2728
DeprecatedFieldComponent,
29+
ModuleImportCopyButton,
2830
],
29-
exports: [DocViewer, ExampleViewer, HeaderLink, DeprecatedFieldComponent],
31+
exports: [DocViewer, ExampleViewer, HeaderLink, DeprecatedFieldComponent, ModuleImportCopyButton],
3032
})
3133
export class DocViewerModule {}

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer.spec.ts

+36
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import {DocViewer} from './doc-viewer';
77
import {DocViewerModule} from './doc-viewer-module';
88
import {ExampleViewer} from '../example-viewer/example-viewer';
99
import {MatTooltip} from '@angular/material/tooltip';
10+
import {MatIconButton} from '@angular/material/button';
11+
import {Clipboard} from '@angular/cdk/clipboard';
1012

1113
describe('DocViewer', () => {
1214
let http: HttpTestingController;
15+
const clipboardSpy = jasmine.createSpyObj<Clipboard>('Clipboard', ['copy']);
1316

1417
beforeEach(waitForAsync(() => {
1518
TestBed.configureTestingModule({
1619
imports: [DocViewerModule, DocsAppTestingModule, DocViewerTestComponent],
20+
providers: [{provide: Clipboard, useValue: clipboardSpy}],
1721
}).compileComponents();
1822
}));
1923

@@ -179,6 +183,27 @@ describe('DocViewer', () => {
179183
expect(deprecatedSymbol.query(By.directive(MatTooltip))).toBeTruthy();
180184
});
181185

186+
it('should show copy icon button for module imports', () => {
187+
const fixture = TestBed.createComponent(DocViewerTestComponent);
188+
fixture.componentInstance.documentUrl = `http://material.angular.io/copy-module-import.html`;
189+
fixture.detectChanges();
190+
191+
const url = fixture.componentInstance.documentUrl;
192+
http.expectOne(url).flush(FAKE_DOCS[url]);
193+
194+
const docViewer = fixture.debugElement.query(By.directive(DocViewer));
195+
expect(docViewer).not.toBeNull();
196+
197+
const iconButton = fixture.debugElement.query(By.directive(MatIconButton));
198+
// icon button for copying module import should exist
199+
expect(iconButton).toBeTruthy();
200+
201+
// click on icon button to trigger copying the module import
202+
iconButton.nativeNode.dispatchEvent(new MouseEvent('click'));
203+
fixture.detectChanges();
204+
expect(clipboardSpy.copy).toHaveBeenCalled();
205+
});
206+
182207
// TODO(mmalerba): Add test that example-viewer is instantiated.
183208
});
184209

@@ -221,6 +246,17 @@ const FAKE_DOCS: {[key: string]: string} = {
221246
222247
<div class="docs-api-deprecated-marker"
223248
deprecated-message="deprecated">Deprecated</div>`,
249+
'http://material.angular.io/copy-module-import.html': `<div class="docs-api-module">
250+
<p class="docs-api-module-import">
251+
<code>
252+
import {MatIconModule} from '@angular/material/icon';
253+
</code>
254+
</p>
255+
256+
<div class="docs-api-module-import-button"
257+
data-docs-api-module-import-button="import {MatIconModule} from '@angular/material/icon';">
258+
</div>
259+
</div>`,
224260
/* eslint-enable @typescript-eslint/naming-convention */
225261
};
226262

‎material.angular.io/src/app/shared/doc-viewer/doc-viewer.ts

+35
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {shareReplay, take, tap} from 'rxjs/operators';
2828
import {ExampleViewer} from '../example-viewer/example-viewer';
2929
import {HeaderLink} from './header-link';
3030
import {DeprecatedFieldComponent} from './deprecated-tooltip';
31+
import {ModuleImportCopyButton} from './module-import-copy-button';
3132

3233
@Injectable({providedIn: 'root'})
3334
class DocFetcher {
@@ -150,6 +151,9 @@ export class DocViewer implements OnDestroy {
150151
// Create tooltips for the deprecated fields
151152
this._createTooltipsForDeprecated();
152153

154+
// Create icon buttons to copy module import
155+
this._createCopyIconForModule();
156+
153157
// Resolving and creating components dynamically in Angular happens synchronously, but since
154158
// we want to emit the output if the components are actually rendered completely, we wait
155159
// until the Angular zone becomes stable.
@@ -235,4 +239,35 @@ export class DocViewer implements OnDestroy {
235239
this._portalHosts.push(elementPortalOutlet);
236240
});
237241
}
242+
243+
_createCopyIconForModule() {
244+
// every module import element will be marked with docs-api-module-import-button attribute
245+
const moduleImportElements = this._elementRef.nativeElement.querySelectorAll(
246+
'[data-docs-api-module-import-button]',
247+
);
248+
249+
[...moduleImportElements].forEach((element: HTMLElement) => {
250+
// get the module import path stored in the attribute
251+
const moduleImport = element.getAttribute('data-docs-api-module-import-button');
252+
253+
const elementPortalOutlet = new DomPortalOutlet(
254+
element,
255+
this._componentFactoryResolver,
256+
this._appRef,
257+
this._injector,
258+
);
259+
260+
const moduleImportPortal = new ComponentPortal(
261+
ModuleImportCopyButton,
262+
this._viewContainerRef,
263+
);
264+
const moduleImportOutlet = elementPortalOutlet.attach(moduleImportPortal);
265+
266+
if (moduleImport) {
267+
moduleImportOutlet.instance.import = moduleImport;
268+
}
269+
270+
this._portalHosts.push(elementPortalOutlet);
271+
});
272+
}
238273
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {Component, inject} from '@angular/core';
2+
import {MatIconButton} from '@angular/material/button';
3+
import {MatIcon} from '@angular/material/icon';
4+
import {MatSnackBar} from '@angular/material/snack-bar';
5+
import {MatTooltip} from '@angular/material/tooltip';
6+
import {Clipboard} from '@angular/cdk/clipboard';
7+
8+
/**
9+
* Shows up an icon button which will allow users to copy the module import
10+
*/
11+
@Component({
12+
selector: 'module-import-copy-button',
13+
template: `<button
14+
mat-icon-button
15+
matTooltip="Copy import to the clipboard"
16+
(click)="copy()">
17+
<mat-icon>content_copy</mat-icon>
18+
</button>`,
19+
standalone: true,
20+
imports: [MatIconButton, MatIcon, MatTooltip],
21+
})
22+
export class ModuleImportCopyButton {
23+
private clipboard = inject(Clipboard);
24+
private snackbar = inject(MatSnackBar);
25+
/** Import path for the module that will be copied */
26+
import = '';
27+
28+
copy(): void {
29+
const message = this.clipboard.copy(this.import)
30+
? 'Copied module import'
31+
: 'Failed to copy module import';
32+
this.snackbar.open(message, undefined, {duration: 2500});
33+
}
34+
}

‎material.angular.io/src/styles/_api.scss

+9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
word-break: break-word;
2828
}
2929

30+
.docs-api-module {
31+
display: flex;
32+
align-items: center;
33+
}
34+
35+
.docs-api-module-import {
36+
margin: 0px;
37+
}
38+
3039
.docs-api-class-name,
3140
.docs-api-module-import,
3241
.docs-api-class-selector-name,

‎tools/dgeni/templates/entry-point.template.html

+9-5
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ <h2>
3737
</h2>
3838

3939
{%- if doc.primaryExportName -%}
40-
<p class="docs-api-module-import">
41-
<code>
42-
import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';
43-
</code>
44-
</p>
40+
<div class="docs-api-module">
41+
<p class="docs-api-module-import">
42+
<code>
43+
import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';
44+
</code>
45+
</p>
46+
47+
<div class="docs-api-module-import-button" data-docs-api-module-import-button="import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';"></div>
48+
</div>
4549
{% else %}
4650
<p>Import symbols from <code>{$ doc.moduleImportPath $}</code></p>
4751
{%- endif -%}

0 commit comments

Comments
 (0)
Please sign in to comment.