Skip to content

Commit 8c6b221

Browse files
committed
refactor(material-experimental/mdc-form-field): remove MDC adapter usage (#24945)
Refactors the MDC form field so that it doesn't use any of MDC's adapters. (cherry picked from commit c2eb11d)
1 parent 34dcd42 commit 8c6b221

File tree

6 files changed

+90
-165
lines changed

6 files changed

+90
-165
lines changed

src/material-experimental/mdc-form-field/BUILD.bazel

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ ng_module(
2222
"//src/material-experimental/mdc-core",
2323
"//src/material/form-field",
2424
"@npm//@angular/forms",
25-
"@npm//@material/line-ripple",
26-
"@npm//@material/textfield",
2725
"@npm//rxjs",
2826
],
2927
)

src/material-experimental/mdc-form-field/directives/floating-label.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {Directive, ElementRef, Input} from '@angular/core';
10-
import {ponyfill} from '@material/dom';
1110

1211
/**
1312
* Internal directive that maintains a MDC floating label. This directive does not
@@ -33,15 +32,38 @@ export class MatFormFieldFloatingLabel {
3332
/** Whether the label is floating. */
3433
@Input() floating: boolean = false;
3534

36-
constructor(private _elementRef: ElementRef) {}
35+
constructor(private _elementRef: ElementRef<HTMLElement>) {}
3736

3837
/** Gets the width of the label. Used for the outline notch. */
3938
getWidth(): number {
40-
return ponyfill.estimateScrollWidth(this._elementRef.nativeElement);
39+
return estimateScrollWidth(this._elementRef.nativeElement);
4140
}
4241

4342
/** Gets the HTML element for the floating label. */
4443
get element(): HTMLElement {
4544
return this._elementRef.nativeElement;
4645
}
4746
}
47+
48+
/**
49+
* Estimates the scroll width of an element.
50+
* via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts
51+
*/
52+
function estimateScrollWidth(element: HTMLElement): number {
53+
// Check the offsetParent. If the element inherits display: none from any
54+
// parent, the offsetParent property will be null (see
55+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
56+
// This check ensures we only clone the node when necessary.
57+
const htmlEl = element as HTMLElement;
58+
if (htmlEl.offsetParent !== null) {
59+
return htmlEl.scrollWidth;
60+
}
61+
62+
const clone = htmlEl.cloneNode(true) as HTMLElement;
63+
clone.style.setProperty('position', 'absolute');
64+
clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
65+
document.documentElement.appendChild(clone);
66+
const scrollWidth = clone.scrollWidth;
67+
clone.remove();
68+
return scrollWidth;
69+
}

src/material-experimental/mdc-form-field/directives/line-ripple.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, ElementRef, OnDestroy} from '@angular/core';
10-
import {MDCLineRipple} from '@material/line-ripple';
9+
import {Directive, ElementRef, NgZone, OnDestroy} from '@angular/core';
10+
11+
/** Class added when the line ripple is active. */
12+
const ACTIVATE_CLASS = 'mdc-line-ripple--active';
13+
14+
/** Class added when the line ripple is being deactivated. */
15+
const DEACTIVATING_CLASS = 'mdc-line-ripple--deactivating';
1116

1217
/**
1318
* Internal directive that creates an instance of the MDC line-ripple component. Using a
@@ -23,12 +28,33 @@ import {MDCLineRipple} from '@material/line-ripple';
2328
'class': 'mdc-line-ripple',
2429
},
2530
})
26-
export class MatFormFieldLineRipple extends MDCLineRipple implements OnDestroy {
27-
constructor(elementRef: ElementRef) {
28-
super(elementRef.nativeElement);
31+
export class MatFormFieldLineRipple implements OnDestroy {
32+
constructor(private _elementRef: ElementRef<HTMLElement>, ngZone: NgZone) {
33+
ngZone.runOutsideAngular(() => {
34+
_elementRef.nativeElement.addEventListener('transitionend', this._handleTransitionEnd);
35+
});
36+
}
37+
38+
activate() {
39+
const classList = this._elementRef.nativeElement.classList;
40+
classList.remove(DEACTIVATING_CLASS);
41+
classList.add(ACTIVATE_CLASS);
2942
}
3043

44+
deactivate() {
45+
this._elementRef.nativeElement.classList.add(DEACTIVATING_CLASS);
46+
}
47+
48+
private _handleTransitionEnd = (event: TransitionEvent) => {
49+
const classList = this._elementRef.nativeElement.classList;
50+
const isDeactivating = classList.contains(DEACTIVATING_CLASS);
51+
52+
if (event.propertyName === 'opacity' && isDeactivating) {
53+
classList.remove(ACTIVATE_CLASS, DEACTIVATING_CLASS);
54+
}
55+
};
56+
3157
ngOnDestroy() {
32-
this.destroy();
58+
this._elementRef.nativeElement.removeEventListener('transitionend', this._handleTransitionEnd);
3359
}
3460
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="mdc-notched-outline__leading"></div>
2-
<div class="mdc-notched-outline__notch">
2+
<div class="mdc-notched-outline__notch" [style.width]="_getNotchWidth()">
33
<ng-content></ng-content>
44
</div>
55
<div class="mdc-notched-outline__trailing"></div>

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

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,18 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Platform} from '@angular/cdk/platform';
109
import {
1110
AfterViewInit,
1211
ChangeDetectionStrategy,
1312
Component,
1413
ElementRef,
1514
Input,
16-
OnChanges,
17-
OnDestroy,
15+
NgZone,
1816
ViewEncapsulation,
1917
} from '@angular/core';
20-
import {MDCNotchedOutline} from '@material/notched-outline';
2118

2219
/**
23-
* Internal component that creates an instance of the MDC notched-outline component. Using
24-
* a directive allows us to conditionally render a notched-outline in the template without
25-
* having to manually create and destroy the `MDCNotchedOutline` component whenever the
26-
* appearance changes.
20+
* Internal component that creates an instance of the MDC notched-outline component.
2721
*
2822
* The component sets up the HTML structure and styles for the notched-outline. It provides
2923
* inputs to toggle the notch state and width.
@@ -40,53 +34,37 @@ import {MDCNotchedOutline} from '@material/notched-outline';
4034
changeDetection: ChangeDetectionStrategy.OnPush,
4135
encapsulation: ViewEncapsulation.None,
4236
})
43-
export class MatFormFieldNotchedOutline implements AfterViewInit, OnChanges, OnDestroy {
37+
export class MatFormFieldNotchedOutline implements AfterViewInit {
4438
/** Width of the notch. */
4539
@Input('matFormFieldNotchedOutlineWidth') width: number = 0;
4640

4741
/** Whether the notch should be opened. */
4842
@Input('matFormFieldNotchedOutlineOpen') open: boolean = false;
4943

50-
/** Instance of the MDC notched outline. */
51-
private _mdcNotchedOutline: MDCNotchedOutline | null = null;
44+
constructor(private _elementRef: ElementRef<HTMLElement>, private _ngZone: NgZone) {}
5245

53-
constructor(private _elementRef: ElementRef, private _platform: Platform) {}
46+
ngAfterViewInit(): void {
47+
const label = this._elementRef.nativeElement.querySelector<HTMLElement>('.mdc-floating-label');
48+
if (label) {
49+
this._elementRef.nativeElement.classList.add('mdc-notched-outline--upgraded');
5450

55-
ngAfterViewInit() {
56-
// The notched outline cannot be attached in the server platform. It schedules tasks
57-
// for the next browser animation frame and relies on element client rectangles to render
58-
// the outline notch. To avoid failures on the server, we just do not initialize it,
59-
// but the actual notched-outline styles will be still displayed.
60-
if (this._platform.isBrowser) {
61-
// The notch component relies on the view to be initialized. This means
62-
// that we cannot extend from the "MDCNotchedOutline".
63-
this._mdcNotchedOutline = MDCNotchedOutline.attachTo(this._elementRef.nativeElement);
64-
}
65-
// Initial sync in case state has been updated before view initialization.
66-
this._syncNotchedOutlineState();
67-
}
68-
69-
ngOnChanges() {
70-
// Whenever the width, or the open state changes, sync the notched outline to be
71-
// based on the new values.
72-
this._syncNotchedOutlineState();
73-
}
74-
75-
ngOnDestroy() {
76-
if (this._mdcNotchedOutline !== null) {
77-
this._mdcNotchedOutline.destroy();
51+
if (typeof requestAnimationFrame === 'function') {
52+
label.style.transitionDuration = '0s';
53+
this._ngZone.runOutsideAngular(() => {
54+
requestAnimationFrame(() => (label.style.transitionDuration = ''));
55+
});
56+
}
57+
} else {
58+
this._elementRef.nativeElement.classList.add('mdc-notched-outline--no-label');
7859
}
7960
}
8061

81-
/** Synchronizes the notched outline state to be based on the `width` and `open` inputs. */
82-
private _syncNotchedOutlineState() {
83-
if (this._mdcNotchedOutline === null) {
84-
return;
85-
}
62+
_getNotchWidth() {
8663
if (this.open) {
87-
this._mdcNotchedOutline.notch(this.width);
88-
} else {
89-
this._mdcNotchedOutline.closeNotch();
64+
const NOTCH_ELEMENT_PADDING = 8;
65+
return `${this.width > 0 ? this.width + NOTCH_ELEMENT_PADDING : 0}px`;
9066
}
67+
68+
return null;
9169
}
9270
}

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

Lines changed: 11 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ import {
3737
MatFormFieldControl,
3838
} from '@angular/material/form-field';
3939
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
40-
import {
41-
MDCTextFieldAdapter,
42-
MDCTextFieldFoundation,
43-
numbers as mdcTextFieldNumbers,
44-
} from '@material/textfield';
4540
import {merge, Subject} from 'rxjs';
4641
import {takeUntil} from 'rxjs/operators';
4742
import {MAT_ERROR, MatError} from './directives/error';
@@ -119,6 +114,9 @@ const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`;
119114
*/
120115
const WRAPPER_HORIZONTAL_PADDING = 16;
121116

117+
/** Amount by which to scale the label when the form field is focused. */
118+
const LABEL_SCALE = 0.75;
119+
122120
/** Container for form controls that applies Material Design styling and behavior. */
123121
@Component({
124122
selector: 'mat-form-field',
@@ -278,90 +276,7 @@ export class MatFormField
278276
private _destroyed = new Subject<void>();
279277
private _isFocused: boolean | null = null;
280278
private _explicitFormFieldControl: MatFormFieldControl<any>;
281-
private _foundation: MDCTextFieldFoundation;
282279
private _needsOutlineLabelOffsetUpdateOnStable = false;
283-
private _adapter: MDCTextFieldAdapter = {
284-
addClass: className => this._textField.nativeElement.classList.add(className),
285-
removeClass: className => this._textField.nativeElement.classList.remove(className),
286-
hasClass: className => this._textField.nativeElement.classList.contains(className),
287-
288-
hasLabel: () => this._hasFloatingLabel(),
289-
isFocused: () => this._control.focused,
290-
hasOutline: () => this._hasOutline(),
291-
292-
// MDC text-field will call this method on focus, blur and value change. It expects us
293-
// to update the floating label state accordingly. Though we make this a noop because we
294-
// want to react to floating label state changes through change detection. Relying on this
295-
// adapter method would mean that the label would not update if the custom form field control
296-
// sets "shouldLabelFloat" to true, or if the "floatLabel" input binding changes to "always".
297-
floatLabel: () => {},
298-
299-
// Label shaking is not supported yet. It will require a new API for form field
300-
// controls to trigger the shaking. This can be a feature in the future.
301-
// TODO(devversion): explore options on how to integrate label shaking.
302-
shakeLabel: () => {},
303-
304-
// MDC by default updates the notched-outline whenever the text-field receives focus, or
305-
// is being blurred. It also computes the label width every time the notch is opened or
306-
// closed. This works fine in the standard MDC text-field, but not in Angular where the
307-
// floating label could change through interpolation. We want to be able to update the
308-
// notched outline whenever the label content changes. Additionally, relying on focus or
309-
// blur to open and close the notch does not work for us since abstract form field controls
310-
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
311-
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
312-
getLabelWidth: () => 0,
313-
314-
// We don't use `setLabelRequired` as it relies on a mutation observer for determining
315-
// when the `required` state changes. This is not reliable and flexible enough for
316-
// our form field, as we support custom controls and detect the required state through
317-
// a public property in the abstract form control.
318-
setLabelRequired: () => {},
319-
notchOutline: () => {},
320-
closeOutline: () => {},
321-
322-
activateLineRipple: () => this._lineRipple && this._lineRipple.activate(),
323-
deactivateLineRipple: () => this._lineRipple && this._lineRipple.deactivate(),
324-
325-
// The foundation tries to register events on the input. This is not matching
326-
// our concept of abstract form field controls. We handle each event manually
327-
// in "stateChanges" based on the form field control state. The following events
328-
// need to be handled: focus, blur. We do not handle the "input" event since
329-
// that one is only needed for the text-field character count, which we do
330-
// not implement as part of the form field, but should be implemented manually
331-
// by consumers using template bindings.
332-
registerInputInteractionHandler: () => {},
333-
deregisterInputInteractionHandler: () => {},
334-
335-
// We do not have a reference to the native input since we work with abstract form field
336-
// controls. MDC needs a reference to the native input optionally to handle character
337-
// counting and value updating. These are both things we do not handle from within the
338-
// form field, so we can just return null.
339-
getNativeInput: () => null,
340-
341-
// This method will never be called since we do not have the ability to add event listeners
342-
// to the native input. This is because the form control is not necessarily an input, and
343-
// the form field deals with abstract form controls of any type.
344-
setLineRippleTransformOrigin: () => {},
345-
346-
// The foundation tries to register click and keyboard events on the form field to figure out
347-
// if the input value changes through user interaction. Based on that, the foundation tries
348-
// to focus the input. Since we do not handle the input value as part of the form field, nor
349-
// it's guaranteed to be an input (see adapter methods above), this is a noop.
350-
deregisterTextFieldInteractionHandler: () => {},
351-
registerTextFieldInteractionHandler: () => {},
352-
353-
// The foundation tries to setup a "MutationObserver" in order to watch for attributes
354-
// like "maxlength" or "pattern" to change. The foundation will update the validity state
355-
// based on that. We do not need this logic since we handle the validity through the
356-
// abstract form control instance.
357-
deregisterValidationAttributeChangeHandler: () => {},
358-
registerValidationAttributeChangeHandler: () => null as any,
359-
360-
// Used by foundation to dynamically remove aria-describedby when the hint text
361-
// is shown only on invalid state, which should not be applicable here.
362-
setInputAttr: () => undefined,
363-
removeInputAttr: () => undefined,
364-
};
365280

366281
constructor(
367282
private _elementRef: ElementRef,
@@ -387,24 +302,6 @@ export class MatFormField
387302
}
388303

389304
ngAfterViewInit() {
390-
this._foundation = new MDCTextFieldFoundation(this._adapter);
391-
392-
// MDC uses the "shouldFloat" getter to know whether the label is currently floating. This
393-
// does not match our implementation of when the label floats because we support more cases.
394-
// For example, consumers can set "@Input floatLabel" to always, or the custom form field
395-
// control can set "MatFormFieldControl#shouldLabelFloat" to true. To ensure that MDC knows
396-
// when the label is floating, we overwrite the property to be based on the method we use to
397-
// determine the current state of the floating label.
398-
Object.defineProperty(this._foundation, 'shouldFloat', {
399-
get: () => this._shouldLabelFloat(),
400-
});
401-
402-
// By default, the foundation determines the validity of the text-field from the
403-
// specified native input. Since we don't pass a native input to the foundation because
404-
// abstract form controls are not necessarily consisting of an input, we handle the
405-
// text-field validity through the abstract form field control state.
406-
this._foundation.isValid = () => !this._control.errorState;
407-
408305
// Initial focus state sync. This happens rarely, but we want to account for
409306
// it in case the form field control has "focused" set to true on init.
410307
this._updateFocusState();
@@ -445,7 +342,6 @@ export class MatFormField
445342
}
446343

447344
ngOnDestroy() {
448-
this._foundation?.destroy();
449345
this._destroyed.next();
450346
this._destroyed.complete();
451347
}
@@ -562,11 +458,16 @@ export class MatFormField
562458
// we handle the focus by checking if the abstract form field control focused state changes.
563459
if (this._control.focused && !this._isFocused) {
564460
this._isFocused = true;
565-
this._foundation.activateFocus();
461+
this._lineRipple?.activate();
566462
} else if (!this._control.focused && (this._isFocused || this._isFocused === null)) {
567463
this._isFocused = false;
568-
this._foundation.deactivateFocus();
464+
this._lineRipple?.deactivate();
569465
}
466+
467+
this._textField?.nativeElement.classList.toggle(
468+
'mdc-text-field--focused',
469+
this._control.focused,
470+
);
570471
}
571472

572473
/**
@@ -652,7 +553,7 @@ export class MatFormField
652553
// The outline notch should be based on the label width, but needs to respect the scaling
653554
// applied to the label if it actively floats. Since the label always floats when the notch
654555
// is open, the MDC text-field floating label scaling is respected in notch width calculation.
655-
this._outlineNotchWidth = this._floatingLabel.getWidth() * mdcTextFieldNumbers.LABEL_SCALE;
556+
this._outlineNotchWidth = this._floatingLabel.getWidth() * LABEL_SCALE;
656557
}
657558

658559
/** Does any extra processing that is required when handling the hints. */

0 commit comments

Comments
 (0)