Skip to content

Commit

Permalink
feat(cdk-input): change autosize to be bindable (#9884) (#11167)
Browse files Browse the repository at this point in the history
* feat(cdk-input): change autosize to be bindable (#9884)

This allows cdkTextareaAutosize selector to be bindable and toggle the internal
enabled / disabled state.

Upon disabling the initial height is restored (textarea.style.height before any changes
were applied).

Also added wrapper into (to be discontinued) matTextareaAutosize.

Fixes #9884

Currently missing unit tests, thus still WIP. Manual tests in demo-app seem to work as expected

* Small code quality improvements (#9884)
  • Loading branch information
pfeigl authored and tinayuangao committed May 15, 2018
1 parent af78b97 commit 2d227b7
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/cdk/text-field/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 53 additions & 0 deletions src/cdk/text-field/autosize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('CdkTextareaAutosize', () => {
AutosizeTextAreaWithContent,
AutosizeTextAreaWithValue,
AutosizeTextareaWithNgModel,
AutosizeTextareaWithoutAutosize,
],
});

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -257,3 +301,12 @@ class AutosizeTextAreaWithValue {
class AutosizeTextareaWithNgModel {
model = '';
}

@Component({
template: `<textarea [cdkTextareaAutosize]="false">{{content}}</textarea>`,
styles: [textareaStyleReset],
})
class AutosizeTextareaWithoutAutosize {
content: string = '';
}

53 changes: 45 additions & 8 deletions src/cdk/text-field/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {
Directive,
ElementRef,
Expand Down Expand Up @@ -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<void>();

private _minRows: number;
private _maxRows: number;
private _enabled: boolean = true;

private _textareaElement: HTMLTextAreaElement;

/** Minimum amount of rows in the textarea. */
@Input('cdkAutosizeMinRows')
Expand All @@ -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 {
Expand All @@ -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(() => {
Expand All @@ -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;
}

/**
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -217,6 +242,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.
}
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 @@ -559,6 +559,12 @@ <h3>&lt;textarea&gt; with ngModel</h3>
Plain textarea with auto size
<textarea cdkTextareaAutosize [(ngModel)]="textareaNgModelValue"></textarea>
</label>

<h3>&lt;textarea&gt; with bindable autosize </h3>
<mat-checkbox [(ngModel)]="textareaAutosizeEnabled" name="autosizeEnabledCheckbox">
Autosize enabled
</mat-checkbox>
<textarea [cdkTextareaAutosize]="textareaAutosizeEnabled"></textarea>
</mat-card-content>
</mat-card>

Expand Down
1 change: 1 addition & 0 deletions src/demo-app/input/input-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class InputDemo {
hideRequiredMarker: boolean;
ctrlDisabled = false;
textareaNgModelValue: string;
textareaAutosizeEnabled = false;
placeholderTestControl = new FormControl('', Validators.required);

name: string;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/input/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

0 comments on commit 2d227b7

Please sign in to comment.