+
+ Example
+
+ This field is required
+
+
+
+ Email
+
+
+ This field is required
+
+
+ Please enter a valid email address
+
+
+
+
+
With hint
+
+
+ Example
+
+ This field is required
+ Please type something here
+
+
+
+
+
+
With a custom error function
+
+ Example
+
+ This field is required
+
+
+
+
+
+
+ Prefix + Suffix
+
+
Text
+
+ Amount
+
+ $
+ .00
+
+
+
Icons
+
+ Amount
+
+ calendar_today
+ mode_edit
+
+
+
Icon buttons
+
+ Amount
+
+
+
+
+
+
+
+ Fill
+ Outline
+
+
+
+
+
+
+ Divider Colors
+
+
Input
+
+ Default color
+
+
+
+ Accent color
+
+
+
+ Warn color
+
+
+
+
Textarea
+
+ Default color
+
+
+
+ Accent color
+
+
+
+ Warn color
+
+
+
+
With error
+
+ Default color
+
+ This field is required
+
+
+ Accent color
+
+ This field is required
+
+
+ Warn color
+
+ This field is required
+
+
+
+
+
+ Hints
+
+
+
+
+
+ I favoritebold label
+
+
+ I also homeitalic hint labels
+
+
+
+
+
+ Show hint label with character count
+
+ {{hintLabelWithCharCount.value.length}}
+
+
+
+
+ Enter text to count
+
+
+ Enter some text to count how many characters are in it. The character count is shown on
+ the right.
+
+
+ Length: {{longHint.value.length}}
+
+
+
+
+ Check to change the color:
+
+
+
+
+
+ Check to make required:
+
+ {{requiredField ? 'Required field' : 'Not required field'}}
+
+
+
+
+ Check to hide the required marker:
+
+
+ {{hideRequiredMarker ? 'Required Without Marker' : 'Required With Marker'}}
+
+
+
+
+
+ Autosize enabled
+
+
+
+
+
+
+ Appearance
+
+
+ Fill appearance
+
+ This field is required
+ Please type something here
+
+
+
+ Outline appearance
+
+ This field is required
+ Please type something here
+
+
+
+
+
+ Fill appearance
+
+ This field is required
+ Please type something here
+
+
+
+
+ Outline appearance
+
+ This field is required
+ Please type something here
+
+
+
+
+
+
+ Autocomplete
+
+
+ Pick Number
+
+
+ {{option}}
+
+
+
+
+
+
+
+ Outline form field in a tab
+
+
+
+
+ Tab 1 input
+
+
+
+
+
+ Tab 2 input
+
+
+
+
+
+
diff --git a/src/dev-app/mdc-input/mdc-input-demo.scss b/src/dev-app/mdc-input/mdc-input-demo.scss
new file mode 100644
index 000000000000..4e7a58d008ec
--- /dev/null
+++ b/src/dev-app/mdc-input/mdc-input-demo.scss
@@ -0,0 +1,45 @@
+@import '../../cdk/text-field/text-field';
+
+.demo-basic {
+ padding: 0;
+}
+
+.demo-basic .mat-card-content {
+ padding: 16px;
+}
+
+.demo-horizontal-spacing {
+ margin: 0 12px;
+}
+
+.demo-full-width {
+ width: 100%;
+}
+
+.demo-card {
+ margin: 16px;
+
+ mat-card-content {
+ font-size: 16px;
+ }
+}
+
+.demo-text-align-end {
+ text-align: end;
+}
+
+.demo-textarea {
+ resize: none;
+ border: none;
+ overflow: auto;
+ padding: 0;
+ background: lightblue;
+}
+
+.demo-custom-autofill-style {
+ @include cdk-text-field-autofill-color(transparent, red);
+}
+
+.demo-rows {
+ width: 30px;
+}
diff --git a/src/dev-app/mdc-input/mdc-input-demo.ts b/src/dev-app/mdc-input/mdc-input-demo.ts
new file mode 100644
index 000000000000..518025d44048
--- /dev/null
+++ b/src/dev-app/mdc-input/mdc-input-demo.ts
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {ChangeDetectionStrategy, Component} from '@angular/core';
+import {FormControl, Validators} from '@angular/forms';
+import {ErrorStateMatcher, FloatLabelType} from '@angular/material/core';
+import {MatFormFieldAppearance} from '@angular/material-experimental/mdc-form-field';
+
+
+let max = 5;
+
+const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'mdc-input-demo',
+ templateUrl: 'mdc-input-demo.html',
+ styleUrls: ['mdc-input-demo.css'],
+})
+export class MdcInputDemo {
+ floatingLabel: FloatLabelType = 'auto';
+ color: boolean;
+ requiredField: boolean;
+ disableTextarea: boolean;
+ hideRequiredMarker: boolean;
+ ctrlDisabled = false;
+ textareaNgModelValue: string;
+ textareaAutosizeEnabled = false;
+ appearance: MatFormFieldAppearance = 'fill';
+ prefixSuffixAppearance: MatFormFieldAppearance = 'fill';
+ placeholderTestControl = new FormControl('', Validators.required);
+ options: string[] = ['One', 'Two', 'Three'];
+
+ name: string;
+ errorMessageExample1: string;
+ errorMessageExample2: string;
+ errorMessageExample3: string;
+ errorMessageExample4: string;
+ dividerColorExample1: string;
+ dividerColorExample2: string;
+ dividerColorExample3: string;
+ items: {value: number}[] = [
+ {value: 10},
+ {value: 20},
+ {value: 30},
+ {value: 40},
+ {value: 50},
+ ];
+ rows = 8;
+ formControl = new FormControl('hello', Validators.required);
+ emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
+ delayedFormControl = new FormControl('');
+ model = 'hello';
+ isAutofilled = false;
+ customAutofillStyle = true;
+
+ legacyAppearance: string;
+ standardAppearance: string;
+ fillAppearance: string;
+ outlineAppearance: string;
+
+ constructor() {
+ setTimeout(() => this.delayedFormControl.setValue('hello'), 100);
+ }
+
+ addABunch(n: number) {
+ for (let x = 0; x < n; x++) {
+ this.items.push({ value: ++max });
+ }
+ }
+
+ customErrorStateMatcher: ErrorStateMatcher = {
+ isErrorState: (control: FormControl | null) => {
+ if (control) {
+ const hasInteraction = control.dirty || control.touched;
+ const isInvalid = control.invalid;
+
+ return !!(hasInteraction && isInvalid);
+ }
+
+ return false;
+ }
+ };
+
+ togglePlaceholderTestValue() {
+ this.placeholderTestControl.setValue(this.placeholderTestControl.value === '' ? 'Value' : '');
+ }
+
+ togglePlaceholderTestTouched() {
+ this.placeholderTestControl.touched ?
+ this.placeholderTestControl.markAsUntouched() :
+ this.placeholderTestControl.markAsTouched();
+ }
+
+ parseNumber(value: string): number {
+ return Number(value);
+ }
+}
diff --git a/src/dev-app/system-config-tmpl.js b/src/dev-app/system-config-tmpl.js
index 4bccbd526fba..8c00c1b22e5f 100644
--- a/src/dev-app/system-config-tmpl.js
+++ b/src/dev-app/system-config-tmpl.js
@@ -121,7 +121,7 @@ var map = Object.assign({
'@material/tab-bar': '@material/tab-bar/dist/mdc.tabBar.js',
'@material/tab-indicator': '@material/tab-indicator/dist/mdc.tabIndicator.js',
'@material/tab-scroller': '@material/tab-scroller/dist/mdc.tabScroller.js',
- '@material/text-field': '@material/textfield/dist/mdc.textfield.js',
+ '@material/textfield': '@material/textfield/dist/mdc.textfield.js',
'@material/top-app-bar': '@material/top-app-bar/dist/mdc.topAppBar.js'
}, pathMapping);
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index 3dd94266f45c..80258a224e98 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -7,7 +7,9 @@ entryPoints = [
"mdc-checkbox/testing",
"mdc-chips",
"mdc-chips/testing",
+ "mdc-form-field",
"mdc-helpers",
+ "mdc-input",
"mdc-list",
"mdc-menu",
"mdc-menu/testing",
diff --git a/src/material-experimental/mdc-form-field/BUILD.bazel b/src/material-experimental/mdc-form-field/BUILD.bazel
new file mode 100644
index 000000000000..ae6ea1ee56c5
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/BUILD.bazel
@@ -0,0 +1,108 @@
+package(default_visibility = ["//visibility:public"])
+
+load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
+load(
+ "//tools:defaults.bzl",
+ "ng_e2e_test_library",
+ "ng_module",
+ "sass_binary",
+ "sass_library",
+)
+
+ng_module(
+ name = "mdc-form-field",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ assets = [":form_field_scss"] + glob(["**/*.html"]),
+ module_name = "@angular/material-experimental/mdc-form-field",
+ deps = [
+ "//src/cdk/observers",
+ "//src/material/core",
+ "//src/material/form-field",
+ "@npm//@angular/forms",
+ "@npm//@material/floating-label",
+ "@npm//@material/line-ripple",
+ "@npm//@material/textfield",
+ "@npm//rxjs",
+ ],
+)
+
+sass_library(
+ name = "mdc_form_field_scss_lib",
+ srcs = ["_mdc-form-field.scss"],
+ deps = [
+ ":form_field_partials",
+ "//src/cdk/a11y:a11y_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ ],
+)
+
+sass_binary(
+ name = "form_field_scss",
+ src = "form-field.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ ":form_field_partials",
+ ":mdc_form_field_scss",
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+# TODO(devversion): Import all of the individual Sass mixins once feature targeting is available
+# for MDC text-field, notched-outline, floating-label and line-ripple.
+sass_library(
+ name = "mdc_form_field_scss",
+ srcs = [
+ "@npm//:node_modules/@material/floating-label/mdc-floating-label.scss",
+ "@npm//:node_modules/@material/line-ripple/mdc-line-ripple.scss",
+ "@npm//:node_modules/@material/notched-outline/mdc-notched-outline.scss",
+ "@npm//:node_modules/@material/ripple/common.scss",
+ "@npm//:node_modules/@material/textfield/_functions.scss",
+ "@npm//:node_modules/@material/textfield/character-counter/mdc-text-field-character-counter.scss",
+ "@npm//:node_modules/@material/textfield/helper-text/mdc-text-field-helper-text.scss",
+ "@npm//:node_modules/@material/textfield/icon/mdc-text-field-icon.scss",
+ "@npm//:node_modules/@material/textfield/mdc-text-field.scss",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+sass_library(
+ name = "form_field_partials",
+ srcs = [
+ "_form-field-bottom-line.scss",
+ "_form-field-sizing.scss",
+ "_form-field-subscript.scss",
+ "_mdc-text-field-structure-overrides.scss",
+ "_mdc-text-field-textarea-overrides.scss",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+###########
+# Testing
+###########
+
+ng_e2e_test_library(
+ name = "e2e_test_sources",
+ srcs = glob(["**/*.e2e.spec.ts"]),
+ deps = [
+ "//src/cdk/testing/private/e2e",
+ ],
+)
+
+e2e_test_suite(
+ name = "e2e_tests",
+ deps = [
+ ":e2e_test_sources",
+ "//src/cdk/testing/private/e2e",
+ ],
+)
diff --git a/src/material-experimental/mdc-form-field/README.md b/src/material-experimental/mdc-form-field/README.md
new file mode 100644
index 000000000000..92c84ec0b6f6
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/README.md
@@ -0,0 +1 @@
+This is a placeholder for the MDC-based implementation of form-field.
diff --git a/src/material-experimental/mdc-form-field/_form-field-bottom-line.scss b/src/material-experimental/mdc-form-field/_form-field-bottom-line.scss
new file mode 100644
index 000000000000..765905659cff
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_form-field-bottom-line.scss
@@ -0,0 +1,36 @@
+@import 'form-field-sizing';
+@import '../mdc-helpers/mdc-helpers';
+
+@mixin _mat-form-field-bottom-line() {
+ // Bottom line for form-fields. MDC by default only has a bottom-line for inputs
+ // and textareas. This does not work for us because we support abstract form-field
+ // controls which might not render an input or textarea. Additionally, the default MDC
+ // bottom-line does only cover the width of the input, while we also want it to cover
+ // prefixes and suffixes.
+ .mat-mdc-form-field-bottom-line {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 1px;
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ }
+}
+
+@mixin _mat-form-field-bottom-line-theme() {
+ // Sets the color of the bottom-line. Our custom bottom-line is based on the default
+ // MDC bottom-line that only works for inputs and textareas (hence we need a custom
+ // bottom-line that works for all type of form-field controls). To replicate the
+ // appearance of the default MDC bottom-line, we use the same theming variables.
+ .mat-mdc-form-field-bottom-line {
+ @include mdc-theme-prop(border-bottom-color, $mdc-text-field-bottom-line-idle);
+
+ .mdc-text-field--disabled & {
+ @include mdc-theme-prop(border-bottom-color, $mdc-text-field-disabled-border);
+ }
+
+ .mdc-text-field--invalid & {
+ @include mdc-theme-prop(border-bottom-color, $mdc-text-field-error);
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/_form-field-sizing.scss b/src/material-experimental/mdc-form-field/_form-field-sizing.scss
new file mode 100644
index 000000000000..91961a0487d6
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_form-field-sizing.scss
@@ -0,0 +1,17 @@
+@import '@material/textfield/variables';
+
+// Top spacing of the form-field outline. MDC does not have a variable for this
+// and just hard-codes it into their styles.
+$mat-form-field-outline-top-spacing: 12px;
+
+// Baseline based on the default height of the MDC text-field.
+$mat-form-field-baseline: $mdc-text-field-height / 2;
+
+// Infix stretches to fit the container, but naturally wants to be this wide. We set
+// this in order to have a a consistent natural size for the various types of controls
+// that can go in a form field.
+$mat-form-field-default-infix-width: 180px !default;
+
+// Minimum amount of space between start and end hints in the subscript. MDC does not
+// have built-in support for hints.
+$mat-form-field-hint-min-space: 1em !default;
diff --git a/src/material-experimental/mdc-form-field/_form-field-subscript.scss b/src/material-experimental/mdc-form-field/_form-field-subscript.scss
new file mode 100644
index 000000000000..2e8af414919a
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_form-field-subscript.scss
@@ -0,0 +1,67 @@
+@import 'form-field-sizing';
+@import '../mdc-helpers/mdc-helpers';
+
+@mixin _mat-form-field-subscript() {
+ // Wrapper for the hints and error messages.
+ .mat-mdc-form-field-subscript-wrapper {
+ box-sizing: border-box;
+ width: 100%;
+ // prevents multi-line errors from overlapping the control.
+ overflow: hidden;
+ }
+
+ // Scale down icons in the subscript to be the same size as the text.
+ .mat-mdc-form-field-subscript-wrapper .mat-icon {
+ width: 1em;
+ height: 1em;
+ font-size: inherit;
+ vertical-align: baseline;
+ }
+
+ // Clears the floats on the hints. This is necessary for the hint animation to work.
+ .mat-mdc-form-field-hint-wrapper {
+ display: flex;
+ }
+
+ // Spacer used to make sure start and end hints have enough space between them.
+ .mat-mdc-form-field-hint-spacer {
+ flex: 1 0 $mat-form-field-hint-min-space;
+ }
+
+ // Single error message displayed beneath the form field underline.
+ .mat-mdc-form-field-error {
+ display: block;
+ }
+}
+
+@mixin _mat-form-field-subscript-theme() {
+ // MDC does not have built-in error treatment.
+ .mat-mdc-form-field-error {
+ @include mdc-theme-prop(color, $mdc-text-field-error);
+ }
+}
+
+@mixin _mat-form-field-subscript-typography($config) {
+ // The unit-less line-height from the font config.
+ $line-height: mat-line-height($config, input);
+ // The amount to scale the font for the subscript.
+ $subscript-font-scale: 0.75;
+ // Font size to use for the subscript text.
+ $subscript-font-size: $subscript-font-scale * 100%;
+ // The space between the bottom of the text-field area and the subscript. Mocks in the spec show
+ // half of the text size, but this margin is applied to an element with the subscript text font
+ // size, so we need to divide by the scale factor to make it half of the original text size.
+ $subscript-margin-top: 0.5em / $subscript-font-scale;
+ // The minimum height applied to the subscript to reserve space for subscript text. This is a
+ // combination of the subscript's margin and line-height, but we need to multiply by the
+ // subscript font scale factor since the subscript has a reduced font size.
+ $subscript-min-height: ($subscript-margin-top + $line-height) * $subscript-font-scale;
+
+ // The subscript wrapper has a minimum height to avoid that the form-field
+ // jumps when hints or errors are displayed.
+ .mat-mdc-form-field-subscript-wrapper {
+ min-height: $subscript-min-height;
+ font-size: $subscript-font-size;
+ margin-top: $subscript-margin-top;
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/_mdc-form-field.scss b/src/material-experimental/mdc-form-field/_mdc-form-field.scss
new file mode 100644
index 000000000000..b68d267f42ea
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_mdc-form-field.scss
@@ -0,0 +1,17 @@
+@import '@material/textfield/variables';
+@import '../mdc-helpers/mdc-helpers';
+@import 'form-field-subscript';
+@import 'form-field-bottom-line';
+
+@mixin mat-form-field-theme-mdc($theme) {
+ @include mat-using-mdc-theme($theme) {
+ @include _mat-form-field-subscript-theme();
+ @include _mat-form-field-bottom-line-theme();
+ }
+}
+
+@mixin mat-form-field-typography-mdc($config) {
+ @include mat-using-mdc-typography($config) {
+ @include _mat-form-field-subscript-typography($config);
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss b/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss
new file mode 100644
index 000000000000..f5df7d8d385e
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_mdc-text-field-structure-overrides.scss
@@ -0,0 +1,80 @@
+@import 'form-field-sizing';
+
+// Mixin that can be included to override the default MDC text-field
+// styles to fit our needs. See individual comments for context on why
+// certain MDC styles need to be modified.
+@mixin _mat-mdc-text-field-structure-overrides() {
+ // Always hide the asterisk displayed by MDC. This is necessary because MDC can only display
+ // the asterisk if the label is directly preceded by the input. MDC does this because it
+ // relies on CSS to figure out if the input/textarea is required. This does not apply for us
+ // because it's not guaranteed that the form control is an input/textarea. The required state
+ // is handled as part of the registered form-field control instance. The asterisk will be
+ // rendered conditionally through the floating label.
+ .mat-mdc-form-field .mdc-floating-label::after {
+ display: none;
+ }
+
+ .mat-mdc-input-element {
+ // By default, MDC limits the height of a form-field to the height of an input, and
+ // overwrites the height to "auto" if a textarea is used. As mentioned in the comment
+ // above for the floating label, we do not know what type of control is used, so we
+ // shift the fixed height to the actual input element and let the form-field adjust
+ // based on the needed height. This makes it work for both input and textarea.
+ height: $mdc-text-field-height;
+
+ // Unset the border set by MDC. We move the border (which serves as the Material Design
+ // text-field bottom line) into its own element. This is necessary because we want the
+ // bottom-line to span across the whole form-field (including prefixes and suffixes).
+ border: none;
+ }
+
+ // Root element of the mdc-text-field. As explained in the height overwrites above, MDC
+ // sets a default height on the text-field root element. This is not desired since we
+ // want the element to be able to expand as needed.
+ .mat-mdc-text-field-wrapper {
+ height: auto;
+ flex: auto;
+ }
+
+ // The default MDC text-field implementation does not support labels which always float.
+ // MDC only renders the placeholder if the input is focused. We extend this to show the
+ // placeholder if the form-field label is set to always float.
+ // TODO(devversion): consider getting a mixin or variables for this (currently not available).
+ // Stylelint no-prefixes rule disabled because MDC text-field only uses "::placeholder" too.
+ /* stylelint-disable-next-line material/no-prefixes */
+ .mat-mdc-form-field-label-always-float .mdc-text-field__input::placeholder {
+ transition-delay: 40ms;
+ transition-duration: 110ms;
+ opacity: 1;
+ }
+
+ // The additional nesting is a temporary until the notched-outline is decoupled from the
+ // floating label. See https://github.com/material-components/material-components-web/issues/5326
+ // TODO(devversion): Remove this workaround/nesting once the feature is available.
+ .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
+ // Reset horizontal spacing of the input. This is necessary because we move the horizontal
+ // spacing to the form-field flex container. We do this because prefixes and suffixes should
+ // adjoin the actual control infix. The spacing is still needed for outline and fill
+ // appearances and should surround the prefixes, suffixes and infix. Note that we need
+ // increased specificity because MDC has high specificity for the default padding styles.
+ .mat-mdc-input-element {
+ padding: {
+ left: 0;
+ right: 0;
+ }
+ }
+
+ // We removed the horizontal inset on input elements, but need to re-add the spacing to
+ // the actual form-field flex container that contains the prefixes, suffixes and infix.
+ .mat-mdc-form-field-flex {
+ padding: 0 $mdc-text-field-input-padding;
+ }
+
+ // Since we moved the horizontal spacing from the input to the form-field flex container
+ // and the MDC floating label tries to account for the horizontal spacing, we need to reset
+ // the shifting since there is no padding the label needs to account for.
+ .mdc-floating-label {
+ left: 0;
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss b/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss
new file mode 100644
index 000000000000..4097d3acdaf7
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/_mdc-text-field-textarea-overrides.scss
@@ -0,0 +1,51 @@
+@import 'form-field-sizing';
+
+// MDCs default textarea styles cannot be used because they only apply if a special
+// class is applied to the "mdc-text-field" wrapper. Since we cannot know whether the
+// registered form-field control is a textarea and MDC by default does not have styles
+// for textareas in the fill appearance, we add our own minimal textarea styles
+// which are scoped to the actual textarea element (i.e. not require a class in the
+// text-field wrapper) and integrate better with the any configured appearance.
+
+// Mixin that can be included to override the default MDC text-field styles
+// to properly support textareas.
+@mixin _mat-mdc-text-field-textarea-overrides() {
+ // Ensures that textarea elements inside of the form-field have proper vertical spacing
+ // to account for the floating label. Also ensures that there is no vertical text overflow.
+ // TODO(devversion): remove extra specificity if we removed the ":not(outline-appearance)"
+ // selector from the structure overwrites. Blocked on material-components-web#5326.
+ .mat-mdc-form-field > .mat-mdc-text-field-wrapper .mat-mdc-form-field-infix
+ .mat-mdc-textarea-input {
+ resize: vertical;
+ box-sizing: border-box;
+ height: auto;
+ // Using padding for textareas causes a bad user experience because the text outside
+ // of the text box will overflow vertically. In order to avoid this overflow where small
+ // parts of the previous/followed lines are visible, we use margin for vertical spacing.
+ margin: $mat-form-field-baseline 0 $mdc-text-field-input-padding-bottom;
+ padding: 0;
+ border: none;
+ }
+
+ // By default, MDC aligns the label using percentage. This will be overwritten based
+ // on whether a textarea is used. This is not possible in our implementation of the
+ // form-field because we do not know what type of form-field control is set up. Hence
+ // we always use a fixed position for the label. This does not have any implications.
+ .mat-mdc-text-field-wrapper .mdc-floating-label {
+ top: $mat-form-field-baseline;
+ }
+
+ // In the outline appearance, the textarea needs slightly reduced top spacing because
+ // the label overflows the outline by 50%. Additionally, horizontal spacing needs to be
+ // added since the outline is part of the "infix" and we need to account for the outline.
+ // TODO(devversion): horizontal spacing and extra specificity can be removed once the
+ // following feature is available: material-components-web#5326.
+ .mat-mdc-form-field > .mat-mdc-text-field-wrapper.mdc-text-field--outlined
+ .mat-mdc-form-field-infix .mat-mdc-textarea-input {
+ margin-top: $mat-form-field-outline-top-spacing;
+ padding: {
+ left: $mdc-text-field-input-padding;
+ right: $mdc-text-field-input-padding;
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/directives/error.ts b/src/material-experimental/mdc-form-field/directives/error.ts
new file mode 100644
index 000000000000..1dc62bab0b43
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/error.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, Input} from '@angular/core';
+
+let nextUniqueId = 0;
+
+/** Single error message to be shown underneath the form-field. */
+@Directive({
+ selector: 'mat-error',
+ host: {
+ 'class': 'mat-mdc-form-field-error',
+ 'role': 'alert',
+ '[id]': 'id',
+ }
+})
+export class MatError {
+ @Input() id: string = `mat-error-${nextUniqueId++}`;
+}
diff --git a/src/material-experimental/mdc-form-field/directives/floating-label.ts b/src/material-experimental/mdc-form-field/directives/floating-label.ts
new file mode 100644
index 000000000000..c9d091154c35
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/floating-label.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, ElementRef, Input, OnDestroy} from '@angular/core';
+import {MDCFloatingLabel} from '@material/floating-label';
+
+/**
+ * Internal directive that creates an instance of the MDC floating label
+ * component. Using a directive allows us to conditionally render a floating label
+ * in the template without having to manually instantiate the `MDCFloatingLabel` component.
+ *
+ * The component is responsible for setting up the floating label styles, and for providing
+ * an @Input that can be used by the form-field to toggle floating state of the label.
+ */
+@Directive({
+ selector: 'label[matFormFieldFloatingLabel]',
+ host: {
+ 'class': 'mdc-floating-label',
+ },
+})
+export class MatFormFieldFloatingLabel extends MDCFloatingLabel implements OnDestroy {
+
+ @Input()
+ get floating() { return this._floating; }
+ set floating(shouldFloat: boolean) {
+ if (shouldFloat !== this._floating) {
+ this._floating = shouldFloat;
+ this.float(shouldFloat);
+ }
+ }
+ private _floating = false;
+
+ constructor(elementRef: ElementRef) {
+ super(elementRef.nativeElement);
+ }
+
+ ngOnDestroy() {
+ this.destroy();
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/directives/hint.ts b/src/material-experimental/mdc-form-field/directives/hint.ts
new file mode 100644
index 000000000000..073a93d4b2dc
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/hint.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, Input} from '@angular/core';
+
+let nextUniqueId = 0;
+
+/** Hint text to be shown underneath the form field control. */
+@Directive({
+ selector: 'mat-hint',
+ host: {
+ 'class': 'mat-mdc-form-field-hint',
+ '[class.mat-form-field-hint-end]': 'align == "end"',
+ '[id]': 'id',
+ // Remove align attribute to prevent it from interfering with layout.
+ '[attr.align]': 'null',
+ }
+})
+export class MatHint {
+ /** Whether to align the hint label at the start or end of the line. */
+ @Input() align: 'start' | 'end' = 'start';
+
+ /** Unique ID for the hint. Used for the aria-describedby on the form field control. */
+ @Input() id: string = `mat-hint-${nextUniqueId++}`;
+}
diff --git a/src/material-experimental/mdc-form-field/directives/label.ts b/src/material-experimental/mdc-form-field/directives/label.ts
new file mode 100644
index 000000000000..3801266ca371
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/label.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+/** The floating label for a `mat-form-field`. */
+@Directive({
+ selector: 'mat-label',
+})
+export class MatLabel {
+}
diff --git a/src/material-experimental/mdc-form-field/directives/line-ripple.ts b/src/material-experimental/mdc-form-field/directives/line-ripple.ts
new file mode 100644
index 000000000000..98a048ac4dff
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/line-ripple.ts
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, ElementRef, OnDestroy} from '@angular/core';
+import {MDCLineRipple} from '@material/line-ripple';
+
+/**
+ * Internal directive that creates an instance of the MDC line-ripple component. Using a
+ * directive allows us to conditionally render a line-ripple in the template without having
+ * to manually create and destroy the `MDCLineRipple` component whenever the condition changes.
+ *
+ * The directive sets up the styles for the line-ripple and provides an API for activating
+ * and deactivating the line-ripple.
+ */
+@Directive({
+ selector: 'div[matFormFieldLineRipple]',
+ host: {
+ 'class': 'mdc-line-ripple',
+ },
+})
+export class MatFormFieldLineRipple extends MDCLineRipple implements OnDestroy {
+ constructor(elementRef: ElementRef) {
+ super(elementRef.nativeElement);
+ }
+
+ ngOnDestroy() {
+ this.destroy();
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/directives/notched-outline.html b/src/material-experimental/mdc-form-field/directives/notched-outline.html
new file mode 100644
index 000000000000..08b287042c07
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/notched-outline.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/material-experimental/mdc-form-field/directives/notched-outline.ts b/src/material-experimental/mdc-form-field/directives/notched-outline.ts
new file mode 100644
index 000000000000..98a8c9d77432
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/notched-outline.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef, OnDestroy,
+ ViewEncapsulation
+} from '@angular/core';
+import {MDCNotchedOutline} from '@material/notched-outline';
+
+/**
+ * Internal directive that creates an instance of the MDC notched-outline component. Using
+ * a directive allows us to conditionally render a notched-outline in the template without
+ * having to manually create and destroy the `MDCNotchedOutline` component whenever the
+ * appearance changes.
+ *
+ * The directive sets up the HTML structure and styles for the notched-outline, but also
+ * exposes a programmatic API to toggle the state of the notch.
+ */
+@Component({
+ selector: 'div[matFormFieldNotchedOutline]',
+ templateUrl: './notched-outline.html',
+ host: {
+ 'class': 'mdc-notched-outline',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+})
+export class MatFormFieldNotchedOutline implements AfterViewInit, OnDestroy {
+ private _mdcNotchedOutline: MDCNotchedOutline;
+
+ constructor(private _elementRef: ElementRef) {}
+
+ ngAfterViewInit() {
+ // The notch component relies on the view to be initialized. This means
+ // that we cannot extend from the "MDCNotchedOutline".
+ this._mdcNotchedOutline = MDCNotchedOutline.attachTo(this._elementRef.nativeElement);
+ }
+
+ ngOnDestroy() {
+ this._mdcNotchedOutline.destroy();
+ }
+
+ /**
+ * Updates classes and styles to open the notch to the specified width.
+ * @param notchWidth The notch width in the outline.
+ */
+ notch(notchWidth: number) {
+ this._mdcNotchedOutline.notch(notchWidth);
+ }
+
+ /** Closes the notch. */
+ closeNotch() {
+ this._mdcNotchedOutline.closeNotch();
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/directives/prefix.ts b/src/material-experimental/mdc-form-field/directives/prefix.ts
new file mode 100644
index 000000000000..8c5ee9da0ca7
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/prefix.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+/** Prefix to be placed in front of the form field. */
+@Directive({
+ selector: '[matPrefix]',
+})
+export class MatPrefix {}
diff --git a/src/material-experimental/mdc-form-field/directives/suffix.ts b/src/material-experimental/mdc-form-field/directives/suffix.ts
new file mode 100644
index 000000000000..9e61eec01cf9
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/directives/suffix.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+/** Suffix to be placed at the end of the form field. */
+@Directive({
+ selector: '[matSuffix]',
+})
+export class MatSuffix {}
diff --git a/src/material-experimental/mdc-form-field/form-field.e2e.spec.ts b/src/material-experimental/mdc-form-field/form-field.e2e.spec.ts
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/material-experimental/mdc-form-field/form-field.html b/src/material-experimental/mdc-form-field/form-field.html
new file mode 100644
index 000000000000..854de64c1267
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/form-field.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{hintLabel}}
+
+
+
+
+
diff --git a/src/material-experimental/mdc-form-field/form-field.scss b/src/material-experimental/mdc-form-field/form-field.scss
new file mode 100644
index 000000000000..ec4716cac20e
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/form-field.scss
@@ -0,0 +1,45 @@
+// TODO(devversion): do not import all text-field styles. Use feature targeting once available.
+@import '@material/textfield/mdc-text-field';
+@import 'form-field-sizing';
+@import 'form-field-subscript';
+@import 'form-field-bottom-line';
+@import 'mdc-text-field-textarea-overrides';
+@import 'mdc-text-field-structure-overrides';
+
+// MDC text-field overwrites.
+@include _mat-mdc-text-field-textarea-overrides();
+@include _mat-mdc-text-field-structure-overrides();
+
+// Include the subscript and bottom-line styles.
+@include _mat-form-field-subscript();
+@include _mat-form-field-bottom-line();
+
+// Host element of the form-field. It contains the mdc-text-field wrapper
+// and the subscript wrapper.
+.mat-mdc-form-field {
+ display: inline-flex;
+ // this container contains the text-field and the subscript. The subscript
+ // should be displayed below the text-field. Hence the column direction.
+ flex-direction: column;
+ vertical-align: middle;
+}
+
+// Container that contains the prefixes, infix and suffixes. These elements should
+// be aligned vertically in the baseline and in a single row.
+.mat-mdc-form-field-flex {
+ display: inline-flex;
+ align-items: baseline;
+ box-sizing: border-box;
+ width: 100%;
+}
+
+// Infix that contains the projected content (usually an input or a textarea). We ensure
+// that the projected form-field control and content can stretch as needed, but we also
+// apply a default infix width to make the form-field's look natural.
+.mat-mdc-form-field-infix {
+ flex: auto;
+ min-width: 0;
+ width: $mat-form-field-default-infix-width;
+ // Needed so that the floating label does not overlap with prefixes or suffixes.
+ position: relative;
+}
diff --git a/src/material-experimental/mdc-form-field/form-field.ts b/src/material-experimental/mdc-form-field/form-field.ts
new file mode 100644
index 000000000000..5c8b6fa60515
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/form-field.ts
@@ -0,0 +1,499 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ AfterContentChecked,
+ AfterContentInit,
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChild,
+ ContentChildren,
+ ElementRef,
+ Inject, InjectionToken,
+ Input,
+ isDevMode,
+ OnDestroy,
+ Optional,
+ QueryList,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {NgControl} from '@angular/forms';
+import {
+ FloatLabelType,
+ LabelOptions,
+ MAT_LABEL_GLOBAL_OPTIONS,
+ ThemePalette
+} from '@angular/material/core';
+import {
+ getMatFormFieldDuplicatedHintError,
+ getMatFormFieldMissingControlError,
+ MatFormField as NonMdcFormField,
+ matFormFieldAnimations,
+ MatFormFieldControl,
+} from '@angular/material/form-field';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {MDCTextFieldAdapter, MDCTextFieldFoundation} from '@material/textfield';
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
+import {MatError} from './directives/error';
+import {MatFormFieldFloatingLabel} from './directives/floating-label';
+import {MatHint} from './directives/hint';
+import {MatLabel} from './directives/label';
+import {MatFormFieldLineRipple} from './directives/line-ripple';
+import {MatFormFieldNotchedOutline} from './directives/notched-outline';
+import {MatPrefix} from './directives/prefix';
+import {MatSuffix} from './directives/suffix';
+
+/** Type for the available floatLabel values. */
+export type FloatLabelType = 'always' | 'auto';
+
+/** Possible appearance styles for the form field. */
+export type MatFormFieldAppearance = 'fill' | 'outline';
+
+/**
+ * Represents the default options for the form field that can be configured
+ * using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
+ */
+export interface MatFormFieldDefaultOptions {
+ appearance?: MatFormFieldAppearance;
+ hideRequiredMarker?: boolean;
+}
+
+/**
+ * Injection token that can be used to configure the
+ * default options for all form field within an app.
+ */
+export const MAT_FORM_FIELD_DEFAULT_OPTIONS =
+ new InjectionToken('MAT_FORM_FIELD_DEFAULT_OPTIONS');
+
+let nextUniqueId = 0;
+
+/** Default appearance used by the form-field. */
+const DEFAULT_APPEARANCE: MatFormFieldAppearance = 'fill';
+
+/** Default appearance used by the form-field. */
+const DEFAULT_FLOAT_LABEL: FloatLabelType = 'auto';
+
+/** Container for form controls that applies Material Design styling and behavior. */
+@Component({
+ selector: 'mat-form-field',
+ exportAs: 'matFormField',
+ templateUrl: './form-field.html',
+ styleUrls: ['./form-field.css'],
+ animations: [matFormFieldAnimations.transitionMessages],
+ host: {
+ 'class': 'mat-mdc-form-field',
+ '[class.mat-mdc-form-field-label-always-float]': '_shouldAlwaysFloat()',
+ '[class.mat-form-field-invalid]': '_control.errorState',
+ '[class.mat-form-field-disabled]': '_control.disabled',
+ '[class.mat-form-field-autofilled]': '_control.autofilled',
+ '[class.mat-focused]': '_control.focused',
+ '[class.mat-accent]': 'color == "accent"',
+ '[class.mat-warn]': 'color == "warn"',
+ '[class.ng-untouched]': '_shouldForward("untouched")',
+ '[class.ng-touched]': '_shouldForward("touched")',
+ '[class.ng-pristine]': '_shouldForward("pristine")',
+ '[class.ng-dirty]': '_shouldForward("dirty")',
+ '[class.ng-valid]': '_shouldForward("valid")',
+ '[class.ng-invalid]': '_shouldForward("invalid")',
+ '[class.ng-pending]': '_shouldForward("pending")',
+ '[class._mat-animation-noopable]': '!_animationsEnabled',
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ // Temporary workaround that allows us to test the MDC form-field against
+ // components which inject the non-mdc form-field (e.g. autocomplete).
+ {provide: NonMdcFormField, useExisting: MatFormField}
+ ]
+})
+export class MatFormField implements AfterViewInit, OnDestroy, AfterContentChecked,
+ AfterContentInit {
+ @ViewChild('textField') _textField: ElementRef;
+ @ViewChild(MatFormFieldFloatingLabel) _floatingLabel: MatFormFieldFloatingLabel|undefined;
+ @ViewChild(MatFormFieldNotchedOutline) _notchedOutline: MatFormFieldNotchedOutline|undefined;
+ @ViewChild(MatFormFieldLineRipple) _lineRipple: MatFormFieldLineRipple|undefined;
+
+ @ContentChild(MatLabel) _labelChildNonStatic: MatLabel|undefined;
+ @ContentChild(MatLabel, {static: true}) _labelChildStatic: MatLabel|undefined;
+ @ContentChild(MatFormFieldControl) _formFieldControl: MatFormFieldControl;
+ @ContentChildren(MatPrefix, {descendants: true}) _prefixChildren: QueryList;
+ @ContentChildren(MatSuffix, {descendants: true}) _suffixChildren: QueryList;
+ @ContentChildren(MatError, {descendants: true}) _errorChildren: QueryList;
+ @ContentChildren(MatHint, {descendants: true}) _hintChildren: QueryList;
+
+ /** Whether the required marker should be hidden. */
+ @Input() hideRequiredMarker: boolean = false;
+
+ /** The color palette for the form-field. */
+ @Input() color: ThemePalette = 'primary';
+
+ /** Whether the label should always float or float as the user types. */
+ @Input()
+ get floatLabel(): FloatLabelType {
+ return this._floatLabel || (this._labelOptions && this._labelOptions.float)
+ || DEFAULT_FLOAT_LABEL;
+ }
+ set floatLabel(value: FloatLabelType) {
+ if (value !== this._floatLabel) {
+ this._floatLabel = value;
+ // For backwards compatibility. Custom form-field controls or directives might set
+ // the "floatLabel" input and expect the form-field view to be updated automatically.
+ // e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just
+ // emit the "stateChanges" observable. TODO(devversion): consider removing.
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+ private _floatLabel: FloatLabelType;
+
+ /** The form-field appearance style. */
+ @Input()
+ get appearance(): MatFormFieldAppearance { return this._appearance; }
+ set appearance(value: MatFormFieldAppearance) {
+ this._appearance = value || (this._defaults && this._defaults.appearance) || DEFAULT_APPEARANCE;
+ }
+ private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
+
+ /** Text for the form field hint. */
+ @Input()
+ get hintLabel(): string { return this._hintLabel; }
+ set hintLabel(value: string) {
+ this._hintLabel = value;
+ this._processHints();
+ }
+ private _hintLabel = '';
+
+ // Unique id for the hint label.
+ _hintLabelId: string = `mat-hint-${nextUniqueId++}`;
+
+ // Unique id for the internal form field label.
+ _labelId = `mat-form-field-label-${nextUniqueId++}`;
+
+ /** Whether the Angular animations are enabled. */
+ _animationsEnabled: boolean;
+
+ /** State of the mat-hint and mat-error animations. */
+ _subscriptAnimationState: string = '';
+
+ /** Gets the current form field control */
+ get _control(): MatFormFieldControl {
+ return this._explicitFormFieldControl || this._formFieldControl;
+ }
+ set _control(value) { this._explicitFormFieldControl = value; }
+
+ private _destroyed = new Subject();
+ private _isFocused: boolean|null = null;
+ private _explicitFormFieldControl: MatFormFieldControl;
+ private _foundation: MDCTextFieldFoundation;
+ 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: () => {},
+
+ getLabelWidth: () => this._floatingLabel ? this._floatingLabel.getWidth() : 0,
+ notchOutline: labelWidth => this._notchedOutline && this._notchedOutline.notch(labelWidth),
+ closeOutline: () => this._notchedOutline && this._notchedOutline.closeNotch(),
+
+ 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 "ngDoCheck" 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,
+ @Optional() @Inject(MAT_FORM_FIELD_DEFAULT_OPTIONS)
+ private _defaults?: MatFormFieldDefaultOptions,
+ @Optional() @Inject(MAT_LABEL_GLOBAL_OPTIONS) private _labelOptions?: LabelOptions,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) _animationMode?: string) {
+ this._animationsEnabled = _animationMode !== 'NoopAnimations';
+
+ if (_defaults && _defaults.appearance) {
+ this.appearance = _defaults.appearance;
+ } else if (_defaults && _defaults.hideRequiredMarker) {
+ this.hideRequiredMarker = true;
+ }
+ }
+
+ ngAfterViewInit() {
+ this._foundation = new MDCTextFieldFoundation(this._adapter);
+
+ // 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.
+ // For example, consumers can set "@Input floatLabel" to always, or the custom form-field
+ // control can set "MatFormFieldControl#shouldLabelFloat" to true. To ensure that MDC knows
+ // when the label is floating, we overwrite the property to be based on the method we use to
+ // determine the current state of the floating label.
+ Object.defineProperty(this._foundation, 'shouldFloat', {
+ get: () => this._shouldLabelFloat(),
+ });
+
+ // Initial focus state sync. This happens rarely, but we want to account for
+ // it in case the form-field control has "focused" set to true on init.
+ this._updateFocusState();
+ // Initial notch update since we overwrote the "shouldFloat" getter.
+ this._rerenderOutlineNotch();
+ // Enable animations now. This ensures we don't animate on initial render.
+ this._subscriptAnimationState = 'enter';
+ }
+
+ ngAfterContentInit() {
+ this._assertFormFieldControl();
+ this._initializeControl();
+ this._initializeSubscript();
+ }
+
+ ngAfterContentChecked() {
+ this._assertFormFieldControl();
+ }
+
+ ngOnDestroy() {
+ this._destroyed.next();
+ this._destroyed.complete();
+ }
+
+ /**
+ * Gets an ElementRef for the element that a overlay attached to the form-field
+ * should be positioned relative to.
+ */
+ getConnectedOverlayOrigin(): ElementRef {
+ return this._textField || this._elementRef;
+ }
+
+ /** Animates the placeholder up and locks it in position. */
+ _animateAndLockLabel(): void {
+ // This is for backwards compatibility only. Consumers of the form-field might use
+ // this method. e.g. the autocomplete trigger. This method has been added to the non-MDC
+ // form-field because setting "floatLabel" to "always" caused the label to float without
+ // animation. This is different in MDC where the label always animates, so this method
+ // is no longer necessary. There doesn't seem any benefit in adding logic to allow changing
+ // the floating label state without animations. The non-MDC implementation was inconsistent
+ // because it always animates if "floatLabel" is set away from "always".
+ // TODO(devversion): consider removing this method when releasing the MDC form-field.
+ if (this._hasFloatingLabel()) {
+ this.floatLabel = 'always';
+ }
+ }
+
+ /** Initializes the registered form-field control. */
+ private _initializeControl() {
+ const control = this._control;
+
+ if (control.controlType) {
+ this._elementRef.nativeElement.classList.add(
+ `mat-mdc-form-field-type-${control.controlType}`);
+ }
+
+ // Subscribe to changes in the child control state in order to update the form field UI.
+ control.stateChanges.subscribe(() => {
+ this._updateFocusState();
+ this._syncDescribedByIds();
+ this._changeDetectorRef.markForCheck();
+ });
+
+ // Run change detection if the value changes.
+ if (control.ngControl && control.ngControl.valueChanges) {
+ control.ngControl.valueChanges
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(() => this._changeDetectorRef.markForCheck());
+ }
+ }
+
+ /**
+ * Initializes the subscript by validating hints and synchronizing "aria-describedby" ids
+ * with the custom form-field control. Also subscribes to hint and error changes in order
+ * to be able to validate and synchronize ids on change.
+ */
+ private _initializeSubscript() {
+ // Re-validate when the number of hints changes.
+ this._hintChildren.changes.subscribe(() => {
+ this._processHints();
+ this._changeDetectorRef.markForCheck();
+ });
+
+ // Update the aria-described by when the number of errors changes.
+ this._errorChildren.changes.subscribe(() => {
+ this._syncDescribedByIds();
+ this._changeDetectorRef.markForCheck();
+ });
+
+ // Initial mat-hint validation and subscript describedByIds sync.
+ this._validateHints();
+ this._syncDescribedByIds();
+ }
+
+ /** Throws an error if the form field's control is missing. */
+ private _assertFormFieldControl() {
+ if (!this._control) {
+ throw getMatFormFieldMissingControlError();
+ }
+ }
+ private _updateFocusState() {
+ // Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever
+ // certain DOM events are emitted. This is not possible in our implementation of the
+ // form-field because we support abstract form field controls which are not necessarily
+ // of type input, nor do we have a reference to a native form-field control element. Instead
+ // we handle the focus by checking if the abstract form-field control focused state changes.
+ if (this._control.focused && !this._isFocused) {
+ this._isFocused = true;
+ this._foundation.activateFocus();
+ } else if (!this._control.focused && (this._isFocused || this._isFocused === null)) {
+ this._isFocused = false;
+ this._foundation.deactivateFocus();
+ }
+ }
+
+ _rerenderOutlineNotch() {
+ if (this._floatingLabel && this._hasOutline()) {
+ this._foundation.notchOutline(this._shouldLabelFloat());
+ }
+ }
+
+ /** Whether the floating label should always float or not. */
+ _shouldAlwaysFloat() {
+ return this.floatLabel === 'always';
+ }
+
+ _hasOutline() {
+ return this.appearance === 'outline';
+ }
+
+ _hasFloatingLabel() {
+ return !!this._labelChildNonStatic || !!this._labelChildStatic;
+ }
+
+ _shouldLabelFloat() {
+ return this._control.shouldLabelFloat || this._shouldAlwaysFloat();
+ }
+
+ /** Determines whether a class from the NgControl should be forwarded to the host element. */
+ _shouldForward(prop: keyof NgControl): boolean {
+ const ngControl = this._control ? this._control.ngControl : null;
+ return ngControl && ngControl[prop];
+ }
+
+ /** Determines whether to display hints or errors. */
+ _getDisplayedMessages(): 'error' | 'hint' {
+ return (this._errorChildren && this._errorChildren.length > 0 &&
+ this._control.errorState) ? 'error' : 'hint';
+ }
+
+ /** Does any extra processing that is required when handling the hints. */
+ private _processHints() {
+ this._validateHints();
+ this._syncDescribedByIds();
+ }
+
+ /**
+ * Ensure that there is a maximum of one of each "mat-hint" alignment specified. The hint
+ * label specified set through the input is being considered as "start" aligned.
+ *
+ * This method is a noop if Angular runs in production mode.
+ */
+ private _validateHints() {
+ if (isDevMode() && this._hintChildren) {
+ let startHint: MatHint;
+ let endHint: MatHint;
+ this._hintChildren.forEach((hint: MatHint) => {
+ if (hint.align === 'start') {
+ if (startHint || this.hintLabel) {
+ throw getMatFormFieldDuplicatedHintError('start');
+ }
+ startHint = hint;
+ } else if (hint.align === 'end') {
+ if (endHint) {
+ throw getMatFormFieldDuplicatedHintError('end');
+ }
+ endHint = hint;
+ }
+ });
+ }
+ }
+
+ /**
+ * Sets the list of element IDs that describe the child control. This allows the control to update
+ * its `aria-describedby` attribute accordingly.
+ */
+ private _syncDescribedByIds() {
+ if (this._control) {
+ let ids: string[] = [];
+
+ if (this._getDisplayedMessages() === 'hint') {
+ const startHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'start') : null;
+ const endHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'end') : null;
+
+ if (startHint) {
+ ids.push(startHint.id);
+ } else if (this._hintLabel) {
+ ids.push(this._hintLabelId);
+ }
+
+ if (endHint) {
+ ids.push(endHint.id);
+ }
+ } else if (this._errorChildren) {
+ ids = this._errorChildren.map(error => error.id);
+ }
+
+ this._control.setDescribedByIds(ids);
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-form-field/index.ts b/src/material-experimental/mdc-form-field/index.ts
new file mode 100644
index 000000000000..676ca90f1ffa
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './public-api';
diff --git a/src/material-experimental/mdc-form-field/module.ts b/src/material-experimental/mdc-form-field/module.ts
new file mode 100644
index 000000000000..6b363af8b1b5
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/module.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {ObserversModule} from '@angular/cdk/observers';
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {MatCommonModule} from '@angular/material/core';
+import {MatError} from './directives/error';
+import {MatFormFieldFloatingLabel} from './directives/floating-label';
+import {MatHint} from './directives/hint';
+import {MatLabel} from './directives/label';
+import {MatFormFieldLineRipple} from './directives/line-ripple';
+import {MatFormFieldNotchedOutline} from './directives/notched-outline';
+import {MatPrefix} from './directives/prefix';
+import {MatSuffix} from './directives/suffix';
+import {MatFormField} from './form-field';
+
+@NgModule({
+ imports: [
+ MatCommonModule,
+ CommonModule,
+ ObserversModule
+ ],
+ exports: [
+ MatFormField,
+ MatLabel,
+ MatHint,
+ MatError,
+ MatPrefix,
+ MatSuffix,
+ MatCommonModule
+ ],
+ declarations: [
+ MatFormField,
+ MatLabel,
+ MatError,
+ MatHint,
+ MatPrefix,
+ MatSuffix,
+ MatFormFieldFloatingLabel,
+ MatFormFieldNotchedOutline,
+ MatFormFieldLineRipple
+ ],
+})
+export class MatFormFieldModule {
+}
diff --git a/src/material-experimental/mdc-form-field/public-api.ts b/src/material-experimental/mdc-form-field/public-api.ts
new file mode 100644
index 000000000000..81be8096f147
--- /dev/null
+++ b/src/material-experimental/mdc-form-field/public-api.ts
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export {
+ MatFormFieldControl,
+ getMatFormFieldDuplicatedHintError,
+ getMatFormFieldMissingControlError,
+} from '@angular/material/form-field';
+
+export * from './directives/label';
+export * from './directives/error';
+export * from './directives/hint';
+export * from './directives/prefix';
+export * from './directives/suffix';
+export * from './form-field';
+export * from './module';
diff --git a/src/material-experimental/mdc-input/BUILD.bazel b/src/material-experimental/mdc-input/BUILD.bazel
new file mode 100644
index 000000000000..db06b3245844
--- /dev/null
+++ b/src/material-experimental/mdc-input/BUILD.bazel
@@ -0,0 +1,88 @@
+package(default_visibility = ["//visibility:public"])
+
+load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
+load(
+ "//tools:defaults.bzl",
+ "ng_e2e_test_library",
+ "ng_module",
+ "ng_test_library",
+ "ng_web_test_suite",
+ "sass_library",
+)
+
+ng_module(
+ name = "mdc-input",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ assets = glob(["**/*.html"]),
+ module_name = "@angular/material-experimental/mdc-input",
+ deps = [
+ "//src/material-experimental/mdc-form-field",
+ "//src/material/core",
+ "//src/material/input",
+ "@npm//@angular/forms",
+ "@npm//@material/textfield",
+ ],
+)
+
+sass_library(
+ name = "mdc_input_scss_lib",
+ srcs = glob(["**/_*.scss"]),
+ deps = [
+ "//src/cdk/a11y:a11y_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ ],
+)
+
+###########
+# Testing
+###########
+
+ng_test_library(
+ name = "input_tests_lib",
+ srcs = glob(
+ ["**/*.spec.ts"],
+ exclude = ["**/*.e2e.spec.ts"],
+ ),
+ deps = [
+ ":mdc-input",
+ "//src/cdk/platform",
+ "//src/cdk/testing/private",
+ "//src/material-experimental/mdc-form-field",
+ "//src/material/core",
+ "@npm//@angular/forms",
+ "@npm//@angular/platform-browser",
+ ],
+)
+
+ng_web_test_suite(
+ name = "unit_tests",
+ static_files = [
+ "@npm//:node_modules/@material/textfield/dist/mdc.textfield.js",
+ "@npm//:node_modules/@material/line-ripple/dist/mdc.lineRipple.js",
+ "@npm//:node_modules/@material/notched-outline/dist/mdc.notchedOutline.js",
+ "@npm//:node_modules/@material/floating-label/dist/mdc.floatingLabel.js",
+ ],
+ deps = [
+ ":input_tests_lib",
+ "//src/material-experimental:mdc_require_config.js",
+ ],
+)
+
+ng_e2e_test_library(
+ name = "e2e_test_sources",
+ srcs = glob(["**/*.e2e.spec.ts"]),
+ deps = [
+ "//src/cdk/testing/private/e2e",
+ ],
+)
+
+e2e_test_suite(
+ name = "e2e_tests",
+ deps = [
+ ":e2e_test_sources",
+ "//src/cdk/testing/private/e2e",
+ ],
+)
diff --git a/src/material-experimental/mdc-input/README.md b/src/material-experimental/mdc-input/README.md
new file mode 100644
index 000000000000..73eeea50298e
--- /dev/null
+++ b/src/material-experimental/mdc-input/README.md
@@ -0,0 +1 @@
+This is a placeholder for the MDC-based implementation of input.
diff --git a/src/material-experimental/mdc-input/_mdc-input.scss b/src/material-experimental/mdc-input/_mdc-input.scss
new file mode 100644
index 000000000000..fc5826be566b
--- /dev/null
+++ b/src/material-experimental/mdc-input/_mdc-input.scss
@@ -0,0 +1,9 @@
+@import '../mdc-helpers/mdc-helpers';
+
+@mixin mat-input-theme-mdc($theme) {
+ @include mat-using-mdc-theme($theme) {}
+}
+
+@mixin mat-input-typography-mdc($config) {
+ @include mat-using-mdc-typography($config) {}
+}
diff --git a/src/material-experimental/mdc-input/index.ts b/src/material-experimental/mdc-input/index.ts
new file mode 100644
index 000000000000..676ca90f1ffa
--- /dev/null
+++ b/src/material-experimental/mdc-input/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './public-api';
diff --git a/src/material-experimental/mdc-input/input.e2e.spec.ts b/src/material-experimental/mdc-input/input.e2e.spec.ts
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/material-experimental/mdc-input/input.spec.ts b/src/material-experimental/mdc-input/input.spec.ts
new file mode 100644
index 000000000000..37c75f4ea473
--- /dev/null
+++ b/src/material-experimental/mdc-input/input.spec.ts
@@ -0,0 +1,1588 @@
+import {Platform, PlatformModule} from '@angular/cdk/platform';
+import {dispatchFakeEvent, wrappedErrorMessage} from '@angular/cdk/testing/private';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Directive,
+ ErrorHandler,
+ Provider,
+ Type,
+ ViewChild,
+} from '@angular/core';
+import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
+import {
+ FormControl,
+ FormGroup,
+ FormGroupDirective,
+ FormsModule,
+ NgForm,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+import {
+ getMatFormFieldDuplicatedHintError,
+ getMatFormFieldMissingControlError,
+ MAT_FORM_FIELD_DEFAULT_OPTIONS,
+ MatFormField,
+ MatFormFieldAppearance,
+ MatFormFieldModule,
+} from '@angular/material-experimental/mdc-form-field';
+import {
+ ErrorStateMatcher,
+ MAT_LABEL_GLOBAL_OPTIONS,
+ ShowOnDirtyErrorStateMatcher,
+} from '@angular/material/core';
+import {By} from '@angular/platform-browser';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {MAT_INPUT_VALUE_ACCESSOR, MatInput, MatInputModule} from './index';
+
+describe('MatMdcInput without forms', () => {
+ it('should default to floating labels', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithLabel);
+ fixture.detectChanges();
+
+ let formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField.floatLabel).toBe('auto',
+ 'Expected MatInput to set floatingLabel to auto by default.');
+ }));
+
+ it('should default to global floating label type', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithLabel, [{
+ provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: {float: 'always'}
+ }]);
+ fixture.detectChanges();
+
+ let formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField.floatLabel).toBe('always',
+ 'Expected MatInput to set floatingLabel to always from global option.');
+ }));
+
+ it('should not be treated as empty if type is date', fakeAsync(() => {
+ const platform = new Platform();
+
+ if (!(platform.TRIDENT || (platform.SAFARI && !platform.IOS))) {
+ const fixture = createComponent(MatInputDateTestController);
+ fixture.detectChanges();
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(false);
+ }
+ }));
+
+ // Safari Desktop and IE don't support type="date" and fallback to type="text".
+ it('should be treated as empty if type is date in Safari Desktop or IE', fakeAsync(() => {
+ const platform = new Platform();
+
+ if (platform.TRIDENT || (platform.SAFARI && !platform.IOS)) {
+ let fixture = createComponent(MatInputDateTestController);
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(true);
+ }
+ }));
+
+ it('should treat text input type as empty at init', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(true);
+ }));
+
+ it('should treat password input type as empty at init', fakeAsync(() => {
+ let fixture = createComponent(MatInputPasswordTestController);
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(true);
+ }));
+
+ it('should treat number input type as empty at init', fakeAsync(() => {
+ let fixture = createComponent(MatInputNumberTestController);
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(true);
+ }));
+
+ it('should not be empty after input entered', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'))!;
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField).toBeTruthy();
+ expect(formField!._control.empty).toBe(true);
+
+ inputEl.nativeElement.value = 'hello';
+ // Simulate input event.
+ inputEl.triggerEventHandler('input', {target: inputEl.nativeElement});
+ fixture.detectChanges();
+ expect(formField!._control.empty).toBe(false);
+ }));
+
+ it('should update the placeholder when input entered', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithStaticLabel);
+ fixture.detectChanges();
+
+ const inputEl = fixture.debugElement.query(By.css('input'))!;
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+
+ expect(formField).toBeTruthy();
+ expect(formField._control.empty).toBe(true);
+ expect(formField._shouldLabelFloat()).toBe(false);
+
+ // Update the value of the input.
+ inputEl.nativeElement.value = 'Text';
+
+ // Fake behavior of the `(input)` event which should trigger a change detection.
+ fixture.detectChanges();
+
+ expect(formField._control.empty).toBe(false);
+ expect(formField._shouldLabelFloat()).toBe(true);
+ }));
+
+ it('should not be empty when the value set before view init', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithValueBinding);
+ fixture.detectChanges();
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!
+ .componentInstance as MatFormField;
+ expect(formField._control.empty).toBe(false);
+
+ fixture.componentInstance.value = '';
+ fixture.detectChanges();
+
+ expect(formField._control.empty).toBe(true);
+ }));
+
+ it('should add id', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input'))!.nativeElement;
+ const labelElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(inputElement.id).toBeTruthy();
+ expect(inputElement.id).toEqual(labelElement.getAttribute('for')!);
+ }));
+
+ it('should add aria-owns to the label for the associated control', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input'))!.nativeElement;
+ const labelElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(labelElement.getAttribute('aria-owns')).toBe(inputElement.id);
+ }));
+
+ it('should add aria-required reflecting the required state', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithRequired);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(inputElement.getAttribute('aria-required'))
+ .toBe('false', 'Expected aria-required to reflect required state of false');
+
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ expect(inputElement.getAttribute('aria-required'))
+ .toBe('true', 'Expected aria-required to reflect required state of true');
+ }));
+
+ it('should not overwrite existing id', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithId);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input'))!.nativeElement;
+ const labelElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(inputElement.id).toBe('test-id');
+ expect(labelElement.getAttribute('for')).toBe('test-id');
+ }));
+
+ it('validates there\'s only one hint label per side', () => {
+ let fixture = createComponent(MatInputInvalidHintTestController);
+ expect(() => fixture.detectChanges()).toThrowError(
+ wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
+ });
+
+ it('validates there\'s only one hint label per side (attribute)', () => {
+ let fixture = createComponent(MatInputInvalidHint2TestController);
+
+ expect(() => fixture.detectChanges()).toThrowError(
+ wrappedErrorMessage(getMatFormFieldDuplicatedHintError('start')));
+ });
+
+ it('validates that matInput child is present', fakeAsync(() => {
+ let fixture = createComponent(MatInputMissingMatInputTestController);
+
+ expect(() => fixture.detectChanges()).toThrowError(
+ wrappedErrorMessage(getMatFormFieldMissingControlError()));
+ }));
+
+ it('validates that matInput child is present after initialization', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithNgIf);
+
+ expect(() => fixture.detectChanges()).not.toThrowError(
+ wrappedErrorMessage(getMatFormFieldMissingControlError()));
+
+ fixture.componentInstance.renderInput = false;
+
+ expect(() => fixture.detectChanges()).toThrowError(
+ wrappedErrorMessage(getMatFormFieldMissingControlError()));
+ }));
+
+ it('validates the type', fakeAsync(() => {
+ let fixture = createComponent(MatInputInvalidTypeTestController);
+
+ // Technically this throws during the OnChanges detection phase,
+ // so the error is really a ChangeDetectionError and it becomes
+ // hard to build a full exception to compare with.
+ // We just check for any exception in this case.
+ expect(() => fixture.detectChanges()).toThrow(
+ /* new MatInputUnsupportedTypeError('file') */);
+ }));
+
+ it('supports hint labels attribute', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabelTestController);
+ fixture.detectChanges();
+
+ // If the hint label is empty, expect no label.
+ expect(fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))).toBeNull();
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))).not.toBeNull();
+ }));
+
+ it('sets an id on hint labels', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabelTestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
+
+ expect(hint.getAttribute('id')).toBeTruthy();
+ }));
+
+ it('supports hint labels elements', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabel2TestController);
+ fixture.detectChanges();
+
+ // In this case, we should have an empty .
+ let el = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
+ expect(el.textContent).toBeFalsy();
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+ el = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
+ expect(el.textContent).toBe('label');
+ }));
+
+ it('sets an id on the hint element', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabel2TestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('mat-hint'))!.nativeElement;
+
+ expect(hint.getAttribute('id')).toBeTruthy();
+ }));
+
+ it('supports label required star', fakeAsync(() => {
+ let fixture = createComponent(MatInputLabelRequiredTestComponent);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label'))!;
+ expect(el).not.toBeNull();
+ expect(el.nativeElement.textContent).toBe('hello *');
+ }));
+
+ it('should hide the required star if input is disabled', () => {
+ const fixture = createComponent(MatInputLabelRequiredTestComponent);
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ const el = fixture.debugElement.query(By.css('label'))!;
+
+ expect(el).not.toBeNull();
+ expect(el.nativeElement.textContent).toBe('hello');
+ });
+
+ it('should hide the required star from screen readers', fakeAsync(() => {
+ let fixture = createComponent(MatInputLabelRequiredTestComponent);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement
+ .query(By.css('.mat-mdc-form-field-required-marker'))!.nativeElement;
+
+ expect(el.getAttribute('aria-hidden')).toBe('true');
+ }));
+
+ it('hide label required star when set to hide the required marker', fakeAsync(() => {
+ let fixture = createComponent(MatInputLabelRequiredTestComponent);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label'))!;
+ expect(el).not.toBeNull();
+ expect(el.nativeElement.textContent).toBe('hello *');
+
+ fixture.componentInstance.hideRequiredMarker = true;
+ fixture.detectChanges();
+
+ expect(el.nativeElement.textContent).toBe('hello');
+ }));
+
+ it('supports the disabled attribute as binding', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithDisabled);
+ fixture.detectChanges();
+
+ const wrapperEl =
+ fixture.debugElement.query(By.css('.mat-mdc-text-field-wrapper'))!.nativeElement;
+ const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
+ .toBe(false, `Expected form field not to start out disabled.`);
+ expect(inputEl.disabled).toBe(false);
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
+ .toBe(true, `Expected form field to look disabled after property is set.`);
+ expect(inputEl.disabled).toBe(true);
+ }));
+
+ it('supports the disabled attribute as binding for select', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const wrapperEl =
+ fixture.debugElement.query(By.css('.mat-mdc-text-field-wrapper'))!.nativeElement;
+ const selectEl = fixture.debugElement.query(By.css('select'))!.nativeElement;
+
+ expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
+ .toBe(false, `Expected form field not to start out disabled.`);
+ expect(selectEl.disabled).toBe(false);
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(wrapperEl.classList.contains('mdc-text-field--disabled'))
+ .toBe(true, `Expected form field to look disabled after property is set.`);
+ expect(selectEl.disabled).toBe(true);
+ }));
+
+ it('should add a class to the form field if it has a native select', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ expect(formField.classList).toContain('mat-mdc-form-field-type-mat-native-select');
+ }));
+
+ it('supports the required attribute as binding', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithRequired);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(inputEl.required).toBe(false);
+
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ expect(inputEl.required).toBe(true);
+ }));
+
+ it('supports the required attribute as binding for select', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const selectEl = fixture.debugElement.query(By.css('select'))!.nativeElement;
+
+ expect(selectEl.required).toBe(false);
+
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ expect(selectEl.required).toBe(true);
+ }));
+
+ it('supports the type attribute as binding', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithType);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(inputEl.type).toBe('text');
+
+ fixture.componentInstance.type = 'password';
+ fixture.detectChanges();
+
+ expect(inputEl.type).toBe('password');
+ }));
+
+ it('supports textarea', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextareaWithBindings);
+ fixture.detectChanges();
+
+ const textarea: HTMLTextAreaElement = fixture.nativeElement.querySelector('textarea');
+ expect(textarea).not.toBeNull();
+ }));
+
+ it('supports select', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const nativeSelect: HTMLTextAreaElement = fixture.nativeElement.querySelector('select');
+ expect(nativeSelect).not.toBeNull();
+ }));
+
+ it('sets the aria-describedby when a hintLabel is set', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabelTestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
+ let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ }));
+
+ it('sets the aria-describedby to the id of the mat-hint', fakeAsync(() => {
+ let fixture = createComponent(MatInputHintLabel2TestController);
+
+ fixture.componentInstance.label = 'label';
+ fixture.detectChanges();
+
+ let hint = fixture.debugElement.query(By.css('.mat-mdc-form-field-hint'))!.nativeElement;
+ let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe(hint.getAttribute('id'));
+ }));
+
+ it('sets the aria-describedby with multiple mat-hint instances', fakeAsync(() => {
+ let fixture = createComponent(MatInputMultipleHintTestController);
+
+ fixture.componentInstance.startId = 'start';
+ fixture.componentInstance.endId = 'end';
+ fixture.detectChanges();
+
+ let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(input.getAttribute('aria-describedby')).toBe('start end');
+ }));
+
+ it('sets the aria-describedby when a hintLabel is set, in addition to a mat-hint',
+ fakeAsync(() => {
+ let fixture = createComponent(MatInputMultipleHintMixedTestController);
+
+ fixture.detectChanges();
+
+ let hintLabel = fixture.debugElement.query(
+ By.css('.mat-mdc-form-field-hint:not(.mat-form-field-hint-end)'))!.nativeElement;
+ let endLabel = fixture.debugElement
+ .query(By.css('.mat-mdc-form-field-hint.mat-form-field-hint-end'))!.nativeElement;
+ let input = fixture.debugElement.query(By.css('input'))!.nativeElement;
+ let ariaValue = input.getAttribute('aria-describedby');
+
+ expect(ariaValue).toBe(`${hintLabel.getAttribute('id')} ${endLabel.getAttribute('id')}`);
+ }));
+
+ it('should float when floatLabel is set to default and text is entered', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithDynamicLabel);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+ let labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+
+ fixture.componentInstance.shouldFloat = 'auto';
+ fixture.detectChanges();
+
+ expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
+
+ // Update the value of the input.
+ inputEl.value = 'Text';
+
+ // Fake behavior of the `(input)` event which should trigger a change detection.
+ fixture.detectChanges();
+
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should always float the label when floatLabel is set to always', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithDynamicLabel);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+ let labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+
+ fixture.detectChanges();
+
+ // Update the value of the input.
+ inputEl.value = 'Text';
+
+ // Fake behavior of the `(input)` event which should trigger a change detection.
+ fixture.detectChanges();
+
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should float labels when select has value', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should not float the label if the selectedIndex is negative', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelect);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+ const selectEl: HTMLSelectElement = fixture.nativeElement.querySelector('select');
+
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+
+ selectEl.selectedIndex = -1;
+ fixture.detectChanges();
+
+ expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should not float labels when select has no value, no option label, ' +
+ 'no option innerHtml', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelectWithNoLabelNoValue);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+ expect(labelEl.classList).not.toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should floating labels when select has no value but has option label',
+ fakeAsync(() => {
+ const fixture = createComponent(MatInputSelectWithLabel);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should floating labels when select has no value but has option innerHTML',
+ fakeAsync(() => {
+ const fixture = createComponent(MatInputSelectWithInnerHtml);
+ fixture.detectChanges();
+
+ const labelEl = fixture.debugElement.query(By.css('label'))!.nativeElement;
+ expect(labelEl.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should not throw if a native select does not have options', fakeAsync(() => {
+ const fixture = createComponent(MatInputSelectWithoutOptions);
+ expect(() => fixture.detectChanges()).not.toThrow();
+ }));
+
+ it('should be able to toggle the floating label programmatically', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithId);
+
+ fixture.detectChanges();
+
+ const formField = fixture.debugElement.query(By.directive(MatFormField))!;
+ const containerInstance = formField.componentInstance as MatFormField;
+ const label = formField.nativeElement.querySelector('label');
+
+ expect(containerInstance.floatLabel).toBe('auto');
+ expect(label.classList).not.toContain('mdc-floating-label--float-above');
+
+ fixture.componentInstance.floatLabel = 'always';
+ fixture.detectChanges();
+
+ expect(containerInstance.floatLabel).toBe('always');
+ expect(label.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should not have prefix and suffix elements when none are specified', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithId);
+ fixture.detectChanges();
+
+ let prefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-prefix'));
+ let suffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-suffix'));
+
+ expect(prefixEl).toBeNull();
+ expect(suffixEl).toBeNull();
+ }));
+
+ it('should add prefix and suffix elements when specified', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithPrefixAndSuffix);
+ fixture.detectChanges();
+
+ const prefixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-prefix'))!;
+ const suffixEl = fixture.debugElement.query(By.css('.mat-mdc-form-field-suffix'))!;
+
+ expect(prefixEl).not.toBeNull();
+ expect(suffixEl).not.toBeNull();
+ expect(prefixEl.nativeElement.innerText.trim()).toEqual('Prefix');
+ expect(suffixEl.nativeElement.innerText.trim()).toEqual('Suffix');
+ }));
+
+ it('should update empty class when value changes programmatically and OnPush', fakeAsync(() => {
+ let fixture = createComponent(MatInputOnPush);
+ fixture.detectChanges();
+
+ let component = fixture.componentInstance;
+ let label = fixture.debugElement.query(By.css('label'))!.nativeElement;
+
+ expect(label.classList).not.toContain('mdc-floating-label--float-above');
+
+ component.formControl.setValue('something');
+ fixture.detectChanges();
+
+ expect(label.classList).toContain('mdc-floating-label--float-above');
+ }));
+
+ it('should set the focused class when the input is focused', fakeAsync(() => {
+ let fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ let input = fixture.debugElement.query(By.directive(MatInput))!
+ .injector.get(MatInput);
+ let container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ // Call the focus handler directly to avoid flakyness where
+ // browsers don't focus elements if the window is minimized.
+ input._focusChanged(true);
+ fixture.detectChanges();
+
+ expect(container.classList).toContain('mat-focused');
+ }));
+
+ it('should remove the focused class if the input becomes disabled while focused',
+ fakeAsync(() => {
+ const fixture = createComponent(MatInputTextTestController);
+ fixture.detectChanges();
+
+ const input = fixture.debugElement.query(By.directive(MatInput))!
+ .injector.get(MatInput);
+ const container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ // Call the focus handler directly to avoid flakyness where
+ // browsers don't focus elements if the window is minimized.
+ input._focusChanged(true);
+ fixture.detectChanges();
+
+ expect(container.classList).toContain('mat-focused');
+
+ input.disabled = true;
+ fixture.detectChanges();
+
+ expect(container.classList).not.toContain('mat-focused');
+ }));
+
+ it('should not highlight when focusing a readonly input', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithReadonlyInput);
+ fixture.detectChanges();
+
+ let input =
+ fixture.debugElement.query(By.directive(MatInput))!.injector.get(MatInput);
+ let container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ // Call the focus handler directly to avoid flakiness where
+ // browsers don't focus elements if the window is minimized.
+ input._focusChanged(true);
+ fixture.detectChanges();
+
+ expect(input.focused).toBe(false);
+ expect(container.classList).not.toContain('mat-focused');
+ }));
+
+ it('should reset the highlight when a readonly input is blurred', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithReadonlyInput);
+ fixture.detectChanges();
+
+ const inputDebugElement = fixture.debugElement.query(By.directive(MatInput))!;
+ const input = inputDebugElement.injector.get(MatInput);
+ const container = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ fixture.componentInstance.isReadonly = false;
+ fixture.detectChanges();
+
+ // Call the focus handler directly to avoid flakyness where
+ // browsers don't focus elements if the window is minimized.
+ input._focusChanged(true);
+ fixture.detectChanges();
+
+ expect(input.focused).toBe(true);
+ expect(container.classList).toContain('mat-focused');
+
+ fixture.componentInstance.isReadonly = true;
+ fixture.detectChanges();
+
+ input._focusChanged(false);
+ fixture.detectChanges();
+
+ expect(input.focused).toBe(false);
+ expect(container.classList).not.toContain('mat-focused');
+ }));
+
+ it('should not add the `placeholder` attribute if there is no placeholder', () => {
+ const fixture = createComponent(MatInputWithoutPlaceholder);
+ fixture.detectChanges();
+ const input = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(input.hasAttribute('placeholder')).toBe(false);
+ });
+
+ it('should not add the native select class if the control is not a native select', () => {
+ const fixture = createComponent(MatInputWithId);
+ fixture.detectChanges();
+ const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ expect(formField.classList).not.toContain('mat-mdc-form-field-type-mat-native-select');
+ });
+
+ it('should use the native input value when determining whether ' +
+ 'the element is empty with a custom accessor', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithCustomAccessor, [], [], [CustomMatInputAccessor]);
+ fixture.detectChanges();
+ let formField = fixture.debugElement
+ .query(By.directive(MatFormField))!.componentInstance as MatFormField;
+
+ expect(formField._control.empty).toBe(true);
+
+ fixture.nativeElement.querySelector('input').value = 'abc';
+ fixture.detectChanges();
+
+ expect(formField._control.empty).toBe(false);
+ }));
+
+});
+
+describe('MatMdcInput with forms', () => {
+ describe('error messages', () => {
+ let fixture: ComponentFixture;
+ let testComponent: MatInputWithFormErrorMessages;
+ let containerEl: HTMLElement;
+ let inputEl: HTMLElement;
+
+ beforeEach(fakeAsync(() => {
+ fixture = createComponent(MatInputWithFormErrorMessages);
+ fixture.detectChanges();
+ testComponent = fixture.componentInstance;
+ containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+ inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+ }));
+
+ it('should not show any errors if the user has not interacted', fakeAsync(() => {
+ expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
+ expect(inputEl.getAttribute('aria-invalid'))
+ .toBe('false', 'Expected aria-invalid to be set to "false".');
+ }));
+
+ it('should display an error message when the input is touched and invalid', fakeAsync(() => {
+ expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
+
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+ flush();
+
+ expect(containerEl.classList)
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error message to have been rendered.');
+ expect(inputEl.getAttribute('aria-invalid'))
+ .toBe('true', 'Expected aria-invalid to be set to "true".');
+ }));
+
+ it('should display an error message when the parent form is submitted', fakeAsync(() => {
+ expect(testComponent.form.submitted).toBe(false, 'Expected form not to have been submitted');
+ expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
+
+ dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
+ fixture.detectChanges();
+ flush();
+
+ expect(testComponent.form.submitted).toBe(true, 'Expected form to have been submitted');
+ expect(containerEl.classList)
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error message to have been rendered.');
+ expect(inputEl.getAttribute('aria-invalid'))
+ .toBe('true', 'Expected aria-invalid to be set to "true".');
+ }));
+
+ it('should display an error message when the parent form group is submitted', fakeAsync(() => {
+ fixture.destroy();
+ TestBed.resetTestingModule();
+
+ let groupFixture = createComponent(MatInputWithFormGroupErrorMessages);
+ let component: MatInputWithFormGroupErrorMessages;
+
+ groupFixture.detectChanges();
+ component = groupFixture.componentInstance;
+ containerEl = groupFixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+ inputEl = groupFixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(component.formGroup.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
+ expect(inputEl.getAttribute('aria-invalid'))
+ .toBe('false', 'Expected aria-invalid to be set to "false".');
+ expect(component.formGroupDirective.submitted)
+ .toBe(false, 'Expected form not to have been submitted');
+
+ dispatchFakeEvent(groupFixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
+ groupFixture.detectChanges();
+ flush();
+
+ expect(component.formGroupDirective.submitted)
+ .toBe(true, 'Expected form to have been submitted');
+ expect(containerEl.classList)
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error message to have been rendered.');
+ expect(inputEl.getAttribute('aria-invalid'))
+ .toBe('true', 'Expected aria-invalid to be set to "true".');
+ }));
+
+ it('should hide the errors and show the hints once the input becomes valid', fakeAsync(() => {
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+ flush();
+
+ expect(containerEl.classList)
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error message to have been rendered.');
+ expect(containerEl.querySelectorAll('mat-hint').length)
+ .toBe(0, 'Expected no hints to be shown.');
+
+ testComponent.formControl.setValue('something');
+ fixture.detectChanges();
+ flush();
+
+ expect(containerEl.classList).not.toContain('mat-form-field-invalid',
+ 'Expected container not to have the invalid class when valid.');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(0, 'Expected no error messages when the input is valid.');
+ expect(containerEl.querySelectorAll('mat-hint').length)
+ .toBe(1, 'Expected one hint to be shown once the input is valid.');
+ }));
+
+ it('should not hide the hint if there are no error messages', fakeAsync(() => {
+ testComponent.renderError = false;
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('mat-hint').length)
+ .toBe(1, 'Expected one hint to be shown on load.');
+
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+ flush();
+
+ expect(containerEl.querySelectorAll('mat-hint').length)
+ .toBe(1, 'Expected one hint to still be shown.');
+ }));
+
+ it('should set the proper role on the error messages', fakeAsync(() => {
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert');
+ }));
+
+ it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => {
+ let hintId = fixture.debugElement
+ .query(By.css('.mat-mdc-form-field-hint'))!.nativeElement.getAttribute('id');
+ let describedBy = inputEl.getAttribute('aria-describedby');
+
+ expect(hintId).toBeTruthy('hint should be shown');
+ expect(describedBy).toBe(hintId);
+
+ fixture.componentInstance.formControl.markAsTouched();
+ fixture.detectChanges();
+
+ let errorIds = fixture.debugElement.queryAll(By.css('.mat-mdc-form-field-error'))
+ .map(el => el.nativeElement.getAttribute('id')).join(' ');
+ describedBy = inputEl.getAttribute('aria-describedby');
+
+ expect(errorIds).toBeTruthy('errors should be shown');
+ expect(describedBy).toBe(errorIds);
+ }));
+ });
+
+ describe('custom error behavior', () => {
+
+ it('should display an error message when a custom error matcher returns true', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithCustomErrorStateMatcher);
+ fixture.detectChanges();
+
+ let component = fixture.componentInstance;
+ let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+
+ const control = component.formGroup.get('name')!;
+
+ expect(control.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(0, 'Expected no error messages');
+
+ control.markAsTouched();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(0, 'Expected no error messages after being touched.');
+
+ component.errorState = true;
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error messages to have been rendered.');
+ }));
+
+ it('should display an error message when global error matcher returns true', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithFormErrorMessages, [{
+ provide: ErrorStateMatcher, useValue: {isErrorState: () => true}}
+ ]);
+
+ fixture.detectChanges();
+
+ let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+ let testComponent = fixture.componentInstance;
+
+ // Expect the control to still be untouched but the error to show due to the global setting
+ expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(1, 'Expected an error message');
+ }));
+
+ it('should display an error message when using ShowOnDirtyErrorStateMatcher', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithFormErrorMessages, [{
+ provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher
+ }]);
+ fixture.detectChanges();
+
+ let containerEl = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+ let testComponent = fixture.componentInstance;
+
+ expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message');
+
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(0, 'Expected no error messages when touched');
+
+ testComponent.formControl.markAsDirty();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('mat-error').length)
+ .toBe(1, 'Expected one error message when dirty');
+ }));
+ });
+
+ it('should update the value when using FormControl.setValue', fakeAsync(() => {
+ let fixture = createComponent(MatInputWithFormControl);
+ fixture.detectChanges();
+
+ let input = fixture.debugElement.query(By.directive(MatInput))!
+ .injector.get(MatInput);
+
+ expect(input.value).toBeFalsy();
+
+ fixture.componentInstance.formControl.setValue('something');
+
+ expect(input.value).toBe('something');
+ }));
+
+ it('should display disabled styles when using FormControl.disable()', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithFormControl);
+ fixture.detectChanges();
+
+ const formFieldEl =
+ fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
+ const inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
+
+ expect(formFieldEl.classList)
+ .not.toContain('mat-form-field-disabled', `Expected form field not to start out disabled.`);
+ expect(inputEl.disabled).toBe(false);
+
+ fixture.componentInstance.formControl.disable();
+ fixture.detectChanges();
+
+ expect(formFieldEl.classList).toContain('mat-form-field-disabled',
+ `Expected form field to look disabled after disable() is called.`);
+ expect(inputEl.disabled).toBe(true);
+ }));
+
+ it('should not treat the number 0 as empty', fakeAsync(() => {
+ let fixture = createComponent(MatInputZeroTestController);
+ fixture.detectChanges();
+ flush();
+
+ fixture.detectChanges();
+
+ let formField = fixture.debugElement
+ .query(By.directive(MatFormField))!.componentInstance as MatFormField;
+ expect(formField).not.toBeNull();
+ expect(formField._control.empty).toBe(false);
+ }));
+
+ it('should update when the form field value is patched without emitting', fakeAsync(() => {
+ const fixture = createComponent(MatInputWithFormControl);
+ fixture.detectChanges();
+
+ let formField = fixture.debugElement
+ .query(By.directive(MatFormField))!.componentInstance as MatFormField;
+
+ expect(formField._control.empty).toBe(true);
+
+ fixture.componentInstance.formControl.patchValue('value', {emitEvent: false});
+ fixture.detectChanges();
+
+ expect(formField._control.empty).toBe(false);
+ }));
+
+});
+
+describe('MatFormField default options', () => {
+ it('should be fill appearance if no default options provided', () => {
+ const fixture = createComponent(MatInputWithAppearance);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.appearance).toBe('fill');
+ });
+
+ it('should be fill appearance if empty default options provided', () => {
+ const fixture = createComponent(MatInputWithAppearance, [{
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {}}
+ ]);
+
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.appearance).toBe('fill');
+ });
+
+ it('should be able to change the default appearance', () => {
+ const fixture = createComponent(MatInputWithAppearance, [{
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}}
+ ]);
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.appearance).toBe('outline');
+ });
+
+ it('should default hideRequiredMarker to false', () => {
+ const fixture = createComponent(MatInputWithAppearance, [{
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {}}
+ ]);
+
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.hideRequiredMarker).toBe(false);
+ });
+
+ it('should be able to change the default value of hideRequiredMarker', () => {
+ const fixture = createComponent(MatInputWithAppearance, [{
+ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {hideRequiredMarker: true}}
+ ]);
+
+ fixture.detectChanges();
+ expect(fixture.componentInstance.formField.hideRequiredMarker).toBe(true);
+ });
+
+});
+
+function createComponent(component: Type,
+ providers: Provider[] = [],
+ imports: any[] = [],
+ declarations: any[] = []): ComponentFixture {
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ BrowserAnimationsModule,
+ PlatformModule,
+ ReactiveFormsModule,
+ ...imports
+ ],
+ declarations: [component, ...declarations],
+ providers: [
+ // Custom error handler that re-throws the error. Errors happening within
+ // change detection phase will be reported through the handler and thrown
+ // in Ivy. Since we do not want to pollute the "console.error", but rather
+ // just rely on the actual error interrupting the test, we re-throw here.
+ {provide: ErrorHandler, useValue: ({handleError: (err: any) => { throw err; }})},
+ ...providers
+ ],
+ }).compileComponents();
+
+ return TestBed.createComponent(component);
+}
+
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputWithId {
+ floatLabel: 'always' | 'auto' = 'auto';
+}
+
+@Component({
+ template: ``
+})
+class MatInputWithDisabled {
+ disabled: boolean;
+}
+
+@Component({
+ template: ``
+})
+class MatInputWithRequired {
+ required: boolean;
+}
+
+@Component({
+ template: ``
+})
+class MatInputWithType {
+ type: string;
+}
+
+@Component({
+ template: `
+
+ hello
+
+ `
+})
+class MatInputLabelRequiredTestComponent {
+ hideRequiredMarker: boolean = false;
+ disabled: boolean = false;
+}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputWithFormControl {
+ formControl = new FormControl();
+}
+
+@Component({
+ template: `{{label}}`
+})
+class MatInputHintLabel2TestController {
+ label: string = '';
+}
+
+@Component({
+ template: ``
+})
+class MatInputHintLabelTestController {
+ label: string = '';
+}
+
+@Component({template: ``})
+class MatInputInvalidTypeTestController {
+ t = 'file';
+}
+
+@Component({
+ template: `
+
+
+ World
+ `
+})
+class MatInputInvalidHint2TestController {}
+
+@Component({
+ template: `
+
+
+ Hello
+ World
+ `
+})
+class MatInputInvalidHintTestController {}
+
+@Component({
+ template: `
+
+
+ Hello
+ World
+ `
+})
+class MatInputMultipleHintTestController {
+ startId: string;
+ endId: string;
+}
+
+@Component({
+ template: `
+
+
+ World
+ `
+})
+class MatInputMultipleHintMixedTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputDateTestController {}
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputTextTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputPasswordTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputNumberTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputZeroTestController {
+ value = 0;
+}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputWithValueBinding {
+ value: string = 'Initial';
+}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class MatInputWithStaticLabel {}
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputWithDynamicLabel {
+ shouldFloat: 'always' | 'auto' = 'always';
+}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputTextareaWithBindings {
+ rows: number = 4;
+ cols: number = 8;
+ wrap: string = 'hard';
+}
+
+@Component({
+ template: ``
+})
+class MatInputMissingMatInputTestController {}
+
+@Component({
+ template: `
+
+ `
+})
+class MatInputWithFormErrorMessages {
+ @ViewChild('form') form: NgForm;
+ formControl = new FormControl('', Validators.required);
+ renderError = true;
+}
+
+@Component({
+ template: `
+
+ `
+})
+class MatInputWithCustomErrorStateMatcher {
+ formGroup = new FormGroup({
+ name: new FormControl('', Validators.required)
+ });
+
+ errorState = false;
+
+ customErrorStateMatcher = {
+ isErrorState: () => this.errorState
+ };
+}
+
+@Component({
+ template: `
+
+ `
+})
+class MatInputWithFormGroupErrorMessages {
+ @ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
+ formGroup = new FormGroup({
+ name: new FormControl('', Validators.required)
+ });
+}
+
+@Component({
+ template: `
+
+
Prefix
+
+
Suffix
+
+ `
+})
+class MatInputWithPrefixAndSuffix {}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class MatInputWithNgIf {
+ renderInput = true;
+}
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ Label
+
+
+ `
+})
+class MatInputOnPush {
+ formControl = new FormControl('');
+}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class MatInputWithReadonlyInput {
+ isReadonly = true;
+}
+
+@Component({
+ template: `
+
+ Label
+
+
+ `
+})
+class MatInputWithLabel {}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class MatInputWithAppearance {
+ @ViewChild(MatFormField) formField: MatFormField;
+ appearance: MatFormFieldAppearance;
+}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class MatInputWithoutPlaceholder {
+}
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputSelect {
+ disabled: boolean;
+ required: boolean;
+}
+
+@Component({
+ template: `
+
+ Form-field label
+
+ `
+})
+class MatInputSelectWithNoLabelNoValue {}
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputSelectWithLabel {}
+
+@Component({
+ template: `
+
+ Label
+
+ `
+})
+class MatInputSelectWithInnerHtml {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputWithCustomAccessor {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MatInputSelectWithoutOptions {}
+
+
+/** Custom component that never has a value. Used for testing the `MAT_INPUT_VALUE_ACCESSOR`. */
+@Directive({
+ selector: 'input[customInputAccessor]',
+ providers: [{
+ provide: MAT_INPUT_VALUE_ACCESSOR,
+ useExisting: CustomMatInputAccessor
+ }]
+})
+class CustomMatInputAccessor {
+ get value() { return this._value; }
+ set value(_value: any) {}
+ private _value = null;
+}
diff --git a/src/material-experimental/mdc-input/input.ts b/src/material-experimental/mdc-input/input.ts
new file mode 100644
index 000000000000..c44f69027cfd
--- /dev/null
+++ b/src/material-experimental/mdc-input/input.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+import {MatFormFieldControl} from '@angular/material/form-field';
+import {MatInput as BaseMatInput} from '@angular/material/input';
+
+// workaround until we have feature targeting for MDC text-field. At that
+// point we can just use the actual "MatInput" class and apply the MDC text-field
+// styles appropriately.
+
+@Directive({
+ selector: `input[matInput], textarea[matInput], select[matNativeControl],
+ input[matNativeControl], textarea[matNativeControl]`,
+ exportAs: 'matInput',
+ host: {
+ 'class': 'mat-mdc-input-element mdc-text-field__input',
+ '[class.mat-input-server]': '_isServer',
+ '[class.mat-mdc-textarea-input]': '_isTextarea()',
+ // Native input properties that are overwritten by Angular inputs need to be synced with
+ // the native input element. Otherwise property bindings for those don't work.
+ '[id]': 'id',
+ '[disabled]': 'disabled',
+ '[required]': 'required',
+ '[attr.placeholder]': 'placeholder',
+ '[attr.readonly]': 'readonly && !_isNativeSelect || null',
+ '[attr.aria-describedby]': '_ariaDescribedby || null',
+ '[attr.aria-invalid]': 'errorState',
+ '[attr.aria-required]': 'required.toString()',
+ '(blur)': '_focusChanged(false)',
+ '(focus)': '_focusChanged(true)',
+ '(input)': '_onInput()',
+ },
+ providers: [{provide: MatFormFieldControl, useExisting: MatInput}],
+})
+export class MatInput extends BaseMatInput {
+ static ngAcceptInputType_disabled: boolean | string | null | undefined;
+ static ngAcceptInputType_readonly: boolean | string | null | undefined;
+ static ngAcceptInputType_required: boolean | string | null | undefined;
+ static ngAcceptInputType_value: any;
+}
+
diff --git a/src/material-experimental/mdc-input/module.ts b/src/material-experimental/mdc-input/module.ts
new file mode 100644
index 000000000000..132584453188
--- /dev/null
+++ b/src/material-experimental/mdc-input/module.ts
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {TextFieldModule} from '@angular/cdk/text-field';
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {MatCommonModule} from '@angular/material/core';
+import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
+import {MatInput} from './input';
+
+@NgModule({
+ imports: [MatCommonModule, MatFormFieldModule, CommonModule],
+ exports: [MatInput, MatFormFieldModule, TextFieldModule, MatCommonModule],
+ declarations: [MatInput],
+})
+export class MatInputModule {}
diff --git a/src/material-experimental/mdc-input/public-api.ts b/src/material-experimental/mdc-input/public-api.ts
new file mode 100644
index 000000000000..59ca77fabb0e
--- /dev/null
+++ b/src/material-experimental/mdc-input/public-api.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export {getMatInputUnsupportedTypeError, MAT_INPUT_VALUE_ACCESSOR} from '@angular/material/input';
+export {MatInput} from './input';
+export {MatInputModule} from './module';
diff --git a/src/material-experimental/mdc-theming/BUILD.bazel b/src/material-experimental/mdc-theming/BUILD.bazel
index 6573f9871a90..5116ac5b2d34 100644
--- a/src/material-experimental/mdc-theming/BUILD.bazel
+++ b/src/material-experimental/mdc-theming/BUILD.bazel
@@ -12,6 +12,8 @@ sass_library(
"//src/material-experimental/mdc-card:mdc_card_scss_lib",
"//src/material-experimental/mdc-checkbox:mdc_checkbox_scss_lib",
"//src/material-experimental/mdc-chips:mdc_chips_scss_lib",
+ "//src/material-experimental/mdc-form-field:mdc_form_field_scss_lib",
+ "//src/material-experimental/mdc-input:mdc_input_scss_lib",
"//src/material-experimental/mdc-list:mdc_list_scss_lib",
"//src/material-experimental/mdc-menu:mdc_menu_scss_lib",
"//src/material-experimental/mdc-progress-bar:mdc_progress_bar_scss_lib",
diff --git a/src/material-experimental/mdc-theming/_all-theme.scss b/src/material-experimental/mdc-theming/_all-theme.scss
index 9fae2830013f..d1ad30ee87e9 100644
--- a/src/material-experimental/mdc-theming/_all-theme.scss
+++ b/src/material-experimental/mdc-theming/_all-theme.scss
@@ -9,6 +9,8 @@
@import '../mdc-tabs/mdc-tabs';
@import '../mdc-table/mdc-table';
@import '../mdc-progress-bar/mdc-progress-bar';
+@import '../mdc-input/mdc-input';
+@import '../mdc-form-field/mdc-form-field';
@mixin angular-material-theme-mdc($theme) {
@include mat-button-theme-mdc($theme);
@@ -23,6 +25,8 @@
@include mat-radio-theme-mdc($theme);
@include mat-slide-toggle-theme-mdc($theme);
@include mat-table-theme-mdc($theme);
+ @include mat-form-field-theme-mdc($theme);
+ @include mat-input-theme-mdc($theme);
// TODO(andrewjs): Add this back when MDC syncs their slider code into Google-internal code
// @include mat-slider-theme-mdc($theme);
@include mat-tabs-theme-mdc($theme);
diff --git a/src/material-experimental/mdc-typography/BUILD.bazel b/src/material-experimental/mdc-typography/BUILD.bazel
index 1be4b16b315f..78f9fbbd449e 100644
--- a/src/material-experimental/mdc-typography/BUILD.bazel
+++ b/src/material-experimental/mdc-typography/BUILD.bazel
@@ -12,6 +12,8 @@ sass_library(
"//src/material-experimental/mdc-card:mdc_card_scss_lib",
"//src/material-experimental/mdc-checkbox:mdc_checkbox_scss_lib",
"//src/material-experimental/mdc-chips:mdc_chips_scss_lib",
+ "//src/material-experimental/mdc-form-field:mdc_form_field_scss_lib",
+ "//src/material-experimental/mdc-input:mdc_input_scss_lib",
"//src/material-experimental/mdc-list:mdc_list_scss_lib",
"//src/material-experimental/mdc-menu:mdc_menu_scss_lib",
"//src/material-experimental/mdc-progress-bar:mdc_progress_bar_scss_lib",
diff --git a/src/material-experimental/mdc-typography/_all-typography.scss b/src/material-experimental/mdc-typography/_all-typography.scss
index 849d1bb54426..8edfe99ccb10 100644
--- a/src/material-experimental/mdc-typography/_all-typography.scss
+++ b/src/material-experimental/mdc-typography/_all-typography.scss
@@ -10,6 +10,8 @@
@import '../mdc-tabs/mdc-tabs';
@import '../mdc-table/mdc-table';
@import '../mdc-progress-bar/mdc-progress-bar';
+@import '../mdc-input/mdc-input';
+@import '../mdc-form-field/mdc-form-field';
@mixin angular-material-typography-mdc($config: null) {
@if $config == null {
@@ -30,4 +32,6 @@
@include mat-tabs-typography-mdc($config);
@include mat-table-typography-mdc($config);
@include mat-progress-bar-typography-mdc($config);
+ @include mat-input-typography-mdc($config);
+ @include mat-form-field-typography-mdc($config);
}
diff --git a/src/material-experimental/mdc_require_config.js b/src/material-experimental/mdc_require_config.js
index 37cd8f410522..bff597ceb6a1 100644
--- a/src/material-experimental/mdc_require_config.js
+++ b/src/material-experimental/mdc_require_config.js
@@ -31,7 +31,7 @@ require.config({
'@material/tab-indicator': '/base/npm/node_modules/@material/tab-indicator/dist/mdc.tabIndicator',
'@material/tab-scroller': '/base/npm/node_modules/@material/tab-scroller/dist/mdc.tabScroller',
'@material/data-table': '/base/npm/node_modules/@material/data-table/dist/mdc.dataTable',
- '@material/text-field': '/base/npm/node_modules/@material/textfield/dist/mdc.textField',
+ '@material/textfield': '/base/npm/node_modules/@material/textfield/dist/mdc.textfield',
'@material/top-app-bar': '/base/npm/node_modules/@material/top-app-bar/dist/mdc.topAppBar',
}
});
diff --git a/src/material/input/input-module.ts b/src/material/input/input-module.ts
index d2a8c27f57b5..871ce745d5d5 100644
--- a/src/material/input/input-module.ts
+++ b/src/material/input/input-module.ts
@@ -14,7 +14,6 @@ import {MatFormFieldModule} from '@angular/material/form-field';
import {MatTextareaAutosize} from './autosize';
import {MatInput} from './input';
-
@NgModule({
declarations: [MatInput, MatTextareaAutosize],
imports: [
diff --git a/src/material/input/input.ts b/src/material/input/input.ts
index ed1d2ca176e0..2363b4046cd3 100644
--- a/src/material/input/input.ts
+++ b/src/material/input/input.ts
@@ -332,6 +332,11 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
}
+ /** Determines if the component host is a textarea. */
+ _isTextarea() {
+ return this._elementRef.nativeElement.nodeName.toLowerCase() === 'textarea';
+ }
+
/** Does some manual dirty checking on the native input `value` property. */
protected _dirtyCheckNativeValue() {
const newValue = this._elementRef.nativeElement.value;
@@ -361,11 +366,6 @@ export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<
return validity && validity.badInput;
}
- /** Determines if the component host is a textarea. */
- protected _isTextarea() {
- return this._elementRef.nativeElement.nodeName.toLowerCase() === 'textarea';
- }
-
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
diff --git a/test/karma-system-config.js b/test/karma-system-config.js
index a4a0539a7cdc..2217f451db57 100644
--- a/test/karma-system-config.js
+++ b/test/karma-system-config.js
@@ -36,7 +36,7 @@ System.config({
'@material/tab-bar': 'node:@material/tab-bar/dist/mdc.tabBar.js',
'@material/tab-indicator': 'node:@material/tab-indicator/dist/mdc.tabIndicator.js',
'@material/tab-scroller': 'node:@material/tab-scroller/dist/mdc.tabScroller.js',
- '@material/text-field': 'node:@material/textfield/dist/mdc.textField.js',
+ '@material/textfield': 'node:@material/textfield/dist/mdc.textfield.js',
'@material/top-app-bar': 'node:@material/top-app-bar/dist/mdc.topAppBar.js',
// Angular specific mappings.
@@ -198,8 +198,12 @@ System.config({
'dist/packages/material-experimental/mdc-checkbox/index.js',
'@angular/material-experimental/mdc-chips':
'dist/packages/material-experimental/mdc-chips/index.js',
+ '@angular/material-experimental/mdc-form-field':
+ 'dist/packages/material-experimental/mdc-form-field/index.js',
'@angular/material-experimental/mdc-helpers':
'dist/packages/material-experimental/mdc-helpers/index.js',
+ '@angular/material-experimental/mdc-input':
+ 'dist/packages/material-experimental/mdc-input/index.js',
'@angular/material-experimental/mdc-list':
'dist/packages/material-experimental/mdc-list/index.js',
'@angular/material-experimental/mdc-menu':
diff --git a/tools/public_api_guard/material/input.d.ts b/tools/public_api_guard/material/input.d.ts
index 13f907211b98..de3b6d09e69a 100644
--- a/tools/public_api_guard/material/input.d.ts
+++ b/tools/public_api_guard/material/input.d.ts
@@ -38,7 +38,7 @@ export declare class MatInput extends _MatInputMixinBase implements MatFormField
_focusChanged(isFocused: boolean): void;
protected _isBadInput(): boolean;
protected _isNeverEmpty(): boolean;
- protected _isTextarea(): boolean;
+ _isTextarea(): boolean;
_onInput(): void;
protected _validateType(): void;
focus(options?: FocusOptions): void;