Skip to content

Commit e579181

Browse files
crisbetommalerba
authored andcommittedDec 6, 2018
fix(form-field): outline gap not being calculated when element starts off invisible (#13477)
Fixes the gaps for a `mat-form-field` with the `outline` appearance not being calculated properly if the element starts off as being invisible and then becomes visible later. Fixes #13328.
1 parent f7dcc27 commit e579181

File tree

2 files changed

+101
-16
lines changed

2 files changed

+101
-16
lines changed
 

‎src/lib/form-field/form-field.ts

+61-16
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
QueryList,
2727
ViewChild,
2828
ViewEncapsulation,
29+
OnDestroy,
2930
} from '@angular/core';
3031
import {
3132
CanColor, CanColorCtor,
@@ -34,8 +35,8 @@ import {
3435
MAT_LABEL_GLOBAL_OPTIONS,
3536
mixinColor,
3637
} from '@angular/material/core';
37-
import {fromEvent, merge} from 'rxjs';
38-
import {startWith, take} from 'rxjs/operators';
38+
import {fromEvent, merge, Subject} from 'rxjs';
39+
import {startWith, take, takeUntil} from 'rxjs/operators';
3940
import {MatError} from './error';
4041
import {matFormFieldAnimations} from './form-field-animations';
4142
import {MatFormFieldControl} from './form-field-control';
@@ -141,9 +142,18 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS =
141142
})
142143

143144
export class MatFormField extends _MatFormFieldMixinBase
144-
implements AfterContentInit, AfterContentChecked, AfterViewInit, CanColor {
145+
implements AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy, CanColor {
145146
private _labelOptions: LabelOptions;
146-
private _outlineGapCalculationNeeded = false;
147+
148+
/**
149+
* Whether the outline gap needs to be calculated
150+
* immediately on the next change detection run.
151+
*/
152+
private _outlineGapCalculationNeededImmediately = false;
153+
154+
/** Whether the outline gap needs to be calculated next time the zone has stabilized. */
155+
private _outlineGapCalculationNeededOnStable = false;
156+
private _destroyed = new Subject<void>();
147157

148158
/** The form-field appearance style. */
149159
@Input()
@@ -283,7 +293,19 @@ export class MatFormField extends _MatFormFieldMixinBase
283293

284294
// Run change detection if the value changes.
285295
if (control.ngControl && control.ngControl.valueChanges) {
286-
control.ngControl.valueChanges.subscribe(() => this._changeDetectorRef.markForCheck());
296+
control.ngControl.valueChanges
297+
.pipe(takeUntil(this._destroyed))
298+
.subscribe(() => this._changeDetectorRef.markForCheck());
299+
}
300+
301+
// @breaking-change 7.0.0 Remove this check once _ngZone is required. Also reconsider
302+
// whether the `ngAfterContentChecked` below is still necessary.
303+
if (this._ngZone) {
304+
this._ngZone.onStable.asObservable().pipe(takeUntil(this._destroyed)).subscribe(() => {
305+
if (this._outlineGapCalculationNeededOnStable) {
306+
this.updateOutlineGap();
307+
}
308+
});
287309
}
288310

289311
// Run change detection and update the outline if the suffix or prefix changes.
@@ -307,7 +329,7 @@ export class MatFormField extends _MatFormFieldMixinBase
307329

308330
ngAfterContentChecked() {
309331
this._validateControlChild();
310-
if (this._outlineGapCalculationNeeded) {
332+
if (this._outlineGapCalculationNeededImmediately) {
311333
this.updateOutlineGap();
312334
}
313335
}
@@ -318,6 +340,11 @@ export class MatFormField extends _MatFormFieldMixinBase
318340
this._changeDetectorRef.detectChanges();
319341
}
320342

343+
ngOnDestroy() {
344+
this._destroyed.next();
345+
this._destroyed.complete();
346+
}
347+
321348
/** Determines whether a class from the NgControl should be forwarded to the host element. */
322349
_shouldForward(prop: keyof NgControl): boolean {
323350
const ngControl = this._control ? this._control.ngControl : null;
@@ -468,19 +495,33 @@ export class MatFormField extends _MatFormFieldMixinBase
468495
// If the element is not present in the DOM, the outline gap will need to be calculated
469496
// the next time it is checked and in the DOM.
470497
if (!document.documentElement!.contains(this._elementRef.nativeElement)) {
471-
this._outlineGapCalculationNeeded = true;
498+
this._outlineGapCalculationNeededImmediately = true;
472499
return;
473500
}
474501

475502
let startWidth = 0;
476503
let gapWidth = 0;
477-
const startEls = this._connectionContainerRef.nativeElement.querySelectorAll(
478-
'.mat-form-field-outline-start');
479-
const gapEls = this._connectionContainerRef.nativeElement.querySelectorAll(
480-
'.mat-form-field-outline-gap');
504+
505+
const container = this._connectionContainerRef.nativeElement;
506+
const startEls = container.querySelectorAll('.mat-form-field-outline-start');
507+
const gapEls = container.querySelectorAll('.mat-form-field-outline-gap');
508+
481509
if (this._label && this._label.nativeElement.children.length) {
482-
const containerStart = this._getStartEnd(
483-
this._connectionContainerRef.nativeElement.getBoundingClientRect());
510+
const containerRect = container.getBoundingClientRect();
511+
512+
// If the container's width and height are zero, it means that the element is
513+
// invisible and we can't calculate the outline gap. Mark the element as needing
514+
// to be checked the next time the zone stabilizes. We can't do this immediately
515+
// on the next change detection, because even if the element becomes visible,
516+
// the `ClientRect` won't be reclaculated immediately. We reset the
517+
// `_outlineGapCalculationNeededImmediately` flag some we don't run the checks twice.
518+
if (containerRect.width === 0 && containerRect.height === 0) {
519+
this._outlineGapCalculationNeededOnStable = true;
520+
this._outlineGapCalculationNeededImmediately = false;
521+
return;
522+
}
523+
524+
const containerStart = this._getStartEnd(containerRect);
484525
const labelStart = this._getStartEnd(labelEl.children[0].getBoundingClientRect());
485526
let labelWidth = 0;
486527

@@ -498,19 +539,23 @@ export class MatFormField extends _MatFormFieldMixinBase
498539
gapEls.item(i).style.width = `${gapWidth}px`;
499540
}
500541

501-
this._outlineGapCalculationNeeded = false;
542+
this._outlineGapCalculationNeededOnStable =
543+
this._outlineGapCalculationNeededImmediately = false;
502544
}
503545

504546
/** Gets the start end of the rect considering the current directionality. */
505547
private _getStartEnd(rect: ClientRect): number {
506548
return this._dir && this._dir.value === 'rtl' ? rect.right : rect.left;
507549
}
508550

509-
/** Updates the outline gap the new time the zone stabilizes. */
551+
/**
552+
* Updates the outline gap the new time the zone stabilizes.
553+
* @breaking-change 7.0.0 Remove this method and only set the property once `_ngZone` is required.
554+
*/
510555
private _updateOutlineGapOnStable() {
511556
// @breaking-change 8.0.0 Remove this check and else block once _ngZone is required.
512557
if (this._ngZone) {
513-
this._ngZone.onStable.pipe(take(1)).subscribe(() => this.updateOutlineGap());
558+
this._outlineGapCalculationNeededOnStable = true;
514559
} else {
515560
Promise.resolve().then(() => this.updateOutlineGap());
516561
}

‎src/lib/input/input.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,35 @@ describe('MatInput with appearance', () => {
13611361
expect(outlineFixture.componentInstance.formField.updateOutlineGap).toHaveBeenCalled();
13621362
}));
13631363

1364+
it('should calculate the outline gaps if the element starts off invisible', fakeAsync(() => {
1365+
fixture.destroy();
1366+
TestBed.resetTestingModule();
1367+
1368+
let zone: MockNgZone;
1369+
const invisibleFixture = createComponent(MatInputWithOutlineInsideInvisibleElement, [{
1370+
provide: NgZone,
1371+
useFactory: () => zone = new MockNgZone()
1372+
}]);
1373+
1374+
invisibleFixture.detectChanges();
1375+
zone!.simulateZoneExit();
1376+
flush();
1377+
invisibleFixture.detectChanges();
1378+
1379+
const wrapperElement = invisibleFixture.nativeElement;
1380+
const formField = wrapperElement.querySelector('.mat-form-field');
1381+
const outlineStart = wrapperElement.querySelector('.mat-form-field-outline-start');
1382+
const outlineGap = wrapperElement.querySelector('.mat-form-field-outline-gap');
1383+
1384+
formField.style.display = '';
1385+
invisibleFixture.detectChanges();
1386+
zone!.simulateZoneExit();
1387+
flush();
1388+
invisibleFixture.detectChanges();
1389+
1390+
expect(parseInt(outlineStart.style.width)).toBeGreaterThan(0);
1391+
expect(parseInt(outlineGap.style.width)).toBeGreaterThan(0);
1392+
}));
13641393

13651394
});
13661395

@@ -1840,6 +1869,17 @@ class MatInputWithAppearanceAndLabel {
18401869
class MatInputWithoutPlaceholder {
18411870
}
18421871

1872+
@Component({
1873+
template: `
1874+
<mat-form-field appearance="outline" style="display: none;">
1875+
<mat-label>Label</mat-label>
1876+
<input matInput>
1877+
</mat-form-field>
1878+
`
1879+
})
1880+
class MatInputWithOutlineInsideInvisibleElement {}
1881+
1882+
18431883
// Styles to reset padding and border to make measurement comparisons easier.
18441884
const textareaStyleReset = `
18451885
textarea {

0 commit comments

Comments
 (0)
Please sign in to comment.