• 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`<iron-autogrow-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-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
99This 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