Reputation: 13296
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
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
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
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
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
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