4

Angular 2 application where most of the State of the application lives in @Injectable services that expose the state via an Observable. At some point the application is dropping out of the Angular Zone, which causes change detection to not work. In my subscribe I need to call ChangeDetectorRef.detectChanges() when this occurs. How do I make sure the application doesn't drop out of the Angular Zone? Or determine what is causing it?

Here is my Service:

@Injectable()
export class CalendarFilterStore {
    private currentState: CalendarFilter;
    private state$ = new ReplaySubject<CalendarFilter>(1);

    constructor() {
        this.currentState = {
            startDate: new Date(2016, 10, 14, 2)
        };
        this.state$.next(this.currentState);
    }

    get state(): Observable<CalendarFilter> {
        console.log('calendar filter state, zone: ' + Zone.current.name);
        return this.state$.asObservable();
    }

Here is the ngOnInit function for the component that is using this service:

ngOnInit() {
    console.log('ng init zone: ' + Zone.current.name);
    this.calendarFilterStore.state
        .filter(state => {
            console.log('got filter state, zone: ' + Zone.current.name);
            return (state.areaIDs !== undefined && state.worksiteID !== undefined && state.startDate !== undefined && state.numberOfShifts !== undefined);
        })
        .switchMap(state => this.getCalendar(state))
        .subscribe(res => {
            console.log('got calendar, dashboard-table, zone: ' + Zone.current.name);
            this.loadCalendar(res);
        }, error => {
            console.log(error);
        });

Here is the console output that shows when we drop out of the Angular Zone. It appears to happen when the Calendar filter emits it's second value.

calendar filter state, zone: angular
calendar-filter - store.ts:21 calendar filter state, zone: angular
calendar-store.ts:22 get calendar state, zone: angular
dashboard-table.component.ts:60 ng init zone: angular
calendar-filter - store.ts:21 calendar filter state, zone: angular
dashboard-table.component.ts:63 got filter state, zone: angular
dashboard-table.component.ts:63 got filter state, zone: <root>
dashboard-table.component.ts:63 got filter state, zone: <root>
calendar.service.ts:25 get calendar, zone: <root>
calendar.service.ts:31 got calendar, zone: <root>
dashboard-table.component.ts:68 got calendar, dashboard-table, zone: <root>
calendar-store.ts:27 segments returned, zone: <root>

I've narrowed it down to components that are several levels deep into my component tree. When I check Zone.current.name in their constructors they are in the root zone and not the angular zone. Any events they emit up remain in the root zone, even as they propagate to the parent->service->subscriber.

Here is a component template that works and runs in the angular zone, but its child, sb-worksite-selector, runs in the root zone, as seen by the output.

<div class="dashboard-navigation" *ngIf="optionsLoaded">
  <button type="button" class="btn btn-secondary" (click)="previousDay()"><i class="fa fa-backward" aria-hidden="true"></i> Previous Day</button>
  <div class="inline-block">
      <sb-worksite-selector [worksiteOptions]="worksiteOptions"
                            (onWorksiteChanged)="worksiteChanged($event)">
      </sb-worksite-selector>
  </div>

sb-worksite-selector

@Component({
    selector: 'sb-worksite-selector',
    templateUrl: 'worksite-selector.component.html',
    styleUrls: ['worksite-selector.component.scss']
})
export class WorksiteSelectorComponent implements OnInit {
    @Input() worksiteOptions: Array<any>;
    @Output() onWorksiteChanged = new EventEmitter<number>();

    constructor() {
        console.log('create worksite selector, zone: ' + Zone.current.name);
    }

    ngOnInit() {
        console.log('init worksite selector, zone: ' + Zone.current.name);
    }
}

Logging for Worksite selector:

calendar filter state, zone: angular
dashboard - table.component.ts:63 got filter state, zone: angular
calendar.service.ts:25 get calendar, zone: angular
dashboard - navigation.component.ts:63 dashboard navigation got areas and worksites, zone: angular
worksite - selector.component.ts:14 create worksite selector, zone: <root>
worksite - selector.component.ts:18 init worksite selector, zone: <root>
Ryan Langton
  • 6,294
  • 15
  • 53
  • 103
  • Can you include all (or at least more) of the code that calls `this.state$.next` within your service? – cartant Nov 10 '16 at 19:24
  • I added more details.. it's a component that is loading inside the root zone instead of angular zone, and this calls the service which triggers this.state$.next (in the root zone) – Ryan Langton Nov 10 '16 at 20:08
  • 1
    I've found the issue and have submitted a bug to angular.. https://github.com/angular/angular/issues/12838 – Ryan Langton Nov 12 '16 at 22:12

0 Answers0