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