mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(textarea): add md-autosize directive (#1846)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -174,3 +174,9 @@
|
||||
</tr>
|
||||
</table>
|
||||
</md-card>
|
||||
|
||||
|
||||
<md-card>
|
||||
<h2>textarea autosize</h2>
|
||||
<textarea md-autosize class="demo-textarea"></textarea>
|
||||
</md-card>
|
||||
|
||||
@@ -18,3 +18,11 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-textarea {
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"../../node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"jasmine"
|
||||
"jasmine",
|
||||
"rx/rx.all"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
||||
Reference in New Issue
Block a user