Skip to content

Commit 6346987

Browse files
committed
feat(material/form-field): add color to default options (#24438)
closes #24438
1 parent 4c0a460 commit 6346987

File tree

6 files changed

+103
-51
lines changed

6 files changed

+103
-51
lines changed

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

Lines changed: 57 additions & 37 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,10 +184,17 @@ 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. */
178-
@Input() color: ThemePalette = 'primary';
189+
/** The color palette for the form field. */
190+
@Input()
191+
get color(): ThemePalette {
192+
return this._color;
193+
}
194+
set color(value: ThemePalette) {
195+
this._color = value || this._defaults?.color || 'primary';
196+
}
197+
private _color: ThemePalette = 'primary';
179198

180199
/** Whether the label should always float or float as the user types. */
181200
@Input()
@@ -185,23 +204,23 @@ export class MatFormField
185204
set floatLabel(value: FloatLabelType) {
186205
if (value !== this._floatLabel) {
187206
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.
207+
// For backwards compatibility. Custom form field controls or directives might set
208+
// the "floatLabel" input and expect the form field view to be updated automatically.
190209
// e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just
191210
// emit the "stateChanges" observable. TODO(devversion): consider removing.
192211
this._changeDetectorRef.markForCheck();
193212
}
194213
}
195214
private _floatLabel: FloatLabelType;
196215

197-
/** The form-field appearance style. */
216+
/** The form field appearance style. */
198217
@Input()
199218
get appearance(): MatFormFieldAppearance {
200219
return this._appearance;
201220
}
202221
set appearance(value: MatFormFieldAppearance) {
203222
const oldValue = this._appearance;
204-
this._appearance = value || (this._defaults && this._defaults.appearance) || DEFAULT_APPEARANCE;
223+
this._appearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE;
205224
if (this._appearance === 'outline' && this._appearance !== oldValue) {
206225
this._refreshOutlineNotchWidth();
207226

@@ -280,7 +299,7 @@ export class MatFormField
280299
// MDC text-field will call this method on focus, blur and value change. It expects us
281300
// to update the floating label state accordingly. Though we make this a noop because we
282301
// 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
302+
// adapter method would mean that the label would not update if the custom form field control
284303
// sets "shouldLabelFloat" to true, or if the "floatLabel" input binding changes to "always".
285304
floatLabel: () => {},
286305

@@ -294,7 +313,7 @@ export class MatFormField
294313
// closed. This works fine in the standard MDC text-field, but not in Angular where the
295314
// floating label could change through interpolation. We want to be able to update the
296315
// 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
316+
// blur to open and close the notch does not work for us since abstract form field controls
298317
// have the ability to control the floating label state (i.e. `shouldLabelFloat`), and we
299318
// want to update the notch whenever the `_shouldLabelFloat()` value changes.
300319
getLabelWidth: () => 0,
@@ -312,28 +331,28 @@ export class MatFormField
312331

313332
// The foundation tries to register events on the input. This is not matching
314333
// 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
334+
// in "stateChanges" based on the form field control state. The following events
316335
// need to be handled: focus, blur. We do not handle the "input" event since
317336
// 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
337+
// not implement as part of the form field, but should be implemented manually
319338
// by consumers using template bindings.
320339
registerInputInteractionHandler: () => {},
321340
deregisterInputInteractionHandler: () => {},
322341

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

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

334-
// The foundation tries to register click and keyboard events on the form-field to figure out
353+
// The foundation tries to register click and keyboard events on the form field to figure out
335354
// 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
355+
// to focus the input. Since we do not handle the input value as part of the form field, nor
337356
// it's guaranteed to be an input (see adapter methods above), this is a noop.
338357
deregisterTextFieldInteractionHandler: () => {},
339358
registerTextFieldInteractionHandler: () => {},
@@ -363,19 +382,20 @@ export class MatFormField
363382
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
364383
@Inject(DOCUMENT) private _document?: any,
365384
) {
366-
if (_defaults && _defaults.appearance) {
367-
this.appearance = _defaults.appearance;
385+
if (_defaults) {
386+
if (_defaults.appearance) {
387+
this.appearance = _defaults.appearance;
388+
}
389+
this._hideRequiredMarker = Boolean(_defaults?.hideRequiredMarker);
368390
}
369-
370-
this._hideRequiredMarker = _defaults?.hideRequiredMarker ?? false;
371391
}
372392

373393
ngAfterViewInit() {
374394
this._foundation = new MDCTextFieldFoundation(this._adapter);
375395

376396
// MDC uses the "shouldFloat" getter to know whether the label is currently floating. This
377397
// 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
398+
// For example, consumers can set "@Input floatLabel" to always, or the custom form field
379399
// control can set "MatFormFieldControl#shouldLabelFloat" to true. To ensure that MDC knows
380400
// when the label is floating, we overwrite the property to be based on the method we use to
381401
// determine the current state of the floating label.
@@ -386,11 +406,11 @@ export class MatFormField
386406
// By default, the foundation determines the validity of the text-field from the
387407
// specified native input. Since we don't pass a native input to the foundation because
388408
// abstract form controls are not necessarily consisting of an input, we handle the
389-
// text-field validity through the abstract form-field control state.
409+
// text-field validity through the abstract form field control state.
390410
this._foundation.isValid = () => !this._control.errorState;
391411

392412
// 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.
413+
// it in case the form field control has "focused" set to true on init.
394414
this._updateFocusState();
395415
// Initial notch width update. This is needed in case the text-field label floats
396416
// on initialization, and renders inside of the notched outline.
@@ -442,7 +462,7 @@ export class MatFormField
442462
}
443463

444464
/**
445-
* Gets an ElementRef for the element that a overlay attached to the form-field
465+
* Gets an ElementRef for the element that a overlay attached to the form field
446466
* should be positioned relative to.
447467
*/
448468
getConnectedOverlayOrigin(): ElementRef {
@@ -451,20 +471,20 @@ export class MatFormField
451471

452472
/** Animates the placeholder up and locks it in position. */
453473
_animateAndLockLabel(): void {
454-
// This is for backwards compatibility only. Consumers of the form-field might use
474+
// This is for backwards compatibility only. Consumers of the form field might use
455475
// 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
476+
// form field because setting "floatLabel" to "always" caused the label to float without
457477
// animation. This is different in MDC where the label always animates, so this method
458478
// is no longer necessary. There doesn't seem any benefit in adding logic to allow changing
459479
// the floating label state without animations. The non-MDC implementation was inconsistent
460480
// because it always animates if "floatLabel" is set away from "always".
461-
// TODO(devversion): consider removing this method when releasing the MDC form-field.
481+
// TODO(devversion): consider removing this method when releasing the MDC form field.
462482
if (this._hasFloatingLabel()) {
463483
this.floatLabel = 'always';
464484
}
465485
}
466486

467-
/** Initializes the registered form-field control. */
487+
/** Initializes the registered form field control. */
468488
private _initializeControl() {
469489
const control = this._control;
470490

@@ -499,7 +519,7 @@ export class MatFormField
499519
/** Initializes the prefix and suffix containers. */
500520
private _initializePrefixAndSuffix() {
501521
this._checkPrefixAndSuffixTypes();
502-
// Mark the form-field as dirty whenever the prefix or suffix children change. This
522+
// Mark the form field as dirty whenever the prefix or suffix children change. This
503523
// is necessary because we conditionally display the prefix/suffix containers based
504524
// on whether there is projected content.
505525
merge(this._prefixChildren.changes, this._suffixChildren.changes).subscribe(() => {
@@ -510,7 +530,7 @@ export class MatFormField
510530

511531
/**
512532
* 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
533+
* with the custom form field control. Also subscribes to hint and error changes in order
514534
* to be able to validate and synchronize ids on change.
515535
*/
516536
private _initializeSubscript() {
@@ -541,9 +561,9 @@ export class MatFormField
541561
private _updateFocusState() {
542562
// Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever
543563
// 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.
564+
// form field because we support abstract form field controls which are not necessarily
565+
// of type input, nor do we have a reference to a native form field control element. Instead
566+
// we handle the focus by checking if the abstract form field control focused state changes.
547567
if (this._control.focused && !this._isFocused) {
548568
this._isFocused = true;
549569
this._foundation.activateFocus();
@@ -556,7 +576,7 @@ export class MatFormField
556576
/**
557577
* The floating label in the docked state needs to account for prefixes. The horizontal offset
558578
* 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
579+
* form field is added to the DOM. This method sets up all subscriptions which are needed to
560580
* trigger the label offset update. In general, we want to avoid performing measurements often,
561581
* so we rely on the `NgZone` as indicator when the offset should be recalculated, instead of
562582
* checking every change detection cycle.
@@ -595,7 +615,7 @@ export class MatFormField
595615
/**
596616
* Whether the label should display in the infix. Labels in the outline appearance are
597617
* 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
618+
* form field prefix content. This won't work in server side rendering since we cannot
599619
* measure the width of the prefix container. To make the docked label appear as if the
600620
* right offset has been calculated, we forcibly render the label inside the infix. Since
601621
* the label is part of the infix, the label cannot overflow the prefix content.
@@ -729,7 +749,7 @@ export class MatFormField
729749
floatingLabel.style.transform = '';
730750
return;
731751
}
732-
// If the form-field is not attached to the DOM yet (e.g. in a tab), we defer
752+
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733753
// the label offset update until the zone stabilizes.
734754
if (!this._isAttachedToDom()) {
735755
this._needsOutlineLabelOffsetUpdateOnStable = true;

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

Lines changed: 12 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(MatInputWithColor, [
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();

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)