Angular 6 - share an object between unrelated components

Viewed 358

The Task:

I am integrating an existing HTML5 project (a Phaser-based scientific software) into Angular, for the purposes of better structuring of the ever-expanding UI.

The software lies in its own component and is working. All functionality of the software is exposed from a controller class. I have an instance of the controller class inside the state of the Phaser project.

Imagining a new component (say, top-menu) I would like to be able to say:

<div (click)="this.controller.makeAction()"></div>

, where "this.controller" is an instance, saved in the top-menu component.

Related research:

I have read in many places, that the recommended way to communicate between unrelated components is through a service and using an rxjs BehaviorSubject. Ok... here comes the problem:

The Problem:

When I create the service, I have to set the instance of the BehaviorSubject. The problem is that I do not know when the game will be ready, in order to acces the state, and thus provide the controller to the service. So.. the BehaviorSubject stays null and I get an error.

I would really like to avoid placing Angular code inside the Phaser project, as they should be kept as decoupled as possible (currently there is no coupling what-so-ever).

Questions:

Is my approach correct? The controller will be given to any UI element, and thus to any component. How to fix this problem?

Related Code:

1. The service

// Omitting imports and decorators
export class UserActionControllerService {
  private _userActionController = new BehaviorSubject<UserActionController>(null);
  userActionController = this._userActionController.asObservable();
  constructor() {  }
  setUAC(userActionController: UserActionController){
    this._userActionController.next(userActionController);
  }
}

2. Using the service in top-menu

//Omitting imports and decorator
export class TopMenuComponent implements OnInit {
  userActionController: UserActionController;
  constructor(private uac: UserActionControllerService) {  }
  ngOnInit() {
    this.uac.userActionController.subscribe((value) => {
      this.userActionController = value;
    });
  }

and the html...

<p>{{this.userActionController | async | json}}</p>

3. Setting the value of the controller in the service

//Ommitting imports and decorator
export class GteCoreComponent implements OnInit {
  game: Phaser.Game;
  constructor(private userActionControllerService: UserActionControllerService) {}
  ngOnInit() {
    this.game = new GTE(width, height);
this.userActionControllerService.setUAC(this.game.state.states.MainScene.userActionController);
  }
}

The last line yields an error, because the game has not been created yet. I tried with setTimeout(), still to no avail.

Thanks for your help in advance!

EDIT:

I managed to get it working with setTimeout, which seems like a hack. Any other suggestions?

EDIT2:

As per request, here is the GTE class:

export class GTE extends Phaser.Game {
  game: Phaser.Game;    
  constructor(width?: number, height?: number) {    
    super(width, height, Phaser.CANVAS, 'phaser-div', null, false, true);    
    this.game = this;
    this.game.state.add('Boot', Boot, false);
    this.game.state.add('MainScene', MainScene, false);
    this.game.state.start('Boot');
  }
}
1 Answers

As you have noted that you have solved the problem using setTimeout, this indicates that it is a timing matter which is causing your issue.

setTimeout does two things:

Firstly, it delays the processing of the callback for some specified number of milliseconds. This can be problematic as if a user's browser is a bit slower/busier, it might take more than the specified time for the thing you are waiting for to become ready. It is always better to trigger such an activity off an event rather than waiting a time - if you wait long enough to be sure it's ready, you'll be waiting much longer than necessary for the majority of users.

The second thing setTimeout does is push the execution of the callback onto the next JS execution frame. This can be required in certain circumstances as it allows other queued work to complete before continuing. This will work even if you set the timeout to 0ms - and if this solves the problem, then setTimeout is safe to use in this scenario.

Nevertheless, even if you are in the second of these scenarios, keying off an event would be much cleaner - and less likely for someone, or your future self, to come along and un-do it later!

Looking at the Phaser.Game documentation there is an isBooted flag, so if there is no event exposed, you could create one in your GTE class:

export class GTE extends Phaser.Game {
  game: Phaser.Game; 
  private readyCallback: () => null;
  constructor(width?: number, height?: number, ready: () => null) {    
    super(width, height, Phaser.CANVAS, 'phaser-div', null, false, true);    
    this.game = this;
    this.game.state.add('Boot', Boot, false);
    this.game.state.add('MainScene', MainScene, false);
    this.game.state.start('Boot');
    this.readyCallback = ready;
    setTimeout(() => this.checkReady(), 100);
  }

  checkReady() {
    if (this.game.isBooted) this.readyCallback();
    else setTimeout(() => this.checkReady(), 100);
  }
}
Related