Search code examples
angulartypescriptasp.net-coreasp.net-core-signalr

*ngFor DOM not refreshing after callback


I've encontered a problem while developping an application using ASP.NET Core, Angular 5 and SignalR.

As a test, I made a simple chat app based on this sample : https://codingblast.com/asp-net-core-signalr-chat-angular/
But, when I modify the chat.component.ts file from

this.hubConnection.on('sendToAll', (nickname: string, message: string) => { this.messages.push(new Message(nickname, message)); });
// or this line
this.hubConnection.on('sendToAll', (nickname: string, message: string) => { this.addMessage(nickname, message); });

to

this.hubConnection.on('sendToAll', this.addMessage);

the DOM is not updated.

I've gone through step-by-step and I managed to figure out that when the callback arrives, it's actually another instance of the component... So every fields are undefined.

Here is the full component :

chat.component.html

<section class="card">
    <div class="card-body" *ngIf="messages !== undefined">
        <div *ngFor="let msg of messages">
            <strong>{{msg.nickname}}</strong>: {{msg.message}}
        </div>
    </div>
    <div class="card-footer">
        <form class="form-inline" (ngSubmit)="sendMessage()" #chatForm="ngForm">
            <div class="form-group">
                <label class="sr-only" for="nickname">Nickname</label>
                <input type="text" id="nickname" name="nickname" placeholder="Nickname" [(ngModel)]="nickname" required />
            </div>
            <div class="form-group">
                <label class="sr-only" for="message">Message</label>
                <input type="text" id="message" name="message" placeholder="Your message" [(ngModel)]="message" required />
            </div>
            <button class="btn btn-sm" type="submit" id="sendmessage" [disabled]="!chatForm.valid">Send</button>
        </form>
    </div>
</section>

chat.component.ts

import { Component, OnInit } from '@angular/core';
import { HubConnection } from '@aspnet/signalr';
import { Message } from '../../models/message';

@Component({
    selector: 'app-chat',
    templateUrl: './chat.component.html',
    styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit {
    private hubConnection: HubConnection;

    public nickname: string;
    public message: string;

    public messages: Message[];

    constructor() {
        this.messages = [];
    }

    ngOnInit() {
        this.hubConnection = new HubConnection('http://localhost:55233/chat');

        this.hubConnection.start().then(() => console.log('connection started')).catch(err => console.error(err));

        this.hubConnection.on('sendToAll', this.addMessage);
    }

    private addMessage(nickname: string, message: string): void {
        // this line is useful to avoid undefined error
        // due to the callback pointing to another instance of this component...
        if (this.messages === undefined) {
            this.messages = [];
        }

        this.messages.push(new Message(nickname, message));
    }

    public sendMessage(): void {
        this.hubConnection
            .invoke('sendToAll', this.nickname, this.message)
            .catch(err => console.error(err));
    }
}

Here is what I've tried :

  • Putting public everywhere
  • Getting rid of ngOnInit

How can I use this line this.hubConnection.on('sendToAll', this.addMessage); in this context?
Thanks for your help.

EDIT :
Thanks to Supamiu, here's the solution :

this.hubConnection.on('sendToAll', this.addMessage.bind(this));

Solution

  • Javascript provides a tool to inject context into a method call: bind.

    You should change this.addMessage to this.addMessage.bind(this).

    Keep in mind that there's other solutions, like the one trichetriche explained, another one would be to change addMessage for an arrow function, as arrow functions keep this context:

    private addMessage = (nickname: string, message: string) => {
        // this line is useful to avoid undefined error
        // due to the callback pointing to another instance of this component...
        if (this.messages === undefined) {
            this.messages = [];
        }
    
        this.messages.push(new Message(nickname, message));
    }
    

    By making this, you could still use this.addMessage without having to bind this context.