Murhaf Sousli
Murhaf Sousli

Reputation: 13296

angular2 property has changed error

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)
   );
 }
}

Upvotes: 2

Views: 9567

Answers (5)

Murhaf Sousli
Murhaf Sousli

Reputation: 13296

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)
   );
 }
}

Upvotes: 0

Murhaf Sousli
Murhaf Sousli

Reputation: 13296

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)
   );
 }
}

Upvotes: 2

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657761

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)
   );
 }
}

Upvotes: 0

Thierry Templier
Thierry Templier

Reputation: 202276

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:

Upvotes: 0

shiv
shiv

Reputation: 393

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

Upvotes: 0

Related Questions