diff --git a/src/cdk/schematics/utils/html-head-element.ts b/src/cdk/schematics/utils/html-manipulation.ts similarity index 56% rename from src/cdk/schematics/utils/html-head-element.ts rename to src/cdk/schematics/utils/html-manipulation.ts index 689ed73f7ce1..7b0769d1ca34 100644 --- a/src/cdk/schematics/utils/html-head-element.ts +++ b/src/cdk/schematics/utils/html-manipulation.ts @@ -27,7 +27,7 @@ export function appendHtmlElementToHead(host: Tree, htmlFilePath: string, elemen const headTag = getHtmlHeadTagElement(htmlContent); if (!headTag) { - throw `Could not find '
' element in HTML file: ${htmlFileBuffer}`; + throw Error(`Could not find '' element in HTML file: ${htmlFileBuffer}`); } // 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 /** Parses the given HTML file and returns the head element if available. */ export function getHtmlHeadTagElement(htmlContent: string): DefaultTreeElement | null { + return getElementByTagName('head', htmlContent); +} + +/** Adds a class to the body of the document. */ +export function addBodyClass(host: Tree, htmlFilePath: string, className: string): void { + const htmlFileBuffer = host.read(htmlFilePath); + + if (!htmlFileBuffer) { + throw new SchematicsException(`Could not read file for path: ${htmlFilePath}`); + } + + const htmlContent = htmlFileBuffer.toString(); + const body = getElementByTagName('body', htmlContent); + + if (!body) { + throw Error(`Could not find element in HTML file: ${htmlFileBuffer}`); + } + + const classAttribute = body.attrs.find(attribute => attribute.name === 'class'); + + if (classAttribute) { + const hasClass = classAttribute.value.split(' ').map(part => part.trim()).includes(className); + + if (!hasClass) { + const classAttributeLocation = body.sourceCodeLocation!.attrs.class; + const recordedChange = host + .beginUpdate(htmlFilePath) + .insertRight(classAttributeLocation.endOffset - 1, ` ${className}`); + host.commitUpdate(recordedChange); + } + } else { + const recordedChange = host + .beginUpdate(htmlFilePath) + .insertRight(body.sourceCodeLocation!.startTag.endOffset - 1, ` class="${className}"`); + host.commitUpdate(recordedChange); + } +} + +/** Finds an element by its tag name. */ +function getElementByTagName(tagName: string, htmlContent: string): + DefaultTreeElement | null { const document = parseHtml(htmlContent, {sourceCodeLocationInfo: true}) as DefaultTreeDocument; const nodeQueue = [...document.childNodes]; while (nodeQueue.length) { const node = nodeQueue.shift() as DefaultTreeElement; - if (node.nodeName.toLowerCase() === 'head') { + if (node.nodeName.toLowerCase() === tagName) { return node; } else if (node.childNodes) { nodeQueue.push(...node.childNodes); diff --git a/src/cdk/schematics/utils/index.ts b/src/cdk/schematics/utils/index.ts index df5a6140aa98..c4bd126e1d0f 100644 --- a/src/cdk/schematics/utils/index.ts +++ b/src/cdk/schematics/utils/index.ts @@ -10,7 +10,7 @@ export * from './ast'; export * from './ast/ng-module-imports'; export * from './build-component'; export * from './get-project'; -export * from './html-head-element'; +export * from './html-manipulation'; export * from './parse5-element'; export * from './project-index-file'; export * from './project-main-file'; diff --git a/src/material/schematics/ng-add/index.spec.ts b/src/material/schematics/ng-add/index.spec.ts index e963311751ee..aa8e36f1e0da 100644 --- a/src/material/schematics/ng-add/index.spec.ts +++ b/src/material/schematics/ng-add/index.spec.ts @@ -313,4 +313,66 @@ describe('ng-add schematic', () => { .toBe('custom-theme', 'Expected the old custom theme content to be unchanged.'); }); }); + + it('should add the global typography class if the body has no classes', async () => { + const tree = await runner.runSchematicAsync('ng-add-setup-project', { + typography: true + }, appTree).toPromise(); + const workspace = getWorkspace(tree); + const project = getProjectFromWorkspace(workspace); + + const indexFiles = getProjectIndexFiles(project); + expect(indexFiles.length).toBe(1); + + indexFiles.forEach(indexPath => { + const buffer = tree.read(indexPath)!; + expect(buffer.toString()).toContain(''); + }); + }); + + it('should add the global typography class if the body has existing classes', async () => { + appTree.overwrite('projects/material/src/index.html', ` + + + + + `); + + const tree = await runner.runSchematicAsync('ng-add-setup-project', { + typography: true + }, appTree).toPromise(); + + const workspace = getWorkspace(tree); + const project = getProjectFromWorkspace(workspace); + const indexFiles = getProjectIndexFiles(project); + expect(indexFiles.length).toBe(1); + + indexFiles.forEach(indexPath => { + const buffer = tree.read(indexPath)!; + expect(buffer.toString()).toContain(''); + }); + }); + + it('should not add the global typography class if it exists already', async () => { + appTree.overwrite('projects/material/src/index.html', ` + + + + + `); + + const tree = await runner.runSchematicAsync('ng-add-setup-project', { + typography: true + }, appTree).toPromise(); + + const workspace = getWorkspace(tree); + const project = getProjectFromWorkspace(workspace); + const indexFiles = getProjectIndexFiles(project); + expect(indexFiles.length).toBe(1); + + indexFiles.forEach(indexPath => { + const buffer = tree.read(indexPath)!; + expect(buffer.toString()).toContain(''); + }); + }); }); diff --git a/src/material/schematics/ng-add/schema.json b/src/material/schematics/ng-add/schema.json index d73cbae47bfa..6f9383303de7 100644 --- a/src/material/schematics/ng-add/schema.json +++ b/src/material/schematics/ng-add/schema.json @@ -27,6 +27,12 @@ ] } }, + "typography": { + "type": "boolean", + "default": false, + "description": "Whether to set up global typography styles.", + "x-prompt": "Set up global Angular Material typography styles?" + }, "animations": { "type": "boolean", "default": true, diff --git a/src/material/schematics/ng-add/schema.ts b/src/material/schematics/ng-add/schema.ts index 9a43c86fc05b..2d672aba99b1 100644 --- a/src/material/schematics/ng-add/schema.ts +++ b/src/material/schematics/ng-add/schema.ts @@ -15,4 +15,7 @@ export interface Schema { /** Name of pre-built theme to install. */ theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom'; + + /** Whether to set up global typography styles. */ + typography: boolean; } diff --git a/src/material/schematics/ng-add/setup-project.ts b/src/material/schematics/ng-add/setup-project.ts index 05b09587f71e..19009f81cd34 100644 --- a/src/material/schematics/ng-add/setup-project.ts +++ b/src/material/schematics/ng-add/setup-project.ts @@ -19,7 +19,7 @@ import {getWorkspace} from '@schematics/angular/utility/config'; import {getAppModulePath} from '@schematics/angular/utility/ng-ast-utils'; import {addFontsToIndex} from './fonts/material-fonts'; import {Schema} from './schema'; -import {addThemeToAppStyles} from './theming/theming'; +import {addThemeToAppStyles, addTypographyClass} from './theming/theming'; /** Name of the Angular module that enables Angular browser animations. */ const browserAnimationsModuleName = 'BrowserAnimationsModule'; @@ -39,6 +39,7 @@ export default function(options: Schema): Rule { addThemeToAppStyles(options), addFontsToIndex(options), addMaterialAppStyles(options), + addTypographyClass(options), ]); } diff --git a/src/material/schematics/ng-add/theming/theming.ts b/src/material/schematics/ng-add/theming/theming.ts index f19257aeeb50..60dd60d71db5 100644 --- a/src/material/schematics/ng-add/theming/theming.ts +++ b/src/material/schematics/ng-add/theming/theming.ts @@ -10,10 +10,12 @@ import {normalize} from '@angular-devkit/core'; import {WorkspaceProject, WorkspaceSchema} from '@angular-devkit/core/src/experimental/workspace'; import {SchematicsException, Tree} from '@angular-devkit/schematics'; import { + addBodyClass, defaultTargetBuilders, getProjectFromWorkspace, getProjectStyleFile, getProjectTargetOptions, + getProjectIndexFiles, } from '@angular/cdk/schematics'; import {InsertChange} from '@schematics/angular/utility/change'; import {getWorkspace} from '@schematics/angular/utility/config'; @@ -45,6 +47,23 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree { }; } +/** Adds the global typography class to the body element. */ +export function addTypographyClass(options: Schema): (host: Tree) => Tree { + return function(host: Tree): Tree { + const workspace = getWorkspace(host); + const project = getProjectFromWorkspace(workspace, options.project); + const projectIndexFiles = getProjectIndexFiles(project); + + if (!projectIndexFiles.length) { + throw new SchematicsException('No project index HTML file could be found.'); + } + + projectIndexFiles.forEach(indexFilePath => addBodyClass(host, indexFilePath, 'mat-typography')); + + return host; + }; +} + /** * Insert a custom theme to project style file. If no valid style file could be found, a new * Scss file for the custom theme will be created.