diff --git a/src/cdk/text-field/BUILD.bazel b/src/cdk/text-field/BUILD.bazel index d1436f158508..d36772408549 100644 --- a/src/cdk/text-field/BUILD.bazel +++ b/src/cdk/text-field/BUILD.bazel @@ -8,6 +8,7 @@ ng_module( module_name = "@angular/cdk/text-field", deps = [ "@rxjs", + "//src/cdk/coercion", "//src/cdk/platform", ], tsconfig = "//src/lib:tsconfig-build.json", diff --git a/src/cdk/text-field/autosize.spec.ts b/src/cdk/text-field/autosize.spec.ts index 746726a61885..a36240394b73 100644 --- a/src/cdk/text-field/autosize.spec.ts +++ b/src/cdk/text-field/autosize.spec.ts @@ -24,6 +24,7 @@ describe('CdkTextareaAutosize', () => { AutosizeTextAreaWithContent, AutosizeTextAreaWithValue, AutosizeTextareaWithNgModel, + AutosizeTextareaWithoutAutosize, ], }); @@ -217,6 +218,49 @@ describe('CdkTextareaAutosize', () => { expect(autosize.resizeToFitContent).toHaveBeenCalled(); })); + + it('should not trigger a resize when it is disabled', fakeAsync(() => { + const fixtureWithoutAutosize = TestBed.createComponent(AutosizeTextareaWithoutAutosize); + textarea = fixtureWithoutAutosize.nativeElement.querySelector('textarea'); + autosize = fixtureWithoutAutosize.debugElement.query(By.css('textarea')) + .injector.get(CdkTextareaAutosize); + + fixtureWithoutAutosize.detectChanges(); + + const previousHeight = textarea.clientHeight; + + fixtureWithoutAutosize.componentInstance.content = ` + Line + Line + Line + Line + Line`; + + // Manually call resizeToFitContent instead of faking an `input` event. + fixtureWithoutAutosize.detectChanges(); + + expect(textarea.clientHeight) + .toEqual(previousHeight, 'Expected textarea to still have the same size.'); + expect(textarea.clientHeight) + .toBeLessThan(textarea.scrollHeight, 'Expected textarea to a have scrollbar.'); + + autosize.enabled = true; + fixtureWithoutAutosize.detectChanges(); + + expect(textarea.clientHeight) + .toBeGreaterThan(previousHeight, + 'Expected textarea to have grown after enabling autosize.'); + expect(textarea.clientHeight) + .toBe(textarea.scrollHeight, 'Expected textarea not to have a scrollbar'); + + autosize.enabled = false; + fixtureWithoutAutosize.detectChanges(); + + expect(textarea.clientHeight) + .toEqual(previousHeight, 'Expected textarea to have the original size.'); + expect(textarea.clientHeight) + .toBeLessThan(textarea.scrollHeight, 'Expected textarea to have a scrollbar.'); + })); }); // Styles to reset padding and border to make measurement comparisons easier. @@ -257,3 +301,12 @@ class AutosizeTextAreaWithValue { class AutosizeTextareaWithNgModel { model = ''; } + +@Component({ + template: ``, + styles: [textareaStyleReset], +}) +class AutosizeTextareaWithoutAutosize { + content: string = ''; +} + diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 9f9bb437126a..576f4fd9a68c 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {coerceBooleanProperty} from '@angular/cdk/coercion'; import { Directive, ElementRef, @@ -35,10 +36,14 @@ import {fromEvent, Subject} from 'rxjs'; export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { /** Keep track of the previous textarea value to avoid resizing when the value hasn't changed. */ private _previousValue: string; + private _initialHeight: string | null; private readonly _destroyed = new Subject(); private _minRows: number; private _maxRows: number; + private _enabled: boolean = true; + + private _textareaElement: HTMLTextAreaElement; /** Minimum amount of rows in the textarea. */ @Input('cdkAutosizeMinRows') @@ -56,13 +61,28 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { this._setMaxHeight(); } + /** Whether autosizing is enabled or not */ + @Input('cdkTextareaAutosize') + get enabled(): boolean { return this._enabled; } + set enabled(value: boolean) { + value = coerceBooleanProperty(value); + + // Only act if the actual value changed. This specifically helps to not run + // resizeToFitContent too early (i.e. before ngAfterViewInit) + if (this._enabled !== value) { + (this._enabled = value) ? this.resizeToFitContent(true) : this.reset(); + } + } + /** Cached height of a textarea with a single row. */ private _cachedLineHeight: number; constructor( private _elementRef: ElementRef, private _platform: Platform, - private _ngZone: NgZone) {} + private _ngZone: NgZone) { + this._textareaElement = this._elementRef.nativeElement as HTMLTextAreaElement; + } /** Sets the minimum height of the textarea as determined by minRows. */ _setMinHeight(): void { @@ -86,6 +106,9 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { ngAfterViewInit() { if (this._platform.isBrowser) { + // Remember the height which we started with in case autosizing is disabled + this._initialHeight = this._textareaElement.style.height; + this.resizeToFitContent(); this._ngZone.runOutsideAngular(() => { @@ -103,8 +126,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { /** Sets a style property on the textarea element. */ private _setTextareaStyle(property: string, value: string): void { - const textarea = this._elementRef.nativeElement as HTMLTextAreaElement; - textarea.style[property] = value; + this._textareaElement.style[property] = value; } /** @@ -119,10 +141,8 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { return; } - 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; + let textareaClone = this._textareaElement.cloneNode(false) as HTMLTextAreaElement; textareaClone.rows = 1; // Use `position: absolute` so that this doesn't cause a browser layout and use @@ -143,9 +163,9 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { // See Firefox bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=33654 textareaClone.style.overflow = 'hidden'; - textarea.parentNode!.appendChild(textareaClone); + this._textareaElement.parentNode!.appendChild(textareaClone); this._cachedLineHeight = textareaClone.clientHeight; - textarea.parentNode!.removeChild(textareaClone); + this._textareaElement.parentNode!.removeChild(textareaClone); // Min and max heights have to be re-calculated if the cached line height changes this._setMinHeight(); @@ -164,6 +184,11 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { * recalculated only if the value changed since the last call. */ resizeToFitContent(force: boolean = false) { + // If autosizing is disabled, just skip everything else + if (!this._enabled) { + return; + } + this._cacheTextareaLineHeight(); // If we haven't determined the line-height yet, we know we're still hidden and there's no point @@ -211,6 +236,18 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { this._previousValue = value; } + /** + * Resets the textarea to it's original size + */ + reset() { + // Do not try to change the textarea, if the initialHeight has not been determined yet + // This might potentially remove styles when reset() is called before ngAfterViewInit + if (this._initialHeight === undefined) { + return; + } + this._textareaElement.style.height = this._initialHeight; + } + _noopInputHandler() { // no-op handler that ensures we're running change detection on input events. } diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 850e0919f235..30f0bdcbdad9 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -559,6 +559,12 @@

<textarea> with ngModel

Plain textarea with auto size + +

<textarea> with bindable autosize

+ + Autosize enabled + + diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index ccac8bf1fd9e..54178dbe7bff 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -29,6 +29,7 @@ export class InputDemo { hideRequiredMarker: boolean; ctrlDisabled = false; textareaNgModelValue: string; + textareaAutosizeEnabled = false; placeholderTestControl = new FormControl('', Validators.required); name: string; diff --git a/src/lib/input/autosize.ts b/src/lib/input/autosize.ts index 7d24c0391c0c..fd4f50c6f9cf 100644 --- a/src/lib/input/autosize.ts +++ b/src/lib/input/autosize.ts @@ -34,4 +34,12 @@ export class MatTextareaAutosize extends CdkTextareaAutosize { @Input() get matAutosizeMaxRows(): number { return this.maxRows; } set matAutosizeMaxRows(value: number) { this.maxRows = value; } + + @Input('mat-autosize') + get matAutosize(): boolean { return this.enabled; } + set matAutosize(value: boolean) { this.enabled = value; } + + @Input() + get matTextareaAutosize(): boolean { return this.enabled; } + set matTextareaAutosize(value: boolean) { this.enabled = value; } }