Skip to content

Commit 01d1658

Browse files
committed
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
1 parent ab1204d commit 01d1658

File tree

6 files changed

+112
-7
lines changed

6 files changed

+112
-7
lines changed

src/cdk/text-field/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ng_module(
88
module_name = "@angular/cdk/text-field",
99
deps = [
1010
"@rxjs",
11+
"//src/cdk/coercion",
1112
"//src/cdk/platform",
1213
],
1314
tsconfig = "//src/lib:tsconfig-build.json",

src/cdk/text-field/autosize.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('CdkTextareaAutosize', () => {
2424
AutosizeTextAreaWithContent,
2525
AutosizeTextAreaWithValue,
2626
AutosizeTextareaWithNgModel,
27+
AutosizeTextareaWithoutAutosize,
2728
],
2829
});
2930

@@ -217,6 +218,50 @@ describe('CdkTextareaAutosize', () => {
217218

218219
expect(autosize.resizeToFitContent).toHaveBeenCalled();
219220
}));
221+
222+
it('should not trigger a resize when it is disabled', fakeAsync(() => {
223+
const fixtureWithoutAutosize = TestBed.createComponent(AutosizeTextareaWithoutAutosize);
224+
textarea = fixtureWithoutAutosize.nativeElement.querySelector('textarea');
225+
autosize = fixtureWithoutAutosize.debugElement.query(By.css('textarea'))
226+
.injector.get(CdkTextareaAutosize);
227+
228+
fixtureWithoutAutosize.detectChanges();
229+
230+
let previousHeight = textarea.clientHeight;
231+
232+
fixtureWithoutAutosize.componentInstance.content = `
233+
Line
234+
Line
235+
Line
236+
Line
237+
Line`;
238+
239+
// Manually call resizeToFitContent instead of faking an `input` event.
240+
fixtureWithoutAutosize.detectChanges();
241+
242+
expect(textarea.clientHeight)
243+
.toEqual(previousHeight, 'Expected textarea to still have the same size.');
244+
expect(textarea.clientHeight)
245+
.toBeLessThan(textarea.scrollHeight,
246+
'Expected textarea height to be less than its scrollHeight');
247+
248+
autosize.enabled = true;
249+
fixtureWithoutAutosize.detectChanges();
250+
251+
expect(textarea.clientHeight)
252+
.toBeGreaterThan(previousHeight, 'Expected textarea to have grown after enabling.');
253+
expect(textarea.clientHeight)
254+
.toBe(textarea.scrollHeight, 'Expected textarea height to match its scrollHeight');
255+
256+
autosize.enabled = false;
257+
fixtureWithoutAutosize.detectChanges();
258+
259+
expect(textarea.clientHeight)
260+
.toEqual(previousHeight, 'Expected textarea to again have the original size.');
261+
expect(textarea.clientHeight)
262+
.toBeLessThan(textarea.scrollHeight,
263+
'Expected textarea height to be less than its scrollHeight again');
264+
}));
220265
});
221266

222267
// Styles to reset padding and border to make measurement comparisons easier.
@@ -257,3 +302,12 @@ class AutosizeTextAreaWithValue {
257302
class AutosizeTextareaWithNgModel {
258303
model = '';
259304
}
305+
306+
@Component({
307+
template: `<textarea [cdkTextareaAutosize]="false">{{content}}</textarea>`,
308+
styles: [textareaStyleReset],
309+
})
310+
class AutosizeTextareaWithoutAutosize {
311+
content: string = '';
312+
}
313+

src/cdk/text-field/autosize.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
910
import {
1011
Directive,
1112
ElementRef,
@@ -35,10 +36,16 @@ import {fromEvent, Subject} from 'rxjs';
3536
export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
3637
/** Keep track of the previous textarea value to avoid resizing when the value hasn't changed. */
3738
private _previousValue: string;
39+
private _initialHeight: string | null;
3840
private readonly _destroyed = new Subject<void>();
3941

4042
private _minRows: number;
4143
private _maxRows: number;
44+
private _enabled: boolean = true;
45+
46+
private get textarea(): HTMLTextAreaElement {
47+
return this._elementRef.nativeElement as HTMLTextAreaElement;
48+
}
4249

4350
/** Minimum amount of rows in the textarea. */
4451
@Input('cdkAutosizeMinRows')
@@ -56,6 +63,19 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
5663
this._setMaxHeight();
5764
}
5865

66+
/** Whether autosizing is enabled or not */
67+
@Input('cdkTextareaAutosize')
68+
get enabled(): boolean { return this._enabled; }
69+
set enabled(value: boolean) {
70+
value = coerceBooleanProperty(value);
71+
72+
// Only act if the actual value changed. This specifically helps to not run
73+
// resizeToFitContent too early (i.e. before ngAfterViewInit)
74+
if (this._enabled !== value) {
75+
(this._enabled = value) ? this.resizeToFitContent(true) : this.reset();
76+
}
77+
}
78+
5979
/** Cached height of a textarea with a single row. */
6080
private _cachedLineHeight: number;
6181

@@ -86,6 +106,9 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
86106

87107
ngAfterViewInit() {
88108
if (this._platform.isBrowser) {
109+
// Remember the height which we started with in case autosizing is disabled
110+
this._initialHeight = this.textarea.style.height;
111+
89112
this.resizeToFitContent();
90113

91114
this._ngZone.runOutsideAngular(() => {
@@ -103,8 +126,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
103126

104127
/** Sets a style property on the textarea element. */
105128
private _setTextareaStyle(property: string, value: string): void {
106-
const textarea = this._elementRef.nativeElement as HTMLTextAreaElement;
107-
textarea.style[property] = value;
129+
this.textarea.style[property] = value;
108130
}
109131

110132
/**
@@ -119,10 +141,8 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
119141
return;
120142
}
121143

122-
let textarea = this._elementRef.nativeElement as HTMLTextAreaElement;
123-
124144
// Use a clone element because we have to override some styles.
125-
let textareaClone = textarea.cloneNode(false) as HTMLTextAreaElement;
145+
let textareaClone = this.textarea.cloneNode(false) as HTMLTextAreaElement;
126146
textareaClone.rows = 1;
127147

128148
// 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 {
143163
// See Firefox bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=33654
144164
textareaClone.style.overflow = 'hidden';
145165

146-
textarea.parentNode!.appendChild(textareaClone);
166+
this.textarea.parentNode!.appendChild(textareaClone);
147167
this._cachedLineHeight = textareaClone.clientHeight;
148-
textarea.parentNode!.removeChild(textareaClone);
168+
this.textarea.parentNode!.removeChild(textareaClone);
149169

150170
// Min and max heights have to be re-calculated if the cached line height changes
151171
this._setMinHeight();
@@ -164,6 +184,11 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
164184
* recalculated only if the value changed since the last call.
165185
*/
166186
resizeToFitContent(force: boolean = false) {
187+
// If autosizing is disabled, just skip everything else
188+
if (!this._enabled) {
189+
return;
190+
}
191+
167192
this._cacheTextareaLineHeight();
168193

169194
// If we haven't determined the line-height yet, we know we're still hidden and there's no point
@@ -211,6 +236,16 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
211236
this._previousValue = value;
212237
}
213238

239+
/**
240+
* Resets the textarea to it's original size
241+
*/
242+
reset() {
243+
if (this._initialHeight === undefined) {
244+
return;
245+
}
246+
this.textarea.style.height = this._initialHeight;
247+
}
248+
214249
_noopInputHandler() {
215250
// no-op handler that ensures we're running change detection on input events.
216251
}

src/demo-app/input/input-demo.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,12 @@ <h3>&lt;textarea&gt; with ngModel</h3>
559559
Plain textarea with auto size
560560
<textarea cdkTextareaAutosize [(ngModel)]="textareaNgModelValue"></textarea>
561561
</label>
562+
563+
<h3>&lt;textarea&gt; with bindable autosize </h3>
564+
<mat-checkbox [(ngModel)]="textareaAutosizeEnabled" name="autosizeEnabledCheckbox">
565+
Autosize enabled
566+
</mat-checkbox>
567+
<textarea [cdkTextareaAutosize]="textareaAutosizeEnabled"></textarea>
562568
</mat-card-content>
563569
</mat-card>
564570

src/demo-app/input/input-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class InputDemo {
2929
hideRequiredMarker: boolean;
3030
ctrlDisabled = false;
3131
textareaNgModelValue: string;
32+
textareaAutosizeEnabled = false;
3233
placeholderTestControl = new FormControl('', Validators.required);
3334

3435
name: string;

src/lib/input/autosize.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ export class MatTextareaAutosize extends CdkTextareaAutosize {
3434
@Input()
3535
get matAutosizeMaxRows(): number { return this.maxRows; }
3636
set matAutosizeMaxRows(value: number) { this.maxRows = value; }
37+
38+
@Input('mat-autosize')
39+
get matAutosize(): boolean { return this.enabled; }
40+
set matAutosize(value: boolean) { this.enabled = value; }
41+
42+
@Input()
43+
get matTextareaAutosize(): boolean { return this.enabled; }
44+
set matTextareaAutosize(value: boolean) { this.enabled = value; }
3745
}

0 commit comments

Comments
 (0)