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