Skip to content

Commit

Permalink
feat(textarea): add md-autosize directive (#1846)
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn authored and kara committed Nov 16, 2016
1 parent 289070e commit 9ec17c0
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 3 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/demo-app/input/input-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,9 @@ <h4>Textarea</h4>
</tr>
</table>
</md-card>


<md-card>
<h2>textarea autosize</h2>
<textarea md-autosize class="demo-textarea"></textarea>
</md-card>
8 changes: 8 additions & 0 deletions src/demo-app/input/input-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@
.demo-card {
margin: 16px;
}

.demo-textarea {
resize: none;
border: none;
overflow: auto;
padding: 0;
background: lightblue;
}
127 changes: 127 additions & 0 deletions src/lib/input/autosize.spec.ts
Original file line number Diff line number Diff line change
@@ -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<AutosizeTextAreaWithContent>;
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: `<textarea md-autosize [minRows]="minRows" [maxRows]="maxRows">{{content}}</textarea>`,
styles: [textareaStyleReset],
})
class AutosizeTextAreaWithContent {
minRows: number = null;
maxRows: number = null;
content: string = '';
}

@Component({
template: `<textarea md-autosize [value]="value"></textarea>`,
styles: [textareaStyleReset],
})
class AutosizeTextAreaWithValue {
value: string = '';
}
81 changes: 81 additions & 0 deletions src/lib/input/autosize.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
}
5 changes: 3 additions & 2 deletions src/lib/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {};
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"../../node_modules/@types"
],
"types": [
"jasmine"
"jasmine",
"rx/rx.all"
]
},
"angularCompilerOptions": {
Expand Down

0 comments on commit 9ec17c0

Please sign in to comment.