Skip to content

Commit c08050d

Browse files
crisbetommalerba
authored andcommitted
feat(schematics): add prompt to add global typography class (#17602)
Adds a prompts to the `ng-add` schematic that allows the user to set the global `mat-typography` class on the `body`.
1 parent e22407d commit c08050d

File tree

7 files changed

+136
-4
lines changed

7 files changed

+136
-4
lines changed

src/cdk/schematics/utils/html-head-element.ts renamed to src/cdk/schematics/utils/html-manipulation.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function appendHtmlElementToHead(host: Tree, htmlFilePath: string, elemen
2727
const headTag = getHtmlHeadTagElement(htmlContent);
2828

2929
if (!headTag) {
30-
throw `Could not find '<head>' element in HTML file: ${htmlFileBuffer}`;
30+
throw Error(`Could not find '<head>' element in HTML file: ${htmlFileBuffer}`);
3131
}
3232

3333
// We always have access to the source code location here because the `getHeadTagElement`
@@ -45,13 +45,54 @@ export function appendHtmlElementToHead(host: Tree, htmlFilePath: string, elemen
4545

4646
/** Parses the given HTML file and returns the head element if available. */
4747
export function getHtmlHeadTagElement(htmlContent: string): DefaultTreeElement | null {
48+
return getElementByTagName('head', htmlContent);
49+
}
50+
51+
/** Adds a class to the body of the document. */
52+
export function addBodyClass(host: Tree, htmlFilePath: string, className: string): void {
53+
const htmlFileBuffer = host.read(htmlFilePath);
54+
55+
if (!htmlFileBuffer) {
56+
throw new SchematicsException(`Could not read file for path: ${htmlFilePath}`);
57+
}
58+
59+
const htmlContent = htmlFileBuffer.toString();
60+
const body = getElementByTagName('body', htmlContent);
61+
62+
if (!body) {
63+
throw Error(`Could not find <body> element in HTML file: ${htmlFileBuffer}`);
64+
}
65+
66+
const classAttribute = body.attrs.find(attribute => attribute.name === 'class');
67+
68+
if (classAttribute) {
69+
const hasClass = classAttribute.value.split(' ').map(part => part.trim()).includes(className);
70+
71+
if (!hasClass) {
72+
const classAttributeLocation = body.sourceCodeLocation!.attrs.class;
73+
const recordedChange = host
74+
.beginUpdate(htmlFilePath)
75+
.insertRight(classAttributeLocation.endOffset - 1, ` ${className}`);
76+
host.commitUpdate(recordedChange);
77+
}
78+
} else {
79+
const recordedChange = host
80+
.beginUpdate(htmlFilePath)
81+
.insertRight(body.sourceCodeLocation!.startTag.endOffset - 1, ` class="${className}"`);
82+
host.commitUpdate(recordedChange);
83+
}
84+
}
85+
86+
/** Finds an element by its tag name. */
87+
function getElementByTagName(tagName: string, htmlContent: string):
88+
DefaultTreeElement | null {
4889
const document = parseHtml(htmlContent, {sourceCodeLocationInfo: true}) as DefaultTreeDocument;
4990
const nodeQueue = [...document.childNodes];
5091

5192
while (nodeQueue.length) {
5293
const node = nodeQueue.shift() as DefaultTreeElement;
5394

54-
if (node.nodeName.toLowerCase() === 'head') {
95+
if (node.nodeName.toLowerCase() === tagName) {
5596
return node;
5697
} else if (node.childNodes) {
5798
nodeQueue.push(...node.childNodes);

src/cdk/schematics/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export * from './ast';
1010
export * from './ast/ng-module-imports';
1111
export * from './build-component';
1212
export * from './get-project';
13-
export * from './html-head-element';
13+
export * from './html-manipulation';
1414
export * from './parse5-element';
1515
export * from './project-index-file';
1616
export * from './project-main-file';

src/material/schematics/ng-add/index.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,66 @@ describe('ng-add schematic', () => {
313313
.toBe('custom-theme', 'Expected the old custom theme content to be unchanged.');
314314
});
315315
});
316+
317+
it('should add the global typography class if the body has no classes', async () => {
318+
const tree = await runner.runSchematicAsync('ng-add-setup-project', {
319+
typography: true
320+
}, appTree).toPromise();
321+
const workspace = getWorkspace(tree);
322+
const project = getProjectFromWorkspace(workspace);
323+
324+
const indexFiles = getProjectIndexFiles(project);
325+
expect(indexFiles.length).toBe(1);
326+
327+
indexFiles.forEach(indexPath => {
328+
const buffer = tree.read(indexPath)!;
329+
expect(buffer.toString()).toContain('<body class="mat-typography">');
330+
});
331+
});
332+
333+
it('should add the global typography class if the body has existing classes', async () => {
334+
appTree.overwrite('projects/material/src/index.html', `
335+
<html>
336+
<head></head>
337+
<body class="one two"></body>
338+
</html>
339+
`);
340+
341+
const tree = await runner.runSchematicAsync('ng-add-setup-project', {
342+
typography: true
343+
}, appTree).toPromise();
344+
345+
const workspace = getWorkspace(tree);
346+
const project = getProjectFromWorkspace(workspace);
347+
const indexFiles = getProjectIndexFiles(project);
348+
expect(indexFiles.length).toBe(1);
349+
350+
indexFiles.forEach(indexPath => {
351+
const buffer = tree.read(indexPath)!;
352+
expect(buffer.toString()).toContain('<body class="one two mat-typography">');
353+
});
354+
});
355+
356+
it('should not add the global typography class if it exists already', async () => {
357+
appTree.overwrite('projects/material/src/index.html', `
358+
<html>
359+
<head></head>
360+
<body class="one mat-typography two"></body>
361+
</html>
362+
`);
363+
364+
const tree = await runner.runSchematicAsync('ng-add-setup-project', {
365+
typography: true
366+
}, appTree).toPromise();
367+
368+
const workspace = getWorkspace(tree);
369+
const project = getProjectFromWorkspace(workspace);
370+
const indexFiles = getProjectIndexFiles(project);
371+
expect(indexFiles.length).toBe(1);
372+
373+
indexFiles.forEach(indexPath => {
374+
const buffer = tree.read(indexPath)!;
375+
expect(buffer.toString()).toContain('<body class="one mat-typography two">');
376+
});
377+
});
316378
});

src/material/schematics/ng-add/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@
2727
]
2828
}
2929
},
30+
"typography": {
31+
"type": "boolean",
32+
"default": false,
33+
"description": "Whether to set up global typography styles.",
34+
"x-prompt": "Set up global Angular Material typography styles?"
35+
},
3036
"animations": {
3137
"type": "boolean",
3238
"default": true,

src/material/schematics/ng-add/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ export interface Schema {
1515

1616
/** Name of pre-built theme to install. */
1717
theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom';
18+
19+
/** Whether to set up global typography styles. */
20+
typography: boolean;
1821
}

src/material/schematics/ng-add/setup-project.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {getWorkspace} from '@schematics/angular/utility/config';
1919
import {getAppModulePath} from '@schematics/angular/utility/ng-ast-utils';
2020
import {addFontsToIndex} from './fonts/material-fonts';
2121
import {Schema} from './schema';
22-
import {addThemeToAppStyles} from './theming/theming';
22+
import {addThemeToAppStyles, addTypographyClass} from './theming/theming';
2323

2424
/** Name of the Angular module that enables Angular browser animations. */
2525
const browserAnimationsModuleName = 'BrowserAnimationsModule';
@@ -39,6 +39,7 @@ export default function(options: Schema): Rule {
3939
addThemeToAppStyles(options),
4040
addFontsToIndex(options),
4141
addMaterialAppStyles(options),
42+
addTypographyClass(options),
4243
]);
4344
}
4445

src/material/schematics/ng-add/theming/theming.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {normalize} from '@angular-devkit/core';
1010
import {WorkspaceProject, WorkspaceSchema} from '@angular-devkit/core/src/experimental/workspace';
1111
import {SchematicsException, Tree} from '@angular-devkit/schematics';
1212
import {
13+
addBodyClass,
1314
defaultTargetBuilders,
1415
getProjectFromWorkspace,
1516
getProjectStyleFile,
1617
getProjectTargetOptions,
18+
getProjectIndexFiles,
1719
} from '@angular/cdk/schematics';
1820
import {InsertChange} from '@schematics/angular/utility/change';
1921
import {getWorkspace} from '@schematics/angular/utility/config';
@@ -45,6 +47,23 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree {
4547
};
4648
}
4749

50+
/** Adds the global typography class to the body element. */
51+
export function addTypographyClass(options: Schema): (host: Tree) => Tree {
52+
return function(host: Tree): Tree {
53+
const workspace = getWorkspace(host);
54+
const project = getProjectFromWorkspace(workspace, options.project);
55+
const projectIndexFiles = getProjectIndexFiles(project);
56+
57+
if (!projectIndexFiles.length) {
58+
throw new SchematicsException('No project index HTML file could be found.');
59+
}
60+
61+
projectIndexFiles.forEach(indexFilePath => addBodyClass(host, indexFilePath, 'mat-typography'));
62+
63+
return host;
64+
};
65+
}
66+
4867
/**
4968
* Insert a custom theme to project style file. If no valid style file could be found, a new
5069
* Scss file for the custom theme will be created.

0 commit comments

Comments
 (0)