31

I am trying to update my view after a websocket event returns updated data.

I injected a service into my app and call getData() method on the service. This method emits a socket.io event to my NodeJS server which in turn performs an external api call and parses some data. The NodeJS server then emits a success event with the new data that I listen for in my service. When the success event is returned I then update my property on the service that is referenced in my view.

However no matter what I try I cannot get the data to show once the property is updated.

I have searched for a few days now and all I find are blog posts that say this change should be seamless, or that I need to incorporate zone.js somehow, or to try the same logic using forms (however im trying to do this without user interaction). Nothing seems to work for me and I am getting a bit frustrated.

For example:

Lets say I receive an array of strings that I want to create an unsorted list with.

app.ts

import {Component, View, bootstrap, NgFor} from 'angular2/angular2';
import {MyService} from 'js/services/MyService';

// Annotation section
@Component({
    selector: 'my-app',
    viewInjector: [MyService]
})
@View({
    templateUrl: 'templates/my-app.tpl.html',
    directives: [NgFor]
})

class MyComponent {
    mySvc:MyService;

    constructor(mySvc:MyService) {
        this.mySvc = mySvc;
        this.mySvc.getData();
    }
}   

bootstrap(MyComponent, [MyService]);

MyService.ts

let socket = io();
export class MyService {
    someList:Array<string>;

    constructor() {
        this.initListeners();
    }

    getData() {
        socket.emit('myevent', {value: 'someValue'});
    }

    initListeners() {
        socket.on('success', (data) => {
            self.someList = data;
        });
    }
 }

my-app.tpl.html

<div>
    <h2>My List</h2>
    <ul>
        <li *ng-for="#item of mySvc.myList">Item: {{item}}</li>
    </ul>
</div>

Interesting enough, I have found that If I incorporate a timeout within my component that updates some arbitrary property that I set on the view after the someList property is updated from the success callback then both property values are updated correctly at the same time.

For instance:

new app.ts

    import {Component, View, bootstrap, NgFor} from 'angular2/angular2';
    import {MyService} from 'js/services/MyService';

    // Annotation section
    @Component({
        selector: 'my-app',
        viewInjector: [MyService]
    })
    @View({
        templateUrl: 'templates/my-app.tpl.html',
        directives: [NgFor]
    })

    class MyComponent {
        mySvc:MyService;
        num:Number;

        constructor(mySvc:MyService) {
            this.mySvc = mySvc;
            this.mySvc.getData();
            setTimeout(() => this.updateNum(), 5000);
        }

        updateNum() {
            this.num = 123456;
        }
    }   

    bootstrap(MyComponent, [MyService]);

new my-app.tpl.html

<div>
    <h2>My List {{num}}</h2>
    <ul>
        <li *ng-for="#item of mySvc.myList">Item: {{item}}</li>
    </ul>
</div>

So how should I go about getting angular2 to recognize that the data has changed after the 'success' event without updating some other property?

Is there something I am missing with the use of the NgFor directive?

Heather92065
  • 7,333
  • 6
  • 31
  • 39
John Gainfort
  • 683
  • 1
  • 6
  • 9
  • Use [onInit](https://angular.io/docs/js/latest/api/annotations/OnInit-interface.html) or [onCheck](https://angular.io/docs/js/latest/api/annotations/OnCheck-interface.html). Since for what I understood you are trying to print a list dynamically, you should use onInit. – Eric Martinez Jul 29 '15 at 17:33
  • How would I use onInit() or onCheck()? The documentation isn't clear to me – John Gainfort Jul 29 '15 at 18:36
  • I have made a bar bones git repo that you can clone that wil setup a basic nodejs/express server with socket.io https://github.com/jgainfort/angular2MyApp The readme includes the simple setup process. – John Gainfort Jul 29 '15 at 21:32
  • 2
    Well, after a lot tries, I came to the conclusion that I have no idea why this happens, lol. Following your solution and to avoid to change a random property, you could pass `ChangeDetectorRef` to the constructor and inside of it write this : `setTimeout(() => {this.cd.requestCheck();}, 500);` (where `this.cd` is `cd: ChangeDetectorRef`, and 500 as a low number enough to get the server and client send and receive their events). I would really like a better answer, but for now I think this might help you. You could ask on their git or in angular's mailing list for a better answer. – Eric Martinez Jul 30 '15 at 06:44
  • Thank you @ericmartinez the workaround using the setTimeout does show the updated data. I agree with you that this is not ideal and Ill take your advice and move this question to angular. I bet this has something to do where the socket event is set outside of the zone and therefore angular is never catching the property update. – John Gainfort Jul 30 '15 at 15:24
  • You might be write with that assumpion, see [this thread](https://github.com/angular/angular/issues/3376#issuecomment-126375561) – Eric Martinez Jul 30 '15 at 16:22

6 Answers6

36

So I finally found a solution that I like. Following the answer in this post How to update view after change in angular2 after google event listener fired I updated myList within zone.run() and now my data is updated in my view like expected.

MyService.ts

/// <reference path="../../../typings/tsd.d.ts" />

// Import
import {NgZone} from 'angular2/angular2';
import {SocketService} from 'js/services/SocketService';

export class MyService {
    zone:NgZone;
    myList:Array<string> = [];
    socketSvc:SocketService;

    constructor() {
        this.zone = new NgZone({enableLongStackTrace: false});
        this.socketSvc = new SocketService();
        this.initListeners();
    }

    getData() {
        this.socketSvc.emit('event');
    }

    initListeners() {
        this.socketSvc.socket.on('success', (data) => {
            this.zone.run(() => {
                this.myList = data;
                console.log('Updated List: ', this.myList);
            });
        });
    }
 }
Community
  • 1
  • 1
John Gainfort
  • 683
  • 1
  • 6
  • 9
  • 1
    Dear God, I was sitting on this issue for 2 days, that zone.run() worked like a charm, thank you! – Starwave Nov 02 '16 at 11:45
  • 1
    My Angular 2 application was working fine without zones until I tested it on an ipad. After adding zones, it worked on ipad as well. I am really not sure why though. – John Feb 15 '17 at 21:50
  • You are the man! I spend one whole day trying to do the same thing too! – Shawn Ang Jun 19 '17 at 14:41
5

Just move your socket.io initialization to Service constructor and it will work.
Take a look at this example:

import {Injectable} from 'angular2/core';  
@Injectable()
export class SocketService {
    socket:SocketIOClient.Socket;

    constructor(){
        this.socket = io.connect("localhost:8000");
    }
    public getSocket():SocketIOClient.Socket{
        return this.socket;
    }
}

Now whenever you inject this service to a component and use a socket, your view will automatically update. But if you leave it in a global scope like you did, you will have to interact with something in order to force the view to update.
Here is an example component that uses this service:

export class PostsComponent {
    socket: SocketIOClient.Socket;
    posts: Array<Post> = [];

    constructor(private _socketService:SocketService){
        this.socket.on('new-post', data => {
            this.posts.push(new Post(data.id, data.text));
        });  
}  
  • For those who are using: `this.socket.on('new-post',function(data){ this.posts.push(new Post(data.id,data.text)); })` Will not work. Your solution works though! – John Feb 02 '16 at 22:16
  • 1
    @John The callback function in your comment here is not bound to the same `this` as the outer context. That might be the issue there. – Zlatko Apr 11 '16 at 08:30
4

A very simple way to do this is just run in zone whatever variable you want to update.

zone.run(()=>{
    this.variable = this.variable;
});

Doesn't seem like it can be that easy, but just assigning it to itself will update it if run in zone. I don't know if this is still an issue in angular2 since I'm running a little bit older version.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • You can import NgZone and then dependency inject it in the constructor to get the zone object. From there you can call run like in the answer above. Thanks it worked great for me. – Daniel Oct 26 '16 at 05:05
  • This is not working for some reason in my App. http://stackoverflow.com/questions/40419533/angular-2-reload-the-current-component-based-on-user-input – user2180794 Nov 04 '16 at 18:50
1

UPDATE

The plunker I linked provided a base example but it frustrates me that I couldn't show an entire "working example". So I created a github repo that you can pull down to see a full working example of the pieces I talked about below.


So in your actual question there were two problems. You had a typo in the original code in the "MyService.ts" file

self.someList = data;//should be this.someList, self is the browser window object

Another issue is Angular2 doesn't recognize change detection the way you're expecting it to. If it had been set to 'this', I still don't think it would have updated your component view.

In your answer, it does work but you're kind of going around the issue the wrong way. What you should implement is an Observable in your service.

When you combine these two features together you can implement sails in a fairly straight forward way. I have created an example plunker but keep in mind that it doesn't actually connect to a sails server because plunker requires https and I'm not going to go buy a ssl cert for my local machine. The code does reflect how you should implement communication with sails in Angular2.

The basic idea can be found in the src/io.service.ts file

constructor() {
  this._ioMessage$ = <Subject<{}>>new Subject();
  //self is the window object in the browser, the 'io' object is actually on global scope
  self.io.sails.connect('https://localhost:1337');//This fails as no sails server is listening and plunker requires https
  this.listenForIOSubmission();
}

get ioMessage$(){
  return this._ioMessage$.asObservable();
}

private listenForIOSubmission():void{
  if(self.io.socket){//since the connect method failed in the constructor the socket object doesn't exist
    //if there is a need to call emit or any other steps to prep Sails on node.js, do it here.
    self.io.socket.on('success', (data) => {//guessing 'success' would be the eventIdentity
      //note - you data object coming back from node.js, won't look like what I am using in this example, you should adjust your code to reflect that.
      this._ioMessage$.next(data);//now IO is setup to submit data to the subscribbables of the observer
    });
  }
}
Dan Simon
  • 824
  • 6
  • 7
0

If you're interested, may I suggest using ngrx/store with OnPush change detection. I have run into similar issues where something happened outside of Angular (whatever that means exactly) and my view did not reflect the change.

Using the Redux pattern with event dispatchers and a single state that holds my data in conjunction with OnPush change detection has solved that issue for me. I do not know why or how it solves this problem though. Cum grano salis.

See this comment specifically for more details.

Dennis Hackethal
  • 13,662
  • 12
  • 66
  • 115
  • 1
    Using ngrx/store is really nice but doesn't help with this error. I'm using it and had this behavior when data are returned from a websocket. The `this.zone.run(() => {...})` solution works well instead! – bertrandg Jun 07 '16 at 16:34
0

I was having the same problem, and the issue was:

I was using "angular2": "2.0.0-beta.1" It seems that there is a bug, because after updating to "angular2": "2.0.0-beta.15" It is working fine.

I hope it helps, I learnt it the painful way

Alan Wagner
  • 2,230
  • 1
  • 16
  • 13