1

I have a simple website that uses AngularJS with a NodeJS backend.

It has multiple pages, like a homepage, a login/register page, etc.

I'd like to implement a "Chat" page where you could send messages to other clients using socket.io. I already got that part working, using a local controller (by local, I mean active on a single page - the Chat page).

The problem is, I would like the chat system to be global (i.e. client can receive messages while being on the homepage, but they'll still only be displayed when going back on the Chat page).


I'm having an issue when setting the Chat controller global (active on all pages).

Here's how I'm including it:

<body ng-controller="AppCtrl"> <!-- include main controller -->
    <div ng-include="'header.tpl.html'"></div>
    <div ng-controller="ChatCtrl" class="page"> <!-- include global Chat controller -->
        <div ng-view class="container"></div>
    </div>
    <div ng-include="'footer.tpl.html'"></div>
    <!-- ...etc. -->
</body>

This works pretty well, but it seems like I can't access a value from my Chat page, though. Functions declared from the Chat controller can still be called, but the "$scope.message" value (which contains the message that's being typed) is always empty.


Here's my Chat controller (which is actually called TravelCtrl)

angular.module('base').controller('TravelCtrl', //['$scope', 'security',
  function($rootScope, $scope, security, NgMap, $geolocation, socket){

    $scope.messages = [];

    // Socket listeners
    // ================

    socket.on('init', function (data) {
      $scope.name = data.name;
      $scope.users = data.users;
    });

    socket.on('send:message', function (message) {
      $scope.messages.push(message);
    });

    socket.on('change:name', function (data) {
      changeName(data.oldName, data.newName);
    });

    socket.on('user:join', function (data) {
      $scope.messages.push({
        user: 'Server',
        text: 'User ' + data.name + ' has joined.'
      });
      $scope.users.push(data.name);
    });

    // add a message to the conversation when a user disconnects or leaves the room
    socket.on('user:left', function (data) {
      $scope.messages.push({
        user: 'chatroom',
        text: 'User ' + data.name + ' has left.'
      });
      var i, user;
      for (i = 0; i < $scope.users.length; i++) {
        user = $scope.users[i];
        if (user === data.name) {
          $scope.users.splice(i, 1);
          break;
        }
      }
    });

    // Private helpers
    // ===============

    var changeName = function (oldName, newName) {
      // rename user in list of users
      var i;
      for (i = 0; i < $scope.users.length; i++) {
        if ($scope.users[i] === oldName) {
          $scope.users[i] = newName;
        }
      }

      $scope.messages.push({
        user: 'Server',
        text: 'User ' + oldName + ' has been authenticated as ' + newName + '.'
      });
    }

    // Methods published to the scope
    // ==============================

    $scope.changeName = function () {
      socket.emit('change:name', {
        name: $scope.newName
      }, function (result) {
        if (!result) {
          alert('There was an error changing your name');
        } else {

          changeName($scope.name, $scope.newName);

          $scope.name = $scope.newName;
          $scope.newName = '';
        }
      });
    };

    $scope.sendMessage = function () {

      socket.emit('send:message', {
        message: $scope.message
      });

      // add the message to our model locally
      $scope.messages.push({
        user: $scope.name,
        text: $scope.message
      });

      // clear message box
      $scope.message = '';

    };

    // ================

    var init = function () {
      $scope.newName = security.currentUser.username;
      $scope.changeName();
    }

    if ($rootScope.hasLoaded() && $scope.name != security.currentUser.username) {
      init();
    } else {
      $rootScope.$on('info-loaded', init);
    }

  }
  //]
);

As well as the Chat page itself. The strange thing is that connected users and messages display correctly, but the controller can't seem to retrieve the typed message.

<div class='col'>
  <h3>Users</h3>
  <div class='overflowable'>
    <p ng-repeat='user in users'>{{user}}</p>
    </div>
</div>

<div class='col'>
    <h3>Messages</h3>
    <div class='overflowable'>
    <p ng-repeat='message in messages' ng-class='{alert: message.user == "chatroom"}'>{{message.user}}: {{message.text}}</p>
    </div>
</div>

<div class='clr'>
  <form ng-submit='sendMessage()'>
    Message: {{message}}<br/>
    <input size='60', ng-model='message'/>
    <input type='submit', value='Send as {{name}}'/>
    </form>
</div>

When pressing the "Send" button, AngularJS successfully calls the sendMessage function, but retrieves the "message" value as an empty string, leading it to send an empty socket.io message.


I'm quite new to AngularJS, so my approach might be totally ridiculous. I'm convinced I'm missing something obvious but after re-reading the docs again, I really can't seem to find what.

Is this a proper way to organise an AngularJS app?

Thanks in advance for your help.

3
  • 3
    you may want to consider putting most of the socket related stuff in a service so that you can inject that service into the controllers that require it. For example you could have ChatPageCtrl for the page view but have some other side-bar notification directive that uses the same service. Commented Dec 22, 2015 at 22:14
  • I edited my answer, it should help you to understand the problem. The problems are 1) no $scope.$apply, 2) you store $scope.messages but it will be removed if the controller changes (so use global var). 3) the socket.on will be called each time you change a page, not sure it unregister the previous callback on the key. Commented Dec 22, 2015 at 23:01
  • Which router are you using? ngRoute or ui-route? Commented Dec 22, 2015 at 23:09

2 Answers 2

1

Having recently built a large scale Angular/Socket.IO application, I strongly suggest that you put all of your Socket implementation into a Service. This service will maintain all of your socket state, and allow you to inject it into any required controllers. This will allow you to have a main page for Chat, however still be able to display notifications, chat user information, etc in other areas of your application.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! I was indeed looking for a cleaner way to do that and setting up a service indeed did the trick! In fact, it seemed that the $scope.message value was being unreferenced when changing it from the actual Chat page (in the text box). As soon as something was entered, $scope.message was being set to "undefined". I fixed it by moving all of the chat functions in a service, setting $scope.chat = chatService, and then modifying the "chat.message" in the Chat page. This said, I'm not modifying the $scope.message value itself, but rather the service's instead.
1

It's not about your problem, but I saw something I suspect to be wrong. When you use another library with angularjs, you should use a bridge to it (angular-socket-io for example).

When you do an $http call with angular, it updates $scope correctly in the callback and the changes are seen in the view.

In your code:

socket.on('send:message', function (message) {
  $scope.messages.push(message);
});

There is a problem: "socket" isn't a library included in angularjs, so when the callback is called, your "$scope" modification isn't correctly noticed to angularjs.

You have to do use $scope.$apply(function() { code here which modifies $scope });

Example:

socket.on('send:message', function (message) {
  $scope.$apply(function() {
    $scope.messages.push(message);
  });
});

EDIT:

I would like the chat system to be global (i.e. client can receive messages while being on the homepage, but they'll still only be displayed when going back on the Chat page).

Either store the datas in a global variable, or use $rootScope which is the parent scope of all the $scope you use in the application.

EDIT 2:

In fact it should solve your problem ;)

Another things: 1) use $rootScope instead of $scope for global variables (or a global variable). In any $scope you will access $rootScope variables ($scope is a copy of either $rooScope or a parent $scope). 2) register socket.io only once. Currently, if you change pages, you will register new callbacks at EACH page change.

1 Comment

Hello Pierre Emmanuel, thanks for your answer! Unfortunately, this didn't solve my problem... First, I forgot to add that I already have socket.io wrapped in a service that ensures $scope.$apply is called. Second, I already tried putting $rootScope instead of $scope but the issue was still occurring, and I feel like there's a more professional, cleaner way of doing it. I just set up my whole Chat as a service and it seemed to work pretty well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.