Home | History | Annotate | Download | only in paper-input
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="../iron-flex-layout/iron-flex-layout.html">
     13 <link rel="import" href="../paper-styles/default-theme.html">
     14 <link rel="import" href="../paper-styles/typography.html">
     15 
     16 <!--
     17 `<paper-input-container>` is a container for a `<label>`, an `<input is="iron-input">` or
     18 `<iron-autogrow-textarea>` and optional add-on elements such as an error message or character
     19 counter, used to implement Material Design text fields.
     20 
     21 For example:
     22 
     23     <paper-input-container>
     24       <label>Your name</label>
     25       <input is="iron-input">
     26     </paper-input-container>
     27 
     28 Do not wrap `<paper-input-container>` around elements that already include it, such as `<paper-input>`.
     29 Doing so may cause events to bounce infintely between the container and its contained element.
     30 
     31 ### Listening for input changes
     32 
     33 By default, it listens for changes on the `bind-value` attribute on its children nodes and perform
     34 tasks such as auto-validating and label styling when the `bind-value` changes. You can configure
     35 the attribute it listens to with the `attr-for-value` attribute.
     36 
     37 ### Using a custom input element
     38 
     39 You can use a custom input element in a `<paper-input-container>`, for example to implement a
     40 compound input field like a social security number input. The custom input element should have the
     41 `paper-input-input` class, have a `notify:true` value property and optionally implements
     42 `Polymer.IronValidatableBehavior` if it is validatable.
     43 
     44     <paper-input-container attr-for-value="ssn-value">
     45       <label>Social security number</label>
     46       <ssn-input class="paper-input-input"></ssn-input>
     47     </paper-input-container>
     48 
     49 
     50 If you're using a `<paper-input-container>` imperatively, it's important to make sure
     51 that you attach its children (the `iron-input` and the optional `label`) before you
     52 attach the `<paper-input-container>` itself, so that it can be set up correctly.
     53 
     54 ### Validation
     55 
     56 If the `auto-validate` attribute is set, the input container will validate the input and update
     57 the container styling when the input value changes.
     58 
     59 ### Add-ons
     60 
     61 Add-ons are child elements of a `<paper-input-container>` with the `add-on` attribute and
     62 implements the `Polymer.PaperInputAddonBehavior` behavior. They are notified when the input value
     63 or validity changes, and may implement functionality such as error messages or character counters.
     64 They appear at the bottom of the input.
     65 
     66 ### Prefixes and suffixes
     67 These are child elements of a `<paper-input-container>` with the `prefix`
     68 or `suffix` attribute, and are displayed inline with the input, before or after.
     69 
     70     <paper-input-container>
     71       <div prefix>$</div>
     72       <label>Total</label>
     73       <input is="iron-input">
     74       <paper-icon-button suffix icon="clear"></paper-icon-button>
     75     </paper-input-container>
     76 
     77 ### Styling
     78 
     79 The following custom properties and mixins are available for styling:
     80 
     81 Custom property | Description | Default
     82 ----------------|-------------|----------
     83 `--paper-input-container-color` | Label and underline color when the input is not focused | `--secondary-text-color`
     84 `--paper-input-container-focus-color` | Label and underline color when the input is focused | `--primary-color`
     85 `--paper-input-container-invalid-color` | Label and underline color when the input is is invalid | `--error-color`
     86 `--paper-input-container-input-color` | Input foreground color | `--primary-text-color`
     87 `--paper-input-container` | Mixin applied to the container | `{}`
     88 `--paper-input-container-disabled` | Mixin applied to the container when it's disabled | `{}`
     89 `--paper-input-container-label` | Mixin applied to the label | `{}`
     90 `--paper-input-container-label-focus` | Mixin applied to the label when the input is focused | `{}`
     91 `--paper-input-container-label-floating` | Mixin applied to the label when floating | `{}`
     92 `--paper-input-container-input` | Mixin applied to the input | `{}`
     93 `--paper-input-container-underline` | Mixin applied to the underline | `{}`
     94 `--paper-input-container-underline-focus` | Mixin applied to the underline when the input is focused | `{}`
     95 `--paper-input-container-underline-disabled` | Mixin applied to the underline when the input is disabled | `{}`
     96 `--paper-input-prefix` | Mixin applied to the input prefix | `{}`
     97 `--paper-input-suffix` | Mixin applied to the input suffix | `{}`
     98 
     99 This element is `display:block` by default, but you can set the `inline` attribute to make it
    100 `display:inline-block`.
    101 -->
    102 
    103 <dom-module id="paper-input-container">
    104   <template>
    105     <style>
    106       :host {
    107         display: block;
    108         padding: 8px 0;
    109 
    110         @apply(--paper-input-container);
    111       }
    112 
    113       :host([inline]) {
    114         display: inline-block;
    115       }
    116 
    117       :host([disabled]) {
    118         pointer-events: none;
    119         opacity: 0.33;
    120 
    121         @apply(--paper-input-container-disabled);
    122       }
    123 
    124       :host([hidden]) {
    125         display: none !important;
    126       }
    127 
    128       .floated-label-placeholder {
    129         @apply(--paper-font-caption);
    130       }
    131 
    132       .underline {
    133         position: relative;
    134       }
    135 
    136       .focused-line {
    137         @apply(--layout-fit);
    138 
    139         background: var(--paper-input-container-focus-color, --primary-color);
    140         height: 2px;
    141 
    142         -webkit-transform-origin: center center;
    143         transform-origin: center center;
    144         -webkit-transform: scale3d(0,1,1);
    145         transform: scale3d(0,1,1);
    146 
    147         @apply(--paper-input-container-underline-focus);
    148       }
    149 
    150       .underline.is-highlighted .focused-line {
    151         -webkit-transform: none;
    152         transform: none;
    153         -webkit-transition: -webkit-transform 0.25s;
    154         transition: transform 0.25s;
    155 
    156         @apply(--paper-transition-easing);
    157       }
    158 
    159       .underline.is-invalid .focused-line {
    160         background: var(--paper-input-container-invalid-color, --error-color);
    161         -webkit-transform: none;
    162         transform: none;
    163         -webkit-transition: -webkit-transform 0.25s;
    164         transition: transform 0.25s;
    165 
    166         @apply(--paper-transition-easing);
    167       }
    168 
    169       .unfocused-line {
    170         @apply(--layout-fit);
    171 
    172         background: var(--paper-input-container-color, --secondary-text-color);
    173         height: 1px;
    174 
    175         @apply(--paper-input-container-underline);
    176       }
    177 
    178       :host([disabled]) .unfocused-line {
    179         border-bottom: 1px dashed;
    180         border-color: var(--paper-input-container-color, --secondary-text-color);
    181         background: transparent;
    182 
    183         @apply(--paper-input-container-underline-disabled);
    184       }
    185 
    186       .label-and-input-container {
    187         @apply(--layout-flex-auto);
    188         @apply(--layout-relative);
    189 
    190         width: 100%;
    191         max-width: 100%;
    192       }
    193 
    194       .input-content {
    195         @apply(--layout-horizontal);
    196         @apply(--layout-center);
    197 
    198         position: relative;
    199       }
    200 
    201       .input-content ::content label,
    202       .input-content ::content .paper-input-label {
    203         position: absolute;
    204         top: 0;
    205         right: 0;
    206         left: 0;
    207         width: 100%;
    208         font: inherit;
    209         color: var(--paper-input-container-color, --secondary-text-color);
    210         -webkit-transition: -webkit-transform 0.25s, width 0.25s;
    211         transition: transform 0.25s, width 0.25s;
    212         -webkit-transform-origin: left top;
    213         transform-origin: left top;
    214 
    215         @apply(--paper-font-common-nowrap);
    216         @apply(--paper-font-subhead);
    217         @apply(--paper-input-container-label);
    218         @apply(--paper-transition-easing);
    219       }
    220 
    221       .input-content.label-is-floating ::content label,
    222       .input-content.label-is-floating ::content .paper-input-label {
    223         -webkit-transform: translateY(-75%) scale(0.75);
    224         transform: translateY(-75%) scale(0.75);
    225 
    226         /* Since we scale to 75/100 of the size, we actually have 100/75 of the
    227         original space now available */
    228         width: 133%;
    229 
    230         @apply(--paper-input-container-label-floating);
    231       }
    232 
    233       :host-context([dir="rtl"]) .input-content.label-is-floating ::content label,
    234       :host-context([dir="rtl"]) .input-content.label-is-floating ::content .paper-input-label {
    235         /* TODO(noms): Figure out why leaving the width at 133% before the animation
    236          * actually makes
    237          * it wider on the right side, not left side, as you would expect in RTL */
    238         width: 100%;
    239         -webkit-transform-origin: right top;
    240         transform-origin: right top;
    241       }
    242 
    243       .input-content.label-is-highlighted ::content label,
    244       .input-content.label-is-highlighted ::content .paper-input-label {
    245         color: var(--paper-input-container-focus-color, --primary-color);
    246 
    247         @apply(--paper-input-container-label-focus);
    248       }
    249 
    250       .input-content.is-invalid ::content label,
    251       .input-content.is-invalid ::content .paper-input-label {
    252         color: var(--paper-input-container-invalid-color, --error-color);
    253       }
    254 
    255       .input-content.label-is-hidden ::content label,
    256       .input-content.label-is-hidden ::content .paper-input-label {
    257         visibility: hidden;
    258       }
    259 
    260       .input-content ::content input,
    261       .input-content ::content textarea,
    262       .input-content ::content iron-autogrow-textarea,
    263       .input-content ::content .paper-input-input {
    264         position: relative; /* to make a stacking context */
    265         outline: none;
    266         box-shadow: none;
    267         padding: 0;
    268         width: 100%;
    269         max-width: 100%;
    270         background: transparent;
    271         border: none;
    272         color: var(--paper-input-container-input-color, --primary-text-color);
    273         -webkit-appearance: none;
    274         text-align: inherit;
    275 
    276         @apply(--paper-font-subhead);
    277         @apply(--paper-input-container-input);
    278       }
    279 
    280       ::content [prefix] {
    281         @apply(--paper-font-subhead);
    282 
    283         @apply(--paper-input-prefix);
    284         @apply(--layout-flex-none);
    285       }
    286 
    287       ::content [suffix] {
    288         @apply(--paper-font-subhead);
    289 
    290         @apply(--paper-input-suffix);
    291         @apply(--layout-flex-none);
    292       }
    293 
    294       /* Firefox sets a min-width on the input, which can cause layout issues */
    295       .input-content ::content input {
    296         min-width: 0;
    297       }
    298 
    299       .input-content ::content textarea {
    300         resize: none;
    301       }
    302 
    303       .add-on-content {
    304         position: relative;
    305       }
    306 
    307       .add-on-content.is-invalid ::content * {
    308         color: var(--paper-input-container-invalid-color, --error-color);
    309       }
    310 
    311       .add-on-content.is-highlighted ::content * {
    312         color: var(--paper-input-container-focus-color, --primary-color);
    313       }
    314     </style>
    315 
    316     <template is="dom-if" if="[[!noLabelFloat]]">
    317       <div class="floated-label-placeholder" aria-hidden="true">&nbsp;</div>
    318     </template>
    319 
    320     <div class$="[[_computeInputContentClass(noLabelFloat,alwaysFloatLabel,focused,invalid,_inputHasContent)]]">
    321       <content select="[prefix]" id="prefix"></content>
    322 
    323       <div class="label-and-input-container" id="labelAndInputContainer">
    324         <content select=":not([add-on]):not([prefix]):not([suffix])"></content>
    325       </div>
    326 
    327       <content select="[suffix]"></content>
    328     </div>
    329 
    330     <div class$="[[_computeUnderlineClass(focused,invalid)]]">
    331       <div class="unfocused-line"></div>
    332       <div class="focused-line"></div>
    333     </div>
    334 
    335     <div class$="[[_computeAddOnContentClass(focused,invalid)]]">
    336       <content id="addOnContent" select="[add-on]"></content>
    337     </div>
    338   </template>
    339 </dom-module>
    340 
    341 <script>
    342   Polymer({
    343     is: 'paper-input-container',
    344 
    345     properties: {
    346       /**
    347        * Set to true to disable the floating label. The label disappears when the input value is
    348        * not null.
    349        */
    350       noLabelFloat: {
    351         type: Boolean,
    352         value: false
    353       },
    354 
    355       /**
    356        * Set to true to always float the floating label.
    357        */
    358       alwaysFloatLabel: {
    359         type: Boolean,
    360         value: false
    361       },
    362 
    363       /**
    364        * The attribute to listen for value changes on.
    365        */
    366       attrForValue: {
    367         type: String,
    368         value: 'bind-value'
    369       },
    370 
    371       /**
    372        * Set to true to auto-validate the input value when it changes.
    373        */
    374       autoValidate: {
    375         type: Boolean,
    376         value: false
    377       },
    378 
    379       /**
    380        * True if the input is invalid. This property is set automatically when the input value
    381        * changes if auto-validating, or when the `iron-input-validate` event is heard from a child.
    382        */
    383       invalid: {
    384         observer: '_invalidChanged',
    385         type: Boolean,
    386         value: false
    387       },
    388 
    389       /**
    390        * True if the input has focus.
    391        */
    392       focused: {
    393         readOnly: true,
    394         type: Boolean,
    395         value: false,
    396         notify: true
    397       },
    398 
    399       _addons: {
    400         type: Array
    401         // do not set a default value here intentionally - it will be initialized lazily when a
    402         // distributed child is attached, which may occur before configuration for this element
    403         // in polyfill.
    404       },
    405 
    406       _inputHasContent: {
    407         type: Boolean,
    408         value: false
    409       },
    410 
    411       _inputSelector: {
    412         type: String,
    413         value: 'input,textarea,.paper-input-input'
    414       },
    415 
    416       _boundOnFocus: {
    417         type: Function,
    418         value: function() {
    419           return this._onFocus.bind(this);
    420         }
    421       },
    422 
    423       _boundOnBlur: {
    424         type: Function,
    425         value: function() {
    426           return this._onBlur.bind(this);
    427         }
    428       },
    429 
    430       _boundOnInput: {
    431         type: Function,
    432         value: function() {
    433           return this._onInput.bind(this);
    434         }
    435       },
    436 
    437       _boundValueChanged: {
    438         type: Function,
    439         value: function() {
    440           return this._onValueChanged.bind(this);
    441         }
    442       }
    443     },
    444 
    445     listeners: {
    446       'addon-attached': '_onAddonAttached',
    447       'iron-input-validate': '_onIronInputValidate'
    448     },
    449 
    450     get _valueChangedEvent() {
    451       return this.attrForValue + '-changed';
    452     },
    453 
    454     get _propertyForValue() {
    455       return Polymer.CaseMap.dashToCamelCase(this.attrForValue);
    456     },
    457 
    458     get _inputElement() {
    459       return Polymer.dom(this).querySelector(this._inputSelector);
    460     },
    461 
    462     get _inputElementValue() {
    463       return this._inputElement[this._propertyForValue] || this._inputElement.value;
    464     },
    465 
    466     ready: function() {
    467       if (!this._addons) {
    468         this._addons = [];
    469       }
    470       this.addEventListener('focus', this._boundOnFocus, true);
    471       this.addEventListener('blur', this._boundOnBlur, true);
    472     },
    473 
    474     attached: function() {
    475       if (this.attrForValue) {
    476         this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
    477       } else {
    478         this.addEventListener('input', this._onInput);
    479       }
    480 
    481       // Only validate when attached if the input already has a value.
    482       if (this._inputElementValue != '') {
    483         this._handleValueAndAutoValidate(this._inputElement);
    484       } else {
    485         this._handleValue(this._inputElement);
    486       }
    487     },
    488 
    489     _onAddonAttached: function(event) {
    490       if (!this._addons) {
    491         this._addons = [];
    492       }
    493       var target = event.target;
    494       if (this._addons.indexOf(target) === -1) {
    495         this._addons.push(target);
    496         if (this.isAttached) {
    497           this._handleValue(this._inputElement);
    498         }
    499       }
    500     },
    501 
    502     _onFocus: function() {
    503       this._setFocused(true);
    504     },
    505 
    506     _onBlur: function() {
    507       this._setFocused(false);
    508       this._handleValueAndAutoValidate(this._inputElement);
    509     },
    510 
    511     _onInput: function(event) {
    512       this._handleValueAndAutoValidate(event.target);
    513     },
    514 
    515     _onValueChanged: function(event) {
    516       this._handleValueAndAutoValidate(event.target);
    517     },
    518 
    519     _handleValue: function(inputElement) {
    520       var value = this._inputElementValue;
    521 
    522       // type="number" hack needed because this.value is empty until it's valid
    523       if (value || value === 0 || (inputElement.type === 'number' && !inputElement.checkValidity())) {
    524         this._inputHasContent = true;
    525       } else {
    526         this._inputHasContent = false;
    527       }
    528 
    529       this.updateAddons({
    530         inputElement: inputElement,
    531         value: value,
    532         invalid: this.invalid
    533       });
    534     },
    535 
    536     _handleValueAndAutoValidate: function(inputElement) {
    537       if (this.autoValidate) {
    538         var valid;
    539         if (inputElement.validate) {
    540           valid = inputElement.validate(this._inputElementValue);
    541         } else {
    542           valid = inputElement.checkValidity();
    543         }
    544         this.invalid = !valid;
    545       }
    546 
    547       // Call this last to notify the add-ons.
    548       this._handleValue(inputElement);
    549     },
    550 
    551     _onIronInputValidate: function(event) {
    552       this.invalid = this._inputElement.invalid;
    553     },
    554 
    555     _invalidChanged: function() {
    556       if (this._addons) {
    557         this.updateAddons({invalid: this.invalid});
    558       }
    559     },
    560 
    561     /**
    562      * Call this to update the state of add-ons.
    563      * @param {Object} state Add-on state.
    564      */
    565     updateAddons: function(state) {
    566       for (var addon, index = 0; addon = this._addons[index]; index++) {
    567         addon.update(state);
    568       }
    569     },
    570 
    571     _computeInputContentClass: function(noLabelFloat, alwaysFloatLabel, focused, invalid, _inputHasContent) {
    572       var cls = 'input-content';
    573       if (!noLabelFloat) {
    574         var label = this.querySelector('label');
    575 
    576         if (alwaysFloatLabel || _inputHasContent) {
    577           cls += ' label-is-floating';
    578           // If the label is floating, ignore any offsets that may have been
    579           // applied from a prefix element.
    580           this.$.labelAndInputContainer.style.position = 'static';
    581 
    582           if (invalid) {
    583             cls += ' is-invalid';
    584           } else if (focused) {
    585             cls += " label-is-highlighted";
    586           }
    587         } else {
    588           // When the label is not floating, it should overlap the input element.
    589           if (label) {
    590             this.$.labelAndInputContainer.style.position = 'relative';
    591           }
    592         }
    593       } else {
    594         if (_inputHasContent) {
    595           cls += ' label-is-hidden';
    596         }
    597       }
    598       return cls;
    599     },
    600 
    601     _computeUnderlineClass: function(focused, invalid) {
    602       var cls = 'underline';
    603       if (invalid) {
    604         cls += ' is-invalid';
    605       } else if (focused) {
    606         cls += ' is-highlighted'
    607       }
    608       return cls;
    609     },
    610 
    611     _computeAddOnContentClass: function(focused, invalid) {
    612       var cls = 'add-on-content';
    613       if (invalid) {
    614         cls += ' is-invalid';
    615       } else if (focused) {
    616         cls += ' is-highlighted'
    617       }
    618       return cls;
    619     }
    620   });
    621 </script>
    622