AngularJS, [$rootScope:infdig] on $apply, or View Update On Next Action

207 views
Skip to first unread message

Hassan Faghihi

unread,
Jun 15, 2015, 7:43:25 AM6/15/15
to ang...@googlegroups.com

Dear sir,

I'm tring to create angular base application joined with .Net MVC & SignalR...

First Let Say i'm new to this world of angular, and even JS framework, and not totally familiar with even JS it self, so it may not be an issue in angular, in other hand it may.

What i'm tring to do is to Use this sample, change it's base structure from JQuery to Angularjs... and in the end change it's data structure, customize it, add and remove feature, and so on

I also create a model, not sure when should i do this, so i did it all on $scope, and create a variable Named ChatModel, and put all variable into that...

the incomplete code is in below, comment are places i didn't convert to angular...

Now lets talk about issues, and what came after another...

first, i needed to put my msg into collection, so i add this:

var MessageList=function(){ //Type Message
            this.values=[];
            this.push=function(username, message){
                this.values.push({UserName: username, Message: message});
                return;//throw exception
            };
            this.get=function(index) {
                return this.values[index];
            }
            this.toArray = function ()
            {
                return angular.copy(this.values);
            }
        }

So i can store collection of data, as i were unaware of how to create strongly typed, also one with constructor, so i leave it with dynamic variable... so it went like that, and this in my model:

$scope.ChatModel = {
            Logged: false,
            Message: '',
            Login: {
                UserName: '',
                UserId: ''
            },
            Messages: new MessageList()//UserName, Message
        };

and like this in my UI:

<div class="message" data-ng-repeat="x in ChatModel.Messages.toArray()">
        <span class="userName">{{x.userName}}</span>: {{x.message}}
</div>

but the issue was that the UI wouldn't get update automatically after function call from server...
for example previously i had $scope.ChatModel.Logged as $scope.logged, but even on that conditon the UI needed to be clicked twice to update UI (Show One Panel And Hide Another One).

i read lot about it, and i used apply function, though it didn't worked. finally, today i understand that theapply is $apply. so it start working. So i then wonder if i can omit $apply, and let angular it self detect the issues, and that's when i came across the $watch, since it didn't worked i went to console, and see there are errors: [$rootScope:infdig], and soon i found out the $apply() it self doesn't work properly.

So can any body help?

I put all the code you may require in below of page. please Help Me:

var app = angular.module("chatApp", []);
var advanceChatHub = $.connection.advanceChatHub;

app.directive('ngEnter', function () {
    return function (scope, element, attrs) {
        element.bind("keydown keypress", function (event) {
            if (event.which === 13) {
                scope.$apply(function () {
                    scope.$eval(attrs.ngEnter);
                });

                event.preventDefault();
            }
        });
    };
});

app.controller("chatCtrl", ['$scope',
    function($scope) { //Chat Ctrl 1 / Full Function

        var MessageList=function(){ //Type Message
            this.values=[];
            this.push=function(username, message){
                this.values.push({UserName: username, Message: message});
                return;//throw exception
            };
            this.get=function(index) {
                return this.values[index];
            }
            this.toArray = function ()
            {
                return angular.copy(this.values);
            }
        }

        $scope.ChatModel = {
            Logged: false,
            Message: '',
            Login: {
                UserName: '',
                UserId: ''
            },
            Messages: new MessageList()//UserName, Message
        };
        //$scope.$watchCollection(function(scope) {
        //        return scope.ChatModel.Messages.values;
        //    },
        //    function(newVal, oldVal, scope) {
        //        if (oldVal.UserName === newVal.UserName && oldVal.Message === newVal.Message) {
        //            console.log("change");
        //        }
        //    }, true
        //);

        $scope.RegisterClientMethod = function(advanceChatHub) {
            // Calls when user successfully logged in
            advanceChatHub.client.onConnected = function(id, userName, allUsers, messages) {

                $scope.ChatModel.Logged = true;

                $scope.ChatModel.Login.UserId = id;
                $scope.ChatModel.Login.UserName = userName;

                //Add All Users
                var i;
                for (i = 0; i < allUsers.length; i++) {
                    $scope.AddUser(advanceChatHub, allUsers[i].ConnectionId, allUsers[i].UserName);
                }

                for (i = 0; i < messages.length; i++) {
                    $scope.AddMessage(messages[i].UserName, messages[i].Message);
                }
                $scope.$apply();
            }

            advanceChatHub.client.onNewUserConnected = function(id, name) {
                $scope.AddUser(advanceChatHub, id, name);
            }

            advanceChatHub.client.onUserDisconnected = function(id, userName) {
                //$('#' + id).remove();

                //var ctrId = 'private_' + id;
                //$('#' + ctrId).remove();


                //var disc = $('<div class="disconnect">"' + userName + '" logged off.</div>');

                //$(disc).hide();
                //$('#divusers').prepend(disc);
                //$(disc).fadeIn(200).delay(2000).fadeOut(200);
            }

            advanceChatHub.client.messageReceived = function(userName, message) {
                $scope.AddMessage(userName, message);
            }

            advanceChatHub.client.sendPrivateMessage = function(windowId, fromUserName, message) {
                //var ctrId = 'private_' + windowId;


                //if ($('#' + ctrId).length == 0) {

                //    createPrivateChatWindow(chatHub, windowId, ctrId, fromUserName);

                //}

                //$('#' + ctrId).find('#divMessage').append('<div class="message"><span class="userName">' + fromUserName + '</span>: ' + message + '</div>');

                //// set scrollbar
                //var height = $('#' + ctrId).find('#divMessage')[0].scrollHeight;
                //$('#' + ctrId).find('#divMessage').scrollTop(height);
            }
        }

        $scope.Events = {
            btnStartChat: function() {
                var name = $scope.ChatModel.Login.UserName;
                if (name.length > 0) {
                    advanceChatHub.server.connect(name);
                } else {
                    alert("Please enter name");
                }
            },
            btnSendMsg: function() {
                var msg = $scope.ChatModel.Message;
                if (msg.length > 0) {

                    var userName = $scope.ChatModel.Login.UserName;

                    advanceChatHub.server.sendMessageToAll(userName, msg);
                    $scope.ChatModel.Message = '';
                }
            },
            txtNickName: function () {
                angular.element('#btnStartChat').trigger('click');
            },
            txtMessage: function () {
                angular.element('#btnSendMsg').trigger('click');
            }
        };

        $scope.AddUser = function(advanceChatHub, id, name) {
            //var userId = $scope.userId;

            //var code = "";

            //if (userId == id) {

            //    code = $('<div class="loginUser">' + name + "</div>");

            //}
            //else {

            //    code = $('<a id="' + id + '" class="user" >' + name + '<a>');

            //    $(code).dblclick(function () {

            //        var id = $(this).attr('id');

            //        if (userId != id)
            //            OpenPrivateChatWindow(chatHub, id, name);

            //    });
            //}

            //$("#divusers").append(code);
        }

        $scope.AddMessage = function (userName, message) {
            $scope.ChatModel.Messages.push(userName, message);

            var divChatWindows = angular.element('#divChatWindow');

            $scope.$apply();
            //var divNewMessage = '<div class="message"><span class="userName">' + userName + '</span>: ' + message + '</div>';
            //angular.element(divNewMessage).appendTo(divChatWindows);

            var height = divChatWindows[0].scrollHeight;
            divChatWindows.scrollTop(height);
        }

        $scope.OpenPrivateChatWindow = function(chatHub, id, userName) {

            var ctrId = 'private_' + id;

            //if ($('#' + ctrId).length > 0) return;

            $scope.createPrivateChatWindow(chatHub, id, ctrId, userName);

        }

        $scope.createPrivateChatWindow = function(chatHub, userId, ctrId, userName) {
            //var div = '<div id="' + ctrId + '" class="ui-widget-content draggable" rel="0">' +
            //           '<div class="header">' +
            //              '<div  style="float:right;">' +
            //                  '<img id="imgDelete"  style="cursor:pointer;" src="/Images/delete.png"/>' +
            //               '</div>' +

            //               '<span class="selText" rel="0">' + userName + '</span>' +
            //           '</div>' +
            //           '<div id="divMessage" class="messageArea">' +

            //           '</div>' +
            //           '<div class="buttonBar">' +
            //              '<input id="txtPrivateMessage" class="msgText" type="text"   />' +
            //              '<input id="btnSendMessage" class="submitButton button" type="button" value="Send"   />' +
            //           '</div>' +
            //        '</div>';

            //var $div = $(div);

            //// DELETE BUTTON IMAGE
            //$div.find('#imgDelete').click(function () {
            //    $('#' + ctrId).remove();
            //});

            //// Send Button event
            //$div.find("#btnSendMessage").click(function () {

            //    $textBox = $div.find("#txtPrivateMessage");
            //    var msg = $textBox.val();
            //    if (msg.length > 0) {

            //        chatHub.server.sendPrivateMessage(userId, msg);
            //        $textBox.val('');
            //    }
            //});

            //// Text Box event
            //$div.find("#txtPrivateMessage").keypress(function (e) {
            //    if (e.which == 13) {
            //        $div.find("#btnSendMessage").click();
            //    }
            //});

            //AddDivToContainer($div);

        }

        $scope.AddDivToContainer = function($div) {
            //$('#divContainer').prepend($div);

            //$div.draggable({

            //    handle: ".header",
            //    stop: function () {

            //    }
            //});


            //////$div.resizable({
            //////    stop: function () {

            //////    }
            //////});

        }

        advanceChatHub.connection.start().done(function() {
            //$scope.registerEvents(hub);
        });

        $scope.RegisterClientMethod(advanceChatHub);
    }
]);
@model dynamic

@{
    ViewBag.Title = "title";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section headers{

}

@section scripts{
    <script src="~/Scripts/angular.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script>
    @* ReSharper disable Html.PathError *@
    <script src="~/SignalR/Hubs"></script>
    @* ReSharper restore Html.PathError *@

    <script src="~/Scripts/custom/advanceChatSurf.js"></script>
}

<h2>SignalR Chat Room</h2>

<br />
<br />
<br/>

<div id="divContainer" data-ng-app="chatApp" data-ng-controller="chatCtrl">
    <div id="divLogin" class="login" data-ng-hide="ChatModel.Logged">
        <div>
            Your Name:<br />
            <input id="txtNickName" data-ng-enter="Events.txtNickName()" type="text" class="textBox" data-ng-model="ChatModel.Login.UserName" />
        </div>
        <div id="divButton">
            <input id="btnStartChat" data-ng-click="Events.btnStartChat()" type="button" class="submitButton" value="Start Chat" />
        </div>
    </div>

    <div id="divChat" data-ng-show="ChatModel.Logged" class="chatRoom">
        <div class="title">
            Welcome to Chat Room [<span id='spanUser'></span>]

        </div>
        <div class="content">
            <div id="divChatWindow" class="chatWindow">
                <div class="message" data-ng-repeat="x in ChatModel.Messages.toArray()">
                    <span class="userName">{{x.userName}}</span>: {{x.message}}
                </div>
            </div>
            <div id="divusers" class="users">
            </div>
        </div>
        <div class="messageBar">
            <input class="textbox" data-ng-enter="Events.txtMessage()" type="text" id="txtMessage" data-ng-model="ChatModel.Message" />
            <input id="btnSendMsg" data-ng-click="Events.btnSendMsg()" type="button" value="Send" class="submitButton" />
        </div>
    </div>


    <input data-ng-model="userId" type="hidden" />
    <input data-ng-model="userName" type="hidden" />
</div>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace ChatSignalR.Hubs
{
    public class AdvanceChatHub : Hub
    {
        readonly List<MessageDetail> _currentMessage = new List<MessageDetail>();
        readonly List<UserDetail> _connectedUsers = new List<UserDetail>();

        public void Hello()
        {
            Clients.All.hello();
        }

        #region Proxy Methods

        public void Connect(string userName) //-->Take User Name From Client Before Application Start
        // , Store In Hidden / OR / Set it from credentials
        {
            var id = Context.ConnectionId; //Get Id And Return It To User Somehow 

            if (_connectedUsers.All(x => x.ConnectionId != id)) // But Only, If Nobody Has This Id
            {
                _connectedUsers.Add(new UserDetail //User Not Exist? Then Add Him To Collection
                {
                    ConnectionId = id,
                    UserName = userName
                });

                //send self information / ConnectedUserLists / Old Messages to User

                // TODO: 1 --> Never Give Other Users UserId To Other Clients, Says Asp.net Forum, as other can take advantage of it

                // TODO: 2 --> Limit Messages To Send, As it may become large over time

                //Send To Caller
                Clients.Caller.onConnected(id, userName, _connectedUsers, _currentMessage);

                //Send To Other
                Clients.AllExcept(id).onNewUserConnected(id, userName);
                // --> Clients.Others.onNewUserConnected(id, userName);
            }
        }

        public void SendMessageToAll(string userName, string message)
        {
            //Get User Name By Connection Id
            //var id = Context.ConnectionId;
            //var userInfo = _connectedUsers.FirstOrDefault(w => w.ConnectionId == id);
            //var userName = (userInfo != null) ? userInfo.UserName : "Unknown Sender";

            //Get User By Credential Name
            //Context.User.Identity.Name

            //Store Last 100 Messages in Cache
            AddMessageinCache(userName, message);

            //Broadcast Message
            Clients.All.messageReceived(userName, message);
        }

        public void SendPrivateMessage(string toUserId /*toUserName*/, string message)
        {
            string fromUserId = Context.ConnectionId;

            var toUser = _connectedUsers.FirstOrDefault(x => x.ConnectionId == toUserId); //Filter By UserName
            var fromUser = _connectedUsers.FirstOrDefault(x => x.ConnectionId == fromUserId);

            if (toUser != null && fromUser!=null)
            {
                //send to 
                Clients.Client(toUserId).sendPrivateMessage(fromUserId /*UserName*/, fromUser.UserName, message);
            }
        }

        #endregion Proxy Methods

        #region Hub Events

        public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled = true) //Ver 2 SignalR
        {
            var userInfo = _connectedUsers.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);
            {
                if (userInfo != null)
                {
                    _connectedUsers.Remove(userInfo);

                    var id = Context.ConnectionId;
                    Clients.All.onDisconnected(id, userInfo.UserName);
                } 
            }

            return base.OnDisconnected(stopCalled);
        }

        #endregion Hub Events

        #region Utility
        #region Cache Management

        private void AddMessageinCache(string userName, string message)
        {
            _currentMessage.Add(new MessageDetail { UserName = userName, Message = message });

            if (_currentMessage.Count > 100)
                _currentMessage.RemoveAt(0);
        }

        #endregion Cache Management
        #endregion Utility
    }

    internal class MessageDetail
    {
        public string UserName { get; set; }
        public string Message { get; set; }
    }

    internal class UserDetail
    {
        public string ConnectionId { get; set; }
        public string UserName { get; set; }
    }
}

And Just One Simple Controller

Sander Elias

unread,
Jun 16, 2015, 12:34:20 AM6/16/15
to ang...@googlegroups.com

Hi Hassan,

Don’t ever use functions for the thing you are iterating on, in your case xxx.toArray(), as this will cause a new digest cycle, which then will cause an infinitive loop.

Regards
Sander

Hassan Faghihi

unread,
Jun 16, 2015, 1:05:42 AM6/16/15
to ang...@googlegroups.com
Hi Sander,

And, thanks for the reply, but how i'm suppose to not do that?
put my variable inside other variable, then pass it to that? BTW, isn't it reference call? so that's what make it hard...

And also what about the $apply? i though angular came to scene to automate the stuff, so if i add some code i have to apply... can't at last i do watch so i don't have to remember to apply in every single piece of my application? how exactly can i watch a object same to one i have alreay...

Thank you again,
Hassan F.


--
You received this message because you are subscribed to a topic in the Google Groups "AngularJS" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/U4ZV2RXAEMU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/d/optout.

Hassan Faghihi

unread,
Jun 16, 2015, 1:13:33 AM6/16/15
to ang...@googlegroups.com
On Tue, Jun 16, 2015 at 8:35 AM, Hassan Faghihi <mimosh.p...@gmail.com> wrote:
Hi Sander,

And, thanks for the reply, but how i'm suppose to not do that?
If i treat the Messages as predefined list, so i can't manage it like it's strongly typed, though i still hate that i can't strongly type it... And this way my MessageList Class is not sure to work, depend on how javascript handle array... if it pass reference maybe i can fix it.. i don't know, game me time for this...

Sander Elias

unread,
Jun 16, 2015, 1:30:46 AM6/16/15
to ang...@googlegroups.com
Hi Hassan,

Ok, You have 2 separate, but somewhat connected issues. 
Currently your msglist is making a fresh copy of the array of messages every time, This means, that on every invocation, (read every row in the table!) angalar needs to kick of an new digest cycle.. an endless loop. Just use the values array directly.

Your second issue is $scope.$apply. You need that only if something is changing data inside your view from outside angular. This means, if you react to server push events for example. It looks like you got that covered. 

If you are really want to learn Angular correctly you should not use jQuery while learning. If you find the need to put an ID on a tag, you are probably doing it wrong, and there is a better way to accomplish that. If you are manipulating the DOM, you are probably doing it wrong too. Angular apps are data-driven, not dom-driven. All dom manipulation should be in directives, and even there you seldom need  really need it.

Regards
Sander

Hassan Faghihi

unread,
Jun 16, 2015, 1:40:25 AM6/16/15
to ang...@googlegroups.com
How i can edit in here? last time i did, and it made new post...

BTW, i apply it like this:

HTML:
<div class="message" data-ng-repeat="x in ChatModel.Messages">

JS:

var MessageList = function () { //Type Message
            this.values = [];
            this.push=function(username, message){
                this.values.push({UserName: username, Message: message});
                return;//throw exception
            };
            this.get=function(index) {
                return this.values[index];
            }
            this.toArray = function ()
            {
                return angular.copy(this.values);
            }
        }

        var groupMessages = new MessageList();

        $scope.ChatModel = {
            Logged: false,
            Message: '',
            Login: {
                UserName: '',
                UserId: ''
            },
            Messages: groupMessages.values//UserName, Message
        };


$scope.AddMessage = function (userName, message) {
            //$scope.ChatModel.Messages.
            groupMessages.push(userName, message);

            var divChatWindows = angular.element('#divChatWindow');

            $scope.$apply();///////////////////////////////////////////////////////////////////////////////////////////////////
            //-var divNewMessage = '<div class="message"><span class="userName">' + userName + '</span>: ' + message + '</div>';
            //-angular.element(divNewMessage).appendTo(divChatWindows);

            var height = divChatWindows[0].scrollHeight;
            divChatWindows.scrollTop(height);
        }

now, even with $apply, i'm one row late (sorry for bad language). i had, but not already use jquery, but angular stuff, like angular.element, etc... and what should i do with id? and does it matter if js find element by id which is globally known, or by something else?

--

Sander Elias

unread,
Jun 16, 2015, 2:47:12 AM6/16/15
to ang...@googlegroups.com
Hi Hassan,

You should build a plunk illustrating your issue. That will make it much easier to help you.
As you won't have a server pushing msgs, you can make a small function that emits an random msg every random time.

Angular.element(xxx) is the same as using jQuery's $(xxx). you should not be needing that, outside directives.

Regards
Sander

Sander Elias

unread,
Jun 16, 2015, 3:16:03 AM6/16/15
to ang...@googlegroups.com
Hi Hassan,

Made you a starting point: http://plnkr.co/edit/3CX85uKFHAQyHukqzyRH

Regards
Sander

Hassan Faghihi

unread,
Jun 16, 2015, 5:11:15 AM6/16/15
to ang...@googlegroups.com
:-s i'm afraid i couldn't wire things up, that's due my noobness in javascript...

and i issued with server angular 1.4, so i defined my own file, but since i coudn't save it, then i again remove it, ...


--

Sander Elias

unread,
Jun 16, 2015, 6:01:20 AM6/16/15
to ang...@googlegroups.com
Hi Hassan,

Did you look at the second plunk I posted for you? I think your answer is in there already!

Regards
Sander

Hassan Faghihi

unread,
Jun 16, 2015, 7:07:26 AM6/16/15
to ang...@googlegroups.com
yes, thanks, you gave me one, and i look at it, ... though not second one
but you see, my application worked with SignalR, and it's meant to call remote method, and then recieve value... that's why i create hub object, but i failed to wire it with the controller... button click, which are under event objects, should call to hub method, and then hub method add the value to the collection, and in the end it notify all client... which in our case we have only one, cause it's client side, so we call to controller again and invoke desired method...

that's how it should work...

you know it's my first month of work as web developer... though i worked with extJS a little bit long ago, that i can use this config ways of js a bit, but i'm new comer, i'm sure they'll wire, but i don't know how

but you can push message to my object too, but it's a bit risky too, cause one of issue is that the UI update on second click of button, now, if you don't have button, and a random timer, how you know how many time you clicked... 

BTW, if you still think that, this will do the job through what i said, i wire your code to my model directly.

Sander Elias

unread,
Jun 16, 2015, 7:42:05 AM6/16/15
to ang...@googlegroups.com
Hassan,


The random timer is just simulating incoming messages, you can map your `advanceChatHub.client.messageReceived` to directly to a similar method in the factory.
As you can see there is also a push method exposed from the factory. The factory is the only place you should mutate your msgs array. 

To prevent double clicks on a button, you can disable the input+button on the ng-click. If needed I can give you a sample of this too.
When you do an async post of your message, you can return a promise that enables the input+button again. you have a very fine level of control in angular! 

Regards
Sander


Hassan Faghihi

unread,
Jun 16, 2015, 9:27:56 AM6/16/15
to ang...@googlegroups.com
advanceChatHub.client.messageReceived is not exist, it used signalr.js, and the auto generated proxy (/SignalR/Hub), which i delete both, out of complexity, and i create my "hub" variable object which is very simple, ... i went for it, but i couldn't understand.

what "mutate" mean? in second sentence?

i don't double click it accidentally, the first time like not working, second, do first action, third to second action, ... and so on, other kinda click or interupt do last action on the button.

I tried put some step along side your codes, but after long time wasting my company time, i'm still issued, and receive errors :(

Sander Elias

unread,
Jun 16, 2015, 11:34:03 PM6/16/15
to ang...@googlegroups.com
Hi Hassan,

what "mutate" mean? in second sentence?
make any change
 
i don't double click it accidentally, the first time like not working, second, do first action, third to second action, ... and so on, other kinda click or interupt do last action on the button.
Hmm, sounds like your click is causing a digest. If this is the case, you probably missing an $scope.$apply somewhere. (or have one to many, that causes an infinite loop, errors out, and do nothing because of that!)
 
I tried put some step along side your codes, but after long time wasting my company time, i'm still issued, and receive errors :(
Allmost none of your script are loading, have a look at the developers console. Have a look at my plunk how I load angular. I'm sorry, but I don't have the time to fix those issues for you. BTW, you are trying linking in an ancient jQuery version!

Regards
Sander

Hassan Faghihi

unread,
Jun 17, 2015, 1:05:05 AM6/17/15
to ang...@googlegroups.com
i load the angular from one Persian host... i don't know why, but i cant fetch any of links plank made to any version of angular. so i put my own files on a host, and get it from there, it work for me

--

Hassan Faghihi

unread,
Jun 17, 2015, 1:10:20 AM6/17/15
to ang...@googlegroups.com
i though angular don't relay on jQuery, maybe my code do....
So i fix it, but ...


Reply all
Reply to author
Forward
0 new messages