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"> </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