Skip to content
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

feat(material-angular-io): allow module imports to be copied from API tab #30389

Merged
merged 1 commit into from
Feb 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
import {HeaderLink} from './header-link';
import {CodeSnippet} from '../example-viewer/code-snippet';
import {DeprecatedFieldComponent} from './deprecated-tooltip';
import {ModuleImportCopyButton} from './module-import-copy-button';

// ExampleViewer is included in the DocViewerModule because they have a circular dependency.
@NgModule({
Expand All @@ -25,7 +26,8 @@ import {DeprecatedFieldComponent} from './deprecated-tooltip';
HeaderLink,
CodeSnippet,
DeprecatedFieldComponent,
ModuleImportCopyButton,
],
exports: [DocViewer, ExampleViewer, HeaderLink, DeprecatedFieldComponent],
exports: [DocViewer, ExampleViewer, HeaderLink, DeprecatedFieldComponent, ModuleImportCopyButton],
})
export class DocViewerModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import {DocViewer} from './doc-viewer';
import {DocViewerModule} from './doc-viewer-module';
import {ExampleViewer} from '../example-viewer/example-viewer';
import {MatTooltip} from '@angular/material/tooltip';
import {MatIconButton} from '@angular/material/button';
import {Clipboard} from '@angular/cdk/clipboard';

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

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [DocViewerModule, DocsAppTestingModule, DocViewerTestComponent],
providers: [{provide: Clipboard, useValue: clipboardSpy}],
}).compileComponents();
}));

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

it('should show copy icon button for module imports', () => {
const fixture = TestBed.createComponent(DocViewerTestComponent);
fixture.componentInstance.documentUrl = `http://material.angular.io/copy-module-import.html`;
fixture.detectChanges();

const url = fixture.componentInstance.documentUrl;
http.expectOne(url).flush(FAKE_DOCS[url]);

const docViewer = fixture.debugElement.query(By.directive(DocViewer));
expect(docViewer).not.toBeNull();

const iconButton = fixture.debugElement.query(By.directive(MatIconButton));
// icon button for copying module import should exist
expect(iconButton).toBeTruthy();

// click on icon button to trigger copying the module import
iconButton.nativeNode.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(clipboardSpy.copy).toHaveBeenCalled();
});

// TODO(mmalerba): Add test that example-viewer is instantiated.
});

Expand Down Expand Up @@ -221,6 +246,17 @@ const FAKE_DOCS: {[key: string]: string} = {

<div class="docs-api-deprecated-marker"
deprecated-message="deprecated">Deprecated</div>`,
'http://material.angular.io/copy-module-import.html': `<div class="docs-api-module">
<p class="docs-api-module-import">
<code>
import {MatIconModule} from '@angular/material/icon';
</code>
</p>

<div class="docs-api-module-import-button"
data-docs-api-module-import-button="import {MatIconModule} from '@angular/material/icon';">
</div>
</div>`,
/* eslint-enable @typescript-eslint/naming-convention */
};

Expand Down
35 changes: 35 additions & 0 deletions material.angular.io/src/app/shared/doc-viewer/doc-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {shareReplay, take, tap} from 'rxjs/operators';
import {ExampleViewer} from '../example-viewer/example-viewer';
import {HeaderLink} from './header-link';
import {DeprecatedFieldComponent} from './deprecated-tooltip';
import {ModuleImportCopyButton} from './module-import-copy-button';

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

// Create icon buttons to copy module import
this._createCopyIconForModule();

// Resolving and creating components dynamically in Angular happens synchronously, but since
// we want to emit the output if the components are actually rendered completely, we wait
// until the Angular zone becomes stable.
Expand Down Expand Up @@ -235,4 +239,35 @@ export class DocViewer implements OnDestroy {
this._portalHosts.push(elementPortalOutlet);
});
}

_createCopyIconForModule() {
// every module import element will be marked with docs-api-module-import-button attribute
const moduleImportElements = this._elementRef.nativeElement.querySelectorAll(
'[data-docs-api-module-import-button]',
);

[...moduleImportElements].forEach((element: HTMLElement) => {
// get the module import path stored in the attribute
const moduleImport = element.getAttribute('data-docs-api-module-import-button');

const elementPortalOutlet = new DomPortalOutlet(
element,
this._componentFactoryResolver,
this._appRef,
this._injector,
);

const moduleImportPortal = new ComponentPortal(
ModuleImportCopyButton,
this._viewContainerRef,
);
const moduleImportOutlet = elementPortalOutlet.attach(moduleImportPortal);

if (moduleImport) {
moduleImportOutlet.instance.import = moduleImport;
}

this._portalHosts.push(elementPortalOutlet);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Component, inject} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatTooltip} from '@angular/material/tooltip';
import {Clipboard} from '@angular/cdk/clipboard';

/**
* Shows up an icon button which will allow users to copy the module import
*/
@Component({
selector: 'module-import-copy-button',
template: `<button
mat-icon-button
matTooltip="Copy import to the clipboard"
(click)="copy()">
<mat-icon>content_copy</mat-icon>
</button>`,
standalone: true,
imports: [MatIconButton, MatIcon, MatTooltip],
})
export class ModuleImportCopyButton {
private clipboard = inject(Clipboard);
private snackbar = inject(MatSnackBar);
/** Import path for the module that will be copied */
import = '';

copy(): void {
const message = this.clipboard.copy(this.import)
? 'Copied module import'
: 'Failed to copy module import';
this.snackbar.open(message, undefined, {duration: 2500});
}
}
9 changes: 9 additions & 0 deletions material.angular.io/src/styles/_api.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
word-break: break-word;
}

.docs-api-module {
display: flex;
align-items: center;
}

.docs-api-module-import {
margin: 0px;
}

.docs-api-class-name,
.docs-api-module-import,
.docs-api-class-selector-name,
Expand Down
14 changes: 9 additions & 5 deletions tools/dgeni/templates/entry-point.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ <h2>
</h2>

{%- if doc.primaryExportName -%}
<p class="docs-api-module-import">
<code>
import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';
</code>
</p>
<div class="docs-api-module">
<p class="docs-api-module-import">
<code>
import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';
</code>
</p>

<div class="docs-api-module-import-button" data-docs-api-module-import-button="import {{$ doc.primaryExportName $}} from '{$ doc.moduleImportPath $}';"></div>
</div>
{% else %}
<p>Import symbols from <code>{$ doc.moduleImportPath $}</code></p>
{%- endif -%}
Expand Down
Loading