3

So I have this basic CRUD Todo app that I've made with AngularJS. I then thought I wanted to spice it up with some Socket.IO to make it a real time web app. However, I'm having some problems getting it to work correctly.

Setup: Yeoman (bower+grunt) AngularJS RequireJS NodeJS + MongoDB Socket.IO

My 'grunt server' runs on localhost:9000 whereas my NodeJS/MongoDB/Socket.IO server runs on localhost:4711

What works with Socket.IO at the moment is creating and deleting a todo, but I can't seem to figure out how to update my todos. Something I thought would be quite easy.

Here is the codez.

Socket.js:

var io = require('socket.io');

exports.initialize = function(server) {
  io = io.listen(server);
  io.sockets.on('connection', function(socket) {
    socket.on('message', function(message) {
      message = JSON.parse(message);
      socket.get('deletedTodo', function(err, nickname) {
        socket.broadcast.send(JSON.stringify(message));
        socket.send(JSON.stringify(message));
      });
    });
    socket.on('delete_todo', function(data) {
      socket.set('deletedTodo', data.title, function() {
        socket.emit('todo_delete', data);

        socket.broadcast.emit('todoDeleted', data);
      });
    });
    socket.on('update_todo', function(data) {
      socket.broadcast.emit('todoUpdated', data);
    })
  });
}

And here is my client side codez.

Services.js:

/*global define*/
define(['angular'], function(angular) {
 'use strict';

 return angular.module('services', [])
   .factory('Todo', function($resource) {
     //var baseUrl = 'http://designit-todo.eu01.aws.af.cm/api/todos/:id';
     var baseUrl = 'http://localhost:port/api/todos/:id';
     return $resource(baseUrl,
       {port: ':4711', id: '@_id'}, {
       'update': {method:'PUT'},
       'delete': {method:'DELETE', isArray: true}
     });
   })
   .factory('socket', function($rootScope) {
     var socket = io.connect('http://localhost:4711');
     return {
       on: function(eventName, callback) {
         socket.on(eventName, function() {
           var args = arguments;
           $rootScope.$apply(function() {
             callback.apply(socket, args);
           })
         });
       },
       emit: function(eventName, data, callback) {
         socket.emit(eventName, data, function() {
           var args = arguments;
           $rootScope.$apply(function() {
             if (callback) {
               callback.apply(socket, args);
             }
           });
         });
       },
       send: function(eventName, data, callback) {
         socket.send(eventName, data, function() {
           var args = arguments;
           $rootScope.$apply(function() {
             if (callback) {
               callback.apply(socket, args);
             }
           });
         });
       }
     }
   });
});

Controllers.js:

/*global define*/
define(['angular', 'services/services'], function(angular) {
'use strict';

return angular.module('controllers', ['services'])
.controller('Todos', ['$scope', '$resource', 'Todo', '$location', 'socket',        function($scope, $resource, Todo, $location, socket) {
  // Grab all todos.
  $scope.todos = Todo.query();

  socket.on('message', function(data) {
    // Grab all todos when new added via socket.
    $scope.todos = Todo.query();

    $scope.todos.push(data);
  });

  // Complete/update todo.
  $scope.todoCompleted = function(todo) {
    todo.$update();
    socket.emit('update_todo', todo);
  }
  socket.on('todoUpdated', function(todo) {
    // Update on all connected clients.
    console.log(todo);
  });

  $scope.removeTodo = function(todo) {
    var index = $scope.todos.indexOf(todo);
    socket.emit("delete_todo", JSON.stringify(index));
    $scope.todos.splice(index, 1);
    todo.$remove();
  }
  socket.on('todoDeleted', function(index) {
    $scope.todos.splice(index, 1);
  });
}])
.controller('Single', ['$scope', '$resource', '$routeParams', 'Todo', '$timeout', '$location', function($scope, $resource, $routeParams, Todo, $timeout, $location) {
  // Grab just a single todo
  $scope.todo = Todo.get({ id: $routeParams.id });
  // Throttle the update PUT request
  var saveTimeout;
  $scope.save = function() {
    $timeout.cancel(saveTimeout);
    saveTimeout = $timeout(function() {
      // Save the todo and then update the scope
      $scope.todo.$update(function(updated_todo) {
        $scope.todo = updated_todo;
      });
    }, 1000);
  };
}])
.controller('Add', ['$scope', '$resource', 'Todo', '$location', 'socket', function($scope, $resource, Todo, $location, socket) {
  $scope.todo = new Todo({});

  $scope.save = function() {
    if ($scope.todo.title) {
      $scope.todo.$save(function(data) {
        console.log(data);
        socket.send(JSON.stringify(data));
        $location.path('/');
      });
    }
  }
}]);
});

So my problem is within this piece of code:

// Complete/update todo.
$scope.todoCompleted = function(todo) {
  todo.$update();
  socket.emit('update_todo', todo);
}
socket.on('todoUpdated', function(todo) {
  // Update on all connected clients.
  console.log(todo);
});

When I console.log(todo), I get the todo, but I'm able to do a "todo.$update" on it. I've also tried something like:

  socket.on('todoUpdated', function(todo) {
    var thisTodo = Todo.get({id: todo._id});
    console.log(thisTodo);
  });

But as soon as I try to do thisTodo.$update() I get an error:

PUT http://localhost:4711/api/todos 404 (Not Found) :4711/api/todos:1

What am I doing wrong? And please correct me if I'm doing anything wrong within my code. I'm still fairly new to AngularJS and Socket.IO.

dgsunesen
  • 93
  • 2
  • 9
  • The problem is not with socket.io, that all seems to work. The problem lies in your API. Apparently, you're trying to update a todo with the `id` `1`, but the server cannot seem find this todo. A good tool to test your API is Postman: https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm/ – RoryKoehein Aug 29 '13 at 08:28
  • Hmm.. I don't think it's wrong with my API? I've tried using Advanced Rest Client for Chrome, and both GET, POST, PUT an DELETE works fine on http://localhost:4711/api/todos – dgsunesen Aug 29 '13 at 09:22
  • Check to see what the exact PUT request is that Angular is trying to do in you dev tools, and compare that to the requests you're doing with your Rest Client. There must be something wrong with the PUT request then. – RoryKoehein Aug 29 '13 at 09:35
  • I just tried this on the .todoCompleted method: `var thisTodo = Todo.get({id: todo._id}); thisTodo.$update();` This now gives me the same error on both clients. – dgsunesen Aug 29 '13 at 09:47
  • You can see the error here: http://cl.ly/image/3u1b0a2m0G1c and here: http://cl.ly/image/432R3n2s062A – dgsunesen Aug 29 '13 at 09:53
  • If I roll back to just doing: `todo.$update();` on the .todoCompleted method, I get this request: http://cl.ly/image/2R3f2W3o242Q and here: http://cl.ly/image/0I2o2V3i0G3x The socket.on('todoUpdated') still gives me an error. – dgsunesen Aug 29 '13 at 09:55
  • Well the first one doesn't have any data (payload), so that's why the API is giving a 404. But the second method now works (you don't get the 404 anymore)? What is the error for `socket.on('todoUpdated')`? What does the log say in todoUpdated `console.log(thisTodo);`? – RoryKoehein Aug 29 '13 at 09:59
  • I get this, when I `console.log(thisTodo);`on the `socket.on('todoUpdated')` - http://cl.ly/image/040L1G3z3c0r – dgsunesen Aug 29 '13 at 12:43
  • But as soon as I try to do `thisTodo.$update()` in `socket.on` I get the 404 error. If i refresh the page, it has updated the model, but I want it to be updated real time. – dgsunesen Aug 29 '13 at 12:45
  • Well, if you try to call $update() on the received data, I don't think Angular knows which model that is. You should first save the received data to the right model in Angular's memory. I think it is explained well here: http://stackoverflow.com/a/11277751/2713854 – RoryKoehein Aug 29 '13 at 13:07
  • Hm.. I don't see how I can save it to the right model? I get the actual model from within the view by `ng-change="todoCompleted(todo)"` - how would I then save it the the $scope, as in the example you linked to? – dgsunesen Aug 29 '13 at 13:34

1 Answers1

1

I found another solution, but not sure if it's the right way to do it:

socket.on('todoUpdated', function(updated_todo) {
  for (var i in $scope.todos) {
    if ($scope.todos[i]._id == updated_todo._id) {
      $scope.todos[i] = updated_todo;
    }
  }
});

So in the Todos controller I have the $scope.todos which holds all todos. I then loop through all these, find a match on the id and sets the found todo to the newly updated todo.

If there's a better way to do it, please do tell me :)

dgsunesen
  • 93
  • 2
  • 9