Ad

Unable To Load Variable From Service In Angular 9

I am trying to create a web-app for a game that involves starting and joining rooms. I am using Angular and Socket.io for the project. I have components named Startpage and Lobby. Startpage contains text boxes for player name and Room ID. It also has buttons for "Start Game" and "Join Game". Clicking on the "Start Game" button triggers the startThisGame() function.

//startpage.component.ts

import { Component, OnInit, Output, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { SocketioService } from '../socketio.service';

@Component({
  selector: 'app-startpage',
  templateUrl: './startpage.component.html',
  styleUrls: ['./startpage.component.css']
})

export class StartpageComponent implements OnInit {

  constructor( private router: Router, public zone: NgZone, private socketService: SocketioService ) {}

  ngOnInit() {
    this.socketService.setupSocketConnection();
  }
  testRoomId = 'xxxx'

  startThisGame() {
    var playerName = (document.getElementById("nameInput") as HTMLInputElement).value;
    if (playerName) {
      this.testRoomId = this.socketService.startGame(playerName);
      alert("In component: " + this.testRoomId)
      this.zone.run(() => {this.router.navigate(['/lobby']); });
    }
    else{
      alert("Enter a player name!")
    }
  }
}

The code inside socketService goes as follows.

import { Injectable } from '@angular/core';
import * as io from 'socket.io-client';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class SocketioService {
  socket;
  public roomId: string;
  public playerName: string;
  constructor() {}

  setupSocketConnection() {
    this.socket = io(environment.SOCKET_ENDPOINT)
    alert("Connection set-up")
  }

  startGame(inputPlayerName) {
    this.socket.emit('startGame', {name: inputPlayerName})
    this.playerName = inputPlayerName
    this.socket.on('recieveRoomId', (data: any) => {
      this.roomId = data.code
      alert("In service startGame: " + this.roomId)
    })
    return this.roomId
  }

  getRoomId() {
    alert("In service getRoomId: " + this.roomId)
    return this.roomId;
  }
}

The server code creates a unique room ID and passes it to the roomId variable in socketService. My first question starts here. If you notice, I have left a trail of alert messages along the way. I would expect the order of alert messages to be alert("In service startGame: " + this.roomId) and then alert("In component: " + this.testRoomId). However, I get the alert messages in the opposite order and the roomId's are undefined even though the server generates and emits a unique roomId. I do not seem to understand why this is happening.

Secondly, you can see that the startThisGame() function causes socketService to store the generated roomId and playerName inside the class and then reroutes the app to the lobby component. The lobby code goes as follows.

import { Component, OnInit, OnChanges, AfterContentInit, SimpleChanges, Input } from '@angular/core';
import { SocketioService } from '../socketio.service';

@Component({
  selector: 'app-lobby',
  templateUrl: './lobby.component.html',
  styleUrls: ['./lobby.component.css']
})
export class LobbyComponent implements OnInit, AfterContentInit {
  constructor(private socketService: SocketioService) { }
  @Input()  roomId: string;

  ngOnInit(): void {
    // alert("On init fetched")
    this.socketService.setupSocketConnection()
    this.roomId = this.socketService.getRoomId()
    if (this.roomId) {
      alert("Value obtained in init: " + this.roomId)
    }
    else {
      alert("No value obtained inside init")
    }
    document.getElementById("generated_code").innerHTML = this.roomId
  }
}

Here, OnInit, the socketService.getRoomId() function returns an undefined value. If I press back, go to the startpage again, enter a playerName and start a new game again, the previously generated roomId is being rendered. What am I missing here? How do I load the roomId and display the same when I am rerouted to the lobby?

Ad

Answer

What you are missing is the concept of asynchronous calls and observable.

Try:

service.ts

export class SocketioService {
  socket;
  public roomId: string;
  public playerName: string;
  private roomData$ = new BehaviorSubject<string>(null);
  constructor() {}

  setupSocketConnection() {
    this.socket = io(environment.SOCKET_ENDPOINT)
    alert("Connection set-up")
  }

  startGame(inputPlayerName) : Observable<string>{
    this.socket.emit('startGame', {name: inputPlayerName})
    this.playerName = inputPlayerName
    this.socket.on('recieveRoomId', (data: any) => {
      this.roomId = data.code;
      this.roomData$.next(this.roomId); // <=== emitting event when the roomId is set.
      alert("In service startGame: " + this.roomId)
    })
    return this.roomData$.asObservable(); // <== returns observable
  }

  getRoomId(){
    alert("In service getRoomId: " + this.roomId)
    return this.roomData$.asObservable();  // <== returns observable
  }
}

StartpageComponent.ts

  startThisGame() {
    // not sure why you are getting value in this way. I can't comment because 
    // I dont have access to HTML code
    var playerName = (document.getElementById("nameInput") as HTMLInputElement).value;
    if (playerName) {
      // subscribe to the observable and get the values when prepared.
      // make sure to unsubscribe it in "ngOnDestroy" to avoid memory leaks
      this.socketService.startGame(playerName).subscribe(data => {
           this.testRoomId = data;
           alert("In component: " + this.testRoomId)
           // again, not sure why would you need "this.zone.run()",
           // Handle input values as Angular way and you wont need these patches.
           this.zone.run(() => {this.router.navigate(['/lobby']); });
       });

    }
    else{
      alert("Enter a player name!")
    }
  }

In LobbyComponent.ts

// You can also use RouteParam to pass RoomId rather than using Observable as I have done.
ngOnInit(): void {
    this.socketService.setupSocketConnection()
    // make sure to unsubscribe in ngOnDestroy
    this.socketService.getRoomId().subscribe(data => {
         this.roomId =  data;
         document.getElementById("generated_code").innerHTML = this.roomId;
    })    
  }

I have added lot of comments to explain you why I have done those changes. I would recommend you to read about Observables and Promises in Javascript to have better understanding of async calls.

You need to wait for the values to come before you use that value. That is why your code is not running in sequence as you are expecting.

Happy learning :)

Ad
source: stackoverflow.com
Ad