8

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!

Dennis
  • 818
  • 2
  • 10
  • 23
  • i hard to figure out with this if you can replicate this it in stackblitz it will be easier to understand. As of now a more simpler explanation to [shared services](https://rahulrsingh09.github.io/AngularConcepts) – Rahul Singh Nov 24 '17 at 11:00
  • you need to provide the shared service in the module which containes both of your components. Is this the case? otherwise it won't be a singleton and the service would miss its purpose – marvstar Nov 24 '17 at 11:09
  • Yes, the service is listed in the providers of the feature module. – Dennis Nov 24 '17 at 11:15
  • did you find any other solution? – Robert Williams Apr 24 '18 at 11:56
  • @RobertWilliams I've posted my solution below. Take a look at the accepted answer. – Dennis Apr 24 '18 at 12:21
  • @RobertWilliams another thing you can do is just pass an id and implement a function on the backend that returns the object based on the id you pass. If that's a possibility, you won't have to write that much code on the frontend – Dennis Apr 24 '18 at 12:23
  • In my project its not viable to share anything using URL bar. I am facing the same issue which you have mentioned in your question. – Robert Williams Apr 25 '18 at 03:43
  • I am going to utilize local storage. – Robert Williams Apr 25 '18 at 03:44

6 Answers6

5

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);
      }
  }
Dennis
  • 818
  • 2
  • 10
  • 23
4

Please Try with below sample:

Step 1: Create Service [DataService]

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class DataService {
  private userIdSource = new BehaviorSubject<number>(0);
  currentUser = this.userIdSource.asObservable();

  private orderNumSource = new BehaviorSubject<number>(0);
  currentOrder = this.orderNumSource.asObservable();

  constructor() { }

  setUser(userid: number) {
    this.userIdSource.next(userid)
  }

   setOrderNumber(userid: number) {
    this.orderNumSource.next(userid)
  }
}

Step 2: Set value in login component

import { Component } from '@angular/core';
import { DataService } from "../services/data.service";

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'] 
})
export class LoginComponent {
  constructor( private dataService:DataService) {     }
   onSubmit() {
        this.dataService.setUser(1); 
  } 
}

Step 3 : Get value in another component

import { Component, OnInit } from '@angular/core';
import { DataService } from "../services/data.service";

@Component({
  selector: 'app-shopping-cart',
  templateUrl: './shopping-cart.component.html',
  styleUrls: ['./shopping-cart.component.css']
})
export class ShoppingCartComponent implements OnInit {
  userId: number = 0;
  constructor(private dataService: DataService) { }
  ngOnInit() {
    this.getUser();
 }
  getUser() {
    this.dataService.currentUser.subscribe(user => {
      this.userId = user
    }, err => {
      console.log(err);
    });
  }
 }

Note: On page refresh the value will lost.

sanjay kumar
  • 838
  • 6
  • 10
  • I'm trying to do exactly what you wrote, but I can't get it to work. My question is, do both components have to be instantiated at the same time for this to work? Because in my case, I want the first component to set the data while the other component is not yet instantiated and I wonder if this is the reason why I get only the initial value from the service. – AsGoodAsItGets Oct 18 '18 at 13:50
  • For data service to work, the component from where you are fetching must be instantiated first with setter before the other component fetches – vgnsh May 27 '20 at 13:03
4

Using service first you create one funtion on service. call that funtion and use subject to other component. write this code

 this.handleReq.handlefilterdata.subscribe(() => {
                this.ngDoCheck(); 
            });

here, handleReq is service. handlefilterdata is rxjs subject.

vishal dobariya
  • 189
  • 1
  • 5
  • What is RXJS ?, Can you please let us know. Thanks! – Affan Pathan Aug 09 '19 at 09:14
  • 1
    Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using observables that makes it easier to compose asynchronous or callback-based code – vishal dobariya Aug 09 '19 at 09:15
  • Wow, Just saved lot of lines ! – Zuber Surya Aug 09 '19 at 09:39
0

try like this :

try to subscribe this.sharedPropertyService.propertyToDisplay instead of this.sharedPropertyService.getPropertyToDisplay()

this.sharedPropertyService.propertyToDisplay.subscribe((property) => {
    this.property = property;
    console.log('Detail Component: ' + property.description);
});

and send the object like below :

editPropertyDetails(property: Property) {
    this.sharedPropertyService.setPropertyToDisplay(property);
}
Chandru
  • 10,864
  • 6
  • 38
  • 53
0

What does your console output? Is this.property ever set on the child component?

I would try to get rid of this function:

getPropertyToDisplay(): Observable<Property>

And try to just access propertyToDisplay directly.

Also .navigate can take data as a second param, so you might try passing the data in the route change.

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

  ngOnInit() {
    this.property = this.route
      .variableYouPassedIntoNavigator
double-beep
  • 5,031
  • 17
  • 33
  • 41
JBoothUA
  • 3,040
  • 3
  • 17
  • 46
  • How would I pass the property in the navigate function? Tried to look at the docs about NavigationExtras but couldn't figure it out. editPropertyDetails(property: Property) { this.router.navigate(['/properties/detail'], property); } – Dennis Nov 24 '17 at 11:36
  • the docs should have all the examples, also is it possible that the ngUnsubsribe is getting called? – JBoothUA Nov 24 '17 at 23:16
  • Unfortunately, passing complicated objects isn't possible through the navigate method. I posted my solution below. – Dennis Nov 27 '17 at 08:31
0

I was working on similar functionality and came across same issue(as undefined). You could initialize like this.

public propertyToDisplay = new BehaviorSubject<Property>(undefined);

After making a change like this. I am able to get the value from Observable in service file and as well as in the component where I am trying to use this service.

Obsidian
  • 3,719
  • 8
  • 17
  • 30