Skip to content

Commit 9946bb3

Browse files
authored
feat(material/form-field): add color to default options (#24438) (#24440)
closes #24438
1 parent 1d5175b commit 9946bb3

File tree

6 files changed

+107
-50
lines changed

6 files changed

+107
-50
lines changed

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

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,18 @@ export type SubscriptSizing = 'fixed' | 'dynamic';
6969
* using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
7070
*/
7171
export interface MatFormFieldDefaultOptions {
72+
/** Default form field appearance style. */
7273
appearance?: MatFormFieldAppearance;
74+
/** Default color of the form field. */
75+
color?: ThemePalette;
76+
/** Whether the required marker should be hidden by default. */
7377
hideRequiredMarker?: boolean;
78+
/**
79+
* Whether the label for form fields should by default float `always`,
80+
* `never`, or `auto` (only when necessary).
81+
*/
7482
floatLabel?: FloatLabelType;
83+
/** Whether the form field should reserve space for one line by default. */
7584
subscriptSizing?: SubscriptSizing;
7685
}
7786

@@ -85,10 +94,13 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken<MatFormFieldDef
8594

8695
let nextUniqueId = 0;
8796

88-
/** Default appearance used by the form-field. */
97+
/** Default appearance used by the form field. */
8998
const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill';
9099

91-
/** Default appearance used by the form-field. */
100+
/**
101+
* Whether the label for form fields should by default float `always`,
102+
* `never`, or `auto`.
103+
*/
92104
const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto';
93105

94106
/** Default way that the subscript element height is set. */
@@ -147,7 +159,7 @@ const WRAPPER_HORIZONTAL_PADDING = 16;
147159
providers: [{provide: MAT_FORM_FIELD, useExisting: MatFormField}],
148160
})
149161
export class MatFormField
150-
implements AfterViewInit, OnDestroy, AfterContentChecked, AfterContentInit
162+
implements AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy
151163
{
152164
@ViewChild('textField') _textField: ElementRef<HTMLElement>;
153165
@ViewChild('iconPrefixContainer') _iconPrefixContainer: ElementRef<HTMLElement>;
@@ -172,9 +184,9 @@ export class MatFormField
172184
set hideRequiredMarker(value: BooleanInput) {
173185
this._hideRequiredMarker = coerceBooleanProperty(value);
174186
}
175-
private _hideRequiredMarker: boolean;
187+
private _hideRequiredMarker = false;
176188

177-
/** The color palette for the form-field. */
189+
/** The color palette for the form field. */
178190
@Input() color: ThemePalette = 'primary';
179191

180192
/** Whether the label should always float or float as the user types. */
@@ -185,23 +197,23 @@ export class MatFormField
185197
set floatLabel(value: FloatLabelType) {
186198
if (value !== this._floatLabel) {
187199
this._floatLabel = value;
188-
// For backwards compatibility. Custom form-field controls or directives might set
189-
// the "floatLabel" input and expect the form-field view to be updated automatically.
200+
// For backwards compatibility. Custom form field controls or directives might set
201+
// the "floatLabel" input and expect the form field view to be updated automatically.
190202
// e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just
191203
// emit the "stateChanges" observable. TODO(devversion): consider removing.
192204
this._changeDetectorRef.markForCheck();
193205
}
194206
}
195207
private _floatLabel: FloatLabelType;
196208

197-
/** The form-field appearance style. */
209+
/** The form field appearance style. */
198210
@Input()
199211
get appearance(): MatFormFieldAppearance {
200212
return this._appearance;
201213
}
202214
set appearance(value: MatFormFieldAppearance) {
203215
const oldValue = this._appearance;
204-
this._appearance = value || (this._defaults && this._defaults.appearance) || DEFAULT_APPEARANCE;
216+
this._appearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE;
205217
if (this._appearance === 'outline' && this._appearance !== oldValue) {
206218
this._refreshOutlineNotchWidth();
207219

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

@@ -294,7 +306,7 @@ export class MatFormField
294306
// closed. This works fine in the standard MDC text-field, but not in Angular where the
295307
// floating label could change through interpolation. We want to be able to update the
296308
// notched outline whenever the label content changes. Additionally, relying on focus or
297-
// blur to open and close the notch does not work for us since abstract form-field controls
309+
// blur to open and close the notch does not work for us since abstract form field controls
298310
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
299311
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
300312
getLabelWidth: () => 0,
@@ -312,28 +324,28 @@ export class MatFormField
312324

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

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

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

334-
// The foundation tries to register click and keyboard events on the form-field to figure out
346+
// The foundation tries to register click and keyboard events on the form field to figure out
335347
// if the input value changes through user interaction. Based on that, the foundation tries
336-
// to focus the input. Since we do not handle the input value as part of the form-field, nor
348+
// to focus the input. Since we do not handle the input value as part of the form field, nor
337349
// it's guaranteed to be an input (see adapter methods above), this is a noop.
338350
deregisterTextFieldInteractionHandler: () => {},
339351
registerTextFieldInteractionHandler: () => {},
@@ -363,19 +375,23 @@ export class MatFormField
363375
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
364376
@Inject(DOCUMENT) private _document?: any,
365377
) {
366-
if (_defaults && _defaults.appearance) {
367-
this.appearance = _defaults.appearance;
378+
if (_defaults) {
379+
if (_defaults.appearance) {
380+
this.appearance = _defaults.appearance;
381+
}
382+
this._hideRequiredMarker = Boolean(_defaults?.hideRequiredMarker);
383+
if (_defaults.color) {
384+
this.color = _defaults.color;
385+
}
368386
}
369-
370-
this._hideRequiredMarker = _defaults?.hideRequiredMarker ?? false;
371387
}
372388

373389
ngAfterViewInit() {
374390
this._foundation = new MDCTextFieldFoundation(this._adapter);
375391

376392
// MDC uses the "shouldFloat" getter to know whether the label is currently floating. This
377393
// does not match our implementation of when the label floats because we support more cases.
378-
// For example, consumers can set "@Input floatLabel" to always, or the custom form-field
394+
// For example, consumers can set "@Input floatLabel" to always, or the custom form field
379395
// control can set "MatFormFieldControl#shouldLabelFloat" to true. To ensure that MDC knows
380396
// when the label is floating, we overwrite the property to be based on the method we use to
381397
// determine the current state of the floating label.
@@ -386,11 +402,11 @@ export class MatFormField
386402
// By default, the foundation determines the validity of the text-field from the
387403
// specified native input. Since we don't pass a native input to the foundation because
388404
// abstract form controls are not necessarily consisting of an input, we handle the
389-
// text-field validity through the abstract form-field control state.
405+
// text-field validity through the abstract form field control state.
390406
this._foundation.isValid = () => !this._control.errorState;
391407

392408
// Initial focus state sync. This happens rarely, but we want to account for
393-
// it in case the form-field control has "focused" set to true on init.
409+
// it in case the form field control has "focused" set to true on init.
394410
this._updateFocusState();
395411
// Initial notch width update. This is needed in case the text-field label floats
396412
// on initialization, and renders inside of the notched outline.
@@ -442,7 +458,7 @@ export class MatFormField
442458
}
443459

444460
/**
445-
* Gets an ElementRef for the element that a overlay attached to the form-field
461+
* Gets an ElementRef for the element that a overlay attached to the form field
446462
* should be positioned relative to.
447463
*/
448464
getConnectedOverlayOrigin(): ElementRef {
@@ -451,20 +467,20 @@ export class MatFormField
451467

452468
/** Animates the placeholder up and locks it in position. */
453469
_animateAndLockLabel(): void {
454-
// This is for backwards compatibility only. Consumers of the form-field might use
470+
// This is for backwards compatibility only. Consumers of the form field might use
455471
// this method. e.g. the autocomplete trigger. This method has been added to the non-MDC
456-
// form-field because setting "floatLabel" to "always" caused the label to float without
472+
// form field because setting "floatLabel" to "always" caused the label to float without
457473
// animation. This is different in MDC where the label always animates, so this method
458474
// is no longer necessary. There doesn't seem any benefit in adding logic to allow changing
459475
// the floating label state without animations. The non-MDC implementation was inconsistent
460476
// because it always animates if "floatLabel" is set away from "always".
461-
// TODO(devversion): consider removing this method when releasing the MDC form-field.
477+
// TODO(devversion): consider removing this method when releasing the MDC form field.
462478
if (this._hasFloatingLabel()) {
463479
this.floatLabel = 'always';
464480
}
465481
}
466482

467-
/** Initializes the registered form-field control. */
483+
/** Initializes the registered form field control. */
468484
private _initializeControl() {
469485
const control = this._control;
470486

@@ -499,7 +515,7 @@ export class MatFormField
499515
/** Initializes the prefix and suffix containers. */
500516
private _initializePrefixAndSuffix() {
501517
this._checkPrefixAndSuffixTypes();
502-
// Mark the form-field as dirty whenever the prefix or suffix children change. This
518+
// Mark the form field as dirty whenever the prefix or suffix children change. This
503519
// is necessary because we conditionally display the prefix/suffix containers based
504520
// on whether there is projected content.
505521
merge(this._prefixChildren.changes, this._suffixChildren.changes).subscribe(() => {
@@ -510,7 +526,7 @@ export class MatFormField
510526

511527
/**
512528
* Initializes the subscript by validating hints and synchronizing "aria-describedby" ids
513-
* with the custom form-field control. Also subscribes to hint and error changes in order
529+
* with the custom form field control. Also subscribes to hint and error changes in order
514530
* to be able to validate and synchronize ids on change.
515531
*/
516532
private _initializeSubscript() {
@@ -541,9 +557,9 @@ export class MatFormField
541557
private _updateFocusState() {
542558
// Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever
543559
// certain DOM events are emitted. This is not possible in our implementation of the
544-
// form-field because we support abstract form field controls which are not necessarily
545-
// of type input, nor do we have a reference to a native form-field control element. Instead
546-
// we handle the focus by checking if the abstract form-field control focused state changes.
560+
// form field because we support abstract form field controls which are not necessarily
561+
// of type input, nor do we have a reference to a native form field control element. Instead
562+
// we handle the focus by checking if the abstract form field control focused state changes.
547563
if (this._control.focused && !this._isFocused) {
548564
this._isFocused = true;
549565
this._foundation.activateFocus();
@@ -556,7 +572,7 @@ export class MatFormField
556572
/**
557573
* The floating label in the docked state needs to account for prefixes. The horizontal offset
558574
* is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
559-
* form-field is added to the DOM. This method sets up all subscriptions which are needed to
575+
* form field is added to the DOM. This method sets up all subscriptions which are needed to
560576
* trigger the label offset update. In general, we want to avoid performing measurements often,
561577
* so we rely on the `NgZone` as indicator when the offset should be recalculated, instead of
562578
* checking every change detection cycle.
@@ -595,7 +611,7 @@ export class MatFormField
595611
/**
596612
* Whether the label should display in the infix. Labels in the outline appearance are
597613
* displayed as part of the notched-outline and are horizontally offset to account for
598-
* form-field prefix content. This won't work in server side rendering since we cannot
614+
* form field prefix content. This won't work in server side rendering since we cannot
599615
* measure the width of the prefix container. To make the docked label appear as if the
600616
* right offset has been calculated, we forcibly render the label inside the infix. Since
601617
* the label is part of the infix, the label cannot overflow the prefix content.
@@ -729,7 +745,7 @@ export class MatFormField
729745
floatingLabel.style.transform = '';
730746
return;
731747
}
732-
// If the form-field is not attached to the DOM yet (e.g. in a tab), we defer
748+
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733749
// the label offset update until the zone stabilizes.
734750
if (!this._isAttachedToDom()) {
735751
this._needsOutlineLabelOffsetUpdateOnStable = true;

src/material-experimental/mdc-input/input.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,18 @@ describe('MatFormField default options', () => {
14051405
expect(fixture.componentInstance.formField.appearance).toBe('outline');
14061406
});
14071407

1408+
it('should be able to change the default color', () => {
1409+
const fixture = createComponent(MatInputSimple, [
1410+
{
1411+
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
1412+
useValue: {color: 'accent'},
1413+
},
1414+
]);
1415+
fixture.detectChanges();
1416+
const formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
1417+
expect(formField.classList).toContain('mat-accent');
1418+
});
1419+
14081420
it('defaults subscriptSizing to false', () => {
14091421
const fixture = createComponent(MatInputWithSubscriptSizing);
14101422
fixture.detectChanges();
@@ -2019,3 +2031,12 @@ class MatInputInsideOutsideFormField {}
20192031
class MatInputWithRequiredFormControl {
20202032
formControl = new FormControl('', [Validators.required]);
20212033
}
2034+
2035+
@Component({
2036+
template: `
2037+
<mat-form-field>
2038+
<input matInput>
2039+
</mat-form-field>
2040+
`,
2041+
})
2042+
class MatInputSimple {}

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
ViewEncapsulation,
2929
OnDestroy,
3030
} from '@angular/core';
31-
import {CanColor, mixinColor} from '@angular/material/core';
31+
import {CanColor, mixinColor, ThemePalette} from '@angular/material/core';
3232
import {fromEvent, merge, Subject} from 'rxjs';
3333
import {startWith, take, takeUntil} from 'rxjs/operators';
3434
import {MAT_ERROR, MatError} from './error';
@@ -66,18 +66,22 @@ const _MatFormFieldBase = mixinColor(
6666
/** Possible appearance styles for the form field. */
6767
export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill' | 'outline';
6868

69-
/** Possible values for the "floatLabel" form-field input. */
69+
/** Possible values for the "floatLabel" form field input. */
7070
export type FloatLabelType = 'always' | 'never' | 'auto';
7171

7272
/**
7373
* Represents the default options for the form field that can be configured
7474
* using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
7575
*/
7676
export interface MatFormFieldDefaultOptions {
77+
/** Default form field appearance style. */
7778
appearance?: MatFormFieldAppearance;
79+
/** Default color of the form field. */
80+
color?: ThemePalette;
81+
/** Whether the required marker should be hidden by default. */
7882
hideRequiredMarker?: boolean;
7983
/**
80-
* Whether the label for form-fields should by default float `always`,
84+
* Whether the label for form fields should by default float `always`,
8185
* `never`, or `auto` (only when necessary).
8286
*/
8387
floatLabel?: FloatLabelType;
@@ -158,15 +162,15 @@ export class MatFormField
158162

159163
private readonly _destroyed = new Subject<void>();
160164

161-
/** The form-field appearance style. */
165+
/** The form field appearance style. */
162166
@Input()
163167
get appearance(): MatFormFieldAppearance {
164168
return this._appearance;
165169
}
166170
set appearance(value: MatFormFieldAppearance) {
167171
const oldValue = this._appearance;
168172

169-
this._appearance = value || (this._defaults && this._defaults.appearance) || 'legacy';
173+
this._appearance = value || this._defaults?.appearance || 'legacy';
170174

171175
if (this._appearance === 'outline' && oldValue !== value) {
172176
this._outlineGapCalculationNeededOnStable = true;
@@ -182,7 +186,7 @@ export class MatFormField
182186
set hideRequiredMarker(value: BooleanInput) {
183187
this._hideRequiredMarker = coerceBooleanProperty(value);
184188
}
185-
private _hideRequiredMarker: boolean;
189+
private _hideRequiredMarker = false;
186190

187191
/** Override for the logic that disables the label animation in certain cases. */
188192
private _showAlwaysAnimate = false;
@@ -282,9 +286,13 @@ export class MatFormField
282286
this._animationsEnabled = _animationMode !== 'NoopAnimations';
283287

284288
// Set the default through here so we invoke the setter on the first run.
285-
this.appearance = _defaults && _defaults.appearance ? _defaults.appearance : 'legacy';
286-
this._hideRequiredMarker =
287-
_defaults && _defaults.hideRequiredMarker != null ? _defaults.hideRequiredMarker : false;
289+
this.appearance = _defaults?.appearance || 'legacy';
290+
if (_defaults) {
291+
this._hideRequiredMarker = Boolean(_defaults.hideRequiredMarker);
292+
if (_defaults.color) {
293+
this.color = this.defaultColor = _defaults.color;
294+
}
295+
}
288296
}
289297

290298
/**
@@ -295,7 +303,7 @@ export class MatFormField
295303
}
296304

297305
/**
298-
* Gets an ElementRef for the element that a overlay attached to the form-field should be
306+
* Gets an ElementRef for the element that a overlay attached to the form field should be
299307
* positioned relative to.
300308
*/
301309
getConnectedOverlayOrigin(): ElementRef {

0 commit comments

Comments
 (0)