From f3342aa5983e565ef7dd84ccc210f05de6e66862 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 2 May 2020 17:02:03 +0200 Subject: [PATCH] fix(input/testing): inconsistently reading name from input with ngModel If an input has a `name` binding and an `ngModel`, the input harness won't be able to read the name from the DOM, because `ngModel` doesn't proxy it. These changes add the proxy behavior to the `MatInput` directive, similarly to what we we're doing for `required`, `placeholder`, `readonly` etc. Fixes #18624. --- src/material-experimental/mdc-input/input.ts | 1 + src/material/input/input.ts | 7 ++++ src/material/input/testing/shared.spec.ts | 38 +++++++++++++------- tools/public_api_guard/material/input.d.ts | 3 +- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/material-experimental/mdc-input/input.ts b/src/material-experimental/mdc-input/input.ts index 6e6b78f922b5..c22c9f7c4301 100644 --- a/src/material-experimental/mdc-input/input.ts +++ b/src/material-experimental/mdc-input/input.ts @@ -31,6 +31,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input'; '[id]': 'id', '[disabled]': 'disabled', '[required]': 'required', + '[attr.name]': 'name', '[attr.placeholder]': 'placeholder', '[attr.readonly]': 'readonly && !_isNativeSelect || null', '[attr.aria-describedby]': '_ariaDescribedby || null', diff --git a/src/material/input/input.ts b/src/material/input/input.ts index 7aaca39e4671..24ab45e9db51 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -80,6 +80,7 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase = '[attr.placeholder]': 'placeholder', '[disabled]': 'disabled', '[required]': 'required', + '[attr.name]': 'name || null', '[attr.readonly]': 'readonly && !_isNativeSelect || null', '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-invalid]': 'errorState', @@ -163,6 +164,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl< */ @Input() placeholder: string; + /** + * Name of the input. + * @docs-private + */ + @Input() name: string; + /** * Implemented as part of MatFormFieldControl. * @docs-private diff --git a/src/material/input/testing/shared.spec.ts b/src/material/input/testing/shared.spec.ts index 0a405f88520d..a0c8c8b53478 100644 --- a/src/material/input/testing/shared.spec.ts +++ b/src/material/input/testing/shared.spec.ts @@ -2,7 +2,7 @@ import {HarnessLoader} from '@angular/cdk/testing'; import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {Component} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {ReactiveFormsModule} from '@angular/forms'; +import {FormsModule} from '@angular/forms'; import {MatInputModule} from '@angular/material/input'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MatInputHarness} from './input-harness'; @@ -16,7 +16,7 @@ export function runHarnessTests( beforeEach(async () => { await TestBed .configureTestingModule({ - imports: [NoopAnimationsModule, inputModule, ReactiveFormsModule], + imports: [NoopAnimationsModule, inputModule, FormsModule], declarations: [InputHarnessTest], }) .compileComponents(); @@ -28,7 +28,7 @@ export function runHarnessTests( it('should load all input harnesses', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); }); it('should load input with specific id', async () => { @@ -49,37 +49,40 @@ export function runHarnessTests( it('should be able to get id of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getId()).toMatch(/mat-input-\d+/); expect(await inputs[1].getId()).toMatch(/mat-input-\d+/); expect(await inputs[2].getId()).toBe('myTextarea'); expect(await inputs[3].getId()).toBe('nativeControl'); expect(await inputs[4].getId()).toMatch(/mat-input-\d+/); + expect(await inputs[5].getId()).toBe('has-ng-model'); }); it('should be able to get name of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getName()).toBe('favorite-food'); expect(await inputs[1].getName()).toBe(''); expect(await inputs[2].getName()).toBe(''); expect(await inputs[3].getName()).toBe(''); expect(await inputs[4].getName()).toBe(''); + expect(await inputs[5].getName()).toBe('has-ng-model'); }); it('should be able to get value of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getValue()).toBe('Sushi'); expect(await inputs[1].getValue()).toBe(''); expect(await inputs[2].getValue()).toBe(''); expect(await inputs[3].getValue()).toBe(''); expect(await inputs[4].getValue()).toBe(''); + expect(await inputs[5].getValue()).toBe(''); }); it('should be able to set value of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getValue()).toBe('Sushi'); expect(await inputs[1].getValue()).toBe(''); expect(await inputs[3].getValue()).toBe(''); @@ -98,13 +101,14 @@ export function runHarnessTests( it('should be able to get disabled state', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].isDisabled()).toBe(false); expect(await inputs[1].isDisabled()).toBe(false); expect(await inputs[2].isDisabled()).toBe(false); expect(await inputs[3].isDisabled()).toBe(false); expect(await inputs[4].isDisabled()).toBe(false); + expect(await inputs[5].isDisabled()).toBe(false); fixture.componentInstance.disabled = true; @@ -113,13 +117,14 @@ export function runHarnessTests( it('should be able to get readonly state', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].isReadonly()).toBe(false); expect(await inputs[1].isReadonly()).toBe(false); expect(await inputs[2].isReadonly()).toBe(false); expect(await inputs[3].isReadonly()).toBe(false); expect(await inputs[4].isReadonly()).toBe(false); + expect(await inputs[5].isReadonly()).toBe(false); fixture.componentInstance.readonly = true; @@ -128,13 +133,14 @@ export function runHarnessTests( it('should be able to get required state', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].isRequired()).toBe(false); expect(await inputs[1].isRequired()).toBe(false); expect(await inputs[2].isRequired()).toBe(false); expect(await inputs[3].isRequired()).toBe(false); expect(await inputs[4].isRequired()).toBe(false); + expect(await inputs[5].isRequired()).toBe(false); fixture.componentInstance.required = true; @@ -143,22 +149,24 @@ export function runHarnessTests( it('should be able to get placeholder of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getPlaceholder()).toBe('Favorite food'); expect(await inputs[1].getPlaceholder()).toBe(''); expect(await inputs[2].getPlaceholder()).toBe('Leave a comment'); expect(await inputs[3].getPlaceholder()).toBe('Native control'); expect(await inputs[4].getPlaceholder()).toBe(''); + expect(await inputs[5].getPlaceholder()).toBe(''); }); it('should be able to get type of input', async () => { const inputs = await loader.getAllHarnesses(inputHarness); - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(6); expect(await inputs[0].getType()).toBe('text'); expect(await inputs[1].getType()).toBe('number'); expect(await inputs[2].getType()).toBe('textarea'); expect(await inputs[3].getType()).toBe('text'); expect(await inputs[4].getType()).toBe('textarea'); + expect(await inputs[5].getType()).toBe('text'); fixture.componentInstance.inputType = 'text'; @@ -220,6 +228,10 @@ function getActiveElementTagName() { + + + + ` }) class InputHarnessTest { @@ -227,4 +239,6 @@ class InputHarnessTest { readonly = false; disabled = false; required = false; + ngModelValue = ''; + ngModelName = 'has-ng-model'; } diff --git a/tools/public_api_guard/material/input.d.ts b/tools/public_api_guard/material/input.d.ts index 70623d856fbe..7b37f6ffeb9b 100644 --- a/tools/public_api_guard/material/input.d.ts +++ b/tools/public_api_guard/material/input.d.ts @@ -26,6 +26,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField focused: boolean; get id(): string; set id(value: string); + name: string; ngControl: NgControl; placeholder: string; get readonly(): boolean; @@ -58,7 +59,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField static ngAcceptInputType_readonly: BooleanInput; static ngAcceptInputType_required: BooleanInput; static ngAcceptInputType_value: any; - static ɵdir: i0.ɵɵDirectiveDefWithMeta; + static ɵdir: i0.ɵɵDirectiveDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; }