1

This is my first post so I hope I'm doing things correctly.

I am trying to workout the best way to both use a resolver and share state between child route components and their parent route's component.

I have an Angular 2 application which has a component/route structure containing a parent route with two child routes as per the code sample below:

Routes:

export const personRoutes: Routes =  [
{ path: 'home/:id', component: MyDetailsComponent,
  resolve: {
    person: PersonResolver
  },
  children: [
    { path: '', component: MyDetailsOverviewComponent, pathMatch: 'full' },
    { path: 'update', component: UpdateMyDetailsComponent }
  ]
}];

I am using a resolver on the parent route to fetch some data before it's view and subsequent child route views are loaded. The data is fetched from a service which makes an api call.

Resolver:

export class PersonResolver implements Resolve<any> {
    constructor(private personService: PersonService) { }

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Person> {
       let id: string = route.params['id'];
       return this.personService.getPerson(id);
    }
}

Service:

getPerson(id: string): Observable<Person> {
    let params = new URLSearchParams();
    params.set('id', id);
    return this.http.get(this.config.apiEndpoint + '/People/', { search: params })
        .map((r: Response) => {
            let person = r.json() as Person;
            this.extractPersonData(person);
            return this.currentPerson.asObservable();
        })
        .catch(error => {
            console.log(error);
            return Observable.throw(error);
        }).share();
}

The service maps the data returned from the http call and uses it to provide the first value for a ReplaySubject which is then subscribed to by the parent component and child route components.

Here is the extractPersonData function called with the data returned from the http/api call/ this.currentPerson is the ReplaySubject with a buffer of 1 which the components will subscribe to.

extractPersonData(person: Person): void {
    PersonService.cleanPersonData(person);
    this.currentPerson.next(person);
}

I want to share the state of the person object between all three components and cannot use @Input/@Output variables as the child routes are dynamically loaded in the parent's router outlet.

Here is the code showing the parent and one of the child components subscribing to the ReplaySubject after the resolve:

Parent Component:

export class MyDetailsComponent implements OnInit {

  private person: Person;
  private subscription: Subscription;

  constructor(private route: ActivatedRoute, private personService: PersonService, private modalService: NgbModal) {
    this.route.data.subscribe((obs: Observable<Person>) => {
      console.log(obs);
      this.subscription = obs['person'].subscribe(person => {
      this.person = person;
    })
  },
  error => {
    console.log('Home Component - Init person error: ' + error);
  });
}

Child Component:

export class MyDetailsOverviewComponent implements OnInit {

  private person: Person;
  private subscription: Subscription;
  isReady: boolean = false;

  constructor(private route: ActivatedRoute) {
    this.route.parent.data.subscribe((obs: Observable<Person>) => {
      this.person = obs['person'];
      this.subscription = obs['person'].subscribe(person => {
        this.person = person;
      });
    });
  }

My idea is to have an update method in the service which each of the components can call. The update method should call this.currentPerson.next(newPerson) internally and then the sibling child components and the parent component should be notified of changes caused by one another.

My question is whether this is a poor approach to sharing state across parent and child routes ? Is there an alternative way to doing this. At the moment I think I am leaking subscribers in the parent and child components and it feels wrong having a subscribe calls inside of another like this:

this.route.data.subscribe((obs: Observable<Person>) => {
  console.log(obs);
  this.subscription = obs['person'].subscribe(person => {
  this.person = person;
})

Thanks.

J.Marler
  • 21
  • 5
  • Sounds a bit like http://stackoverflow.com/questions/36271899/what-is-the-correct-way-to-share-the-result-of-an-angular-2-http-network-call-in – Günter Zöchbauer Nov 26 '16 at 16:25
  • Thanks Gunter, It's definitely a similar ish scenario but a slightly different use case I think as that question relates to sharing the result of an http call whilst I'd like to keep ReplaySubject which is updated by the http call initially and can then continue to be updated by the components consuming the service so that state changes are propagated across all the components. Thanks for the link. It pointed a few things out to me! I think a found the right approach in my last answer in the end – J.Marler Nov 26 '16 at 17:49

3 Answers3

0

yes subscribe inside subscribe is noobish. use flatmap instead.

some code to digest

  public tagsTextStream = this.tagsTextSubject.asObservable().flatMap((q: string) => {
    // noinspection UnnecessaryLocalVariableJS
    let restStream = this.restQueryService.getTagsList(q)
      .map((tags: any) => {
        // filter out any tags that already exist on the document
        let allTags = _.map(tags, 'name');
        let documentTags = _.map(this.tags, 'name');
        return _.pull(allTags, ...documentTags);
      })
      .catch((err: any) => {
        return Observable.throw(err);
      });
    return restStream;
  }).publish().refCount();
danday74
  • 52,471
  • 49
  • 232
  • 283
  • Hi danday74, I had tried with the flatMap operator but the components subscribing just seemed to get an empty object after changing the nested subscribe calls. I'll take another look using flatMap and post back. Cheers – J.Marler Nov 26 '16 at 10:36
  • https://github.com/danday74/plunks these might help - some flatmap RxJS stuff here – danday74 Nov 27 '16 at 13:28
0

It looks like you're setting an observable for "currentPerson" on the service already in extractPersonData() with this.currentPerson.next(person); -

Typically a component requiring that user, would subscribe to PersonService.currentPerson :)

I'm not 100% if that answers the question, if not, let me know and I'll try and help!

Jess
  • 90
  • 7
  • I'm returning PersonService.currentPerson in the call to map() after the http call. This should return a reference right ? – J.Marler Nov 26 '16 at 10:34
0

Thanks guys.

I think I've solved things so their a bit tidier.

I still subscribe to the route.data but now I set the components internal person objects to be of type Observable and use the asynch pipe to compose the "dumb" child components.

So here is the parent:

export class PersonResolver implements Resolve<any> {
    constructor(private personService: PersonService) { }

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
      let id: string = route.params['id'];
      return this.personService.getPerson(id);
    }
}

And here is the child routes component:

export class UpdateMyDetailsComponent implements OnInit {
  private person: Observable<Person>;
  private subscription: Subscription;

  constructor(private route: ActivatedRoute) {
    this.route.parent.data.subscribe((result: Observable<Person>) => {
      this.person = result['person'];
    });
  }

Here's the child routes view/template:

<div class="hidden-xs-down">
  <div class="row">
    <div class="card offset-sm-2 col-sm-3">
      <div class="card-title">
        <h4>Personal Details</h4>
      </div>
      <update-personal-details class="card-block" [person]="person | async"></update-personal-details>
    </div>
    <div class="card offset-sm-1 col-sm-3">
      <div class="card-title">
        <h4>Contact Details</h4>
      </div>
      <update-contact-details class="card-block" [person]="person | async"></update-contact-details>
    </div>
  </div>
  <br />
 <div class="row">
    <div class="card offset-sm-2 col-sm-7">
      <div class="card-title">
        <h4>Address Details</h4>
      </div>
      <update-address-details class="card-block" [person]="person | async"></update-address-details>
    </div>
  </div>
</div>

The basic resolve logic can stay the same:

@Injectable()
export class PersonResolver implements Resolve<Observable<Person>> {
    constructor(private personService: PersonService) { }

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Person> {
      let id: string = route.params['id'];
      return this.personService.getPerson(id);
    }
}

I think that's the right approach. Lets me share the observable state between all the components and I should be able to trigger updates via the service calling next on the shared observable etc.

Hope this helps anyone messing around with a similar structure.

Thanks for the answers all.

Open to critique if anyone thinks this is the wrong approach!

J.Marler
  • 21
  • 5