Skip to content

perf(form-field): convert adapter to a class object #19982

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

Closed
wants to merge 4 commits into from
Closed
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
191 changes: 113 additions & 78 deletions src/material-experimental/mdc-form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,118 @@ const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto';
*/
const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`;

class TextFieldAdapter implements MDCTextFieldAdapter {

constructor(private _delegate: MatFormField) {}

addClass(className: string) {
this._delegate._textField.nativeElement.classList.add(className);
}

removeClass(className: string) {
this._delegate._textField.nativeElement.classList.remove(className);
}

hasClass(className: string) {
return this._delegate._textField.nativeElement.classList.contains(className);
}

hasLabel() {
return this._delegate._hasFloatingLabel();
}

isFocused() {
return this._delegate._control.focused;
}

hasOutline() {
return this._delegate._hasOutline();
}

// MDC text-field will call this method on focus, blur and value change. It expects us
// to update the floating label state accordingly. Though we make this a noop because we
// want to react to floating label state changes through change detection. Relying on this
// adapter method would mean that the label would not update if the custom form-field control
// sets "shouldLabelFloat" to true, or if the "floatLabel" input binding changes to "always".
floatLabel() {}

// Label shaking is not supported yet. It will require a new API for form field
// controls to trigger the shaking. This can be a feature in the future.
// TODO(devversion): explore options on how to integrate label shaking.
shakeLabel() {}

// MDC by default updates the notched-outline whenever the text-field receives focus, or
// is being blurred. It also computes the label width every time the notch is opened or
// closed. This works fine in the standard MDC text-field, but not in Angular where the
// floating label could change through interpolation. We want to be able to update the
// notched outline whenever the label content changes. Additionally, relying on focus or
// blur to open and close the notch does not work for us since abstract form-field controls
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
getLabelWidth() {
return 0;
}

// We don't use `setLabelRequired` as it relies on a mutation observer for determining
// when the `required` state changes. This is not reliable and flexible enough for
// our form field, as we support custom controls and detect the required state through
// a public property in the abstract form control.
setLabelRequired() {}

notchOutline() {}

closeOutline() {}

activateLineRipple() {
return this._delegate._lineRipple && this._delegate._lineRipple.activate();
}

deactivateLineRipple() {
return this._delegate._lineRipple && this._delegate._lineRipple.deactivate();
}

// The foundation tries to register events on the input. This is not matching
// our concept of abstract form field controls. We handle each event manually
// in "stateChanges" based on the form-field control state. The following events
// need to be handled: focus, blur. We do not handle the "input" event since
// that one is only needed for the text-field character count, which we do
// not implement as part of the form-field, but should be implemented manually
// by consumers using template bindings.
registerInputInteractionHandler() {}
deregisterInputInteractionHandler() {}

// We do not have a reference to the native input since we work with abstract form field
// controls. MDC needs a reference to the native input optionally to handle character
// counting and value updating. These are both things we do not handle from within the
// form-field, so we can just return null.
getNativeInput() {
return null;
}

// This method will never be called since we do not have the ability to add event listeners
// to the native input. This is because the form control is not necessarily an input, and
// the form field deals with abstract form controls of any type.
setLineRippleTransformOrigin() {}

// The foundation tries to register click and keyboard events on the form-field to figure out
// if the input value changes through user interaction. Based on that, the foundation tries
// to focus the input. Since we do not handle the input value as part of the form-field, nor
// it's guaranteed to be an input (see adapter methods above), this is a noop.
deregisterTextFieldInteractionHandler() {}

registerTextFieldInteractionHandler() {}

// The foundation tries to setup a "MutationObserver" in order to watch for attributes
// like "maxlength" or "pattern" to change. The foundation will update the validity state
// based on that. We do not need this logic since we handle the validity through the
// abstract form control instance.
deregisterValidationAttributeChangeHandler() {}

registerValidationAttributeChangeHandler() {
return null as any;
}
}

/** Container for form controls that applies Material Design styling and behavior. */
@Component({
selector: 'mat-form-field',
Expand Down Expand Up @@ -214,83 +326,6 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
private _explicitFormFieldControl: MatFormFieldControl<any>;
private _foundation: MDCTextFieldFoundation;
private _needsOutlineLabelOffsetUpdateOnStable = false;
private _adapter: MDCTextFieldAdapter = {
addClass: className => this._textField.nativeElement.classList.add(className),
removeClass: className => this._textField.nativeElement.classList.remove(className),
hasClass: className => this._textField.nativeElement.classList.contains(className),

hasLabel: () => this._hasFloatingLabel(),
isFocused: () => this._control.focused,
hasOutline: () => this._hasOutline(),

// MDC text-field will call this method on focus, blur and value change. It expects us
// to update the floating label state accordingly. Though we make this a noop because we
// want to react to floating label state changes through change detection. Relying on this
// adapter method would mean that the label would not update if the custom form-field control
// sets "shouldLabelFloat" to true, or if the "floatLabel" input binding changes to "always".
floatLabel: () => {},

// Label shaking is not supported yet. It will require a new API for form field
// controls to trigger the shaking. This can be a feature in the future.
// TODO(devversion): explore options on how to integrate label shaking.
shakeLabel: () => {},

// MDC by default updates the notched-outline whenever the text-field receives focus, or
// is being blurred. It also computes the label width every time the notch is opened or
// closed. This works fine in the standard MDC text-field, but not in Angular where the
// floating label could change through interpolation. We want to be able to update the
// notched outline whenever the label content changes. Additionally, relying on focus or
// blur to open and close the notch does not work for us since abstract form-field controls
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
getLabelWidth: () => 0,

// We don't use `setLabelRequired` as it relies on a mutation observer for determining
// when the `required` state changes. This is not reliable and flexible enough for
// our form field, as we support custom controls and detect the required state through
// a public property in the abstract form control.
setLabelRequired: () => {},
notchOutline: () => {},
closeOutline: () => {},

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

// The foundation tries to register events on the input. This is not matching
// our concept of abstract form field controls. We handle each event manually
// in "stateChanges" based on the form-field control state. The following events
// need to be handled: focus, blur. We do not handle the "input" event since
// that one is only needed for the text-field character count, which we do
// not implement as part of the form-field, but should be implemented manually
// by consumers using template bindings.
registerInputInteractionHandler: () => {},
deregisterInputInteractionHandler: () => {},

// We do not have a reference to the native input since we work with abstract form field
// controls. MDC needs a reference to the native input optionally to handle character
// counting and value updating. These are both things we do not handle from within the
// form-field, so we can just return null.
getNativeInput: () => null,

// This method will never be called since we do not have the ability to add event listeners
// to the native input. This is because the form control is not necessarily an input, and
// the form field deals with abstract form controls of any type.
setLineRippleTransformOrigin: () => {},

// The foundation tries to register click and keyboard events on the form-field to figure out
// if the input value changes through user interaction. Based on that, the foundation tries
// to focus the input. Since we do not handle the input value as part of the form-field, nor
// it's guaranteed to be an input (see adapter methods above), this is a noop.
deregisterTextFieldInteractionHandler: () => {},
registerTextFieldInteractionHandler: () => {},

// The foundation tries to setup a "MutationObserver" in order to watch for attributes
// like "maxlength" or "pattern" to change. The foundation will update the validity state
// based on that. We do not need this logic since we handle the validity through the
// abstract form control instance.
deregisterValidationAttributeChangeHandler: () => {},
registerValidationAttributeChangeHandler: () => null as any,
};

constructor(private _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
Expand All @@ -309,7 +344,7 @@ export class MatFormField implements AfterViewInit, OnDestroy, AfterContentCheck
}

ngAfterViewInit() {
this._foundation = new MDCTextFieldFoundation(this._adapter);
this._foundation = new MDCTextFieldFoundation(new TextFieldAdapter(this));

// MDC uses the "shouldFloat" getter to know whether the label is currently floating. This
// does not match our implementation of when the label floats because we support more cases.
Expand Down