Ion-texarea elastic directive

I’ve been working on a project recently which has a form with an ion-textarea inside that contains long text. A problem with ion-textarea for me was that it shows only two lines of text and users have to scroll through the text to change it.

So, I decided to create a directive which would resize the height of ion-textarea base on its contents.
The main requirement for the directive was that it should be a simple attribute directive. In other words, it shouldn’t be a wrapper on ion-textarea, so it’s possible to use the API of ion-textarea.

Here is how it looks - ion-textarea is resized initially to fit its text.

There is also a Plunker demo.

Note: for some reason vertical scrollbar appears on ion-textarea in Plunker, which doesn’t happen when testing in a browser or in a device though.

Usage:
<ion-textarea elasticTextArea [(ngModel)]="description"></ion-textarea>

2 Likes

Nice!

For the demo to be more impressive, maybe add ad “default” ion-textarea with the same text above oder beneath your elastic one so that the difference is clearly visible.

Hi @Sujan12, good idea!
I updated the plunk.

2 Likes

Shame that http://plnkr.co/ seems to be having problems :confused:

@eakoriakin

really cool idea :slight_smile:

Is it possible that your plunkr url in the first post shows to another example? When I open it it’s about : Ionic 2 Select with Searchbar

1 Like

Hi @tobika! Oh, sorry, wrong plunk. I think I changed it accidentally :slight_smile:
Here is a correct link.

2 Likes

Nice snip. Thanks for posting this.

I did make one small modification.

Instead of:

            this.textInput._native.valueChange.subscribe((inputValue: any) => {
                this.resize();
            });

I am using:

            this.textInput._native.ngControl.valueChanges.subscribe(() => {
                this.resize();
            });

Why? The problem with TextInput._native.valueChange (for me at least) is that this method is monitoring specifically the value of the textarea it is subscribed to.

If you do any manipulation of the textarea’s ngModel in the backend, TextInput._native.valueChange won’t pick it up. However by pointing the subscriber directly to the ngControl, we can make this elastic directive hook to something that is more static. Since the ngControl value updates whenever the textarea value updates, this likewise still triggers the script as it should.

Cheers

2 Likes

Quick update here, if anyone is using my modification for the ion-textarea elastic directive, you no longer need the _native property. That means that the proper code is:

this.textInput.ngControl.valueChanges.subscribe(() => {
      this.resize();
});

I also had to change (under resize):

this.textInput._elementRef.nativeElement.style.height = height + 'px';
1 Like

The below is the modified code I’ve written to get it working in Ionic 4.

import { Directive, ElementRef, Renderer2 } from '@angular/core';
import { DomController, Platform } from '@ionic/angular';

@Directive({
    selector: '[elasticText]',
	host: {
		'(ionChange)': 'onTextChange($event)'
	}})

export class ElasticTextArea {


    isIos: boolean;
    iosTextAreaMinHeight = 50;
	mdTextAreaMinHeight = 48;

    constructor(
		public element: ElementRef,
		public renderer: Renderer2,
		private domCtrl: DomController,
		private platform: Platform
	) { }
	
	onTextChange(ev:any){
		let ionTextArea:any = this.element.nativeElement;
		let textArea:any = ionTextArea.querySelector('textarea');
		this.resize(ionTextArea, textArea);
	}

    resize(ionTextArea:any, textArea:any) {
		let height = this.getTextAreaHeight(textArea, this.isIos ? this.iosTextAreaMinHeight : this.mdTextAreaMinHeight);
		this.domCtrl.write(() => {
    	    this.renderer.setStyle(ionTextArea, 'height', height + 'px');
			this.renderer.setStyle(textArea, 'height', height + 'px');
		});
    }
	
    getTextAreaHeight(textArea:any, minHeight: number): number {
        // Get textarea styles.
        let body = <HTMLElement>document.querySelector('body'),
            style = window.getComputedStyle(textArea, null),
            paddingHeight = parseInt(style.getPropertyValue('padding-top')) + parseInt(style.getPropertyValue('padding-bottom')),
            paddingWidth = parseInt(style.getPropertyValue('padding-left')) + parseInt(style.getPropertyValue('padding-right')),
            borderHeight = parseInt(style.getPropertyValue('border-top-width')) + parseInt(style.getPropertyValue('border-bottom-width')),
            width = parseInt(style.getPropertyValue('width')) - paddingWidth,
            lineHeight = style.getPropertyValue('line-height');

        // IE and Firefox do not support 'font' property, so we need to get it ourselves.
        let font = style.getPropertyValue('font-style') + ' ' +
            style.getPropertyValue('font-variant') + ' ' +
            style.getPropertyValue('font-weight') + ' ' +
            style.getPropertyValue('font-size') + ' ' +
            style.getPropertyValue('font-height') + ' ' +
            style.getPropertyValue('font-family');

        // Prepare a temporary textarea to determine the height for a real one.
        let newTextAreaElement = <HTMLTextAreaElement>document.createElement('TEXTAREA'),
            newTextAreaElementId = '__newTextAreaElementId__';
        newTextAreaElement.setAttribute('rows', '1');
        newTextAreaElement.setAttribute('id', newTextAreaElementId);
        newTextAreaElement.style.font = font;
        newTextAreaElement.style.width = width + 'px';
        newTextAreaElement.style.border = '0';
        newTextAreaElement.style.overflow = 'hidden';
        newTextAreaElement.style.padding = '0';
        newTextAreaElement.style.outline = '0';
        newTextAreaElement.style.resize = 'none';
        newTextAreaElement.style.lineHeight = lineHeight;

        // To measure sizes we need to add the textarea to DOM.
        body.insertAdjacentHTML('beforeend', newTextAreaElement.outerHTML);
        newTextAreaElement = <HTMLTextAreaElement>document.getElementById(newTextAreaElementId);
        newTextAreaElement.value = textArea.value;

        // Measure the height.
        newTextAreaElement.style.height = 'auto';
        newTextAreaElement.style.height = newTextAreaElement.scrollHeight + 'px';
        let height = parseInt(newTextAreaElement.style.height) + paddingHeight + borderHeight;

        if (height < minHeight) {
            height = minHeight;
        }

        // Remove the remporary textarea.
        body.removeChild(newTextAreaElement);

        return height;
    }
	
}

The css is:

	ion-textarea {
		height: 50px;
		textarea {
			overflow:hidden;
		}
	}