1

i have some experience with JavaScript and angular but i cant quite get my head around some concepts. I am trying to Build a service to store some settings that is able to notify components that implement it of changes have occurred.

Setting up setters that call a onChange function for each property seemed the to be the old fashioned way and i got it working. But i does not feel like the angular way, as it bloats the class and for each property i want to add i would need to implement new get & set methods. So i'd like to have a interface that calls the onChange function each time a property was set to something new. But i cant find the syntax for this.

import { Injectable}        from '@angular/core';
import { Observable }       from 'rxjs/Observable';
import { Subject }          from 'rxjs/Subject';

@Injectable()
export class serviceSettings {
  private constructed: boolean = false;

  private _targetFPS:          number  = 30;
  private _showFPS:            boolean = true;
  private _viewDistance:       number  = 7000;
  private _enabelAntialiasing: boolean = true;

  private settingsChanged =   new Subject();

  constructor() {
    if (this.constructed) {
        console.log('serviceSettings aready exists');
        return this
    }
    this.constructed = true;
  }

  onChange() {
    this.settingsChanged.next();
    //save setting in local storage
    //send them to a api to store them server sided
    console.log('settings changed');
  }

  onChangeNotice():Observable<any> {
    return this.settingsChanged.asObservable();
  }

  set targetFPS(val:number) {
    this._targetFPS = val
    this.onChange()
  }

  get targetFPS():number{
    return this._targetFPS
  }

  set showFPS(val:boolean) {
    this._showFPS = val
    this.onChange()
  }

  get showFPS():boolean {
    return this._showFPS
  }

  set enabelAntialiasing(val:boolean) {
    this._enabelAntialiasing = val
    this.onChange()
  }

  get enabelAntialiasing():boolean {
    return this._enabelAntialiasing
  }

  set viewDistance(val:number) {
    this._viewDistance = val
    this.onChange()
  }

  get viewDistance():number{
    return this._viewDistance
  }
}

In this particular context it is a settings service for a little game-"engine" i am working on with THREE.js. It needs to reinstatiate the renderer if i want to enable/disable anti-aliasing but not if any other settings have changed.

TL:DR: I have to react on changes differently depending on what has changed.

MaDsaiboT
  • 13
  • 4

1 Answers1

0

My approach would be to put all changes in one object, so that I need only one setter and one getter. I would also standardize how a change is emitted, so that different portions of the app that care about different changes only get what they need.

First, the change interface:

export interface stateChange{
    propName:string,
    oldVal:any,
    newVal:any
}

Next, the settings service:

// Stores current settings
private state = {
    fps:          30,
    showFPS:      true,
    antiAliasing: false,
    viewDistance: 7000
};

public settings = new Subject();

// Notifier is shared across all subscribers
// Notice this is not a function that returns, but the Observable itself
// (sharing needs the same Observable object, not a new one created on each func call)
public changeNotices$:Observable<stateChange> = this.settings.asObservable().share();

changeState(prop:string,val:any){
    let oldVal = this.state[prop];
    if(oldVal == val) return; // no change
    this.state[prop] = val; // save new state
    this.settings.next( // emit state change notice
        {propName:prop, oldVal:oldVal, newVal:val}
    );
}

getState(prop:string){return this.state[prop];}

Here is how I would consume the service, if I only cared about anti-aliasing:

antiAliasingChanges$ = this.settingsService.changeNotices$
    .filter((e:stateChange) => e.propName == 'antiAliasing')
    .subscribe((e:stateChange) => {
        if(e.newVal === true) console.log('antiAliasing is ON');
    });

This is basic code with few checks but you get the idea. One of the first improvements I would make is use enums, so that in the last bit of code, I wouldn't hardcode the property name.

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165
  • thanks for this fast answer! The only concern that comes to mind on a fist glace is that this setter does no type checking – MaDsaiboT Aug 22 '17 at 17:06
  • @MaDsaiboT you're correct. There are several concerns. As I wrote, "*This is basic code with few checks*" This is not production code, but something to put you in the direction I would go. You could easily implement type-checking if you wanted by having a `Map` and checking against it before setting properties. – BeetleJuice Aug 22 '17 at 17:12
  • @MaDsaiboT As an alternative, you could require the `changeState()` to accept a `StateInterface` object (an interface with the properties of `state`). Just make all the properties optional in the interface, and adjust the `changeState()` code to read which property was supplied and to copy values to the local `state` property. This will ensure type-checking. It has the added benefit of allowing you to change many settings at once, while emitting only one change notice. It's just more complex code but it's what I've done in my own app – BeetleJuice Aug 22 '17 at 17:23
  • thanks again i will give this a go! Your refactoring on the observable is also very helpful. Just one little thing i don't get what is the meaning of $ in antiAliasingChanges$ = this.settingsService.changeNotices$ ? is this in any way mandatory or just a convention thing? – MaDsaiboT Aug 22 '17 at 17:48
  • @MaDsaiboT [it's a convention](https://stackoverflow.com/questions/37671700/angular2-style-guide-property-with-dollar-sign) but there is nothing special about it and some people certainly choose not to use it. Just be consistent. – BeetleJuice Aug 22 '17 at 18:45