Skip to content

Commit b5f8248

Browse files
devversionmmalerba
authored andcommitted
fix(material-experimental/mdc-form-field): properly render notched-outline on the server
Currently, form fields which use the outline appearance and have an initial value, are not properly rendered on the server. This is because we don't create the MDC notched-outline component on the server, nor do we provide the proper initial notched-outline HTML markup. The initial markup that we render on the server, should include the information that the notch is initially open. This is done by adding the `mdc-notched-outline--notched` class based on the notched-outline `open` state. No longer relying on the focus/blur to update the notched-outline also fixes a bug where the label overlaps the closed outline-notch if the input value changes programmatically.
1 parent 99e83a6 commit b5f8248

File tree

4 files changed

+72
-34
lines changed

4 files changed

+72
-34
lines changed

src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
// the horizontal adjustment for the label is applied through inline styles, and we want to
101101
// make sure that the label can still float as expected. It should be okay using `!important`
102102
// because it's unlikely that developers commonly overwrite the floating label transform.
103-
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-floating-label--float-above {
103+
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded
104+
.mdc-floating-label--float-above {
104105
// This transform has been extracted from the MDC text-field styles. We can't access it
105106
// through a variable because MDC generates this label transform through a mixin.
106107
transform: translateY(-$mdc-text-field-outlined-label-position-y) scale(0.75) !important;

src/material-experimental/mdc-form-field/directives/notched-outline.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,43 @@ import {
1111
AfterViewInit,
1212
ChangeDetectionStrategy,
1313
Component,
14-
ElementRef, OnDestroy,
14+
ElementRef,
15+
Input,
16+
OnChanges,
17+
OnDestroy,
1518
ViewEncapsulation
1619
} from '@angular/core';
1720
import {MDCNotchedOutline} from '@material/notched-outline';
1821

1922
/**
20-
* Internal directive that creates an instance of the MDC notched-outline component. Using
23+
* Internal component that creates an instance of the MDC notched-outline component. Using
2124
* a directive allows us to conditionally render a notched-outline in the template without
2225
* having to manually create and destroy the `MDCNotchedOutline` component whenever the
2326
* appearance changes.
2427
*
25-
* The directive sets up the HTML structure and styles for the notched-outline, but also
26-
* exposes a programmatic API to toggle the state of the notch.
28+
* The component sets up the HTML structure and styles for the notched-outline. It provides
29+
* inputs to toggle the notch state and width.
2730
*/
2831
@Component({
2932
selector: 'div[matFormFieldNotchedOutline]',
3033
templateUrl: './notched-outline.html',
3134
host: {
3235
'class': 'mdc-notched-outline',
36+
// Besides updating the notch state through the MDC component, we toggle this class through
37+
// a host binding in order to ensure that the notched-outline renders correctly on the server.
38+
'[class.mdc-notched-outline--notched]': 'open',
3339
},
3440
changeDetection: ChangeDetectionStrategy.OnPush,
3541
encapsulation: ViewEncapsulation.None,
3642
})
37-
export class MatFormFieldNotchedOutline implements AfterViewInit, OnDestroy {
43+
export class MatFormFieldNotchedOutline implements AfterViewInit, OnChanges, OnDestroy {
44+
/** Width of the notch. */
45+
@Input('matFormFieldNotchedOutlineWidth') width: number = 0;
46+
47+
/** Whether the notch should be opened. */
48+
@Input('matFormFieldNotchedOutlineOpen') open: boolean = false;
49+
50+
/** Instance of the MDC notched outline. */
3851
private _mdcNotchedOutline: MDCNotchedOutline|null = null;
3952

4053
constructor(private _elementRef: ElementRef, private _platform: Platform) {}
@@ -51,25 +64,26 @@ export class MatFormFieldNotchedOutline implements AfterViewInit, OnDestroy {
5164
}
5265
}
5366

67+
ngOnChanges() {
68+
// Whenever the width, or the open state changes, sync the notched outline to be
69+
// based on the new values.
70+
this._syncNotchedOutlineState();
71+
}
72+
5473
ngOnDestroy() {
5574
if (this._mdcNotchedOutline !== null) {
5675
this._mdcNotchedOutline.destroy();
5776
}
5877
}
5978

60-
/**
61-
* Updates classes and styles to open the notch to the specified width.
62-
* @param notchWidth The notch width in the outline.
63-
*/
64-
notch(notchWidth: number) {
65-
if (this._mdcNotchedOutline !== null) {
66-
this._mdcNotchedOutline.notch(notchWidth);
79+
/** Synchronizes the notched outline state to be based on the `width` and `open` inputs. */
80+
private _syncNotchedOutlineState() {
81+
if (this._mdcNotchedOutline === null) {
82+
return;
6783
}
68-
}
69-
70-
/** Closes the notch. */
71-
closeNotch() {
72-
if (this._mdcNotchedOutline !== null) {
84+
if (this.open) {
85+
this._mdcNotchedOutline.notch(this.width);
86+
} else {
7387
this._mdcNotchedOutline.closeNotch();
7488
}
7589
}

src/material-experimental/mdc-form-field/form-field.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
read if it comes before the control in the DOM. -->
44
<label matFormFieldFloatingLabel [floating]="_shouldLabelFloat()"
55
*ngIf="_hasFloatingLabel()"
6-
(cdkObserveContent)="_rerenderOutlineNotch()"
6+
(cdkObserveContent)="_refreshOutlineNotchWidth()"
77
[cdkObserveContentDisabled]="!_hasOutline()"
88
[id]="_labelId"
99
[attr.for]="_control.id"
@@ -26,7 +26,9 @@
2626
[class.mdc-text-field--invalid]="_control.errorState"
2727
(click)="_control.onContainerClick && _control.onContainerClick($event)">
2828
<div class="mat-mdc-form-field-flex">
29-
<div matFormFieldNotchedOutline *ngIf="_hasOutline()">
29+
<div *ngIf="_hasOutline()" matFormFieldNotchedOutline
30+
[matFormFieldNotchedOutlineOpen]="_shouldLabelFloat()"
31+
[matFormFieldNotchedOutlineWidth]="_outlineNotchWidth">
3032
<ng-template [ngIf]="!_forceDisplayInfixLabel()">
3133
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
3234
</ng-template>

src/material-experimental/mdc-form-field/form-field.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ import {
4242
MatFormFieldControl,
4343
} from '@angular/material/form-field';
4444
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
45-
import {MDCTextFieldAdapter, MDCTextFieldFoundation} from '@material/textfield';
45+
import {
46+
MDCTextFieldAdapter,
47+
MDCTextFieldFoundation,
48+
numbers as mdcTextFieldNumbers
49+
} from '@material/textfield';
4650
import {merge, Subject} from 'rxjs';
4751
import {takeUntil} from 'rxjs/operators';
4852
import {MatError} from './directives/error';
@@ -197,6 +201,9 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
197201
/** State of the mat-hint and mat-error animations. */
198202
_subscriptAnimationState: string = '';
199203

204+
/** Width of the outline notch. */
205+
_outlineNotchWidth: number;
206+
200207
/** Gets the current form field control */
201208
get _control(): MatFormFieldControl<any> {
202209
return this._explicitFormFieldControl || this._formFieldControl;
@@ -229,16 +236,24 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
229236
// TODO(devversion): explore options on how to integrate label shaking.
230237
shakeLabel: () => {},
231238

232-
getLabelWidth: () => this._floatingLabel ? this._floatingLabel.getWidth() : 0,
233-
notchOutline: labelWidth => this._notchedOutline && this._notchedOutline.notch(labelWidth),
234-
closeOutline: () => this._notchedOutline && this._notchedOutline.closeNotch(),
239+
// MDC by default updates the notched-outline whenever the text-field receives focus, or
240+
// is being blurred. It also computes the label width every time the notch is opened or
241+
// closed. This works fine in the standard MDC text-field, but not in Angular where the
242+
// floating label could change through interpolation. We want to be able to update the
243+
// notched outline whenever the label content changes. Additionally, relying on focus or
244+
// blur to open and close the notch does not work for us since abstract form-field controls
245+
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
246+
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
247+
getLabelWidth: () => 0,
248+
notchOutline: () => {},
249+
closeOutline: () => {},
235250

236251
activateLineRipple: () => this._lineRipple && this._lineRipple.activate(),
237252
deactivateLineRipple: () => this._lineRipple && this._lineRipple.deactivate(),
238253

239254
// The foundation tries to register events on the input. This is not matching
240255
// our concept of abstract form field controls. We handle each event manually
241-
// in "ngDoCheck" based on the form-field control state. The following events
256+
// in "stateChanges" based on the form-field control state. The following events
242257
// need to be handled: focus, blur. We do not handle the "input" event since
243258
// that one is only needed for the text-field character count, which we do
244259
// not implement as part of the form-field, but should be implemented manually
@@ -310,8 +325,9 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
310325
// Initial focus state sync. This happens rarely, but we want to account for
311326
// it in case the form-field control has "focused" set to true on init.
312327
this._updateFocusState();
313-
// Initial notch update since we overwrote the "shouldFloat" getter.
314-
this._rerenderOutlineNotch();
328+
// Initial notch width update. This is needed in case the text-field label floats
329+
// on initialization, and renders inside of the notched outline.
330+
this._refreshOutlineNotchWidth();
315331
// Enable animations now. This ensures we don't animate on initial render.
316332
this._subscriptAnimationState = 'enter';
317333
}
@@ -462,12 +478,6 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
462478
.subscribe(() => this._needsOutlineLabelOffsetUpdateOnStable = true);
463479
}
464480

465-
_rerenderOutlineNotch() {
466-
if (this._floatingLabel && this._hasOutline()) {
467-
this._foundation.notchOutline(this._shouldLabelFloat());
468-
}
469-
}
470-
471481
/** Whether the floating label should always float or not. */
472482
_shouldAlwaysFloat() {
473483
return this.floatLabel === 'always';
@@ -486,7 +496,7 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
486496
* the label is part of the infix, the label cannot overflow the prefix content.
487497
*/
488498
_forceDisplayInfixLabel() {
489-
return !this._platform.isBrowser && this._prefixContainer && !this._shouldLabelFloat();
499+
return !this._platform.isBrowser && this._prefixChildren.length && !this._shouldLabelFloat();
490500
}
491501

492502
_hasFloatingLabel() {
@@ -509,6 +519,17 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
509519
this._control.errorState) ? 'error' : 'hint';
510520
}
511521

522+
/** Refreshes the width of the outline-notch, if present. */
523+
_refreshOutlineNotchWidth() {
524+
if (!this._hasOutline() || !this._floatingLabel) {
525+
return;
526+
}
527+
// The outline notch should be based on the label width, but needs to respect the scaling
528+
// applied to the label if it actively floats. Since the label always floats when the notch
529+
// is open, the MDC text-field floating label scaling is respected in notch width calculation.
530+
this._outlineNotchWidth = this._floatingLabel.getWidth() * mdcTextFieldNumbers.LABEL_SCALE;
531+
}
532+
512533
/** Does any extra processing that is required when handling the hints. */
513534
private _processHints() {
514535
this._validateHints();

0 commit comments

Comments
 (0)