diff --git a/package.json b/package.json
index 835fe16491eb..97fd10eff170 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"@types/minimist": "^1.1.28",
"@types/node": "^6.0.34",
"@types/run-sequence": "0.0.27",
+ "@types/rx": "^2.5.33",
"browserstacktunnel-wrapper": "^2.0.0",
"conventional-changelog": "^1.1.0",
"express": "^4.14.0",
diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html
index 2130e211064e..1bef8bfd2df4 100644
--- a/src/demo-app/input/input-demo.html
+++ b/src/demo-app/input/input-demo.html
@@ -174,3 +174,9 @@
Textarea
+
+
+
+ textarea autosize
+
+
diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss
index 97a9f93ea2df..b70dd6f1e573 100644
--- a/src/demo-app/input/input-demo.scss
+++ b/src/demo-app/input/input-demo.scss
@@ -18,3 +18,11 @@
.demo-card {
margin: 16px;
}
+
+.demo-textarea {
+ resize: none;
+ border: none;
+ overflow: auto;
+ padding: 0;
+ background: lightblue;
+}
diff --git a/src/lib/input/autosize.spec.ts b/src/lib/input/autosize.spec.ts
new file mode 100644
index 000000000000..102ec41875db
--- /dev/null
+++ b/src/lib/input/autosize.spec.ts
@@ -0,0 +1,127 @@
+import {Component} from '@angular/core';
+import {ComponentFixture, TestBed, async} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {MdInputModule} from './input';
+import {MdTextareaAutosize} from './autosize';
+
+
+describe('MdTextareaAutosize', () => {
+ let fixture: ComponentFixture;
+ let textarea: HTMLTextAreaElement;
+ let autosize: MdTextareaAutosize;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdInputModule],
+ declarations: [AutosizeTextAreaWithContent, AutosizeTextAreaWithValue],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AutosizeTextAreaWithContent);
+ fixture.detectChanges();
+
+ textarea = fixture.nativeElement.querySelector('textarea');
+ autosize = fixture.debugElement.query(
+ By.directive(MdTextareaAutosize)).injector.get(MdTextareaAutosize);
+ });
+
+ it('should resize the textarea based on its content', () => {
+ let previousHeight = textarea.offsetHeight;
+
+ fixture.componentInstance.content = `
+ Once upon a midnight dreary, while I pondered, weak and weary,
+ Over many a quaint and curious volume of forgotten lore—
+ While I nodded, nearly napping, suddenly there came a tapping,
+ As of some one gently rapping, rapping at my chamber door.
+ “’Tis some visitor,” I muttered, “tapping at my chamber door—
+ Only this and nothing more.”`;
+
+ // Manually call resizeToFitContent instead of faking an `input` event.
+ fixture.detectChanges();
+ autosize.resizeToFitContent();
+
+ expect(textarea.offsetHeight)
+ .toBeGreaterThan(previousHeight, 'Expected textarea to have grown with added content.');
+ expect(textarea.offsetHeight)
+ .toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
+
+ previousHeight = textarea.offsetHeight;
+ fixture.componentInstance.content += `
+ Ah, distinctly I remember it was in the bleak December;
+ And each separate dying ember wrought its ghost upon the floor.
+ Eagerly I wished the morrow;—vainly I had sought to borrow
+ From my books surcease of sorrow—sorrow for the lost Lenore—
+ For the rare and radiant maiden whom the angels name Lenore—
+ Nameless here for evermore.`;
+
+ fixture.detectChanges();
+ autosize.resizeToFitContent();
+
+ expect(textarea.offsetHeight)
+ .toBeGreaterThan(previousHeight, 'Expected textarea to have grown with added content.');
+ expect(textarea.offsetHeight)
+ .toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
+ });
+
+ it('should set a min-width based on minRows', () => {
+ expect(textarea.style.minHeight).toBeFalsy();
+
+ fixture.componentInstance.minRows = 4;
+ fixture.detectChanges();
+
+ expect(textarea.style.minHeight).toBeDefined('Expected a min-height to be set via minRows.');
+
+ let previousMinHeight = parseInt(textarea.style.minHeight);
+ fixture.componentInstance.minRows = 6;
+ fixture.detectChanges();
+
+ expect(parseInt(textarea.style.minHeight))
+ .toBeGreaterThan(previousMinHeight, 'Expected increased min-height with minRows increase.');
+ });
+
+ it('should set a max-width based on maxRows', () => {
+ expect(textarea.style.maxHeight).toBeFalsy();
+
+ fixture.componentInstance.maxRows = 4;
+ fixture.detectChanges();
+
+ expect(textarea.style.maxHeight).toBeDefined('Expected a max-height to be set via maxRows.');
+
+ let previousMaxHeight = parseInt(textarea.style.maxHeight);
+ fixture.componentInstance.maxRows = 6;
+ fixture.detectChanges();
+
+ expect(parseInt(textarea.style.maxHeight))
+ .toBeGreaterThan(previousMaxHeight, 'Expected increased max-height with maxRows increase.');
+ });
+});
+
+
+// Styles to reset padding and border to make measurement comparisons easier.
+const textareaStyleReset = `
+ textarea {
+ padding: 0;
+ border: none;
+ overflow: auto;
+ }`;
+
+@Component({
+ template: ``,
+ styles: [textareaStyleReset],
+})
+class AutosizeTextAreaWithContent {
+ minRows: number = null;
+ maxRows: number = null;
+ content: string = '';
+}
+
+@Component({
+ template: ``,
+ styles: [textareaStyleReset],
+})
+class AutosizeTextAreaWithValue {
+ value: string = '';
+}
diff --git a/src/lib/input/autosize.ts b/src/lib/input/autosize.ts
new file mode 100644
index 000000000000..458ce88dc855
--- /dev/null
+++ b/src/lib/input/autosize.ts
@@ -0,0 +1,81 @@
+import {Directive, ElementRef, Input, OnInit} from '@angular/core';
+
+
+/**
+ * Directive to automatically resize a textarea to fit its content.
+ */
+@Directive({
+ selector: 'textarea[md-autosize]',
+ host: {
+ '(input)': 'resizeToFitContent()',
+ '[style.min-height]': '_minHeight',
+ '[style.max-height]': '_maxHeight',
+ },
+})
+export class MdTextareaAutosize implements OnInit {
+ /** Minimum number of rows for this textarea. */
+ @Input() minRows: number;
+
+ /** Maximum number of rows for this textarea. */
+ @Input() maxRows: number;
+
+ /** Cached height of a textarea with a single row. */
+ private _cachedLineHeight: number;
+
+ constructor(private _elementRef: ElementRef) { }
+
+ /** The minimum height of the textarea as determined by minRows. */
+ get _minHeight() {
+ return this.minRows ? `${this.minRows * this._cachedLineHeight}px` : null;
+ }
+
+ /** The maximum height of the textarea as determined by maxRows. */
+ get _maxHeight() {
+ return this.maxRows ? `${this.maxRows * this._cachedLineHeight}px` : null;
+ }
+
+ ngOnInit() {
+ this._cacheTextareaLineHeight();
+ this.resizeToFitContent();
+ }
+
+ /**
+ * Cache the hight of a single-row textarea.
+ *
+ * We need to know how large a single "row" of a textarea is in order to apply minRows and
+ * maxRows. For the initial version, we will assume that the height of a single line in the
+ * textarea does not ever change.
+ */
+ private _cacheTextareaLineHeight(): void {
+ let textarea = this._elementRef.nativeElement as HTMLTextAreaElement;
+
+ // Use a clone element because we have to override some styles.
+ let textareaClone = textarea.cloneNode(false) as HTMLTextAreaElement;
+ textareaClone.rows = 1;
+
+ // Use `position: absolute` so that this doesn't cause a browser layout and use
+ // `visibility: hidden` so that nothing is rendered. Clear any other styles that
+ // would affect the height.
+ textareaClone.style.position = 'absolute';
+ textareaClone.style.visibility = 'hidden';
+ textareaClone.style.border = 'none';
+ textareaClone.style.padding = '';
+ textareaClone.style.height = '';
+ textareaClone.style.minHeight = '';
+ textareaClone.style.maxHeight = '';
+
+ textarea.parentNode.appendChild(textareaClone);
+ this._cachedLineHeight = textareaClone.offsetHeight;
+ textarea.parentNode.removeChild(textareaClone);
+ }
+
+ /** Resize the textarea to fit its content. */
+ resizeToFitContent() {
+ let textarea = this._elementRef.nativeElement as HTMLTextAreaElement;
+ // Reset the textarea height to auto in order to shrink back to its default size.
+ textarea.style.height = 'auto';
+
+ // Use the scrollHeight to know how large the textarea *would* be if fit its entire value.
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ }
+}
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
index 3d8b10a3c613..3003b3e3e71b 100644
--- a/src/lib/input/input.ts
+++ b/src/lib/input/input.ts
@@ -22,6 +22,7 @@ import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/for
import {CommonModule} from '@angular/common';
import {MdError, coerceBooleanProperty} from '../core';
import {Observable} from 'rxjs/Observable';
+import {MdTextareaAutosize} from './autosize';
const noop = () => {};
@@ -360,9 +361,9 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
@NgModule({
- declarations: [MdPlaceholder, MdInput, MdHint],
+ declarations: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
imports: [CommonModule, FormsModule],
- exports: [MdPlaceholder, MdInput, MdHint],
+ exports: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
})
export class MdInputModule {
static forRoot(): ModuleWithProviders {
diff --git a/src/lib/tsconfig.json b/src/lib/tsconfig.json
index 616568e24c79..0d25173aaf70 100644
--- a/src/lib/tsconfig.json
+++ b/src/lib/tsconfig.json
@@ -22,7 +22,8 @@
"../../node_modules/@types"
],
"types": [
- "jasmine"
+ "jasmine",
+ "rx/rx.all"
]
},
"angularCompilerOptions": {