<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../paper-styles/paper-styles.html"> <!-- `<paper-input-container>` is a container for a `<label>`, an `<input is="iron-input">` or `<iron-autogrow-textarea>` and optional add-on elements such as an error message or character counter, used to implement Material Design text fields. For example: <paper-input-container> <label>Your name</label> <input is="iron-input"> </paper-input-container> ### Listening for input changes By default, it listens for changes on the `bind-value` attribute on its children nodes and perform tasks such as auto-validating and label styling when the `bind-value` changes. You can configure the attribute it listens to with the `attr-for-value` attribute. ### Using a custom input element You can use a custom input element in a `<paper-input-container>`, for example to implement a compound input field like a social security number input. The custom input element should have the `paper-input-input` class, have a `notify:true` value property and optionally implements `Polymer.IronValidatableBehavior` if it is validatable. <paper-input-container attr-for-value="ssn-value"> <label>Social security number</label> <ssn-input class="paper-input-input"></ssn-input> </paper-input-container> ### Validation If the `auto-validate` attribute is set, the input container will validate the input and update the container styling when the input value changes. ### Add-ons Add-ons are child elements of a `<paper-input-container>` with the `add-on` attribute and implements the `Polymer.PaperInputAddonBehavior` behavior. They are notified when the input value or validity changes, and may implement functionality such as error messages or character counters. They appear at the bottom of the input. ### Prefixes and suffixes These are child elements of a `<paper-input-container>` with the `prefix` or `suffix` attribute, and are displayed inline with the input, before or after. <paper-input-container> <div prefix>$</div> <label>Total</label> <input is="iron-input"> <paper-icon-button suffix icon="clear"></paper-icon-button> </paper-input-container> ### Styling The following custom properties and mixins are available for styling: Custom property | Description | Default ----------------|-------------|---------- `--paper-input-container-color` | Label and underline color when the input is not focused | `--secondary-text-color` `--paper-input-container-focus-color` | Label and underline color when the input is focused | `--default-primary-color` `--paper-input-container-invalid-color` | Label and underline color when the input is is invalid | `--google-red-500` `--paper-input-container-input-color` | Input foreground color | `--primary-text-color` `--paper-input-container` | Mixin applied to the container | `{}` `--paper-input-container-disabled` | Mixin applied to the container when it's disabled | `{}` `--paper-input-container-label` | Mixin applied to the label | `{}` `--paper-input-container-label-focus` | Mixin applied to the label when the input is focused | `{}` `--paper-input-container-input` | Mixin applied to the input | `{}` `--paper-input-container-underline` | Mixin applied to the underline | `{}` `--paper-input-container-underline-focus` | Mixin applied to the underline when the input is focued | `{}` `--paper-input-container-underline-disabled` | Mixin applied to the underline when the input is disabled | `{}` `--paper-input-prefix` | Mixin applied to the input prefix | `{}` `--paper-input-suffix` | Mixin applied to the input suffix | `{}` This element is `display:block` by default, but you can set the `inline` attribute to make it `display:inline-block`. --> <dom-module id="paper-input-container"> <template> <style> :host { display: block; padding: 8px 0; @apply(--paper-input-container); } :host[inline] { display: inline-block; } :host([disabled]) { pointer-events: none; opacity: 0.33; @apply(--paper-input-container-disabled); } .floated-label-placeholder { @apply(--paper-font-caption); } .underline { position: relative; } .focused-line { height: 2px; -webkit-transform-origin: center center; transform-origin: center center; -webkit-transform: scale3d(0,1,1); transform: scale3d(0,1,1); background: var(--paper-input-container-focus-color, --default-primary-color); @apply(--paper-input-container-underline-focus); } .underline.is-highlighted .focused-line { -webkit-transform: none; transform: none; -webkit-transition: -webkit-transform 0.25s; transition: transform 0.25s; @apply(--paper-transition-easing); } .underline.is-invalid .focused-line { background: var(--paper-input-container-invalid-color, --google-red-500); -webkit-transform: none; transform: none; -webkit-transition: -webkit-transform 0.25s; transition: transform 0.25s; @apply(--paper-transition-easing); } .unfocused-line { height: 1px; background: var(--paper-input-container-color, --secondary-text-color); @apply(--paper-input-container-underline); } :host([disabled]) .unfocused-line { border-bottom: 1px dashed; border-color: var(--paper-input-container-color, --secondary-text-color); background: transparent; @apply(--paper-input-container-underline-disabled); } .label-and-input-container { @apply(--layout-flex); @apply(--layout-relative); } .input-content { position: relative; @apply(--layout-horizontal); @apply(--layout-end); } .input-content ::content label, .input-content ::content .paper-input-label { position: absolute; top: 0; right: 0; left: 0; font: inherit; color: var(--paper-input-container-color, --secondary-text-color); @apply(--paper-font-common-nowrap); @apply(--paper-font-subhead); @apply(--paper-input-container-label); } .input-content.label-is-floating ::content label, .input-content.label-is-floating ::content .paper-input-label { -webkit-transform: translateY(-75%) scale(0.75); transform: translateY(-75%) scale(0.75); -webkit-transition: -webkit-transform 0.25s; transition: transform 0.25s; -webkit-transform-origin: left top; transform-origin: left top; /* Since we scale to 75/100 of the size, we actually have 100/75 of the original space now available */ width: 133%; @apply(--paper-transition-easing); } :host-context([dir="rtl"]) .input-content.label-is-floating ::content label, :host-context([dir="rtl"]) .input-content.label-is-floating ::content .paper-input-label { /* TODO(noms): Figure out why leaving the width at 133% before the animation * actually makes * it wider on the right side, not left side, as you would expect in RTL */ width: 100%; -webkit-transform-origin: right top; transform-origin: right top; } .input-content.label-is-highlighted ::content label, .input-content.label-is-highlighted ::content .paper-input-label { color: var(--paper-input-container-focus-color, --default-primary-color); @apply(--paper-input-container-label-focus); } .input-content.is-invalid ::content label, .input-content.is-invalid ::content .paper-input-label { color: var(--paper-input-container-invalid-color, --google-red-500); } .input-content.label-is-hidden ::content label, .input-content.label-is-hidden ::content .paper-input-label { visibility: hidden; } .input-content ::content input, .input-content ::content textarea, .input-content ::content iron-autogrow-textarea, .input-content ::content .paper-input-input { position: relative; /* to make a stacking context */ outline: none; box-shadow: none; padding: 0; width: 100%; background: transparent; border: none; color: var(--paper-input-container-input-color, --primary-text-color); -webkit-appearance: none; text-align: inherit; @apply(--paper-font-subhead); @apply(--paper-input-container-input); } ::content [prefix] { @apply(--paper-font-subhead); @apply(--paper-input-prefix); } ::content [suffix] { @apply(--paper-font-subhead); @apply(--paper-input-suffix); } /* Firefox sets a min-width on the input, which can cause layout issues */ .input-content ::content input { min-width: 0; } .input-content ::content textarea { resize: none; } .add-on-content { position: relative; } .add-on-content.is-invalid ::content * { color: var(--paper-input-container-invalid-color, --google-red-500); } .add-on-content.is-highlighted ::content * { color: var(--paper-input-container-focus-color, --default-primary-color); } </style> <template is="dom-if" if="[[!noLabelFloat]]"> <div class="floated-label-placeholder"> </div> </template> <div class$="[[_computeInputContentClass(noLabelFloat,alwaysFloatLabel,focused,invalid,_inputHasContent)]]"> <content select="[prefix]" id="prefix"></content> <div class="label-and-input-container" id="labelAndInputContainer"> <content select=":not([add-on]):not([prefix]):not([suffix])"></content> </div> <content select="[suffix]"></content> </div> <div class$="[[_computeUnderlineClass(focused,invalid)]]"> <div class="unfocused-line fit"></div> <div class="focused-line fit"></div> </div> <div class$="[[_computeAddOnContentClass(focused,invalid)]]"> <content id="addOnContent" select="[add-on]"></content> </div> </template> </dom-module> <script> Polymer({ is: 'paper-input-container', properties: { /** * Set to true to disable the floating label. The label disappears when the input value is * not null. */ noLabelFloat: { type: Boolean, value: false }, /** * Set to true to always float the floating label. */ alwaysFloatLabel: { type: Boolean, value: false }, /** * The attribute to listen for value changes on. */ attrForValue: { type: String, value: 'bind-value' }, /** * Set to true to auto-validate the input value when it changes. */ autoValidate: { type: Boolean, value: false }, /** * True if the input is invalid. This property is set automatically when the input value * changes if auto-validating, or when the `iron-input-validate` event is heard from a child. */ invalid: { observer: '_invalidChanged', type: Boolean, value: false }, /** * True if the input has focus. */ focused: { readOnly: true, type: Boolean, value: false, notify: true }, _addons: { type: Array // do not set a default value here intentionally - it will be initialized lazily when a // distributed child is attached, which may occur before configuration for this element // in polyfill. }, _inputHasContent: { type: Boolean, value: false }, _inputSelector: { type: String, value: 'input,textarea,.paper-input-input' }, _boundOnFocus: { type: Function, value: function() { return this._onFocus.bind(this); } }, _boundOnBlur: { type: Function, value: function() { return this._onBlur.bind(this); } }, _boundOnInput: { type: Function, value: function() { return this._onInput.bind(this); } }, _boundValueChanged: { type: Function, value: function() { return this._onValueChanged.bind(this); } } }, listeners: { 'addon-attached': '_onAddonAttached', 'iron-input-validate': '_onIronInputValidate' }, get _valueChangedEvent() { return this.attrForValue + '-changed'; }, get _propertyForValue() { return Polymer.CaseMap.dashToCamelCase(this.attrForValue); }, get _inputElement() { return Polymer.dom(this).querySelector(this._inputSelector); }, get _inputElementValue() { return this._inputElement[this._propertyForValue] || this._inputElement.value; }, ready: function() { if (!this._addons) { this._addons = []; } this.addEventListener('focus', this._boundOnFocus, true); this.addEventListener('blur', this._boundOnBlur, true); if (this.attrForValue) { this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged); } else { this.addEventListener('input', this._onInput); } }, attached: function() { // Only validate when attached if the input already has a value. if (this._inputElementValue != '') { this._handleValueAndAutoValidate(this._inputElement); } else { this._handleValue(this._inputElement); } this._numberOfPrefixNodes = 0; this._prefixObserver = Polymer.dom(this.$.prefix).observeNodes( function(mutations) { // Keep track whether there's at least one prefix node, since it // affects laying out the floating label. this._numberOfPrefixNodes += mutations.addedNodes.length - mutations.removedNodes.length; }.bind(this)); }, detached: function() { if (this._prefixObserver) { Polymer.dom(this.$.prefix).unobserveNodes(this._prefixObserver); } }, _onAddonAttached: function(event) { if (!this._addons) { this._addons = []; } var target = event.target; if (this._addons.indexOf(target) === -1) { this._addons.push(target); if (this.isAttached) { this._handleValue(this._inputElement); } } }, _onFocus: function() { this._setFocused(true); }, _onBlur: function() { this._setFocused(false); this._handleValueAndAutoValidate(this._inputElement); }, _onInput: function(event) { this._handleValueAndAutoValidate(event.target); }, _onValueChanged: function(event) { this._handleValueAndAutoValidate(event.target); }, _handleValue: function(inputElement) { var value = this._inputElementValue; // type="number" hack needed because this.value is empty until it's valid if (value || value === 0 || (inputElement.type === 'number' && !inputElement.checkValidity())) { this._inputHasContent = true; } else { this._inputHasContent = false; } this.updateAddons({ inputElement: inputElement, value: value, invalid: this.invalid }); }, _handleValueAndAutoValidate: function(inputElement) { if (this.autoValidate) { var valid; if (inputElement.validate) { valid = inputElement.validate(this._inputElementValue); } else { valid = inputElement.checkValidity(); } this.invalid = !valid; } // Call this last to notify the add-ons. this._handleValue(inputElement); }, _onIronInputValidate: function(event) { this.invalid = this._inputElement.invalid; }, _invalidChanged: function() { if (this._addons) { this.updateAddons({invalid: this.invalid}); } }, /** * Call this to update the state of add-ons. * @param {Object} state Add-on state. */ updateAddons: function(state) { for (var addon, index = 0; addon = this._addons[index]; index++) { addon.update(state); } }, _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) { var cls = 'input-content'; if (!noLabelFloat) { var label = this.querySelector('label'); if (alwaysFloatLabel || _inputHasContent) { cls += ' label-is-floating'; if (invalid) { cls += ' is-invalid'; } else if (focused) { cls += " label-is-highlighted"; } // If a prefix element exists, the label has a horizontal offset // which needs to be undone when displayed as a floating label. if (this._numberOfPrefixNodes > 0) { this.$.labelAndInputContainer.style.position = 'static'; } } else { // When the label is not floating, it should overlap the input element. if (label) { this.$.labelAndInputContainer.style.position = 'relative'; } } } else { if (_inputHasContent) { cls += ' label-is-hidden'; } } return cls; }, _computeUnderlineClass: function(focused, invalid) { var cls = 'underline'; if (invalid) { cls += ' is-invalid'; } else if (focused) { cls += ' is-highlighted' } return cls; }, _computeAddOnContentClass: function(focused, invalid) { var cls = 'add-on-content'; if (invalid) { cls += ' is-invalid'; } else if (focused) { cls += ' is-highlighted' } return cls; } }); </script>