Elastic ion-textarea

Has anybody managed to make the TextArea resize dynamically per new lines of text?

I found this topic, but I’m not sure how to implement it in Ionic2?

1 Like

i have a ionic 1 directive typescript-version, if that helps a little bit :wink:

export interface ExpandedTextAreaScope extends ng.IScope {
    expandText: Function;
}

export class ExpandedTextareaDirective implements ng.IDirective {

    public static ID = "expandedTextarea";

    public static get $inject(): string[] {
        return [Services.Plugins.ID];
    }

    constructor(private Plugins: Services.Plugins) {
        this.link = _.bind(this.link, this);
    }

    public restrict = "E";
    public replace  = true;
    public template = "<textarea cols='1' placeholder='{{placeholder}}' ng-keyup='expandText();'  ng-keydown='expandText();'></textarea>";
    public scope = {
        placeholder: "@",
        ngModel: "="
    };

    public link(scope: ExpandedTextAreaScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller: any, transclude: ng.ITranscludeFunction): void {
        var self = this;

        element.bind("keydown keypress", function(event) {
            if (event.which === 13) {
                console.log("keyboard down");
                event.preventDefault();
                self.Plugins.keyboard.close();
            }
        });

        scope.$watch("ngModel", () => {
            scope.expandText();
        });

        scope.expandText = () => {
            element[0].style.height = "33px";
            element[0].style.height =  element[0].scrollHeight + "px";
        };

        element[0].style.height = "33px";
    }
}`

use it:
<expanded-textarea ng-model="viewModel.mytext" placeholder="MyPlaceholder"></expanded-textarea>

1 Like

Hey @Bastian,

Thank you for that code, it really helped me get started. I ended up writing this component:

import {Component, ViewChild} from '@angular/core';

@Component({
  selector: 'elastic-textarea',
  inputs: ['placeholder', 'lineHeight'],
  template:
  `
  <ion-textarea #ionTxtArea
    placeholder='{{placeholder}}'
    [(ngModel)]="content"
    (ngModelChange)='onChange($event)'></ion-textarea>
  `,
  queries: {
    ionTxtArea: new ViewChild('ionTxtArea')
  }
})
export class ElasticTextarea {
  constructor() {
    this.content = "";
    this.lineHeight = "22px";
  }

  ngAfterViewInit(){
    this.txtArea = this.ionTxtArea._elementRef.nativeElement.children[0];
    this.txtArea.style.height = this.lineHeight + "px";
  }

  onChange(newValue){
    this.txtArea.style.height = this.lineHeight + "px";
    this.txtArea.style.height =  this.txtArea.scrollHeight + "px";
  }
}

I know that this.ionTxtArea._elementRef.nativeElement.children[0] looks pretty bad. If somebody has any other suggestions on how to get the <textarea> element that’s a child of the <ion-textarea>, please let me know.

USAGE:
I ended up using the component like this:

<ion-item>
    <ion-label style="margin:0px;"></ion-label>
    <div item-content style="width:100%;">
      <elastic-textarea placeholder="Type to compose" lineHeight="22"></elastic-textarea>
    </div>
 </ion-item>

To get the below look at the bottom of my chat page:

But it’s also usable on its own:

<elastic-textarea placeholder="Type to compose" lineHeight="22"></elastic-textarea>

I hope this helps somebody until a better solution is created.

3 Likes
import {HostListener, Directive} from '@angular/core';
@Directive({
    selector: '[elastic]'
})
export class Elastic {
    @HostListener('input',['$event.target'])
    onInput(nativeElement: any): void {
        nativeElement.style.height = nativeElement.scrollHeight + "px";
    }
}
<ion-textarea elastic></ion-textarea>
4 Likes

hiii …am using the same exmple but when the new msg come it not show next to echa other … current user msg show next to each each and then in left side the other msg show…any help

Sababa thank you for posting your solution. While this directive increases the height of the textarea it does not bring it back down when a user deletes text or removes a line (like elastic chats normally have). Does anyone have a solution for this?

I found a solution to the issue that the textarea was not resizing if a user deleted their text. You need to add a height: auto and overflow hidden. I edited sabada’s solution below.

import {HostListener, Directive} from '@angular/core';
@Directive({
    selector: '[elastic]'
})
export class Elastic {
    @HostListener('input',['$event.target'])
    onInput(nativeElement: any): void {
      nativeElement.style.overflow = 'hidden';
      nativeElement.style.height = 'auto';
      nativeElement.style.height = nativeElement.scrollHeight + "px";
    }
}

I’ve taken inspiration from the ideas here and created angular2-elastic, which supports Ionic 2. Hopefully this should serve most of the basic needs, but I hope to incorporate some of the ideas from the original angular-elastic implementation soon.

4 Likes

Thank you for your help @fiznool. Could you publish an example using an ion-footer and the keyboard-attach directive, please?

Thank you!

Hello @fiznool,

I’d also be interested to know how to use this in ion-footer. Seems like the footer and content don’t automatically resize with the text-area.

1 Like

I saw some problems with some directives here in the case the textarea grow to the extent to make the content need to have a scroll, and also erasing the content in such a case (to shrink the textarea). So I created a directive based on this:

import { Directive, ElementRef, HostListener, OnInit } from '@angular/core';

@Directive({
	selector: `[elastic]`
})
export class ElasticDirective implements OnInit {

	constructor(
		private el: ElementRef,
	) { }

	public ngOnInit() {
		setTimeout(() => this.adjust(), 0);
	}

	@HostListener('input', ['$event.target']) 
	public onInput() {
		this.adjust();
	}
	
	private adjust(): void {
		let ta = this.el.nativeElement.querySelector('textarea');

		if (ta) {
			let overflow = ta.style.overflow;
			ta.style.overflow = 'hidden';
			ta.style.height = 'auto';
			ta.style.height = ta.scrollHeight + 'px';

			if (overflow !== ta.style.overflow) {
				ta.style.overflow = overflow;
			}
		}
	}
}

@Oldstadt I’m also hoping for a solution to use this in ion-footer, because for now it occupies the content space and I can’t scroll to the content bottom. Having to call content.resize() is not practical in this context, because not every component with the textarea have ion-content in it (most of them, actually, are inside custom components), and having to propagate the change of the textarea through each and every parent component till the page component (the one that has the ion-content tag) is just a big NO, a maintenance nightmare and a very poor design (although I don’t see another way yet :confused:).

Perhaps one approach is to listen to the footer height change and resize the content in such cases. Haven’t tried yet though.

@Oldstadt I managed to use an elastic textarea in ion-footer resizing the content when the footer or header height change using a directive that listen to changes in the dimensions of the ion-header and ion-footer components.

Actually you can do this even under other circumstances aside from having a textarea in footer, its a general approach to resize the content without having to explicitly call it every time its height change.

The directive:

import { AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core';

interface Subscription {
	unsubscribe: () => void;
}

@Directive({
	selector: '[myResize]'
})
export class ResizeDirective implements AfterViewInit, OnDestroy {

	@Output('myResizeChange')
	public resizeChange = new EventEmitter<ElementRef>();

	private subscription: Subscription;

	constructor(
		private elementRef: ElementRef,
	) { }

	public ngAfterViewInit(): void {
		let element: HTMLElement = this.elementRef.nativeElement;
		this.subscription = this.subscribe(element, () => {
			this.resizeChange.emit(this.elementRef);
		});
	}

	private subscribe(element: HTMLElement, callback: () => void): Subscription {
		let offsetWidth = element.offsetWidth;
		let offsetHeight = element.offsetHeight;

		let checkForChanges = () => {
			if (
				(offsetWidth !== element.offsetWidth)
				||
				(offsetHeight !== element.offsetHeight)
			) {
				offsetWidth = element.offsetWidth;
				offsetHeight = element.offsetHeight;
				callback();
			}
		};

		let observer = new MutationObserver(() => checkForChanges());
		let config = {
			attributes: true,
			childList: true,
			characterData: true,
			subtree: true,
			attributeFilter: ['style'],
		};
		observer.observe(element, config);

		let subscription: Subscription = {
			unsubscribe: () => observer.disconnect(),
		};

		return subscription;
	}

	public ngOnDestroy(): void {
		this.subscription && this.subscription.unsubscribe();
	}
}

Then in your page HTML just do:

<ion-header myResize (myResizeChange)="onResize()">
	...
</ion-header>

<ion-content>
	...
</ion-content>

<ion-footer myResize (myResizeChange)="onResize()">
	...
</ion-footer>

And in your page .ts file:

import { Component, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';

@Component({
	selector: 'my-page',
	templateUrl: 'my.page.html',
})
export class MyPage {

	@ViewChild(Content) 
	public content: Content;

	constructor() { }

	public onResize(): void {
		console.log('resize');		
		this.content.resize();
	}
}

(Remember to declare the directive and the page in your module.)

And that’s it! It worked fine for me.

This works perfectly thank you !! It just needs a way to scroll down automatically after resizing, I used :

    this.content.scrollToBottom(0);

but it seemed that it was not scrolling down. Thus I put a timeout :

    setTimeout(() => {
      this.content.scrollToBottom(0);
    }, 20)

Which is not ideal but seems to work. If you have a better idea tell me :slight_smile: