<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../iron-behaviors/iron-control-state.html"> <link rel="import" href="../iron-flex-layout/iron-flex-layout.html"> <link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html"> <link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> <!-- `iron-autogrow-textarea` is an element containing a textarea that grows in height as more lines of input are entered. Unless an explicit height or the `maxRows` property is set, it will never scroll. Example: <iron-autogrow-textarea></iron-autogrow-textarea> Because the `textarea`'s `value` property is not observable, you should use this element's `bind-value` instead for imperative updates. ### Styling The following custom properties and mixins are available for styling: Custom property | Description | Default ----------------|-------------|---------- `--iron-autogrow-textarea` | Mixin applied to the textarea | `{}` @group Iron Elements @hero hero.svg @demo demo/index.html --> <dom-module id="iron-autogrow-textarea"> <style> :host { display: inline-block; position: relative; width: 400px; border: 1px solid; padding: 2px; -moz-appearance: textarea; -webkit-appearance: textarea; } .mirror-text { visibility: hidden; word-wrap: break-word; } .fit { @apply(--layout-fit); } textarea { position: relative; outline: none; border: none; resize: none; background: inherit; color: inherit; /* see comments in template */ width: 100%; height: 100%; font-size: inherit; font-family: inherit; line-height: inherit; text-align: inherit; @apply(--iron-autogrow-textarea); } ::content textarea:invalid { box-shadow: none; } </style> <template> <!-- the mirror sizes the input/textarea so it grows with typing --> <!-- use   instead of to allow this element to be used in XHTML --> <div id="mirror" class="mirror-text" aria-hidden="true"> </div> <!-- size the input/textarea with a div, because the textarea has intrinsic size in ff --> <div class="textarea-container fit"> <textarea id="textarea" autocomplete$="[[autocomplete]]" autofocus$="[[autofocus]]" inputmode$="[[inputmode]]" placeholder$="[[placeholder]]" readonly$="[[readonly]]" required$="[[required]]" disabled$="[[disabled]]" rows$="[[rows]]" maxlength$="[[maxlength]]"></textarea> </div> </template> </dom-module> <script> Polymer({ is: 'iron-autogrow-textarea', behaviors: [ Polymer.IronFormElementBehavior, Polymer.IronValidatableBehavior, Polymer.IronControlState ], properties: { /** * Use this property instead of `value` for two-way data binding. * * @type {string|number|undefined|null} */ bindValue: { observer: '_bindValueChanged', type: String }, /** * The initial number of rows. * * @attribute rows * @type number * @default 1 */ rows: { type: Number, value: 1, observer: '_updateCached' }, /** * The maximum number of rows this element can grow to until it * scrolls. 0 means no maximum. * * @attribute maxRows * @type number * @default 0 */ maxRows: { type: Number, value: 0, observer: '_updateCached' }, /** * Bound to the textarea's `autocomplete` attribute. */ autocomplete: { type: String, value: 'off' }, /** * Bound to the textarea's `autofocus` attribute. */ autofocus: { type: Boolean, value: false }, /** * Bound to the textarea's `inputmode` attribute. */ inputmode: { type: String }, /** * Bound to the textarea's `name` attribute. */ name: { type: String }, /** * The value for this input, same as `bindValue` */ value: { notify: true, type: String, computed: '_computeValue(bindValue)' }, /** * Bound to the textarea's `placeholder` attribute. */ placeholder: { type: String }, /** * Bound to the textarea's `readonly` attribute. */ readonly: { type: String }, /** * Set to true to mark the textarea as required. */ required: { type: Boolean }, /** * The maximum length of the input value. */ maxlength: { type: Number } }, listeners: { 'input': '_onInput' }, /** * Returns the underlying textarea. * @type HTMLTextAreaElement */ get textarea() { return this.$.textarea; }, /** * Returns textarea's selection start. * @type Number */ get selectionStart() { return this.$.textarea.selectionStart; }, /** * Returns textarea's selection end. * @type Number */ get selectionEnd() { return this.$.textarea.selectionEnd; }, /** * Sets the textarea's selection start. */ set selectionStart(value) { this.$.textarea.selectionStart = value; }, /** * Sets the textarea's selection end. */ set selectionEnd(value) { this.$.textarea.selectionEnd = value; }, /** * Returns true if `value` is valid. The validator provided in `validator` * will be used first, if it exists; otherwise, the `textarea`'s validity * is used. * @return {boolean} True if the value is valid. */ validate: function() { // Empty, non-required input is valid. if (!this.required && this.value == '') { this.invalid = false; return true; } var valid; if (this.hasValidator()) { valid = Polymer.IronValidatableBehavior.validate.call(this, this.value); } else { valid = this.$.textarea.validity.valid; this.invalid = !valid; } this.fire('iron-input-validate'); return valid; }, _bindValueChanged: function() { var textarea = this.textarea; if (!textarea) { return; } // If the bindValue changed manually, then we need to also update // the underlying textarea's value. Otherwise this change was probably // generated from the _onInput handler, and the two values are already // the same. if (textarea.value !== this.bindValue) { textarea.value = !(this.bindValue || this.bindValue === 0) ? '' : this.bindValue; } this.$.mirror.innerHTML = this._valueForMirror(); // manually notify because we don't want to notify until after setting value this.fire('bind-value-changed', {value: this.bindValue}); }, _onInput: function(event) { this.bindValue = event.path ? event.path[0].value : event.target.value; }, _constrain: function(tokens) { var _tokens; tokens = tokens || ['']; // Enforce the min and max heights for a multiline input to avoid measurement if (this.maxRows > 0 && tokens.length > this.maxRows) { _tokens = tokens.slice(0, this.maxRows); } else { _tokens = tokens.slice(0); } while (this.rows > 0 && _tokens.length < this.rows) { _tokens.push(''); } // Use   instead of to allow this element to be used in XHTML. return _tokens.join('<br/>') + ' '; }, _valueForMirror: function() { var input = this.textarea; if (!input) { return; } this.tokens = (input && input.value) ? input.value.replace(/&/gm, '&').replace(/"/gm, '"').replace(/'/gm, ''').replace(/</gm, '<').replace(/>/gm, '>').split('\n') : ['']; return this._constrain(this.tokens); }, _updateCached: function() { this.$.mirror.innerHTML = this._constrain(this.tokens); }, _computeValue: function() { return this.bindValue; } }); </script>