-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(textarea): add md-autosize directive #1846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = ''; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = ''; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also clear maxHeight and border |
||
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`; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you also do an @input() for value so you can resize when the content is changed programmatically?