Angular 4 - Router resolve not working with BehaviorSubject
Asked Answered
G

2

6

Angular gurus your help is greatly appreciated in this regard.

Here is what I am trying to do, when I access main app page it calls getConfigs() from config.service.ts and get the data from backend then updates this.configStringSource.next(config). Right after that I am trying to redirect it to this.router.navigate(['/clone/status']), but redirect is not happening.

app.routing.ts

import { Routes, RouterModule } from '@angular/router';

import { CloneComponent } from './clone/clone.component';
import { StatusComponent } from './status/status.component';
import { ConfigurationComponent } from './configuration/configuration.component';
import { LogsComponent } from './logs/logs.component';
import { ConfigResolver } from './_services/config-resolver.service';

const appRoutes: Routes = [
    { path: 'clone', component: CloneComponent, children: [
        {path: 'status', component: StatusComponent, resolve: {config: ConfigResolver} },
        ]
    },
    { path: 'logstream', component: LogstreamComponent },
];

export const AppRouting = RouterModule.forRoot(appRoutes);

config.ts

export class Config {
    configID: string;
    sourceDbNodes: string;
    targetDbNodes: string;
}

config.service.ts

import { Injectable, OnInit } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
//import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Router } from '@angular/router';
import {Subject} from 'rxjs/Subject';

import { Config } from '../_models/config';

@Injectable()
export class ConfigService {

    // Observable string source
    private configsStringSource = new BehaviorSubject<Config>({ configID: "", sourceDbNodes: "", targetDbNodes: ""});

    // Observable string stream
    configsString$ = this.configsStringSource.asObservable();

    // Service message commands
    updateConfigs(configs: Config) {
      this.configsStringSource.next(configs)
    }

    constructor(private http: Http, private router:Router) { }

    getConfigs() {
      let headers = new Headers();
      headers.append('Content-Type','application/json');
      return this.http.get('http://localhost:8080/sample1/api/config', { headers: headers })
        .map((response: Response) => response.json());
    }
}

config-resolver.service.ts

import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Injectable } from '@angular/core';

import { ConfigService } from './config.service';
import { Config } from '../_models/config';

interface Server {
  id: number;
  name: string;
  status: string;
}

@Injectable()
export class ConfigResolver implements Resolve<Config> {

  config: Config;

  constructor(private configService: ConfigService) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Config> | Promise<Config> | Config {
    return this.configService.configsString$.map(
      data => data[1]);
    }
}

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { Config } from './_models/config';
import { ConfigService } from './_services/config.service';

@Component({
  moduleId: module.id.toString(),
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnInit {
  configs: Config[];

  constructor(private router:Router, private configService:ConfigService ) { }

  title = 'Angular 4 Proeject';

  private getConfigs() {
    this.configService.getConfigs().subscribe(configs => { 
        this.configs = configs;
                this.configService.updateConfigs(configs);
console.log('app.component.ts sourceDbNode = '+this.configs[0].sourceDbNodes);
    });
  }

  ngOnInit() {
    this.getConfigs();
    this.router.navigate(['/clone/status']);
  }

}

status.component.ts

import { Component, Input, OnInit, AfterContentChecked } from '@angular/core';
import { ActivatedRoute, Params, Router, Data } from '@angular/router';

import { Config } from '../_models/config';
import { ConfigService } from '../_services/config.service';

@Component({
  selector: 'app-status',
  template: `
    <p>
      status Works! {{config}}
    </p>
  `,
  styleUrls: ['./status.component.scss']
})

export class StatusComponent implements OnInit {

  configs: string;
  config: Config;
  servers: Array<any>;
  server: { id: number; name: string; status: string; };

  constructor(private configService:ConfigService,
              private route: ActivatedRoute,
              private router: Router) { }

  ngOnInit() {
    this.route.data.subscribe(
        (data: Data) => {
          this.config = data['config'];
console.log('status.component.ts data = ', data['config']);
console.log('status.component.ts this.config = ', this.config);
        }
    );
  }
}
Genaro answered 6/9, 2017 at 12:4 Comment(1)
FYI, there is a better way of doing this. You can use an APP_INITIALIZER provider to ensure that a service is fully loaded at bootstrap. I've put an example together here: #47230918Heteromerous
H
13

Your issue is that resolved observables need to complete, so you just need to add .take(1) or .first() to your resolver observable like:

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Config> | Promise<Config> | Config {
    return this.configService.configsString$.map(
      data => data[1]).take(1);
}

However, there's a much cleaner way of loading a config service:

import { Injectable, APP_INITIALIZER } from '@angular/core';
import { Config } from './_models/config';

@Injectable()
export class ConfigService {
  protected config: Config;

  constructor(private http: Http) {
  }

  getConfigs(): Observable<any> {
    let headers = new Headers();
    headers.append('Content-Type','application/json');
    return this.http.get('http://localhost:8080/sample1/api/config', { headers: headers })
               .map((response: Response) => response.json());
  }

  public load() {
     return new Promise((resolve, reject) => {
       this.getConfigs()
         .subscribe(
           config => {
             this.config = config;
             resolve(true);
           },
           err => resolve(err)
         );
     });
  }
}

export function ConfigServiceInitFactory(configService: ConfigService) {
  return () => configService.load();
}

export const ConfigServiceInitProvider = {
  provide: APP_INITIALIZER,
  useFactory: ConfigServiceInitFactory,
  deps: [ConfigService],
  multi: true
}

then in your app module, import both ConfigService and ConfigServiceInitProvider and provide them both like this:

providers: [
  ConfigService,
  ConfigServiceInitProvider,
  ... remaining service providers..
]

this will ensure that your config is loaded before pretty much anything else, and you can inject your config service where ever it's needed and the config will always be available in a synchronous manner.

Heteromerous answered 15/11, 2017 at 13:58 Comment(0)
L
1

It helped to me. In config.service change

// Observable string stream
configsString$ = this.configsStringSource.asObservable();

to stack.

Add .first() to .asObservable().

Laity answered 15/11, 2017 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.