Angular 4 - Pass an object from one component to another (no parent - child hierarchy)

DennisN picture DennisN · Nov 24, 2017 · Viewed 40.2k times · Source

My situation:

I have a component showing tiles with each tile representing an object from an array that is looped over with an ngfor. When a tile is clicked I want to pass the object to a different component, which is responsable for displaying all properties of that object in fields that can be modified.

What I have tried:

After doing some research and coming across multiple posts showing me how to achieve this for a parent - child hierarchy and some posts explaining that it is necessary to use a shared service in order to achieve the wanted funcitonality, I decided to try and setup such a service.

However, what I don't seem to get is when I should navigate to the different route. It seems that the navigation finds place to early as the object passed to the service is undefined when retrieving it in the detail component.

My code:

The component showing the tiles has the following function to pass the clicked object to the shared service:

editPropertyDetails(property: Property) {
    console.log('Edit property called');

    return new Promise(resolve => {
      this.sharedPropertyService.setPropertyToDisplay(property);
      resolve();
    }).then(
      () => this.router.navigate(['/properties/detail'])
    )
  }

The shared service has a function to set a property object and one to retrieve it and looks like this:

@Injectable()
export class SharedPropertyService {
  // Observable
  public propertyToDisplay = new Subject<Property>();

  constructor( private router: Router) {}

  setPropertyToDisplay(property: Property) {
    console.log('setPropertyToDisplay called');
    this.propertyToDisplay.next(property);
  }

  getPropertyToDisplay(): Observable<Property> {
    console.log('getPropertyToDisplay called');
    return this.propertyToDisplay.asObservable();
  }
}

Finally the detail component which has to receive the object that was clicked on but gets an undefined object:

export class PropertyDetailComponent implements OnDestroy {

  property: Property;
  subscription: Subscription;

  constructor(private sharedPropertyService: SharedPropertyService) {
        this.subscription = this.sharedPropertyService.getPropertyToDisplay()
          .subscribe(
            property => { this.property = property; console.log('Detail Component: ' + property.description);}
          );
  }

  ngOnDestroy() {
    // When view destroyed, clear the subscription to prevent memory leaks
    this.subscription.unsubscribe();
  }
}

Thanks in advance!

Answer

DennisN picture DennisN · Nov 24, 2017

I solved the problem by passing the id of the object of the tile that was clicked as in the navigation extras of the route and then use a service in the detail component to fetch the object based on the id passed through the route.

I will provide the code below so hopefully nobody has to go through all of this ever again.

The component showing the tiles that can be clicked in order to see the details of the object they contain:

  editPropertyDetails(property: Property) {
    console.log('Edit property called');

    let navigationExtras: NavigationExtras = {
            queryParams: {
                "property_id": property.id
            }
        };

    this.router.navigate(['/properties/detail'], navigationExtras);
  }

the detail component that receives the object that was clicked on

  private sub: any;
  propertyToDisplay: Property;

  constructor
  (
    private sharedPropertyService: SharedPropertyService,
    private router: Router,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
  this.sub = this.route.queryParams.subscribe(params => {
        let id = params["property_id"];

        if(id) {
          this.getPropertyToDisplay(id);
        }

    });
  }

  getPropertyToDisplay(id: number) {
    this.sharedPropertyService.getPropertyToDisplay(id).subscribe(
            property => {
              this.propertyToDisplay = property;
            },
            error => console.log('Something went wrong'));
  }

  // Prevent memory leaks
  ngOnDestroy() {
    this.sub.unsubscribe();
  }

The service

  properties: Property[];

  constructor( private propertyService: PropertyService) {}

  public getPropertyToDisplay(id: number): Observable<Property> {
    if (this.properties) {
      return this.findPropertyObservable(id);
    } else {
            return Observable.create((observer: Observer<Property>) => {
                this.getProperties().subscribe((properties: Property[]) => {
                    this.properties = properties;
                    const prop = this.filterProperties(id);
                    observer.next(prop);
                    observer.complete();
                })
            }).catch(this.handleError);
    }
  }

  private findPropertyObservable(id: number): Observable<Property> {
    return this.createObservable(this.filterProperties(id));
  }

  private filterProperties(id: number): Property {
        const props = this.properties.filter((prop) => prop.id == id);
        return (props.length) ? props[0] : null;
    }

  private createObservable(data: any): Observable<any> {
        return Observable.create((observer: Observer<any>) => {
            observer.next(data);
            observer.complete();
        });
    }

  private handleError(error: any) {
      console.error(error);
      return Observable.throw(error.json().error || 'Server error');
  }

  private getProperties(): Observable<Property[]> {
    if (!this.properties) {
    return this.propertyService.getProperties().map((res: Property[]) => {
      this.properties = res;
      console.log('The properties: ' + JSON.stringify(this.properties));
      return this.properties;
    })
      .catch(this.handleError);
    } else {
      return this.createObservable(this.properties);
      }
  }