Skip to content

feat(cdk-input): change autosize to be bindable (#9884) #11167

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 2 commits into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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.
}
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')
Copy link
Contributor

@rafaelss95 rafaelss95 May 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correctly named, @mmalerba?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be, see the selector definition: selector: 'textarea[mat-autosize], textarea[matTextareaAutosize]',

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, yes :)

get matAutosize(): boolean { return this.enabled; }
set matAutosize(value: boolean) { this.enabled = value; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerceBooleanProperty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled is a setter within CdkTextareaAutosize which internally uses coerceBooleanProperty


@Input()
get matTextareaAutosize(): boolean { return this.enabled; }
set matTextareaAutosize(value: boolean) { this.enabled = value; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerceBooleanProperty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled is a setter within CdkTextareaAutosize which internally uses coerceBooleanProperty

}