Skip to content

Commit 0d43581

Browse files
authored
fix(input): changed after checked error if input has static placeholder (#20015)
The placeholder attribute is set based on a child query which is prone to "changed after checked" errors. These changes switch to assigning it ourselves directly to the DOM manually.
1 parent 7c49399 commit 0d43581

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

src/material/input/input.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,12 @@ describe('MatInput without forms', () => {
996996
}).not.toThrow();
997997
}));
998998

999+
it('should not throw when there is a default ngIf on the input element', fakeAsync(() => {
1000+
expect(() => {
1001+
createComponent(MatInputWithAnotherNgIf).detectChanges();
1002+
}).not.toThrow();
1003+
}));
1004+
9991005
});
10001006

10011007
describe('MatInput with forms', () => {
@@ -2251,3 +2257,18 @@ class CustomMatInputAccessor {
22512257
`
22522258
})
22532259
class MatInputWithDefaultNgIf {}
2260+
2261+
2262+
// Note that the DOM structure is slightly weird, but it's
2263+
// testing a specific g3 issue. See the discussion on #10466.
2264+
@Component({
2265+
template: `
2266+
<mat-form-field>
2267+
<mat-label>App name</mat-label>
2268+
<input matInput *ngIf="true" placeholder="My placeholder" [value]="inputValue">
2269+
</mat-form-field>
2270+
`
2271+
})
2272+
class MatInputWithAnotherNgIf {
2273+
inputValue = 'test';
2274+
}

src/material/input/input.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase =
7777
// Native input properties that are overwritten by Angular inputs need to be synced with
7878
// the native input element. Otherwise property bindings for those don't work.
7979
'[attr.id]': 'id',
80-
'[attr.placeholder]': '_getPlaceholderAttribute()',
8180
// At the time of writing, we have a lot of customer tests that look up the input based on its
8281
// placeholder. Since we sometimes omit the placeholder attribute from the DOM to prevent screen
8382
// readers from reading it twice, we have to keep it somewhere in the DOM for the lookup.
@@ -96,6 +95,8 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
9695
protected _uid = `mat-input-${nextUniqueId++}`;
9796
protected _previousNativeValue: any;
9897
private _inputValueAccessor: {value: any};
98+
private _previousPlaceholder: string | null;
99+
99100
/** The aria-describedby attribute on the input for improved a11y. */
100101
_ariaDescribedby: string;
101102

@@ -315,6 +316,10 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
315316
// we won't be notified when it changes (e.g. the consumer isn't using forms or they're
316317
// updating the value using `emitEvent: false`).
317318
this._dirtyCheckNativeValue();
319+
320+
// We need to dirty-check and set the placeholder attribute ourselves, because whether it's
321+
// present or not depends on a query which is prone to "changed after checked" errors.
322+
this._dirtyCheckPlaceholder();
318323
}
319324

320325
/** Focuses the input. */
@@ -354,14 +359,21 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
354359
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
355360
}
356361

357-
/** Determines the value of the native `placeholder` attribute that should be used in the DOM. */
358-
_getPlaceholderAttribute() {
362+
/** Does some manual dirty checking on the native input `placeholder` attribute. */
363+
private _dirtyCheckPlaceholder() {
359364
// If we're hiding the native placeholder, it should also be cleared from the DOM, otherwise
360365
// screen readers will read it out twice: once from the label and once from the attribute.
361366
// TODO: can be removed once we get rid of the `legacy` style for the form field, because it's
362367
// the only one that supports promoting the placeholder to a label.
363368
const formField = this._formField;
364-
return (!formField || !formField._hideControlPlaceholder()) ? this.placeholder : undefined;
369+
const placeholder =
370+
(!formField || !formField._hideControlPlaceholder()) ? this.placeholder : null;
371+
if (placeholder !== this._previousPlaceholder) {
372+
const element = this._elementRef.nativeElement;
373+
this._previousPlaceholder = placeholder;
374+
placeholder ?
375+
element.setAttribute('placeholder', placeholder) : element.removeAttribute('placeholder');
376+
}
365377
}
366378

367379
/** Does some manual dirty checking on the native input `value` property. */

tools/public_api_guard/material/input.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
4343
ngControl: NgControl, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, inputValueAccessor: any, _autofillMonitor: AutofillMonitor, ngZone: NgZone, _formField?: MatFormField | undefined);
4444
protected _dirtyCheckNativeValue(): void;
4545
_focusChanged(isFocused: boolean): void;
46-
_getPlaceholderAttribute(): string | undefined;
4746
protected _isBadInput(): boolean;
4847
protected _isNeverEmpty(): boolean;
4948
_onInput(): void;

0 commit comments

Comments
 (0)