Ionic Elastichat - Chat Demo w/ Auto Resizing Textarea

Codepen direct link

A couple of weeks ago I came across this awesome angular-elastic directive. I decided to integrate it into an app I am currently creating (coming out soon!). It works great with Ionic and adds that native feel to messaging similar to iMessage/Tinder. I’ve read that iMessage uses HTML/JS but I’m not sure on this though.

I want to share my experience with using it as there a couple of hacks needed to get it to play nice. I’m going to try and summarize what is needed to get it to work with Ionic below.

CSS

/* allows the bar-footer to be elastic /*
/* optionally set a max-height */
/* maxlength on the textarea will prevent /*
/* it from getting too large also */
.bar-footer {
  overflow: visible !important;
}

.bar-footer textarea {
  resize: none;
  height: 25px;
}

JavaScript

// I emit this event from the monospaced.elastic directive, read line 480 in the Codepen JS
$scope.$on('taResize', function(e, ta) {
  console.log('taResize');
  if (!ta) return;

  var taHeight = ta[0].offsetHeight;
  console.log('taHeight: ' + taHeight);

  if (!footerBar) return;

  var newFooterHeight = taHeight + 10;
  newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;

  footerBar.style.height = newFooterHeight + 'px';

  // for iOS you will need to add the keyboardHeight to the scroller.style.bottom
  if (device.platform.toLowerCase() === 'ios') {
    scroller.style.bottom = newFooterHeight + keyboardHeight + 'px'; 
  } else {
    scroller.style.bottom = newFooterHeight + 'px'; 
  }
});

You will notice that I grab the footer bar element in the $ionicView.enter event.

This codepen has a lot of of extra jazz in it also. I have integrated angular moment for displaying the relative time for messages. There is also autolinker.js integrated and I’m sharing a custom directive I made for it to work with Ionic/Cordova (URL links work in the message text and you can see how to open them with the in-app browser plugin in the autolinker directive I made). You can hold a message to show an $ionicActionSheet to delete the message or copy the text. CordovaClipboard is a nifty plugin that can copy text to a device’s clipboard.

This is using the Ionic beta 14.

I <3 Ionic.

Happy hacking :sunny:

22 Likes

Very nice job. I love the resizing footer.

3 Likes

I just downloaded it. Demo looks pretty nice.

1 Like

I just noticed an issue where the bottom textarea was totally out of place. This CSS fixed it -

.has-tabs-top {
  top: initial !important;
}

This is running off Ionic nightlies so it can break at any time. When beta 14 is released I’ll update the codepen so that won’t happen.

/edit This is no longer needed and was fixed before the beta 14 release.

Hi. Wow I wish to implent this in my app.
Is it possible to put this code into my app as a module. And then how do I connect the code to a chat-page in the app??

I wonder if this is simple to do??

Thanks in advance and keep on hacking

2 Likes

Ehi I’m interesting on using on my app, but I take the code and don’t work for my.
I’ve separated app.js, controller.js and service.js… so I think the problem is that!

How I could divide the js and put in mine?

1 Like

@digitalxp It could be possible that you are forgetting to emit the taResize event in the monospaced.elastic directive. Make sure to add this line of code to the directive in the appropriate spot -

scope.$emit('taResize', $ta); // subscribe to this event in your controller

In your controller you will do this every time that occurs -

// I emit this event from the monospaced.elastic directive, read line 480 in the Codepen JS
$scope.$on('taResize', function(e, ta) {
  console.log('taResize');
  if (!ta) return;
  
  var taHeight = ta[0].offsetHeight;
  console.log('taHeight: ' + taHeight);
  
  if (!footerBar) return;
  
  var newFooterHeight = taHeight + 10;
  newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
  
  footerBar.style.height = newFooterHeight + 'px';
  scroller.style.bottom = newFooterHeight + 'px'; 
});

@pcr You can implement this in your app, I have it working flawless in an app I will be publishing for iOS/Android very soon. I had to add some extra work for iOS because I use the keyboard-attach directive provided by Ionic. You’ll want to be using the ionic-keyboard plugin as well with this setting cordova.plugins.Keyboard.disableScroll(true).

In order to prevent the content from scrolling into the message on iOS you will need to get the keyboard height and add it to the scroller.style.bottom like this below. You’ll also need the keyboardHideHandler logic also.

var keyboardHeight = 0;

window.addEventListener('native.keyboardshow', keyboardShowHandler);
window.addEventListener('native.keyboardhide', keyboardHideHandler);

function keyboardShowHandler(e) {
    console.log('Keyboard height is: ' + e.keyboardHeight);
    keyboardHeight = e.keyboardHeight;
}

function keyboardHideHandler(e) {
    console.log('Goodnight, sweet prince');
    keyboardHeight = 0;
    $timeout(function() {
      scroller.style.bottom = footerBar.clientHeight + 'px'; 
    }, 0);
}


// For iOS you will need to add the keyboardHeight to the scroller.style.bottom
$scope.$on('taResize', function(e, ta) {
  console.log('taResize');
  if (!ta) return;
  
  var taHeight = ta[0].offsetHeight;
  console.log('taHeight: ' + taHeight);
  
  if (!footerBar) return;
  
  var newFooterHeight = taHeight + 10;
  newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
  
  footerBar.style.height = newFooterHeight + 'px';

  if (device.platform.toLowerCase() === 'ios') {
    scroller.style.bottom = newFooterHeight + keyboardHeight + 'px'; 
  } else {
    scroller.style.bottom = newFooterHeight + 'px'; 
  }
});

I suggest removing the event listeners for the keyboard handlers when your view is left like so -

$scope.$on('$ionicView.leave', function() {
    window.removeEventListener('native.keyboardshow', keyboardShowHandler);
    window.removeEventListener('native.keyboardhide', keyboardHideHandler);
});

@claw Thank you for your addition to my Codepen that prevents the content from scrolling into the message when the textarea gets increased in size. Above is the solution I use for iOS :~)

2 Likes

Ok i found the problem…the script is not compatible with ionic 13! I just change ionic 14 and work, but at this time I’ve a lot of problem converting my app from 13 to 14!

I succesfully integrate elastichat in my app, but now do you think it’s so difficult build for $http and save the message on MYSQL (I need only the angular part, for php/mysql I’ve not problem!)?

nice addition regarding the keyboard size!

1 Like

I recommend keeping all of your $http usage in a service and out of your controllers. I am keeping the below example simple to illustrate the main points.

Create a service/factory -

.factory('MessageService', ['$http', '$rootScope', '$q',
  function($http, $rootScope, $q) {

    var me = {};

    me.sendMessage = function(data) {
      // show a loader maybe?

      return $http.post($rootScope.serverURL + '/SendMessage', data).then(function(response) {
        return response.data;
      }, function(err) {
        console.log('send message error, err: ' + JSON.stringify(err, null, 2));
      }).finally(function() {
        // hide your loader maybe?
      });
    };

    // other methods here, "getMessages()", etc.

    return me;

}])

Use it from your controller -

.controller('UserMessagesCtrl', ['$scope', 'MessageService',
  function($scope, MessageService) {

    $scope.input = {
      text: '',
      toId: toUserId // this would come from $stateParams likely
    };

    $scope.sendMessage = function(sendMessageForm) {
      MessageService.sendMessage($scope.input).then(function(data) {
        console.log('message sent to server and got a successful response');
      });
    };

}]);

Looks like a very sleek demo. Very impressed with what’s possible in ionic with so little code.

Am working on a kind of messaging app myself at the moment and this tackles quite a few features I had on my To-Do list. Was wondering or you also implemented infinite-scroll to update the messages? And if so in what way you solved the initial loop it’s get stuck in if you don’t have enough messages to fill the screen?

By the way can’t wait to see what your final app is gonna be.

Thanks for posting about this! I think it is fantastic what you did. I am trying to implement this myself, however it is not working out so smoothly for me. As you can see in the image, the textbox is pushing everything up. Including the header. Content is also going under the footer too. I guess Ill just keep playing with it til I get it right.

So I’ve to remove

.factory('MockService', ['$http', '$q',
  function($http, $q) {
    var me = {};

    me.getUserMessages = function(d) {
      /*
      var endpoint =
        'http://www.mocky.io/v2/547cf341501c337f0c9a63fd?callback=JSON_CALLBACK';
      return $http.jsonp(endpoint).then(function(response) {
        return response.data;
      }, function(err) {
        console.log('get user messages error, err: ' + JSON.stringify(
          err, null, 2));
      });
      */
      var deferred = $q.defer();
      
		 setTimeout(function() {
      	deferred.resolve(getMockMessages());
	    }, 1500);
      
      return deferred.promise;
    };

    me.getMockMessage = function() {
      return {
        userId: '534b8e5aaa5e7afc1b23e69b',
        date: new Date(),
        text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
      };
    }

    return me;
  }
])

Ok finally I rewrite your code for me, and mixed it:

.controller('chatCtrl', function($scope, $http, $ionicLoading, $stateParams, $ionicScrollDelegate, $localstorage, $timeout) {
	var viewScroll = $ionicScrollDelegate.$getByHandle('userMessageScroll');
    var footerBar; // gets set in $ionicView.enter
    var scroller;
    var txtInput; // ^^^
	$scope.tuaPhoto = $localstorage.get('avatar');
	$scope.toUser = {"username":$stateParams.id};
	$scope.fromUser = {"user":$localstorage.get('name')};
    $scope.$on('$ionicView.enter', function() {
      $http.post('http://www.digitalxp.it/appwork/include/read_chat.php?U_FROM='+$localstorage.get('name')+'&U_TO='+$stateParams.id)
		.success(function(data){
			$scope.messages = data;
			$scope.doneLoading = true;
			$timeout(function(){viewScroll.scrollBottom()}, 0);
			})
		.error(function(){alert("Errore di comunicazione!")});
      
      $timeout(function() {
        footerBar = document.body.querySelector('#userMessagesView .bar-footer');
        scroller = document.body.querySelector('#userMessagesView .scroll-content');
        txtInput = angular.element(footerBar.querySelector('textarea'));
      }, 0);
    });
	
	$scope.sendMessage = function(sendMessageForm) {
		  keepKeyboardOpen();
		  $http.post('http://www.digitalxp.it/appwork/include/read_chat.php?U_FROM='+$localstorage.get('name')+'&msg=1&U_TO='+$stateParams.id, $scope.input)
			.success(function(data){$scope.messages = data;$scope.input.message = '';viewScroll.scrollBottom(true);})
			.error(function(){alert("Errore di comunicazione!")});
		  $timeout(function(){keepKeyboardOpen();viewScroll.scrollBottom(true)}, 0);
	};
	
	$scope.onMessageHold = function(event, index, message) {
		$http.post('http://www.digitalxp.it/appwork/include/read_chat.php?U_FROM='+$localstorage.get('name')+'&id='+message+'&U_TO='+$stateParams.id)
			.success(function(data){$scope.messages = data;viewScroll.scrollBottom(true);})
			.error(function(){alert("Errore di comunicazione!")});
	}
	
	
	function keepKeyboardOpen() {
      console.log('keepKeyboardOpen');
      txtInput.one('blur', function() {
        txtInput[0].focus();
      });
    }
	
	$scope.$on('taResize', function(e, ta) {
      console.log('taResize');
      if (!ta) return;
      var taHeight = ta[0].offsetHeight;
      if (!footerBar) return;
      var newFooterHeight = taHeight + 10;
      newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
      footerBar.style.height = newFooterHeight + 'px';
      scroller.style.bottom = newFooterHeight + 'px'; 
    });
})

my question now is: How could I refresh for example each 5 sec the $http calling?

add an ng-if='nbrMsg > 20" on your ion-infinite-scroll tag :slight_smile:

why? To show max 20 msg?
This is my ion-content:

<ion-content has-bouncing="true" class="has-header has-footer" delegate-handle="userMessageScroll">
    <div ng-repeat="message in messages" class="message-wrapper" on-hold="onMessageHold('del', $index, message.id_msg)">
      <div ng-if="fromUser.user === message.user"> <img ng-if="tuaPhoto != '0' && tuaPhoto != ''" class="profile-pic right" ng-src="{{tuaPhoto}}"> <img ng-if="tuaPhoto == '0' || tuaPhoto == ''" src="img/anonimus.png" class="profile-pic right">
        <div class="chat-bubble right">
          <div class="message" ng-bind-html="message.text | nl2br" autolinker> </div>
          <div class="message-detail"> <span ng-click="viewProfile(message)" class="bold">{{message.from}}</span>, <span am-time-ago="message.inviato"></span> </div>
        </div>
      </div>
      <div ng-if="fromUser.user !== message.user"> <img ng-if="message.photo_url != '0' && message.photo_url != ''" class="profile-pic left" ng-src="http://www.digitalxp.it/public/speedjob/users/{{message.from}}/{{message.photo_url}}"> <img ng-if="message.photo_url == '0' || message.photo_url == ''" src="img/anonimus.png" class="profile-pic left">
        <div class="chat-bubble left">
          <div class="message" ng-bind-html="message.text | nl2br" autolinker> </div>
          <div class="message-detail"> <span ng-click="viewProfile(message)" class="bold">{{message.from}}</span>, <span am-time-ago="message.inviato"></span> </div>
        </div>
      </div>
      <div class="cf"></div>
    </div>
  </ion-content>

I was answering to @TobiasS sorry!

@WidawskiJ That is certainly an option however I would like to update the list if even just one message is available. Furthermore I’m getting the messages from my back-end and and thus don’t know how many messages are available until I perform a GET request. Something I would like to to initialize by the infinite-scroll.
For now I implemented a minimum passed time between refreshes. This results in a polling of the refresh call (in my case every 5 seconds). Once the list is filled the refresher works as expected.

HTML:

<ion-infinite-scroll on-infinite="updateMessages()" distance="-1%" ng-if="backendReady && updateAllowed">
</ion-infinite-scroll>

Angular Controller:

$scope.updateMessages = function() {
// Get messages from backend and add to scope
$scope.addMessagesToScope();

// Set updateAllowed to false until 5seconds after update
$scope.updateAllowed = false;

// Resolves the double call by infinite-scroll (recognized ionic bug)
$timeout(function() {$scope.$broadcast('scroll.infiniteScrollComplete');}, 10);

// Time delay before next trigger of refresher is allowed
$timeout(function() {$scope.updateAllowed = true;}, 5000);
}
1 Like

and for me anyone answer??? :frowning: