Skip to content

Commit

Permalink
fix(material/schematics): support standalone projects in navigation s…
Browse files Browse the repository at this point in the history
…chematic

Updates the `ng generate navigation` schematic to support standalone projects.

(cherry picked from commit 8897b64)
  • Loading branch information
crisbeto committed Apr 25, 2023
1 parent 313348e commit 186c874
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 21 deletions.
@@ -1,11 +1,10 @@
import { LayoutModule } from '@angular/cdk/layout';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';<% if(!standalone) { %>
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatToolbarModule } from '@angular/material/toolbar';<% } %>

import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';

Expand All @@ -14,17 +13,17 @@ describe('<%= classify(name) %>Component', () => {
let fixture: ComponentFixture<<%= classify(name) %>Component>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
TestBed.configureTestingModule({<% if(standalone) { %>
imports: [NoopAnimationsModule]<% } else { %>
declarations: [<%= classify(name) %>Component],
imports: [
NoopAnimationsModule,
LayoutModule,
MatButtonModule,
MatIconModule,
MatListModule,
MatSidenavModule,
MatToolbarModule,
]
]<% } %>
}).compileComponents();
}));

Expand Down
@@ -1,5 +1,10 @@
import { Component<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Component, inject<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';<% if(standalone) { %>
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';<% } %>
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';

Expand All @@ -14,16 +19,22 @@ import { map, shareReplay } from 'rxjs/operators';
`]<% } else { %>
styleUrls: ['./<%= dasherize(name) %>.component.<%= style %>']<% } %><% if(!!viewEncapsulation) { %>,
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %><% if(standalone) { %>,
standalone: true,
imports: [
MatToolbarModule,
MatButtonModule,
MatSidenavModule,
MatListModule,
MatIconModule
]<% } %>
})
export class <%= classify(name) %>Component {
private breakpointObserver = inject(BreakpointObserver);

isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches),
shareReplay()
);

constructor(private breakpointObserver: BreakpointObserver) {}

}
40 changes: 38 additions & 2 deletions src/material/schematics/ng-generate/navigation/index.spec.ts
Expand Up @@ -18,13 +18,11 @@ describe('material-navigation-schematic', () => {

function expectNavigationSchematicModuleImports(tree: UnitTestTree) {
const moduleContent = getFileContent(tree, '/projects/material/src/app/app.module.ts');
expect(moduleContent).toMatch(/LayoutModule,\s+/);
expect(moduleContent).toMatch(/MatToolbarModule,\s+/);
expect(moduleContent).toMatch(/MatButtonModule,\s+/);
expect(moduleContent).toMatch(/MatSidenavModule,\s+/);
expect(moduleContent).toMatch(/MatIconModule,\s+/);
expect(moduleContent).toMatch(/MatListModule\s+],/);
expect(moduleContent).toContain(`import { LayoutModule } from '@angular/cdk/layout';`);
expect(moduleContent).toContain(`import { MatButtonModule } from '@angular/material/button';`);
expect(moduleContent).toContain(`import { MatIconModule } from '@angular/material/icon';`);
expect(moduleContent).toContain(`import { MatListModule } from '@angular/material/list';`);
Expand Down Expand Up @@ -71,6 +69,44 @@ describe('material-navigation-schematic', () => {
).toBeRejectedWithError(/required property 'name'/);
});

describe('standalone option', () => {
it('should generate a standalone component', async () => {
const app = await createTestApp(runner);
const tree = await runner.runSchematic('navigation', {...baseOptions, standalone: true}, app);
const module = getFileContent(tree, '/projects/material/src/app/app.module.ts');
const component = getFileContent(tree, '/projects/material/src/app/foo/foo.component.ts');
const requiredModules = [
'MatToolbarModule',
'MatButtonModule',
'MatSidenavModule',
'MatListModule',
'MatIconModule',
];

requiredModules.forEach(name => {
expect(module).withContext('Module should not import dependencies').not.toContain(name);
expect(component).withContext('Component should import dependencies').toContain(name);
});

expect(module).not.toContain('FooComponent');
expect(component).toContain('standalone: true');
expect(component).toContain('imports: [');
});

it('should infer the standalone option from the project structure', async () => {
const app = await createTestApp(runner, {standalone: true});
const tree = await runner.runSchematic('navigation', baseOptions, app);
const componentContent = getFileContent(
tree,
'/projects/material/src/app/foo/foo.component.ts',
);

expect(tree.exists('/projects/material/src/app/app.module.ts')).toBe(false);
expect(componentContent).toContain('standalone: true');
expect(componentContent).toContain('imports: [');
});
});

describe('style option', () => {
it('should respect the option value', async () => {
const tree = await runner.runSchematic(
Expand Down
18 changes: 11 additions & 7 deletions src/material/schematics/ng-generate/navigation/index.ts
Expand Up @@ -11,6 +11,7 @@ import {
addModuleImportToModule,
buildComponent,
findModuleFromOptions,
isStandaloneSchematic,
} from '@angular/cdk/schematics';
import {Schema} from './schema';

Expand Down Expand Up @@ -38,12 +39,15 @@ export default function (options: Schema): Rule {
*/
function addNavModulesToModule(options: Schema) {
return async (host: Tree) => {
const modulePath = (await findModuleFromOptions(host, options))!;
addModuleImportToModule(host, modulePath, 'LayoutModule', '@angular/cdk/layout');
addModuleImportToModule(host, modulePath, 'MatToolbarModule', '@angular/material/toolbar');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material/button');
addModuleImportToModule(host, modulePath, 'MatSidenavModule', '@angular/material/sidenav');
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material/icon');
addModuleImportToModule(host, modulePath, 'MatListModule', '@angular/material/list');
const isStandalone = await isStandaloneSchematic(host, options);

if (!isStandalone) {
const modulePath = (await findModuleFromOptions(host, options))!;
addModuleImportToModule(host, modulePath, 'MatToolbarModule', '@angular/material/toolbar');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material/button');
addModuleImportToModule(host, modulePath, 'MatSidenavModule', '@angular/material/sidenav');
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material/icon');
addModuleImportToModule(host, modulePath, 'MatListModule', '@angular/material/list');
}
};
}
4 changes: 4 additions & 0 deletions src/material/schematics/ng-generate/navigation/schema.json
Expand Up @@ -39,6 +39,10 @@
"type": "boolean",
"alias": "t"
},
"standalone": {
"description": "Whether the generated component is standalone.",
"type": "boolean"
},
"viewEncapsulation": {
"description": "Specifies the view encapsulation strategy.",
"enum": ["Emulated", "None"],
Expand Down

0 comments on commit 186c874

Please sign in to comment.