Skip to content

Commit f3342aa

Browse files
committed
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.
1 parent 658896f commit f3342aa

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
@@ -31,6 +31,7 @@ import {MatInput as BaseMatInput} from '@angular/material/input';
3131
'[id]': 'id',
3232
'[disabled]': 'disabled',
3333
'[required]': 'required',
34+
'[attr.name]': 'name',
3435
'[attr.placeholder]': 'placeholder',
3536
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
3637
'[attr.aria-describedby]': '_ariaDescribedby || null',

src/material/input/input.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase =
8080
'[attr.placeholder]': 'placeholder',
8181
'[disabled]': 'disabled',
8282
'[required]': 'required',
83+
'[attr.name]': 'name || null',
8384
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
8485
'[attr.aria-describedby]': '_ariaDescribedby || null',
8586
'[attr.aria-invalid]': 'errorState',
@@ -163,6 +164,12 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
163164
*/
164165
@Input() placeholder: string;
165166

167+
/**
168+
* Name of the input.
169+
* @docs-private
170+
*/
171+
@Input() name: string;
172+
166173
/**
167174
* Implemented as part of MatFormFieldControl.
168175
* @docs-private

src/material/input/testing/shared.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 {NoopAnimationsModule} from '@angular/platform-browser/animations';
88
import {MatInputHarness} from './input-harness';
@@ -16,7 +16,7 @@ export function runHarnessTests(
1616
beforeEach(async () => {
1717
await TestBed
1818
.configureTestingModule({
19-
imports: [NoopAnimationsModule, inputModule, ReactiveFormsModule],
19+
imports: [NoopAnimationsModule, inputModule, FormsModule],
2020
declarations: [InputHarnessTest],
2121
})
2222
.compileComponents();
@@ -28,7 +28,7 @@ export function runHarnessTests(
2828

2929
it('should load all input harnesses', async () => {
3030
const inputs = await loader.getAllHarnesses(inputHarness);
31-
expect(inputs.length).toBe(5);
31+
expect(inputs.length).toBe(6);
3232
});
3333

3434
it('should load input with specific id', async () => {
@@ -49,37 +49,40 @@ export function runHarnessTests(
4949

5050
it('should be able to get id of input', async () => {
5151
const inputs = await loader.getAllHarnesses(inputHarness);
52-
expect(inputs.length).toBe(5);
52+
expect(inputs.length).toBe(6);
5353
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/);
5454
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/);
5555
expect(await inputs[2].getId()).toBe('myTextarea');
5656
expect(await inputs[3].getId()).toBe('nativeControl');
5757
expect(await inputs[4].getId()).toMatch(/mat-input-\d+/);
58+
expect(await inputs[5].getId()).toBe('has-ng-model');
5859
});
5960

6061
it('should be able to get name of input', async () => {
6162
const inputs = await loader.getAllHarnesses(inputHarness);
62-
expect(inputs.length).toBe(5);
63+
expect(inputs.length).toBe(6);
6364
expect(await inputs[0].getName()).toBe('favorite-food');
6465
expect(await inputs[1].getName()).toBe('');
6566
expect(await inputs[2].getName()).toBe('');
6667
expect(await inputs[3].getName()).toBe('');
6768
expect(await inputs[4].getName()).toBe('');
69+
expect(await inputs[5].getName()).toBe('has-ng-model');
6870
});
6971

7072
it('should be able to get value of input', async () => {
7173
const inputs = await loader.getAllHarnesses(inputHarness);
72-
expect(inputs.length).toBe(5);
74+
expect(inputs.length).toBe(6);
7375
expect(await inputs[0].getValue()).toBe('Sushi');
7476
expect(await inputs[1].getValue()).toBe('');
7577
expect(await inputs[2].getValue()).toBe('');
7678
expect(await inputs[3].getValue()).toBe('');
7779
expect(await inputs[4].getValue()).toBe('');
80+
expect(await inputs[5].getValue()).toBe('');
7881
});
7982

8083
it('should be able to set value of input', async () => {
8184
const inputs = await loader.getAllHarnesses(inputHarness);
82-
expect(inputs.length).toBe(5);
85+
expect(inputs.length).toBe(6);
8386
expect(await inputs[0].getValue()).toBe('Sushi');
8487
expect(await inputs[1].getValue()).toBe('');
8588
expect(await inputs[3].getValue()).toBe('');
@@ -98,13 +101,14 @@ export function runHarnessTests(
98101

99102
it('should be able to get disabled state', async () => {
100103
const inputs = await loader.getAllHarnesses(inputHarness);
101-
expect(inputs.length).toBe(5);
104+
expect(inputs.length).toBe(6);
102105

103106
expect(await inputs[0].isDisabled()).toBe(false);
104107
expect(await inputs[1].isDisabled()).toBe(false);
105108
expect(await inputs[2].isDisabled()).toBe(false);
106109
expect(await inputs[3].isDisabled()).toBe(false);
107110
expect(await inputs[4].isDisabled()).toBe(false);
111+
expect(await inputs[5].isDisabled()).toBe(false);
108112

109113
fixture.componentInstance.disabled = true;
110114

@@ -113,13 +117,14 @@ export function runHarnessTests(
113117

114118
it('should be able to get readonly state', async () => {
115119
const inputs = await loader.getAllHarnesses(inputHarness);
116-
expect(inputs.length).toBe(5);
120+
expect(inputs.length).toBe(6);
117121

118122
expect(await inputs[0].isReadonly()).toBe(false);
119123
expect(await inputs[1].isReadonly()).toBe(false);
120124
expect(await inputs[2].isReadonly()).toBe(false);
121125
expect(await inputs[3].isReadonly()).toBe(false);
122126
expect(await inputs[4].isReadonly()).toBe(false);
127+
expect(await inputs[5].isReadonly()).toBe(false);
123128

124129
fixture.componentInstance.readonly = true;
125130

@@ -128,13 +133,14 @@ export function runHarnessTests(
128133

129134
it('should be able to get required state', async () => {
130135
const inputs = await loader.getAllHarnesses(inputHarness);
131-
expect(inputs.length).toBe(5);
136+
expect(inputs.length).toBe(6);
132137

133138
expect(await inputs[0].isRequired()).toBe(false);
134139
expect(await inputs[1].isRequired()).toBe(false);
135140
expect(await inputs[2].isRequired()).toBe(false);
136141
expect(await inputs[3].isRequired()).toBe(false);
137142
expect(await inputs[4].isRequired()).toBe(false);
143+
expect(await inputs[5].isRequired()).toBe(false);
138144

139145
fixture.componentInstance.required = true;
140146

@@ -143,22 +149,24 @@ export function runHarnessTests(
143149

144150
it('should be able to get placeholder of input', async () => {
145151
const inputs = await loader.getAllHarnesses(inputHarness);
146-
expect(inputs.length).toBe(5);
152+
expect(inputs.length).toBe(6);
147153
expect(await inputs[0].getPlaceholder()).toBe('Favorite food');
148154
expect(await inputs[1].getPlaceholder()).toBe('');
149155
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment');
150156
expect(await inputs[3].getPlaceholder()).toBe('Native control');
151157
expect(await inputs[4].getPlaceholder()).toBe('');
158+
expect(await inputs[5].getPlaceholder()).toBe('');
152159
});
153160

154161
it('should be able to get type of input', async () => {
155162
const inputs = await loader.getAllHarnesses(inputHarness);
156-
expect(inputs.length).toBe(5);
163+
expect(inputs.length).toBe(6);
157164
expect(await inputs[0].getType()).toBe('text');
158165
expect(await inputs[1].getType()).toBe('number');
159166
expect(await inputs[2].getType()).toBe('textarea');
160167
expect(await inputs[3].getType()).toBe('text');
161168
expect(await inputs[4].getType()).toBe('textarea');
169+
expect(await inputs[5].getType()).toBe('text');
162170

163171
fixture.componentInstance.inputType = 'text';
164172

@@ -220,11 +228,17 @@ function getActiveElementTagName() {
220228
<option value="first">First</option>
221229
</select>
222230
</mat-form-field>
231+
232+
<mat-form-field>
233+
<input [(ngModel)]="ngModelValue" [name]="ngModelName" id="has-ng-model" matNativeControl>
234+
</mat-form-field>
223235
`
224236
})
225237
class InputHarnessTest {
226238
inputType = 'number';
227239
readonly = false;
228240
disabled = false;
229241
required = false;
242+
ngModelValue = '';
243+
ngModelName = 'has-ng-model';
230244
}

tools/public_api_guard/material/input.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
2626
focused: boolean;
2727
get id(): string;
2828
set id(value: string);
29+
name: string;
2930
ngControl: NgControl;
3031
placeholder: string;
3132
get readonly(): boolean;
@@ -58,7 +59,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
5859
static ngAcceptInputType_readonly: BooleanInput;
5960
static ngAcceptInputType_required: BooleanInput;
6061
static ngAcceptInputType_value: any;
61-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatInput, "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", ["matInput"], { "disabled": "disabled"; "id": "id"; "placeholder": "placeholder"; "required": "required"; "type": "type"; "errorStateMatcher": "errorStateMatcher"; "value": "value"; "readonly": "readonly"; }, {}, never>;
62+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<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"; "value": "value"; "readonly": "readonly"; }, {}, never>;
6263
static ɵfac: i0.ɵɵFactoryDef<MatInput, [null, null, { optional: true; self: true; }, { optional: true; }, { optional: true; }, null, { optional: true; self: true; }, null, null]>;
6364
}
6465

0 commit comments

Comments
 (0)