2

I have a directive app.service.ts which stores the app state data, and I'm trying to use this directive from other components so I can get and set the state of the app (which is working).

however it gives me this error when I try to bind a property from this directive to the view

EXCEPTION: Expression 'loading: {{loadState}} in App@0:23' has changed after it was checked. Previous value: 'false'. Current value: 'true' in [loading: {{loadState}} in App@0:23]

here I'm trying to show loading text, when appState.get().loadState == true

app.service.ts - source

import {Injectable} from 'angular2/core';
import {WebpackState} from 'angular2-hmr';

@Injectable()
export class AppState {
  _state = {}; // you must set the initial value
  constructor(webpackState: WebpackState) {
    this._state = webpackState.select('AppState', () => this._state);
  }

  get(prop?: any) {
    return this._state[prop] || this._state;
  }

  set(prop: string, value: any) {
    return this._state[prop] = value;
  }
} 

app.ts

import {AppState} from './app.service';

export class App{

   constructor(public appState: AppState) {
      this.appState.set('loadState', false);
   }
   get loadState() { 
      return this.appState.get().loadState;
   }
}

app.html

<div class="app-inner">
  <p>loading: {{loadState}}</p>
    <header></header>
  <main layout="column" layout-fill>
    <router-outlet></router-outlet>
  </main>
</div>

assume app.ts has a child component home.ts and loaded to the view

home.ts

 export class HomeCmp {
 page;

 constructor(private wp: WPModels, private appState: AppState) {}

 ngOnInit() {
   var pageId = this.appState.get().config.home_id;
   this.appState.set('loadState', true);    // before http request

   this.wp.fetch(WPEnpoint.Pages, pageId).subscribe(
     res => {
       this.page = res;
       this.appState.set('loadState', false);  // after http request
     },
     err => console.log(err)
   );
 }
}
Murhaf Sousli
  • 12,622
  • 20
  • 119
  • 185
  • you can use shared object for this. I think `{{loadState}}` in HTML will always throw error this way. Rather try to bind some shared variable used in service. eg. _state={} is a shared object. set some property to it and then in html you can access it through `{{appState._state.someProperty}}` – micronyks Mar 28 '16 at 03:58
  • @micronyks same error – Murhaf Sousli Mar 28 '16 at 16:00

5 Answers5

2

The solution is pretty simple, by making a special component to display loading state from the state service app.service.ts, here is the code:

LoaderCmp displays loadState attribute from appState service directly.

import {Component} from 'angular2/core';
import {AppState} from "../../app.service";

@Component({
  selector: 'loader',
  template: `
   loading state : {{appState._state.loadState}}
  `
})

export class LoaderCmp{
  constructor(private appState: AppState) {}
}

app.service.ts holds our app states.

import {Injectable} from 'angular2/core';
import {WebpackState} from 'angular2-hmr';

@Injectable()
export class AppState {
  _state = {}; // you must set the initial value
  constructor(webpackState: WebpackState) {
    this._state = webpackState.select('AppState', () => this._state);
  }

  get(prop?: any) {
    return this._state[prop] || this._state;
  }

  set(prop: string, value: any) {
    return this._state[prop] = value;
  }
}

app.ts no configuration needed, just add LoaderCmp to app's directives.

@Component({
  selector: 'app',
  directives: [HeaderCmp, LoaderCmp],
  template: `
  <div class="app-inner">
    <loader></loader>
    <header></header>
    <main>
      <router-outlet></router-outlet>
    </main>
  </div>
  `
})
export class App  {
    constructor() { }
}

update app state from any component, for example: home.ts

export class HomeCmp {
 page;

 constructor(private wp: WPModels, private appState: AppState) {}

 ngOnInit() {
   var pageId = this.appState.get().config.home_id;
   this.appState.set('loadState', true);    // before http request

   this.wp.fetch(WPEnpoint.Pages, pageId).subscribe(
     res => {
       this.page = res;
       this.appState.set('loadState', false);  // after http request
     },
     err => console.log(err)
   );
 }
}
Murhaf Sousli
  • 12,622
  • 20
  • 119
  • 185
0

I think that you could leverage the ChangeDetectorRef class and its method detectChanges.

For this inject it into your App component and call the method in its ngAfterViewInit.

 constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
      this.cdr.detectChanges();
  }

See this question for more details:

That being said I would use an observable property into your state service to notify components when it's read.

@Injectable()
export class AppStateService {
  state: string;
  state$: Observable<string>;
  private stateObserver : Observer<string>;

  constructor(){
    this.state$ = new Observable(observer => this.stateObserver = observer).share();
  }

  updateState(newState) {
    this.state = newState;
    this.stateObserver.next(newState);
  }
}

And in the component:

@Component({...})
export class SomeComponent {
  constructor(service: AppStateService) {
    service.state$.subscribe(newState => {
      (...)
    });
  }
}

See these links for more details:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • I tried this but it's giving me exactly the same error – Murhaf Sousli Mar 28 '16 at 15:58
  • You mean you tried to call `detectChanges`? In which component lifecycle hook? Otherwise I made a typo in my snippet. It's `ngAfterViewInit` instead... – Thierry Templier Mar 28 '16 at 17:01
  • It seems that there is no need for `detectChanges`, I liked the idea of using observable for the state service. but I choice the normal one `app.service.ts` because it support multiple properties. I wrote the solution for both ways. – Murhaf Sousli Mar 28 '16 at 19:14
0

https://github.com/angular/angular/issues/6005 It's a feature of dev mode working as intended. Calling enableProdMode( ) - see when bootstrapping the app prevents the exception from being thrown.

You need to trigger change detection again. Triggering Angular2 change detection manually

Community
  • 1
  • 1
shiv
  • 383
  • 1
  • 4
  • 17
0

wp somehow seems to run code outside of Angulars zone. To force the code back into Angulars zone, use zone.run() as shown below for code that updates properties, your view binds to:

import {NgZone} from 'angular2/core';

export class HomeCmp {
 page;

 constructor(private zone:NgZone, private wp: WPModels, private appState: AppState) {}

 ngOnInit() {
   var pageId = this.appState.get().config.home_id;
   this.appState.set('loadState', true);    // before http request

   this.wp.fetch(WPEnpoint.Pages, pageId).subscribe(
     res => {
       this.zone.run(() => {
         this.page = res;
         this.appState.set('loadState', false);  // after http request
       });
     },
     err => console.log(err)
   );
 }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
0

Another solution by using observable and share function, here we changed the app.service.ts with app.state.ts which support only one attribute.

LoaderCmp displays loadState attribute from appState service directly.

import {Component} from 'angular2/core';
import {AppStateService} from "../../app.state";

@Component({
  selector: 'loader',
  template: `
   loading state : {{ active }}
  `
})

export class LoaderCmp{
  active;
  constructor(private service: AppStateService) {}
    ngOnInit() {
       this.service.state$.subscribe(newState => {
       this.active = newState;
    });
  }
}

app.state.ts holds our load state.

import "rxjs/add/operator/share";
import {Observable} from "rxjs/Observable";
import {Observer} from "rxjs/Observer";
import {Injectable} from "angular2/core";

@Injectable()
export class AppStateService {
  state: boolean = false;
  state$: Observable<boolean>;
  private stateObserver : Observer<boolean>;

  constructor(){
    this.state$ = new Observable(observer => this.stateObserver = observer).share();
  }

  updateState(newState) {
      this.state = newState;
      this.stateObserver.next(newState);
  }
}

app.ts no configuration needed, just add LoaderCmp to app's directives.

@Component({
  selector: 'app',
  directives: [HeaderCmp, LoaderCmp],
  template: `
  <div class="app-inner">
    <loader></loader>
    <header></header>
    <main>
      <router-outlet></router-outlet>
    </main>
  </div>
  `
})
export class App  {
    constructor() { }
}

update app state from any component, for example: home.ts

export class HomeCmp {
 page;

 constructor(private wp: WPModels, private service: AppStateService) {}

 ngOnInit() {
   this.service.updateState(true);    // before http request

   this.wp.fetch(WPEnpoint.Pages, pageId).subscribe(
     res => {
       this.page = res;
       this.service.updateState(false);  // after http request
     },
     err => console.log(err)
   );
 }
}
Murhaf Sousli
  • 12,622
  • 20
  • 119
  • 185