Skip to content

Commit 155ece6

Browse files
committed
fix(material/input): 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.
1 parent b17ed9d commit 155ece6

File tree

4 files changed

+36
-13
lines changed

4 files changed

+36
-13
lines changed

src/material-experimental/mdc-input/input.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input';
3535
'[id]': 'id',
3636
'[disabled]': 'disabled',
3737
'[required]': 'required',
38+
'[attr.name]': 'name',
3839
'[attr.placeholder]': 'placeholder',
3940
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
4041
// Only mark the input as invalid for assistive technology if it has a value since the

src/material/input/input.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const _MatInputBase = mixinErrorState(class {
8080
'[attr.data-placeholder]': 'placeholder',
8181
'[disabled]': 'disabled',
8282
'[required]': 'required',
83+
'[attr.name]': 'name || null',
8384
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
8485
// Only mark the input as invalid for assistive technology if it has a value since the
8586
// state usually overlaps with `aria-required` when the input is empty and can be redundant.
@@ -169,6 +170,12 @@ export class MatInput extends _MatInputBase implements MatFormFieldControl<any>,
169170
*/
170171
@Input() placeholder: string;
171172

173+
/**
174+
* Name of the input.
175+
* @docs-private
176+
*/
177+
@Input() name: string;
178+
172179
/**
173180
* Implemented as part of MatFormFieldControl.
174181
* @docs-private

src/material/input/testing/shared-input.spec.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {HarnessLoader} from '@angular/cdk/testing';
22
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
33
import {Component} from '@angular/core';
44
import {ComponentFixture, TestBed} from '@angular/core/testing';
5-
import {ReactiveFormsModule} from '@angular/forms';
5+
import {FormsModule} from '@angular/forms';
66
import {MatInputModule} from '@angular/material/input';
77
import {getSupportedInputTypes} from '@angular/cdk/platform';
88
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -17,7 +17,7 @@ export function runInputHarnessTests(
1717
beforeEach(async () => {
1818
await TestBed
1919
.configureTestingModule({
20-
imports: [NoopAnimationsModule, inputModule, ReactiveFormsModule],
20+
imports: [NoopAnimationsModule, inputModule, FormsModule],
2121
declarations: [InputHarnessTest],
2222
})
2323
.compileComponents();
@@ -29,7 +29,7 @@ export function runInputHarnessTests(
2929

3030
it('should load all input harnesses', async () => {
3131
const inputs = await loader.getAllHarnesses(inputHarness);
32-
expect(inputs.length).toBe(6);
32+
expect(inputs.length).toBe(7);
3333
});
3434

3535
it('should load input with specific id', async () => {
@@ -67,37 +67,40 @@ export function runInputHarnessTests(
6767

6868
it('should be able to get id of input', async () => {
6969
const inputs = await loader.getAllHarnesses(inputHarness);
70-
expect(inputs.length).toBe(6);
70+
expect(inputs.length).toBe(7);
7171
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/);
7272
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/);
7373
expect(await inputs[2].getId()).toBe('myTextarea');
7474
expect(await inputs[3].getId()).toBe('nativeControl');
7575
expect(await inputs[4].getId()).toMatch(/mat-input-\d+/);
76+
expect(await inputs[5].getId()).toBe('has-ng-model');
7677
});
7778

7879
it('should be able to get name of input', async () => {
7980
const inputs = await loader.getAllHarnesses(inputHarness);
80-
expect(inputs.length).toBe(6);
81+
expect(inputs.length).toBe(7);
8182
expect(await inputs[0].getName()).toBe('favorite-food');
8283
expect(await inputs[1].getName()).toBe('');
8384
expect(await inputs[2].getName()).toBe('');
8485
expect(await inputs[3].getName()).toBe('');
8586
expect(await inputs[4].getName()).toBe('');
87+
expect(await inputs[5].getName()).toBe('has-ng-model');
8688
});
8789

8890
it('should be able to get value of input', async () => {
8991
const inputs = await loader.getAllHarnesses(inputHarness);
90-
expect(inputs.length).toBe(6);
92+
expect(inputs.length).toBe(7);
9193
expect(await inputs[0].getValue()).toBe('Sushi');
9294
expect(await inputs[1].getValue()).toBe('');
9395
expect(await inputs[2].getValue()).toBe('');
9496
expect(await inputs[3].getValue()).toBe('');
9597
expect(await inputs[4].getValue()).toBe('');
98+
expect(await inputs[5].getValue()).toBe('');
9699
});
97100

98101
it('should be able to set value of input', async () => {
99102
const inputs = await loader.getAllHarnesses(inputHarness);
100-
expect(inputs.length).toBe(6);
103+
expect(inputs.length).toBe(7);
101104
expect(await inputs[0].getValue()).toBe('Sushi');
102105
expect(await inputs[1].getValue()).toBe('');
103106
expect(await inputs[3].getValue()).toBe('');
@@ -116,13 +119,14 @@ export function runInputHarnessTests(
116119

117120
it('should be able to get disabled state', async () => {
118121
const inputs = await loader.getAllHarnesses(inputHarness);
119-
expect(inputs.length).toBe(6);
122+
expect(inputs.length).toBe(7);
120123

121124
expect(await inputs[0].isDisabled()).toBe(false);
122125
expect(await inputs[1].isDisabled()).toBe(false);
123126
expect(await inputs[2].isDisabled()).toBe(false);
124127
expect(await inputs[3].isDisabled()).toBe(false);
125128
expect(await inputs[4].isDisabled()).toBe(false);
129+
expect(await inputs[5].isDisabled()).toBe(false);
126130

127131
fixture.componentInstance.disabled = true;
128132

@@ -131,13 +135,14 @@ export function runInputHarnessTests(
131135

132136
it('should be able to get readonly state', async () => {
133137
const inputs = await loader.getAllHarnesses(inputHarness);
134-
expect(inputs.length).toBe(6);
138+
expect(inputs.length).toBe(7);
135139

136140
expect(await inputs[0].isReadonly()).toBe(false);
137141
expect(await inputs[1].isReadonly()).toBe(false);
138142
expect(await inputs[2].isReadonly()).toBe(false);
139143
expect(await inputs[3].isReadonly()).toBe(false);
140144
expect(await inputs[4].isReadonly()).toBe(false);
145+
expect(await inputs[5].isReadonly()).toBe(false);
141146

142147
fixture.componentInstance.readonly = true;
143148

@@ -146,13 +151,14 @@ export function runInputHarnessTests(
146151

147152
it('should be able to get required state', async () => {
148153
const inputs = await loader.getAllHarnesses(inputHarness);
149-
expect(inputs.length).toBe(6);
154+
expect(inputs.length).toBe(7);
150155

151156
expect(await inputs[0].isRequired()).toBe(false);
152157
expect(await inputs[1].isRequired()).toBe(false);
153158
expect(await inputs[2].isRequired()).toBe(false);
154159
expect(await inputs[3].isRequired()).toBe(false);
155160
expect(await inputs[4].isRequired()).toBe(false);
161+
expect(await inputs[5].isRequired()).toBe(false);
156162

157163
fixture.componentInstance.required = true;
158164

@@ -161,22 +167,24 @@ export function runInputHarnessTests(
161167

162168
it('should be able to get placeholder of input', async () => {
163169
const inputs = await loader.getAllHarnesses(inputHarness);
164-
expect(inputs.length).toBe(6);
170+
expect(inputs.length).toBe(7);
165171
expect(await inputs[0].getPlaceholder()).toBe('Favorite food');
166172
expect(await inputs[1].getPlaceholder()).toBe('');
167173
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment');
168174
expect(await inputs[3].getPlaceholder()).toBe('Native control');
169175
expect(await inputs[4].getPlaceholder()).toBe('');
176+
expect(await inputs[5].getPlaceholder()).toBe('');
170177
});
171178

172179
it('should be able to get type of input', async () => {
173180
const inputs = await loader.getAllHarnesses(inputHarness);
174-
expect(inputs.length).toBe(6);
181+
expect(inputs.length).toBe(7);
175182
expect(await inputs[0].getType()).toBe('text');
176183
expect(await inputs[1].getType()).toBe('number');
177184
expect(await inputs[2].getType()).toBe('textarea');
178185
expect(await inputs[3].getType()).toBe('text');
179186
expect(await inputs[4].getType()).toBe('textarea');
187+
expect(await inputs[5].getType()).toBe('text');
180188

181189
fixture.componentInstance.inputType = 'text';
182190

@@ -247,6 +255,10 @@ export function runInputHarnessTests(
247255
</select>
248256
</mat-form-field>
249257
258+
<mat-form-field>
259+
<input [(ngModel)]="ngModelValue" [name]="ngModelName" id="has-ng-model" matNativeControl>
260+
</mat-form-field>
261+
250262
<mat-form-field>
251263
<input matNativeControl placeholder="Color control" id="colorControl" type="color">
252264
</mat-form-field>
@@ -257,4 +269,6 @@ class InputHarnessTest {
257269
readonly = false;
258270
disabled = false;
259271
required = false;
272+
ngModelValue = '';
273+
ngModelName = 'has-ng-model';
260274
}

tools/public_api_guard/material/input.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class MatInput extends _MatInputBase implements MatFormFieldControl<any>,
6565
protected _isNeverEmpty(): boolean;
6666
readonly _isServer: boolean;
6767
readonly _isTextarea: boolean;
68+
name: string;
6869
// (undocumented)
6970
protected _neverEmptyInputTypes: string[];
7071
// (undocumented)
@@ -111,7 +112,7 @@ export class MatInput extends _MatInputBase implements MatFormFieldControl<any>,
111112
get value(): string;
112113
set value(value: string);
113114
// (undocumented)
114-
static ɵdir: i0.ɵɵDirectiveDeclaration<MatInput, "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", ["matInput"], { "disabled": "disabled"; "id": "id"; "placeholder": "placeholder"; "required": "required"; "type": "type"; "errorStateMatcher": "errorStateMatcher"; "userAriaDescribedBy": "aria-describedby"; "value": "value"; "readonly": "readonly"; }, {}, never>;
115+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatInput, "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", ["matInput"], { "disabled": "disabled"; "id": "id"; "placeholder": "placeholder"; "name": "name"; "required": "required"; "type": "type"; "errorStateMatcher": "errorStateMatcher"; "userAriaDescribedBy": "aria-describedby"; "value": "value"; "readonly": "readonly"; }, {}, never>;
115116
// (undocumented)
116117
static ɵfac: i0.ɵɵFactoryDeclaration<MatInput, [null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }, null, { optional: true; self: true; }, null, null, { optional: true; }]>;
117118
}

0 commit comments

Comments
 (0)