angulartypescriptweb

Array shown as undefined in child component when using parametric route Angular


in a few words what I'm trying to do here is displaying some property of a specific item of the jobs array in a child component when navigating to /jobs/:id

Partent Component

export class HomeComponent implements OnInit {

  public jobs!: JobInterface[]

  constructor(
    private jobsService: JobsService
  ) {
  }

  ngOnInit() {
    this.getJobs()
  }


  getJobs(page = 1, filters = {city: 'Rome, Italy', level: ''}): void {
    this.jobsService.getJobs(page, filters)
      .subscribe((response) => {
        this.jobs = response.results.map(job => ({
            id: job.id,
            contents: job.contents,
            name: job.name,
            publication_date: job.publication_date,
            locations: job.locations,
            levels: job.levels,
            company: job.company

          })
        )
      })
  }

}

everything works on the routing perspective but into the child component the jobs array is shown as undefined making me unable to it for the desired item:

Child component

export class JobDetailsComponent implements OnInit {
  jobId : any
  @Input() jobs! : JobInterface[]
  selectedJob : any
  constructor(private ActivatedRoute : ActivatedRoute) { }

  ngOnInit(): void {
    this.jobId = this.ActivatedRoute.snapshot.paramMap.get('id')
    console.log(this.jobs)
    this.selectedJob = this.jobs.filter(this.jobId)

  }

}

Partent component's HTML

<app-search-bar (OutputFilters)="getFilteredResults($event)" ></app-search-bar>
<app-job-cards [jobs]="jobs"></app-job-cards>
<app-job-details [jobs]="jobs"></app-job-details>

what is the correct way to do this? What am I doing wrong here?


Solution

  • Well, because the route changes, there's no jobs or job in the job-details component. Also, components render before data is retrieved.

    You could add a flag to render component when data is ready and then you could move job-details component to job-cards component, and pass a single job from there via router, like so:

    export class HomeComponent implements OnInit {
      dataReady:boolean = false;
    
    // add after the response..
    this.dataReady = true;
    

    and in the template:

    <app-job-cards *ngIf="dataReady" [jobs]="jobs"></app-job-cards>
    

    and then:

    pass job to job-details component via router from job-cards template:

    <a mat-stroked-button routerLink="jobs/{{job.id}}" [state]="job">See More</a>
    

    and then in the job-details component use it via history API:

    ngOnInit(): void {
        this.selectedJob = window.history.state;
    }
    

    But that's a bad approach, and also the route would be empty when accessed directly.. you should decide what to do with that..

    A better approach would be to store jobs in the jobs-service instead of home-component and passing them via parent/child, which will be shared between components, and add getJobs and getJobDetails methods that would get or fetch jobs, or find (rather than filter) a single job for job-details component (and maybe fetch a single job if you want details page to always have some data), which is a lot of refactoring to do..

    edit: sharing service data:

    move processing from home components to jobs-service. There, combine fetch and processing methods, and fetch jobs if there are no any, else return jobs, as an Observable. Then, subscribe to that method to get jobs from each component.

    Try:

      getJobs(): Observable<JobInterface[]> {
    
        if(this.jobs?.length > 0) {
          return of(this.jobs);
        }
    
        return this.fetchJobs();
      }
        
    
      fetchJobs(page = 1, filters = { city: 'Rome, Italy', level: '' }): Observable<JobInterface[]> {
    
        const params = new HttpParams({ encoder: new CustomHttpParamsEncoder() })
          .set('category', 'Science and Engineering')
          .set('page', page)
          .set('location', filters.city)
          .set('level', filters.level)
    
        return  this.http.get<APIResponseInterface>(this.url, { params })
            .pipe(map(response => {
    
              this.jobs = response.results.map(job => ({
                id: job.id,
                contents: job.contents,
                name: job.name,
                publication_date: job.publication_date,
                locations: job.locations,
                levels: job.levels,
                company: job.company
              })
              )
    
              return this.jobs;
    
            })
        );
    
      }
    
    getJobs(): void {
    
      this.jobsService.getJobs().subscribe((jobs: JobInterface[]) => {
    
        this.jobs = jobs;
        this.dataReady = true;
    
      });
    }
    
    ngOnInit(): void {
    
    this.jobId = this.ActivatedRoute.snapshot.paramMap.get('id')
    
    this.jobsService.getJobs().subscribe((jobs:JobInterface[]) => {
    
      this.selectedJob = jobs.find(job=> job.id == this.jobId);
      });
    }  
    

    filter method would then call jobsService.fetchJobs directly, and it would modify jobs throughout at the same time.

    have a look at this example

    How can i share data from http Response between components?

    also check other solutions here