diff --git a/integration/mdc-migration/BUILD.bazel b/integration/mdc-migration/BUILD.bazel index 737d5f02309c..3d3889394fbd 100644 --- a/integration/mdc-migration/BUILD.bazel +++ b/integration/mdc-migration/BUILD.bazel @@ -40,6 +40,16 @@ migration_test( "sample-project/**/*", ]), approve = False, + verify = [ + "! grep" + + " --include=*.{ts,css,scss,html}" + + " --exclude-dir={node_modules,.angular,.yarn_cache_folder}" + + " -ir legacy" + + " || { echo \"Error: golden project contains 'legacy'.\" >&2; exit 1; }", + "yarn build", + # TODO(mmalerba): add back once slider tests pass. + # "yarn test", + ], ) migration_test( diff --git a/integration/mdc-migration/golden/angular.json b/integration/mdc-migration/golden/angular.json index 2ea486ad4f2a..3b3031758564 100644 --- a/integration/mdc-migration/golden/angular.json +++ b/integration/mdc-migration/golden/angular.json @@ -36,7 +36,7 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumError": "2mb" }, { "type": "anyComponentStyle", diff --git a/integration/mdc-migration/golden/src/app/app.module.ts b/integration/mdc-migration/golden/src/app/app.module.ts index aa2071676861..0f32d7cfd921 100644 --- a/integration/mdc-migration/golden/src/app/app.module.ts +++ b/integration/mdc-migration/golden/src/app/app.module.ts @@ -25,6 +25,7 @@ import {MatSnackBarModule} from '@angular/material/snack-bar'; import {MatTableModule} from '@angular/material/table'; import {MatTabsModule} from '@angular/material/tabs'; import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatOptionModule, VERSION} from '@angular/material/core'; import {AutocompleteComponent} from './components/autocomplete/autocomplete.component'; import {ButtonComponent} from './components/button/button.component'; import {CardComponent} from './components/card/card.component'; @@ -94,9 +95,12 @@ import {TooltipComponent} from './components/tooltip/tooltip.component'; MatTableModule, MatTabsModule, MatTooltipModule, + MatOptionModule, ReactiveFormsModule, ], providers: [], bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { + version = VERSION; +} diff --git a/integration/mdc-migration/golden/src/app/components/form-field/form-field.component.html b/integration/mdc-migration/golden/src/app/components/form-field/form-field.component.html index e5dafc96c321..77bcfc0ddea7 100644 --- a/integration/mdc-migration/golden/src/app/components/form-field/form-field.component.html +++ b/integration/mdc-migration/golden/src/app/components/form-field/form-field.component.html @@ -2,5 +2,8 @@

Form field example

Enter some input - {{input.value?.length || 0}}/10 + {{input.value.length}}/10 + + + diff --git a/integration/mdc-migration/golden/src/styles.scss b/integration/mdc-migration/golden/src/styles.scss index caa63eef72c2..914651b6c30b 100644 --- a/integration/mdc-migration/golden/src/styles.scss +++ b/integration/mdc-migration/golden/src/styles.scss @@ -7,8 +7,8 @@ // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! -@include mat.all-component-typographies(); -@include mat.legacy-core(); +@include mat.all-component-typographies(mat.define-typography-config()); +@include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker @@ -26,9 +26,11 @@ $sample-project-theme: mat.define-light-theme(( primary: $sample-project-primary, accent: $sample-project-accent, warn: $sample-project-warn, - ) + ), + typography: mat.define-typography-config(), )); +@include mat.core-theme($sample-project-theme); @include mat.autocomplete-theme($sample-project-theme); @include mat.button-theme($sample-project-theme); @include mat.fab-theme($sample-project-theme); @@ -45,13 +47,14 @@ $sample-project-theme: mat.define-light-theme(( @include mat.progress-bar-theme($sample-project-theme); @include mat.progress-spinner-theme($sample-project-theme); @include mat.radio-theme($sample-project-theme); -@include mat.legacy-select-theme($sample-project-theme); @include mat.slide-toggle-theme($sample-project-theme); @include mat.slider-theme($sample-project-theme); @include mat.snack-bar-theme($sample-project-theme); @include mat.table-theme($sample-project-theme); @include mat.tabs-theme($sample-project-theme); @include mat.tooltip-theme($sample-project-theme); +@include mat.option-theme($sample-project-theme); +@include mat.optgroup-theme($sample-project-theme); /* You can add global styles to this file, and also import other style files */ diff --git a/integration/mdc-migration/migration-test.bzl b/integration/mdc-migration/migration-test.bzl index 635f5cf72bad..56ff9d4ccb38 100644 --- a/integration/mdc-migration/migration-test.bzl +++ b/integration/mdc-migration/migration-test.bzl @@ -21,7 +21,7 @@ IGNORED_FILES = [ "yarn.lock", ] -def migration_test(name, srcs, approve): +def migration_test(name, srcs, approve, verify = []): node_integration_test( name = name, srcs = srcs, @@ -32,15 +32,13 @@ def migration_test(name, srcs, approve): # TODO(devversion): determine if a solution/workaround could live in the test runner. "yarn install --cache-folder .yarn_cache_folder/", "yarn ng generate @angular/material:mdc-migration --components all", - # TODO(amysorto): add back once MDC components are in @angular/material - # "yarn test", " ".join([ "$(rootpath :verify_golden)", "%s" % approve, "../golden", "integration/mdc-migration/golden", ] + IGNORED_FILES), - ], + ] + verify, data = [ ":golden_project", ":test_project", diff --git a/integration/mdc-migration/sample-project/angular.json b/integration/mdc-migration/sample-project/angular.json index 2ea486ad4f2a..3b3031758564 100644 --- a/integration/mdc-migration/sample-project/angular.json +++ b/integration/mdc-migration/sample-project/angular.json @@ -36,7 +36,7 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumError": "2mb" }, { "type": "anyComponentStyle", diff --git a/integration/mdc-migration/sample-project/src/app/app.module.ts b/integration/mdc-migration/sample-project/src/app/app.module.ts index ce2c7171427a..e947003d16d5 100644 --- a/integration/mdc-migration/sample-project/src/app/app.module.ts +++ b/integration/mdc-migration/sample-project/src/app/app.module.ts @@ -25,6 +25,7 @@ import {MatLegacySnackBarModule as MatSnackBarModule} from '@angular/material/le import {MatLegacyTableModule as MatTableModule} from '@angular/material/legacy-table'; import {MatLegacyTabsModule as MatTabsModule} from '@angular/material/legacy-tabs'; import {MatLegacyTooltipModule as MatTooltipModule} from '@angular/material/legacy-tooltip'; +import {MatLegacyOptionModule, LEGACY_VERSION} from '@angular/material/legacy-core'; import {AutocompleteComponent} from './components/autocomplete/autocomplete.component'; import {ButtonComponent} from './components/button/button.component'; import {CardComponent} from './components/card/card.component'; @@ -94,9 +95,12 @@ import {TooltipComponent} from './components/tooltip/tooltip.component'; MatTableModule, MatTabsModule, MatTooltipModule, + MatLegacyOptionModule, ReactiveFormsModule, ], providers: [], bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { + version = LEGACY_VERSION; +} diff --git a/integration/mdc-migration/sample-project/src/app/components/form-field/form-field.component.html b/integration/mdc-migration/sample-project/src/app/components/form-field/form-field.component.html index e5dafc96c321..ae3d5dc73e2f 100644 --- a/integration/mdc-migration/sample-project/src/app/components/form-field/form-field.component.html +++ b/integration/mdc-migration/sample-project/src/app/components/form-field/form-field.component.html @@ -2,5 +2,8 @@

Form field example

Enter some input - {{input.value?.length || 0}}/10 + {{input.value.length}}/10 + + + diff --git a/integration/mdc-migration/sample-project/src/app/components/progress-spinner/progress-spinner.component.spec.ts b/integration/mdc-migration/sample-project/src/app/components/progress-spinner/progress-spinner.component.spec.ts index 252afcca0109..6e31495e80a9 100644 --- a/integration/mdc-migration/sample-project/src/app/components/progress-spinner/progress-spinner.component.spec.ts +++ b/integration/mdc-migration/sample-project/src/app/components/progress-spinner/progress-spinner.component.spec.ts @@ -1,6 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatProgressSpinnerModule} from '@angular/material/legacy-progress-spinner'; +import {MatLegacyProgressSpinnerModule as MatProgressSpinnerModule} from '@angular/material/legacy-progress-spinner'; import {ProgressSpinnerComponent} from './progress-spinner.component'; describe('ProgressSpinnerComponent', () => { diff --git a/integration/mdc-migration/sample-project/src/app/components/slide-toggle/slide-toggle.component.spec.ts b/integration/mdc-migration/sample-project/src/app/components/slide-toggle/slide-toggle.component.spec.ts index f0dd9f1f7996..0f2efb09ffd4 100644 --- a/integration/mdc-migration/sample-project/src/app/components/slide-toggle/slide-toggle.component.spec.ts +++ b/integration/mdc-migration/sample-project/src/app/components/slide-toggle/slide-toggle.component.spec.ts @@ -1,6 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatSlideToggleModule} from '@angular/material/legacy-slide-toggle'; +import {MatLegacySlideToggleModule as MatSlideToggleModule} from '@angular/material/legacy-slide-toggle'; import {SlideToggleComponent} from './slide-toggle.component'; describe('SlideToggleComponent', () => { diff --git a/integration/mdc-migration/sample-project/src/app/components/snack-bar/snack-bar.component.spec.ts b/integration/mdc-migration/sample-project/src/app/components/snack-bar/snack-bar.component.spec.ts index 6f3df4abb170..271826d5c30a 100644 --- a/integration/mdc-migration/sample-project/src/app/components/snack-bar/snack-bar.component.spec.ts +++ b/integration/mdc-migration/sample-project/src/app/components/snack-bar/snack-bar.component.spec.ts @@ -1,6 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatSnackBarModule} from '@angular/material/legacy-snack-bar'; +import {MatLegacySnackBarModule as MatSnackBarModule} from '@angular/material/legacy-snack-bar'; import {SnackBarComponent} from './snack-bar.component'; describe('SnackBarComponent', () => { diff --git a/integration/mdc-migration/sample-project/src/app/components/table/table.component.spec.ts b/integration/mdc-migration/sample-project/src/app/components/table/table.component.spec.ts index 33f4828d8c83..126b4a7ec3a0 100644 --- a/integration/mdc-migration/sample-project/src/app/components/table/table.component.spec.ts +++ b/integration/mdc-migration/sample-project/src/app/components/table/table.component.spec.ts @@ -1,6 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatTableModule} from '@angular/material/legacy-table'; +import {MatLegacyTableModule as MatTableModule} from '@angular/material/legacy-table'; import {TableComponent} from './table.component'; describe('TableComponent', () => { diff --git a/integration/mdc-migration/sample-project/src/app/components/tabs/tabs.component.spec.ts b/integration/mdc-migration/sample-project/src/app/components/tabs/tabs.component.spec.ts index fd2d18d138af..6123b38fe221 100644 --- a/integration/mdc-migration/sample-project/src/app/components/tabs/tabs.component.spec.ts +++ b/integration/mdc-migration/sample-project/src/app/components/tabs/tabs.component.spec.ts @@ -1,6 +1,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatTabsModule} from '@angular/material/legacy-tabs'; +import {MatLegacyTabsModule as MatTabsModule} from '@angular/material/legacy-tabs'; import {TabsComponent} from './tabs.component'; describe('TabsComponent', () => { diff --git a/integration/mdc-migration/sample-project/src/styles.scss b/integration/mdc-migration/sample-project/src/styles.scss index aad7a527813f..324e725b8970 100644 --- a/integration/mdc-migration/sample-project/src/styles.scss +++ b/integration/mdc-migration/sample-project/src/styles.scss @@ -7,7 +7,7 @@ // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! -@include mat.all-legacy-component-typographies(); +@include mat.all-legacy-component-typographies(mat.define-legacy-typography-config()); @include mat.legacy-core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss @@ -26,9 +26,11 @@ $sample-project-theme: mat.define-light-theme(( primary: $sample-project-primary, accent: $sample-project-accent, warn: $sample-project-warn, - ) + ), + typography: mat.define-legacy-typography-config(), )); +@include mat.legacy-core-theme($sample-project-theme); @include mat.legacy-autocomplete-theme($sample-project-theme); @include mat.legacy-button-theme($sample-project-theme); @include mat.legacy-card-theme($sample-project-theme); @@ -49,6 +51,8 @@ $sample-project-theme: mat.define-light-theme(( @include mat.legacy-table-theme($sample-project-theme); @include mat.legacy-tabs-theme($sample-project-theme); @include mat.legacy-tooltip-theme($sample-project-theme); +@include mat.legacy-option-theme($sample-project-theme); +@include mat.legacy-optgroup-theme($sample-project-theme); /* You can add global styles to this file, and also import other style files */ diff --git a/integration/mdc-migration/verify-golden.ts b/integration/mdc-migration/verify-golden.ts index 4e789b38c31b..e0cb0d1a1496 100644 --- a/integration/mdc-migration/verify-golden.ts +++ b/integration/mdc-migration/verify-golden.ts @@ -64,8 +64,8 @@ async function compareFiles( const [testContent, goldenContent] = await Promise.allSettled(contentPromises); const diff = { filename, - actual: getDiffValue(goldenStats, goldenContent), - expected: getDiffValue(testStats, testContent), + actual: getDiffValue(testStats, testContent), + expected: getDiffValue(goldenStats, goldenContent), }; if (testStats.status === 'rejected' && goldenStats.status === 'rejected') { return null; // Neither file exists. @@ -92,9 +92,9 @@ function showDiffs(diffs: FileDiff[]) { console.error( [ ''.padEnd(80, '='), - `----- ${diff.filename} (actual) `.padEnd(80, '-'), + `----- ${diff.filename} (this run) `.padEnd(80, '-'), diff.actual, - `----- ${diff.filename} (expected) `.padEnd(80, '-'), + `----- ${diff.filename} (golden) `.padEnd(80, '-'), diff.expected, '', ].join('\n'), diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/components/card/card-template.ts b/src/material/schematics/ng-generate/mdc-migration/rules/components/card/card-template.ts index 6ed14ecddd99..f99c22420e19 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/components/card/card-template.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/components/card/card-template.ts @@ -8,7 +8,7 @@ import * as compiler from '@angular/compiler'; import {TemplateMigrator} from '../../template-migrator'; -import {addAttribute, visitElements} from '../../tree-traversal'; +import {updateAttribute, visitElements} from '../../tree-traversal'; import {Update} from '../../../../../migration-utilities'; export class CardTemplateMigrator extends TemplateMigrator { @@ -22,7 +22,7 @@ export class CardTemplateMigrator extends TemplateMigrator { updates.push({ offset: node.startSourceSpan.start.offset, - updateFn: html => addAttribute(html, node, 'appearance', 'outlined'), + updateFn: html => updateAttribute(html, node, 'appearance', () => 'outlined'), }); }); diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.spec.ts b/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.spec.ts new file mode 100644 index 000000000000..11b7b3ab945b --- /dev/null +++ b/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.spec.ts @@ -0,0 +1,61 @@ +import {createTestApp, patchDevkitTreeToExposeTypeScript} from '@angular/cdk/schematics/testing'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {createNewTestRunner, migrateComponents, TEMPLATE_FILE} from '../test-setup-helper'; + +describe('form-field template migrator', () => { + let runner: SchematicTestRunner; + let cliAppTree: UnitTestTree; + + async function runMigrationTest(oldFileContent: string, newFileContent: string) { + cliAppTree.overwrite(TEMPLATE_FILE, oldFileContent); + const tree = await migrateComponents(['form-field'], runner, cliAppTree); + expect(tree.readContent(TEMPLATE_FILE)).toBe(newFileContent); + } + + beforeEach(async () => { + runner = createNewTestRunner(); + cliAppTree = patchDevkitTreeToExposeTypeScript(await createTestApp(runner)); + }); + + it('should not update other elements appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); + + it('should not update default appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); + + it('should not update outline appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); + + it('should not update fill appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); + + it('should update standard appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); + + it('should update legacy appearance', async () => { + await runMigrationTest( + '', + '', + ); + }); +}); diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.ts b/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.ts new file mode 100644 index 000000000000..b58962afd155 --- /dev/null +++ b/src/material/schematics/ng-generate/mdc-migration/rules/components/form-field/form-field-template.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as compiler from '@angular/compiler'; +import {TemplateMigrator} from '../../template-migrator'; +import {updateAttribute, visitElements} from '../../tree-traversal'; +import {Update} from '../../../../../migration-utilities'; + +export class FormFieldTemplateMigrator extends TemplateMigrator { + getUpdates(ast: compiler.ParsedTemplate): Update[] { + const updates: Update[] = []; + + visitElements(ast.nodes, (node: compiler.TmplAstElement) => { + if (node.name !== 'mat-form-field') { + return; + } + + updates.push({ + offset: node.startSourceSpan.start.offset, + updateFn: html => + updateAttribute(html, node, 'appearance', old => + ['legacy', 'standard'].includes(old || '') ? null : old, + ), + }); + }); + + return updates; + } +} diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/components/multiple-components-styles.spec.ts b/src/material/schematics/ng-generate/mdc-migration/rules/components/multiple-components-styles.spec.ts index d4db7d01bc1d..6cb67bf2beaa 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/components/multiple-components-styles.spec.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/components/multiple-components-styles.spec.ts @@ -60,12 +60,33 @@ describe('multiple component styles', () => { ); }); + it('should remove legacy mixin if all replacements are already accounted for', async () => { + await runMigrationTest( + ['paginator', 'select'], + ` + @use '@angular/material' as mat; + $theme: (); + @include mat.legacy-paginator-theme($theme); + @include mat.legacy-select-theme($theme); + `, + ` + @use '@angular/material' as mat; + $theme: (); + @include mat.paginator-theme($theme); + @include mat.icon-button-theme($theme); + @include mat.form-field-theme($theme); + @include mat.select-theme($theme); + `, + ); + }); + it('should migrate all component mixins for a full migration', async () => { await runMigrationTest( ['all'], ` @use '@angular/material' as mat; $theme: (); + @include mat.legacy-core(); @include mat.all-legacy-component-themes($sample-project-themes); @include mat.all-legacy-component-colors($sample-colors); @include mat.all-legacy-component-typographies($sample-typographies); @@ -73,6 +94,7 @@ describe('multiple component styles', () => { ` @use '@angular/material' as mat; $theme: (); + @include mat.core(); @include mat.all-component-themes($sample-project-themes); @include mat.all-component-colors($sample-colors); @include mat.all-component-typographies($sample-typographies); @@ -86,6 +108,7 @@ describe('multiple component styles', () => { ` @use '@angular/material' as mat; $theme: (); + @include mat.legacy-core(); @include mat.all-legacy-component-themes($sample-project-themes); @include mat.all-legacy-component-colors($sample-colors); @include mat.all-legacy-component-typographies($sample-typographies); @@ -93,6 +116,9 @@ describe('multiple component styles', () => { ` @use '@angular/material' as mat; $theme: (); + /* TODO(mdc-migration): Remove legacy-core once all legacy components are migrated */ + @include mat.legacy-core(); + @include mat.core(); /* TODO(mdc-migration): Remove all-legacy-component-themes once all legacy components are migrated */ @include mat.all-legacy-component-themes($sample-project-themes); @include mat.all-component-themes($sample-project-themes); diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/components/option/option-styles.ts b/src/material/schematics/ng-generate/mdc-migration/rules/components/option/option-styles.ts index a7b78f462eba..e62565e57466 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/components/option/option-styles.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/components/option/option-styles.ts @@ -26,6 +26,18 @@ export class OptionStylesMigrator extends StyleMigrator { old: 'legacy-option-typography', new: ['option-typography'], }, + { + old: 'legacy-core-theme', + new: ['core-theme'], + }, + { + old: 'legacy-core-color', + new: ['core-color'], + }, + { + old: 'legacy-core-typography', + new: ['core-typography'], + }, ]; classChanges: ClassNameChange[] = [ diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/index.ts b/src/material/schematics/ng-generate/mdc-migration/rules/index.ts index 3022be5b2045..ee1ee5fc1c61 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/index.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/index.ts @@ -34,6 +34,7 @@ import {TabsStylesMigrator} from './components/tabs/tabs-styles'; import {TooltipStylesMigrator} from './components/tooltip/tooltip-styles'; import {OptgroupStylesMigrator} from './components/optgroup/optgroup-styles'; import {OptionStylesMigrator} from './components/option/option-styles'; +import {FormFieldTemplateMigrator} from './components/form-field/form-field-template'; /** Contains the migrators to migrate a single component. */ export interface ComponentMigrator { @@ -121,6 +122,7 @@ export const MIGRATORS: ComponentMigrator[] = [ { component: 'form-field', styles: new FormFieldStylesMigrator(), + template: new FormFieldTemplateMigrator(), }, { component: 'input', diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/style-migrator.ts b/src/material/schematics/ng-generate/mdc-migration/rules/style-migrator.ts index 52e23dcdb793..26d5184bebd6 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/style-migrator.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/style-migrator.ts @@ -26,7 +26,7 @@ export interface MixinChange { old: string; /** The name(s) of the new scss mixin(s). */ - new: string[]; + new: string[] | null; /** Optional check to see if new scss mixin(s) already exist in the styles */ checkForDuplicates?: boolean; @@ -75,7 +75,7 @@ export abstract class StyleMigrator { } // Check if mixin replacements already exist in the stylesheet - const replacements = [...change.new]; + const replacements = [...(change.new ?? [])]; if (change.checkForDuplicates) { const mixinArgumentMatches = atRule.params?.match(MIXIN_ARGUMENTS_REGEX); atRule.root().walkAtRules(rule => { @@ -94,12 +94,7 @@ export abstract class StyleMigrator { }); } - // Don't do anything if all the new changes already exist in the stylesheet - if (replacements.length < 1) { - return null; - } - - return {old: change.old, new: replacements}; + return {old: change.old, new: replacements.length ? replacements : null}; } /** diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts b/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts index 74054ddba117..9fe3d21bfec2 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/theming-styles.ts @@ -12,17 +12,21 @@ import * as postcss from 'postcss'; import * as scss from 'postcss-scss'; import {ComponentMigrator, MIGRATORS} from '.'; -const ALL_LEGACY_COMPONENTS_MIXIN_NAME = '(?:\\.)(.*)(?:\\()'; +const COMPONENTS_MIXIN_NAME = /\.([^(;]*)/; export class ThemingStylesMigration extends Migration { enabled = true; namespace: string; override visitStylesheet(stylesheet: ResolvedResource) { + const migratedContent = this.migrate(stylesheet.content, stylesheet.filePath).replace( + new RegExp(`${this.namespace}.define-legacy-typography-config\\(`, 'g'), + `${this.namespace}.define-typography-config(`, + ); this.fileSystem .edit(stylesheet.filePath) .remove(stylesheet.start, stylesheet.content.length) - .insertRight(stylesheet.start, this.migrate(stylesheet.content, stylesheet.filePath)); + .insertRight(stylesheet.start, migratedContent); } migrate(styles: string, filename: string): string { @@ -59,23 +63,33 @@ export class ThemingStylesMigration extends Migration new RegExp(r).test(mixinText)); + } + isPartialMigration() { return this.upgradeData.length !== MIGRATORS.length; } @@ -157,7 +171,7 @@ function addLegacyCommentForPartialMigrations( /** * Adds comment before postcss rule or at rule node * - * @param rule a postcss rule. + * @param node a postcss rule. * @param comment the text content for the comment */ function addCommentBeforeNode(node: postcss.Rule | postcss.AtRule, comment: string): void { @@ -174,15 +188,18 @@ function addCommentBeforeNode(node: postcss.Rule | postcss.AtRule, comment: stri } /** - * Replaces mixin prefixed with `all-legacy-component` to the MDC equivalent. + * Replaces a cross-cutting mixin that affects multiple components with the MDC equivalent. * - * @param allComponentThemesNode a all-components-theme mixin node + * @param atRule A mixin inclusion node + * @param namespace The @angular/material namespace */ -function replaceAllComponentsMixin(allComponentNode: postcss.AtRule) { - allComponentNode.cloneBefore({ - params: allComponentNode.params.replace('all-legacy-component', 'all-component'), +function replaceCrossCuttingMixin(atRule: postcss.AtRule, namespace: string) { + atRule.cloneBefore({ + params: atRule.params + .replace(`${namespace}.all-legacy-component`, `${namespace}.all-component`) + .replace(`${namespace}.legacy-core`, `${namespace}.core`), }); - allComponentNode.remove(); + atRule.remove(); } /** diff --git a/src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts b/src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts index 518d55f7f532..ff90a640bd17 100644 --- a/src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts +++ b/src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts @@ -1,5 +1,5 @@ import { - addAttribute, + updateAttribute, visitElements, parseTemplate, replaceStartTag, @@ -21,7 +21,28 @@ function runTagNameDuplicationTest(html: string, result: string): void { function runAddAttributeTest(html: string, result: string): void { visitElements(parseTemplate(html).nodes, undefined, node => { - html = addAttribute(html, node, 'attr', 'val'); + html = updateAttribute(html, node, 'add', () => 'val'); + }); + expect(html).toBe(result); +} + +function runRemoveAttributeTest(html: string, result: string): void { + visitElements(parseTemplate(html).nodes, undefined, node => { + html = updateAttribute(html, node, 'rm', () => null); + }); + expect(html).toBe(result); +} + +function runChangeAttributeTest(html: string, result: string): void { + visitElements(parseTemplate(html).nodes, undefined, node => { + html = updateAttribute(html, node, 'change', old => (old == ':(' ? ':)' : old)); + }); + expect(html).toBe(result); +} + +function runClearAttributeTest(html: string, result: string): void { + visitElements(parseTemplate(html).nodes, undefined, node => { + html = updateAttribute(html, node, 'clear', () => ''); }); expect(html).toBe(result); } @@ -92,39 +113,103 @@ describe('#visitElements', () => { describe('add attribute tests', () => { it('should handle single element', async () => { - runAddAttributeTest('', ''); + runAddAttributeTest('', ''); }); it('should handle multiple unnested', async () => { - runAddAttributeTest('', ''); + runAddAttributeTest('', ''); }); it('should handle multiple nested', async () => { - runAddAttributeTest('', ''); + runAddAttributeTest('', ''); }); it('should handle multiple nested and unnested', async () => { runAddAttributeTest( '', - '', + '', ); }); it('should handle adding multiple attrs to a single element', async () => { let html = ''; visitElements(parseTemplate(html).nodes, undefined, node => { - html = addAttribute(html, node, 'attr1', 'val1'); - html = addAttribute(html, node, 'attr2', 'val2'); + html = updateAttribute(html, node, 'attr1', () => 'val1'); + html = updateAttribute(html, node, 'attr2', () => 'val2'); }); expect(html).toBe(''); }); it('should replace value of existing attribute', async () => { - runAddAttributeTest('', ''); + runAddAttributeTest('', ''); }); it('should add value to existing attribute that does not have a value', async () => { - runAddAttributeTest('', ''); + runAddAttributeTest('', ''); + }); + }); + + describe('remove attribute tests', () => { + it('should remove attribute', () => { + runRemoveAttributeTest('', ''); + }); + + it('should remove empty attribute', () => { + runRemoveAttributeTest('', ''); + }); + + it('should remove unquoted attribute', () => { + runRemoveAttributeTest('', ''); + }); + + it('should remove value-less attribute', () => { + runRemoveAttributeTest('', ''); + }); + + it('should not change element without attribute', () => { + runRemoveAttributeTest('', ''); + }); + + it('should not remove other attributes', () => { + runRemoveAttributeTest( + ` + + + `, + ` + + + `, + ); + }); + }); + + describe('change attribute tests', () => { + it('should change attribute with matching value', () => { + runChangeAttributeTest('', ''); + }); + + it('should not change attribute with non-matching value', () => { + runChangeAttributeTest('', ''); + }); + }); + + describe('clear attribute tests', () => { + it('should clear attribute with value', () => { + runClearAttributeTest('', ''); + }); + + it('should preserve value-less attribute', () => { + runClearAttributeTest('', ''); + }); + + it('should add attribute to element without it', () => { + runClearAttributeTest('', ''); }); }); @@ -139,7 +224,7 @@ describe('#visitElements', () => { `, ` { > `; visitElements(parseTemplate(html).nodes, undefined, node => { - html = addAttribute(html, node, 'attr1', 'val1'); - html = addAttribute(html, node, 'attr2', 'val2'); + html = updateAttribute(html, node, 'attr1', () => 'val1'); + html = updateAttribute(html, node, 'attr2', () => 'val2'); }); expect(html).toBe(` string | null, ): string { const existingAttr = node.attributes.find(currentAttr => currentAttr.name === name); - if (existingAttr) { - // If the attribute has a value already, replace it. - if (existingAttr.valueSpan) { + // If the attribute has a value already, replace it. + if (existingAttr && existingAttr.keySpan) { + const updatedValue = update(existingAttr.valueSpan?.toString() || ''); + if (updatedValue == null) { + // Delete attribute return ( - html.slice(0, existingAttr.valueSpan.start.offset) + - value + - html.slice(existingAttr.valueSpan.end.offset) + html.slice(0, existingAttr.sourceSpan.start.offset).trimEnd() + + html.slice(existingAttr.sourceSpan.end.offset) ); - } else if (existingAttr.keySpan) { - // Otherwise add a value to a value-less attribute. Note that the `keySpan` null check is - // only necessary for the compiler. Technically an attribute should always have a key. + } else if (updatedValue == '') { + // Delete value from attribute return ( html.slice(0, existingAttr.keySpan.end.offset) + - `="${value}"` + - html.slice(existingAttr.keySpan.end.offset) + html.slice(existingAttr.sourceSpan.end.offset) ); + } else { + // Set attribute value + if (existingAttr.valueSpan) { + // Replace attribute value + return ( + html.slice(0, existingAttr.valueSpan.start.offset) + + updatedValue + + html.slice(existingAttr.valueSpan.end.offset) + ); + } else { + // Add value to attribute + return ( + html.slice(0, existingAttr.keySpan.end.offset) + + `="${updatedValue}"` + + html.slice(existingAttr.keySpan.end.offset) + ); + } } } + const newValue = update(null); + + // No change needed if attribute should be deleted and is already not present. + if (newValue == null) { + return html; + } + // Otherwise insert a new attribute. const index = node.startSourceSpan.start.offset + node.name.length + 1; const prefix = html.slice(0, index); const suffix = html.slice(index); + const attrText = newValue ? `${name}="${newValue}"` : `${name}`; if (node.startSourceSpan.start.line === node.startSourceSpan.end.line) { - return prefix + ` ${name}="${value}"` + suffix; + return `${prefix} ${attrText}${suffix}`; } const attr = node.attributes[0]; const ctx = attr.sourceSpan.start.getContext(attr.sourceSpan.start.col + 1, 1)!; const indentation = ctx.before; - return prefix + indentation + `${name}="${value}"` + suffix; + return prefix + indentation + attrText + suffix; } /** diff --git a/src/material/schematics/ng-update/test-cases/v15/legacy-components-v15.spec.ts b/src/material/schematics/ng-update/test-cases/v15/legacy-components-v15.spec.ts index cbed08f8c524..19dfab508fa8 100644 --- a/src/material/schematics/ng-update/test-cases/v15/legacy-components-v15.spec.ts +++ b/src/material/schematics/ng-update/test-cases/v15/legacy-components-v15.spec.ts @@ -286,6 +286,7 @@ describe('v15 legacy components migration', () => { `@use '@angular/material' as mat;`, `@include mat.core();`, `@include mat.core(mat.define-typography-config());`, + `@include mat.core-theme(())`, ], new: [ `@use '@angular/material' as mat;`, @@ -299,6 +300,7 @@ describe('v15 legacy components migration', () => { `// If you add typography styles elsewhere, you may want to remove this.`, `@include mat.all-legacy-component-typographies(mat.define-legacy-typography-config());`, `@include mat.legacy-core();`, + `@include mat.legacy-core-theme(())`, ], }); });