5

I have course-detail component that contain data (named course) from my backend app and I want to pass that data to another component (course-play) that's not related to the component. I want to display the same data I got from my backend in this two components. This are the relevant files:

app-routing-module:

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

import { CourseListComponent } from './courses/course-list/course-list.component';
import { CourseDetailComponent } from './courses/course-detail/course-detail.component';
import { CoursePlayComponent } from './courses/course-play/course-play.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const appRoutes: Routes = [
  { path: '', redirectTo: '/courses', pathMatch: 'full' },
  { path: 'courses', component: CourseListComponent,  pathMatch: 'full' },
  { path: 'courses/:id', component: CourseDetailComponent, pathMatch: 'full'},
  { path: 'courses/:id/:id', component: CoursePlayComponent, pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent, pathMatch: 'full' }];

@NgModule({
  imports: [RouterModule.forRoot(appRoutes)],
  exports: [RouterModule]
})

export class AppRoutingModule {  }

courses/course (interface)

export interface ICourse {
  course_id: number;
  title: string;
  autor: string;
  segments: ISegment[];
}

export interface ISegment {
  segment_id: number;
  unit_id: number;
  unit_title: string;
  name: string;
  type: string;
  data: string;
}

courses/course.service:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { Observable, throwError } from 'rxjs';
import { catchError, groupBy } from 'rxjs/operators';

import { ICourse } from './course';

// Inject Data from Rails app to Angular app
@Injectable()
export class CourseService{
  constructor(private http: HttpClient) {  }

  private url = 'http://localhost:3000/courses';
  private courseUrl = 'http://localhost:3000/courses.json';

  // Handle Any Kind of Errors
  private handleError(error: HttpErrorResponse) {

    // A client-side or network error occured. Handle it accordingly.
    if (error.error instanceof ErrorEvent) {
      console.error('An error occured:', error.error.message);
    }

    // The backend returned an unsuccessful response code.
    // The response body may contain clues as to what went wrong.
    else {
      console.error(
        'Backend returned code ${error.status}, ' +
        'body was ${error.error}');
    }

    // return an Observable with a user-facing error error message
    return throwError(
      'Something bad happend; please try again later.');
  }

  // Get All Courses from Rails API App
  getCourses(): Observable<ICourse[]> {
  const coursesUrl = `${this.url}` + '.json';

  return this.http.get<ICourse[]>(coursesUrl)
      .pipe(catchError(this.handleError));
  }

  // Get Single Course by id. will 404 if id not found
  getCourse(id: number): Observable<ICourse> {
    const detailUrl = `${this.url}/${id}` + '.json';

    return this.http.get<ICourse>(detailUrl)
        .pipe(catchError(this.handleError));
  }


}

courses/course-detail/course-detail.ts:

import { Component, OnInit, Pipe, PipeTransform } from '@angular/core';
import { ActivatedRoute, Router, Routes } from '@angular/router';

import { ICourse } from '../course';
import { CourseService } from '../course.service';

@Component({
  selector: 'lg-course-detail',
  templateUrl: './course-detail.component.html',
  styleUrls: ['./course-detail.component.sass']
})

export class CourseDetailComponent implements OnInit {
  course: ICourse;
  errorMessage: string;

  constructor(private courseService: CourseService,
        private route: ActivatedRoute,
        private router: Router) {
  }

  ngOnInit() {
    const id = +this.route.snapshot.paramMap.get('id');
    this.getCourse(id);
    }

   // Get course detail by id
   getCourse(id: number) {
     this.courseService.getCourse(id).subscribe(
       course => this.course = course,
       error  => this.errorMessage = <any>error);
   }

   onBack(): void {
     this.router.navigate(['/courses']);
   }

}

courses/course-play/course-play.ts:

import { Component, OnInit} from '@angular/core';
import { ActivatedRoute, Router, Routes, NavigationEnd } from '@angular/router';
import { MatSidenavModule } from '@angular/material/sidenav';

import { ICourse } from '../course';
import { CourseService } from '../course.service';

@Component({
  selector: 'lg-course-play-course-play',
  templateUrl: './course-play.component.html',
  styleUrls: ['./course-play.component.sass']
})

export class CoursePlayComponent implements OnInit {
  courseId: number;
  errorMessage: string;
  private sub: any;

  constructor(private courseService: CourseService,
      private route: ActivatedRoute,
      private router: Router) {

    }

    ngOnInit() {


      }


     onBack(): void {
       this.router.navigate(['/courses/:id']);
     }

}
Ofir Sasson
  • 673
  • 4
  • 16
  • 39

5 Answers5

5

Without pulling in any other libraries, Angular specifies a few ways for components to talk to each other. Documentation

Since your components are not parent/child but siblings, the options are even more limited.

  1. Have a shared parent component pass the data to both children
  2. Store the data in a shared service

Based on the code you showed, I believe #2 is your best option. So, you can add a property to your CourseService:

public selectedCourse: ICourse;

Then you can access it in both components:

this.courseService.selectedCourse;

The issues with this approach are that you then have to manage a psuedo-global state and make sure the the service is only injected/provided once (otherwise each component will get its own instance of the service and you can't share data).


As noted in a comment on the question by Pavan, you should probably use an Observable and subscribe to it. With the approach mentioned above, the components will not receive notifications when the value changes and will need to proactively check for changes on load.

Vlad274
  • 6,514
  • 2
  • 32
  • 44
  • how do I do that? I use Observable when I call getCourse and get course: ICourse in the detail-course component. I want the same variable to pass to the course-play component – Ofir Sasson Jul 19 '18 at 13:41
  • You would add a new property to the `CourseService`, then when you call `getCourse` instead of passing the result to either component, you would set the value on `CourseService`, since each of the components uses the value from `CourseService` they will both get the value. If you're using an `Observable`, you would add a new `Observable` to `CourseService` and the call `.next` on it with the result of `getCourse`. Each of the components should then `.subscribe` to respond to changes – Vlad274 Jul 19 '18 at 13:46
  • you mean with Subject? like in here?: https://stackoverflow.com/questions/46487255/pass-observable-data-from-parent-component-to-a-child-component-created-with-com – Ofir Sasson Jul 19 '18 at 13:50
  • @OfirSasson Yep! That's a really good example of how to organize this type of communication – Vlad274 Jul 19 '18 at 13:51
  • ok I'll try it and if I had errors I'll edit the post and comment. Thanks! – Ofir Sasson Jul 19 '18 at 13:52
  • @OfirSasson If you run into issues with the new pattern, you should probably post a new question rather than edit this one. It will help it be seen an answered better – Vlad274 Jul 19 '18 at 13:54
  • I opened a new question with some changes: https://stackoverflow.com/questions/51425917/angular-6-pass-data-from-component-to-unrelated-one – Ofir Sasson Jul 19 '18 at 15:09
1

If you have id in your path then try this

import { Component, OnInit} from '@angular/core';
import { ActivatedRoute, Router, Routes, NavigationEnd } from '@angular/router';
import { MatSidenavModule } from '@angular/material/sidenav';

import { ICourse } from '../course';
import { CourseService } from '../course.service';

@Component({
    selector: 'lg-course-play-course-play',
    templateUrl: './course-play.component.html',
    styleUrls: ['./course-play.component.sass']
})

export class CoursePlayComponent implements OnInit {
    courseId: number;
    errorMessage: string;
    private sub: any;

    constructor(private courseService: CourseService,
        private route: ActivatedRoute,
        private router: Router) {

    }

    ngOnInit() {
        const id = +this.route.snapshot.paramMap.get('id');
        this.getCourse(id);
    }

    // Get course detail by id
    getCourse(id: number) {
        this.courseService.getCourse(id).subscribe(
        course => this.course = course,
        error  => this.errorMessage = <any>error);
    }

    onBack(): void {
        this.router.navigate(['/courses/:id']);
    }

}
1

You can use static attributes (or methods) to share data across multiple components. Below is an example:

header component:

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
   public static headerTitle: string = "hello world";
}

footer compoennt:

import { HeaderComponent } from '../header/header.component';

@Component({
  selector: 'app-footer',
  templateUrl: './footer.component.html',
  styleUrls: ['./footer.component.scss'],
})
export class FooterComponent {
   public footerText: string = HeaderComponent.headerTitle;
}

Although there is drastically reduced overhead using this method instead of creating a service, it is not recommended more complex logic. If the logic becomes more complex you may run into circular dependency warnings and coupling issues. An example of this is if the header needs data from the footer and vice versa. In the case of complex logic use a service with rxjs' BehaviorSubject. Services are meant to be self-reliant and should not rely heavily on other services or components. This will eliminate the possibility of all circular dependency warnings.

otboss
  • 621
  • 1
  • 7
  • 16
0
    //inside service
    export class LocalService {
      obj: BehaviorSubject<any>;
      constructor() {
        this.obj = new BehaviorSubject(this.obj);
      }
      loadData(selectedObj: any): void {
        this.obj.next(selectedObj);
      }
    }

    //inside component1
    const data = {
          object_id: 7444,
          name: 'Relaince'
    };
    this.localService.nextCount(data);

    //inside component2 - subscribe to obj
    this.localService.obj.subscribe(data => {
         this.selectedObj = data;
    });

   // inside component2 html
    <div class="pl-20 color-black font-18 ml-auto">
                    {{selectedObj?.name}}  
         <span><b>{{selectedObj?.object_id}}</b></span>
    </div>
0

When it comes to unrelated components, we can take advantage of the Mediator Pattern.

With the mediator pattern, communication between objects is encapsulated within a mediator object. Objects no longer communicate directly with each other, but instead communicate through the mediator.

I've seen the term unrelated components being used to describe components with or without common parent.


If there's a common parent (siblings), then the parent can be used as mediator. Let's say we have the following components' hierarchy

-Component A
--Component B
--Component C

and we want to pass data from Component B to Component C. The way this would work is to pass data from Child to Parent (Component B to Component A) and then pass data from Parent to Child (Component A to Component B).

So, in Component B using Output binding, with the @Output decorator, we emit an event (EventEmitter) and Component A receives the payload of that event. Then, Component A invokes the event handler and Component C receives the data with the @Input decorator.

Else (if there's no common parent), we can use a service. In this case, simply inject the service in the components and subscribe to its events.

Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145