From adf9b5b9783f44d503ef68733bbbaaea816206ac Mon Sep 17 00:00:00 2001 From: mmalerba Date: Wed, 4 Oct 2017 15:46:10 -0700 Subject: [PATCH] docs(form-field): add docs, examples, and guide (#7424) --- .../creating-a-custom-form-field-control.md | 337 ++++++++++++++++++ src/lib/form-field/README.md | 1 + src/lib/form-field/form-field-control.ts | 2 +- src/lib/form-field/form-field.md | 129 +++++++ src/material-examples/example-module.ts | 56 +++ .../form-field-custom-control-example.css | 21 ++ .../form-field-custom-control-example.html | 7 + .../form-field-custom-control-example.ts | 137 +++++++ .../form-field-error-example.css | 8 + .../form-field-error-example.html | 6 + .../form-field-error-example.ts | 18 + .../form-field-hint-example.css | 8 + .../form-field-hint-example.html | 13 + .../form-field-hint-example.ts | 9 + .../form-field-overview-example.css | 8 + .../form-field-overview-example.html | 15 + .../form-field-overview-example.ts | 9 + .../form-field-placeholder-example.css | 20 ++ .../form-field-placeholder-example.html | 29 ++ .../form-field-placeholder-example.ts | 19 + .../form-field-prefix-suffix-example.css | 17 + .../form-field-prefix-suffix-example.html | 12 + .../form-field-prefix-suffix-example.ts | 11 + .../form-field-theming-example.css | 8 + .../form-field-theming-example.html | 14 + .../form-field-theming-example.ts | 23 ++ 26 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 guides/creating-a-custom-form-field-control.md create mode 100644 src/lib/form-field/README.md create mode 100644 src/lib/form-field/form-field.md create mode 100644 src/material-examples/form-field-custom-control/form-field-custom-control-example.css create mode 100644 src/material-examples/form-field-custom-control/form-field-custom-control-example.html create mode 100644 src/material-examples/form-field-custom-control/form-field-custom-control-example.ts create mode 100644 src/material-examples/form-field-error/form-field-error-example.css create mode 100644 src/material-examples/form-field-error/form-field-error-example.html create mode 100644 src/material-examples/form-field-error/form-field-error-example.ts create mode 100644 src/material-examples/form-field-hint/form-field-hint-example.css create mode 100644 src/material-examples/form-field-hint/form-field-hint-example.html create mode 100644 src/material-examples/form-field-hint/form-field-hint-example.ts create mode 100644 src/material-examples/form-field-overview/form-field-overview-example.css create mode 100644 src/material-examples/form-field-overview/form-field-overview-example.html create mode 100644 src/material-examples/form-field-overview/form-field-overview-example.ts create mode 100644 src/material-examples/form-field-placeholder/form-field-placeholder-example.css create mode 100644 src/material-examples/form-field-placeholder/form-field-placeholder-example.html create mode 100644 src/material-examples/form-field-placeholder/form-field-placeholder-example.ts create mode 100644 src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.css create mode 100644 src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.html create mode 100644 src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.ts create mode 100644 src/material-examples/form-field-theming/form-field-theming-example.css create mode 100644 src/material-examples/form-field-theming/form-field-theming-example.html create mode 100644 src/material-examples/form-field-theming/form-field-theming-example.ts diff --git a/guides/creating-a-custom-form-field-control.md b/guides/creating-a-custom-form-field-control.md new file mode 100644 index 00000000000..7cf5bb6bed4 --- /dev/null +++ b/guides/creating-a-custom-form-field-control.md @@ -0,0 +1,337 @@ +It is possible to create custom form field controls that can be used inside ``. This +can be useful if you need to create a component that shares a lot of common behavior with a form +field, but adds some additional logic. + +For example in this guide we'll learn how to create a custom input for inputting US telephone +numbers and hook it up to work with ``. Here is what we'll build by the end of this +guide: + + + +In order to learn how to build custom form field controls, let's start with a simple input component +that we want to work inside the form field. For example, a phone number input that segments the +parts of the number into their own inputs. (Note: this is not intended to be a robust component, +just a starting point for us to learn.) + +```ts +class MyTel { + constructor(public area: string, public exchange: string, public subscriber: string) {} +} + +@Component({ + selector: 'my-tel-input', + template: ` +
+ + – + + – + +
+ `, + styles: [` + div { + display: flex; + } + input { + border: none; + background: none; + padding: 0; + outline: none; + font: inherit; + text-align: center; + } + `], +}) +class MyTelInput { + parts: FormGroup; + + @Input() + get value(): MyTel | null { + let n = this.parts.value; + if (n.area.length == 3 && n.exchange.length == 3 && n.subscriber.length == 4) { + return new MyTel(n.area, n.exchange, n.subscriber); + } + return null; + } + set value(tel: MyTel | null) { + tel = tel || new MyTel('', '', ''); + this.parts.setValue({area: tel.area, exchange: tel.exchange, subscriber: tel.subscriber}); + } + + constructor(fb: FormBuilder) { + this.parts = fb.group({ + 'area': '', + 'exchange': '', + 'subscriber': '', + }); + } +} +``` + +### Providing our component as a MatFormFieldControl +The first step is to provide our new component as an implementation of the `MatFormFieldControl` +interface that the `` knows how to work with. To do this, we will have our class +implement `MatFormFieldControl`. Since this is a generic interface, we'll need to include a type +parameter indicating the type of data our control will work with, in this case `MyTel`. We then add +a provider to our component so that the form field will be able to inject it as a +`MatFormFieldControl`. + +```ts +@Component({ + ... + providers: [{provide: MatFormFieldControl, useExisting: MyTelInput}], +}) +class MyTelInput implements MatFormFieldControl { + ... +} +``` + +This sets up our component so it can work with ``, but now we need to implement the +various methods and properties declared by the interface we just implemented. To learn more about +the `MatFormFieldControl` interface, see its +[definition](https://github.com/angular/material2/blob/master/src/lib/form-field/form-field-control.ts). +(Unfortunately generated API docs are not available yet, but we'll go through the methods and +properties in this guide.) + +### Implementing the methods and properties of MatFormFieldControl +#### `value` +This property allows someone to set or get the value of our control. Its type should be the same +type we used for the type parameter when we implemented `MatFormFieldControl`. Since our component +already has a value property, we don't need to do anything for this one. + +#### `stateChanges` +Because the `` uses the `OnPush` change detection strategy, we need to let it know +when something happens in the form field control that may require the form field to run change +detection. We do this via the `stateChanges` property. So far the only thing the form field needs to +know about is when the value changes. We'll need to emit on the stateChanges stream when that +happens, and as we continue flushing out these properties we'll likely find more places we need to +emit. We should also make sure to complete `stateChanges` when our component is destroyed. + +```ts +stateChanges = new Subject(); + +set value(tel: MyTel | null) { + ... + this.stateChanges.next(); +} + +ngOnDestroy() { + this.stateChanges.complete(); +} +``` + +#### `id` +This property should return the ID of an element in the component's template that we want the +`` to associate all of its labels and hints with. In this case, we'll use the host +element and just generate a unique ID for it. + +```ts +static nextId = 0; + +@HostBinding() id = `my-tel-input-${MyTelInput.nextId++}`; +``` + +#### `placeholder` +This property allows us to tell the `` what to use as a placeholder. In this +example, we'll do the same thing as `matInput` and `` and allow the user to specify it +via an `@Input()`. Since the value of the placeholder may change over time, we need to make sure to +trigger change detection in the parent form field by emitting on the `stateChanges` stream when the +placeholder changes. + +```ts +@Input() +get placeholder() { + return this._placeholder; +} +set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); +} +private _placeholder: string; +``` + +#### `ngControl` +This property allows the form field control to specify the `@angular/forms` control that is bound to +this component. Since we haven't set up our component to act as a `ControlValueAccessor`, we'll just +set this to `null` in our component. In any real component, you would probably want to implement +`ControlValueAccessor` so that your component can work with `formControl` and `ngModel`. + +```ts +ngControl = null; +``` + +If you did implement `ControlValueAccessor`, you could simply inject the `NgControl` and make it +publicly available. (For additional information about `ControlValueAccessor` see the +[API docs](https://angular.io/api/forms/ControlValueAccessor).) + +```ts +constructor(..., @Optional() @Self() public ngControl: NgControl) { ... } +``` + +#### `focused` +This property indicates whether or not the form field control should be considered to be in a +focused state. When it is in a focused state, the form field is displayed with a solid color +underline. For the purposes of our component, we want to consider it focused if any of the part +inputs are focused. We can use the `FocusMonitor` from `@angular/cdk` to easily check this. We also +need to remember to emit on the `stateChanges` stream so change detection can happen. + +```ts +focused = false; + +constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef, + renderer: Renderer2) { + ... + fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => { + this.focused = !!origin; + this.stateChanges.next(); + }); +} + +ngOnDestroy() { + ... + this.fm.stopMonitoring(this.elRef.nativeElement); +} +``` + +#### `empty` +This property indicates whether the form field control is empty. For our control, we'll consider it +empty if all of the parts are empty. + +```ts +get empty() { + let n = this.parts.value; + return !n.area && !n.exchange && !n.subscriber; +} +``` + +#### `shouldPlaceholderFloat` +This property is used to indicate whether the placeholder should be in the floating position. We'll +use the same logic as `matInput` and float the placeholder when the input is focused or non-empty. +Since the placeholder will be overlapping our control when when it's not floating, we should hide +the `–` characters when it's not floating. + +```ts +@HostBinding('class.floating') +get shouldPlaceholderFloat() { + return this.focused || !this.empty; +} +``` +```css +span { + opacity: 0; + transition: opacity 200ms; +} +:host.floating span { + opacity: 1; +} +``` + +#### `required` +This property is used to indicate whether the input is required. `` uses this +information to add a required indicator to the placeholder. Again, we'll want to make sure we run +change detection if the required state changes. + +```ts +@Input() +get required() { + return this._required; +} +set required(req) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); +} +private _required = false; +``` + +#### `disabled` +This property tells the form field when it should be in the disabled state. In addition to reporting +the right state to the form field, we need to set the disabled state on the individual inputs that +make up our component. + +```ts +@Input() +get disabled() { + return this._disabled; +} +set disabled(dis) { + this._disabled = coerceBooleanProperty(dis); + this.stateChanges.next(); +} +private _disabled = false; +``` +```html + +– + +– + +``` + +#### `errorState` +This property indicates whether the associated `NgControl` is in an error state. Since we're not +using an `NgControl` in this example, we don't need to do anything other than just set it to `false`. + +```ts +errorState = false; +``` + +#### `controlType` +This property allows us to specify a unique string for the type of control in form field. The +`` will add an additional class based on this type that can be used to easily apply +special styles to a `` that contains a specific type of control. In this example +we'll use `my-tel-input` as our control type which will result in the form field adding the class +`mat-form-field-my-tel-input`. + +```ts +controlType = 'my-tel-input'; +``` + +#### `setAriaDescribedByIds(ids: string[])` +This method is used by the `` to specify the IDs that should be used for the +`aria-describedby` attribute of your component. The method has one parameter, the list of IDs, we +just need to apply the given IDs to our host element. + +```ts +@HostBinding('attr.aria-describedby') describedBy = ''; + +setDescribedByIds(ids: string[]) { + this.describedBy = ids.join(' '); +} +``` + +#### `onContainerClick(event: MouseEvent)` +This method will be called when the form field is clicked on. It allows your component to hook in +and handle that click however it wants. The method has one parameter, the `MouseEvent` for the +click. In our case we'll just focus the first `` if the user isn't about to click an +`` anyways. + +```ts +onContainerClick(event: MouseEvent) { + if ((event.target as Element).tagName.toLowerCase() != 'input') { + this.elRef.nativeElement.querySelector('input').focus(); + } +} +``` + +### Trying it out +Now that we've fully implemented the interface, we're ready to try our component out! All we need to +do is place it inside of a `` + +```html + + + +``` + +We also get all of the features that come with `` such as floating placeholder, +prefix, suffix, hints, and errors (if we've given the form field an `NgControl` and correctly report +the error state). + +```html + + + phone + Include area code + +``` diff --git a/src/lib/form-field/README.md b/src/lib/form-field/README.md new file mode 100644 index 00000000000..7a1234123cc --- /dev/null +++ b/src/lib/form-field/README.md @@ -0,0 +1 @@ +Please see the official documentation at https://material.angular.io/components/component/form-field diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts index f2f006421a4..c8d02dd6e97 100644 --- a/src/lib/form-field/form-field-control.ts +++ b/src/lib/form-field/form-field-control.ts @@ -13,7 +13,7 @@ import {NgControl} from '@angular/forms'; /** An interface which allows a control to work inside of a `MatFormField`. */ export abstract class MatFormFieldControl { /** The value of the control. */ - value: T; + value: T | null; /** * Stream that emits whenever the state of the control changes such that the parent `MatFormField` diff --git a/src/lib/form-field/form-field.md b/src/lib/form-field/form-field.md new file mode 100644 index 00000000000..7872e9c8ba0 --- /dev/null +++ b/src/lib/form-field/form-field.md @@ -0,0 +1,129 @@ +`` is a component used to wrap several Angular Material components and apply common +[Text field](https://material.io/guidelines/components/text-fields.html) styles such as the +underline, floating label, and hint messages. + +In this document, "form field" refers to the wrapper component `` and +"form field control" refers to the component that the `` is wrapping +(e.g. the input, textarea, select, etc.) + +The following Angular Material components are designed to work inside a ``: +* [<input matInput> & <textarea matInput>](https://material.angular.io/components/input/overview) +* [<mat-select>](https://material.angular.io/components/select/overview) +* [<mat-chip-list>](https://material.angular.io/components/chips/overview) + + + +### Floating placeholder +The floating placeholder is a text label displayed on top of the form field control when +the control does not contain any text. By default, when text is present the floating placeholder +floats above the form field control. + +Placeholder text can be specified using the `placeholder` property on the form field control, or +by adding a `` element inside the form field. Only one of these options should be +used, specifying both will raise an error. + +If the form field control is marked with a `required` attribute, an asterisk will be appended to the +placeholder to indicate the fact that it is a required field. If unwanted, this can be disabled by +setting the `hideRequiredMarker` property on `` + +The `floatPlaceholder` property of `` can be used to change this default floating +behavior. It can set to `never` to hide the placeholder instead of float it when text is present in +the form field control. It can be set to `always` to float the placeholder even when no text is +present in the form field control. It can also be set to `auto` to restore the default behavior. + + + +Global default placeholder options can be specified by setting providing a value for +`MAT_PLACEHOLDER_GLOBAL_OPTIONS` in your application's root module. Like the property, the global +setting can be either `always`, `never`, or `auto`. + +```ts +@NgModule({ + providers: [ + {provide: MAT_PLACEHOLDER_GLOBAL_OPTIONS, useValue: {float: 'always'}} + ] +}) +``` + +### Hint labels +Hint labels are additional descriptive text that appears below the form field's underline. A +`` can have up to two hint labels; one start-aligned (left in an LTR language, right +in RTL), and one end-aligned. + +Hint labels are specified in one of two ways: either by using the `hintLabel` property of +``, or by adding a `` element inside the form field. When adding a hint +via the `hintLabel` property, it will be treated as the start hint. Hints added via the +`` hint element can be added to either side by setting the `align` property on +`` to either `start` or `end`. Attempting to add multiple hints to the same side will +raise an error. + + + +### Error messages +Error messages can be shown under the form field underline by adding `mat-error` elements inside the +form field. Errors are hidden initially and will be displayed on invalid form fields after the user +has interacted with the element or the parent form has been submitted. Since the errors occupy the +same space as the hints, the hints are hidden when the errors are shown. + +If a form field can have more than one error state, it is up to the consumer to toggle which +messages should be displayed. This can be done with CSS, `ngIf` or `ngSwitch`. Multiple error +messages can be shown at the same time if desired, but the `` only reserves enough +space to display one error message at a time. Ensuring that enough space is available to display +multiple errors is up to the user. + + + +### Prefix & suffix +Custom content can be included before and after the input tag, as a prefix or suffix. It will be +included within the visual container that wraps the form control as per the Material specification. + +Adding the `matPrefix` directive to an element inside the `` will designate it as +the prefix. Similarly, adding `matSuffix` will designate it as the suffix. + + + +### Custom form field controls +In addition to the form field controls that Angular Material provides, it is possible to create +custom form field controls that work with `` in the same way. For additional +information on this see the guide on +[Creating Custom mat-form-field Controls](https://material.angular.io/guide/creating-a-custom-form-field-control). + +### Theming +`` has a `color` property which can be set to `primary`, `accent`, or `warn`. This +will set the color of the form field underline and floating placeholder based on the theme colors +of your app. + +`` inherits its `font-size` from its parent element. This can be overridden to an +explicit size using CSS. We recommend a specificity of at least 1 element + 1 class. + +```css +mat-form-field.mat-form-field { + font-size: 16px; +} +``` + + + +### Accessibility +If a floating placeholder is specified, it will be automatically used as the label for the form field +control. If no floating placeholder is specified, the user should label the form field control +themselves using `aria-label`, `aria-labelledby` or ` + + + + Option + + + diff --git a/src/material-examples/form-field-overview/form-field-overview-example.ts b/src/material-examples/form-field-overview/form-field-overview-example.ts new file mode 100644 index 00000000000..25200209782 --- /dev/null +++ b/src/material-examples/form-field-overview/form-field-overview-example.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +/** @title Simple form field */ +@Component({ + selector: 'form-field-overview-example', + templateUrl: 'form-field-overview-example.html', + styleUrls: ['form-field-overview-example.css'] +}) +export class FormFieldOverviewExample {} diff --git a/src/material-examples/form-field-placeholder/form-field-placeholder-example.css b/src/material-examples/form-field-placeholder/form-field-placeholder-example.css new file mode 100644 index 00000000000..333087a490e --- /dev/null +++ b/src/material-examples/form-field-placeholder/form-field-placeholder-example.css @@ -0,0 +1,20 @@ +.example-container { + display: flex; + flex-direction: column; +} + +.example-container > * { + width: 100%; +} + +.example-container form { + margin-bottom: 20px; +} + +.example-container form > * { + margin: 5px 0; +} + +.example-container .mat-radio-button { + margin: 0 5px; +} diff --git a/src/material-examples/form-field-placeholder/form-field-placeholder-example.html b/src/material-examples/form-field-placeholder/form-field-placeholder-example.html new file mode 100644 index 00000000000..01ef48d3431 --- /dev/null +++ b/src/material-examples/form-field-placeholder/form-field-placeholder-example.html @@ -0,0 +1,29 @@ +
+
+ Hide required marker +
+ + + Auto + Always + Never + +
+
+ + + + + + + + -- None -- + Option + + favorite Fancy placeholder + +
diff --git a/src/material-examples/form-field-placeholder/form-field-placeholder-example.ts b/src/material-examples/form-field-placeholder/form-field-placeholder-example.ts new file mode 100644 index 00000000000..8d21f1ebb24 --- /dev/null +++ b/src/material-examples/form-field-placeholder/form-field-placeholder-example.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; + +/** @title Form field with placeholder */ +@Component({ + selector: 'form-field-placeholder-example', + templateUrl: 'form-field-placeholder-example.html', + styleUrls: ['form-field-placeholder-example.css'] +}) +export class FormFieldPlaceholderExample { + options: FormGroup; + + constructor(fb: FormBuilder) { + this.options = fb.group({ + hideRequired: false, + floatPlaceholder: 'auto', + }); + } +} diff --git a/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.css b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.css new file mode 100644 index 00000000000..0a7a621eff5 --- /dev/null +++ b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.css @@ -0,0 +1,17 @@ +.example-container { + display: flex; + flex-direction: column; +} + +.example-container > * { + width: 100%; +} + +.example-right-align { + text-align: right; +} + +input.example-right-align::-webkit-outer-spin-button, +input.example-right-align::-webkit-inner-spin-button { + display: none; +} diff --git a/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.html b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.html new file mode 100644 index 00000000000..e3099d90680 --- /dev/null +++ b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.html @@ -0,0 +1,12 @@ +
+ + + {{hide ? 'visibility' : 'visibility_off'}} + + + + + $  + .00 + +
diff --git a/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.ts b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.ts new file mode 100644 index 00000000000..16b24e7b45e --- /dev/null +++ b/src/material-examples/form-field-prefix-suffix/form-field-prefix-suffix-example.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; + +/** @title Form field with prefix & suffix */ +@Component({ + selector: 'form-field-prefix-suffix-example', + templateUrl: 'form-field-prefix-suffix-example.html', + styleUrls: ['form-field-prefix-suffix-example.css'] +}) +export class FormFieldPrefixSuffixExample { + hide = true; +} diff --git a/src/material-examples/form-field-theming/form-field-theming-example.css b/src/material-examples/form-field-theming/form-field-theming-example.css new file mode 100644 index 00000000000..cfd4505c7c7 --- /dev/null +++ b/src/material-examples/form-field-theming/form-field-theming-example.css @@ -0,0 +1,8 @@ +.example-container { + display: flex; + flex-direction: column; +} + +.example-container > * { + width: 100%; +} diff --git a/src/material-examples/form-field-theming/form-field-theming-example.html b/src/material-examples/form-field-theming/form-field-theming-example.html new file mode 100644 index 00000000000..8545ebefd2a --- /dev/null +++ b/src/material-examples/form-field-theming/form-field-theming-example.html @@ -0,0 +1,14 @@ +
+ + + Primary + Accent + Warn + + + + + + Min size: 10px + +
diff --git a/src/material-examples/form-field-theming/form-field-theming-example.ts b/src/material-examples/form-field-theming/form-field-theming-example.ts new file mode 100644 index 00000000000..1e27dbc40b2 --- /dev/null +++ b/src/material-examples/form-field-theming/form-field-theming-example.ts @@ -0,0 +1,23 @@ +import {Component} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; + +/** @title Form field theming */ +@Component({ + selector: 'form-field-theming-example', + templateUrl: 'form-field-theming-example.html', + styleUrls: ['form-field-theming-example.css'] +}) +export class FormFieldThemingExample { + options: FormGroup; + + constructor(fb: FormBuilder) { + this.options = fb.group({ + 'color': 'primary', + 'fontSize': [16, Validators.min(10)], + }); + } + + getFontSize() { + return Math.max(10, this.options.value.fontSize); + } +}