Skip to content

fix(form-field): outline gap not being calculated when element starts off invisible #13477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 61 additions & 16 deletions src/lib/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
QueryList,
ViewChild,
ViewEncapsulation,
OnDestroy,
} from '@angular/core';
import {
CanColor, CanColorCtor,
Expand All @@ -34,8 +35,8 @@ import {
MAT_LABEL_GLOBAL_OPTIONS,
mixinColor,
} from '@angular/material/core';
import {fromEvent, merge} from 'rxjs';
import {startWith, take} from 'rxjs/operators';
import {fromEvent, merge, Subject} from 'rxjs';
import {startWith, take, takeUntil} from 'rxjs/operators';
import {MatError} from './error';
import {matFormFieldAnimations} from './form-field-animations';
import {MatFormFieldControl} from './form-field-control';
Expand Down Expand Up @@ -141,9 +142,18 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS =
})

export class MatFormField extends _MatFormFieldMixinBase
implements AfterContentInit, AfterContentChecked, AfterViewInit, CanColor {
implements AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy, CanColor {
private _labelOptions: LabelOptions;
private _outlineGapCalculationNeeded = false;

/**
* Whether the outline gap needs to be calculated
* immediately on the next change detection run.
*/
private _outlineGapCalculationNeededImmediately = false;

/** Whether the outline gap needs to be calculated next time the zone has stabilized. */
private _outlineGapCalculationNeededOnStable = false;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmalerba I'm not a fan of this extra flag, but it was necessary, because in some cases the resizing has to happen a little bit after change detection has run, otherwise the element may not be laid out yet. It may be worth considering removing the ngAfterContentChecked and only relying on NgZone.onStable, however I'm not sure whether it wouldn't cause users to see the wrong outline for a split second.

private _destroyed = new Subject<void>();

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

// Run change detection if the value changes.
if (control.ngControl && control.ngControl.valueChanges) {
control.ngControl.valueChanges.subscribe(() => this._changeDetectorRef.markForCheck());
control.ngControl.valueChanges
.pipe(takeUntil(this._destroyed))
.subscribe(() => this._changeDetectorRef.markForCheck());
}

// @breaking-change 7.0.0 Remove this check once _ngZone is required. Also reconsider
// whether the `ngAfterContentChecked` below is still necessary.
if (this._ngZone) {
this._ngZone.onStable.asObservable().pipe(takeUntil(this._destroyed)).subscribe(() => {
if (this._outlineGapCalculationNeededOnStable) {
this.updateOutlineGap();
}
});
}

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

ngAfterContentChecked() {
this._validateControlChild();
if (this._outlineGapCalculationNeeded) {
if (this._outlineGapCalculationNeededImmediately) {
this.updateOutlineGap();
}
}
Expand All @@ -318,6 +340,11 @@ export class MatFormField extends _MatFormFieldMixinBase
this._changeDetectorRef.detectChanges();
}

ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}

/** Determines whether a class from the NgControl should be forwarded to the host element. */
_shouldForward(prop: keyof NgControl): boolean {
const ngControl = this._control ? this._control.ngControl : null;
Expand Down Expand Up @@ -468,19 +495,33 @@ export class MatFormField extends _MatFormFieldMixinBase
// If the element is not present in the DOM, the outline gap will need to be calculated
// the next time it is checked and in the DOM.
if (!document.documentElement!.contains(this._elementRef.nativeElement)) {
this._outlineGapCalculationNeeded = true;
this._outlineGapCalculationNeededImmediately = true;
return;
}

let startWidth = 0;
let gapWidth = 0;
const startEls = this._connectionContainerRef.nativeElement.querySelectorAll(
'.mat-form-field-outline-start');
const gapEls = this._connectionContainerRef.nativeElement.querySelectorAll(
'.mat-form-field-outline-gap');

const container = this._connectionContainerRef.nativeElement;
const startEls = container.querySelectorAll('.mat-form-field-outline-start');
const gapEls = container.querySelectorAll('.mat-form-field-outline-gap');

if (this._label && this._label.nativeElement.children.length) {
const containerStart = this._getStartEnd(
this._connectionContainerRef.nativeElement.getBoundingClientRect());
const containerRect = container.getBoundingClientRect();

// If the container's width and height are zero, it means that the element is
// invisible and we can't calculate the outline gap. Mark the element as needing
// to be checked the next time the zone stabilizes. We can't do this immediately
// on the next change detection, because even if the element becomes visible,
// the `ClientRect` won't be reclaculated immediately. We reset the
// `_outlineGapCalculationNeededImmediately` flag some we don't run the checks twice.
if (containerRect.width === 0 && containerRect.height === 0) {
this._outlineGapCalculationNeededOnStable = true;
this._outlineGapCalculationNeededImmediately = false;
return;
}

const containerStart = this._getStartEnd(containerRect);
const labelStart = this._getStartEnd(labelEl.children[0].getBoundingClientRect());
let labelWidth = 0;

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

this._outlineGapCalculationNeeded = false;
this._outlineGapCalculationNeededOnStable =
this._outlineGapCalculationNeededImmediately = false;
}

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

/** Updates the outline gap the new time the zone stabilizes. */
/**
* Updates the outline gap the new time the zone stabilizes.
* @breaking-change 7.0.0 Remove this method and only set the property once `_ngZone` is required.
*/
private _updateOutlineGapOnStable() {
// @breaking-change 8.0.0 Remove this check and else block once _ngZone is required.
if (this._ngZone) {
this._ngZone.onStable.pipe(take(1)).subscribe(() => this.updateOutlineGap());
this._outlineGapCalculationNeededOnStable = true;
} else {
Promise.resolve().then(() => this.updateOutlineGap());
}
Expand Down
40 changes: 40 additions & 0 deletions src/lib/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,35 @@ describe('MatInput with appearance', () => {
expect(outlineFixture.componentInstance.formField.updateOutlineGap).toHaveBeenCalled();
}));

it('should calculate the outline gaps if the element starts off invisible', fakeAsync(() => {
fixture.destroy();
TestBed.resetTestingModule();

let zone: MockNgZone;
const invisibleFixture = createComponent(MatInputWithOutlineInsideInvisibleElement, [{
provide: NgZone,
useFactory: () => zone = new MockNgZone()
}]);

invisibleFixture.detectChanges();
zone!.simulateZoneExit();
flush();
invisibleFixture.detectChanges();

const wrapperElement = invisibleFixture.nativeElement;
const formField = wrapperElement.querySelector('.mat-form-field');
const outlineStart = wrapperElement.querySelector('.mat-form-field-outline-start');
const outlineGap = wrapperElement.querySelector('.mat-form-field-outline-gap');

formField.style.display = '';
invisibleFixture.detectChanges();
zone!.simulateZoneExit();
flush();
invisibleFixture.detectChanges();

expect(parseInt(outlineStart.style.width)).toBeGreaterThan(0);
expect(parseInt(outlineGap.style.width)).toBeGreaterThan(0);
}));

});

Expand Down Expand Up @@ -1820,6 +1849,17 @@ class MatInputWithAppearanceAndLabel {
class MatInputWithoutPlaceholder {
}

@Component({
template: `
<mat-form-field appearance="outline" style="display: none;">
<mat-label>Label</mat-label>
<input matInput>
</mat-form-field>
`
})
class MatInputWithOutlineInsideInvisibleElement {}


// Styles to reset padding and border to make measurement comparisons easier.
const textareaStyleReset = `
textarea {
Expand Down