1
\$\begingroup\$

I have recently come up with an idea to retrieve additional data through Pipe, and I am not sure if I created a cool solution or terrible abomination. I will try to present the problem and my solution. I will include most important parts of the code, but should something be unclear please let me know.

Angular version is 4.2.3

Problem

There is a model of a BaseModel class that I retrieve from database in a traditional way:

export class BaseModel {
    // some properties
    googlePlaceId: string = '';
}

In my application, I want to display details about the given place (the one with googlePlaceId) that I get from GooglePlacesAPI. Now, what I would normally do, I would create an additional property inside a component to which I would write a result of a call, like that:

// part of the Component
getBaseModel() {
  this.baseModelService.get()
    .subscribe(model => {
      this.baseModel = model;
      this.googlePlacesService.getDetails(model.googlePlaceId)
        .subscribe(googlePlaceDetails => {
          this.googlePlaceModel = googlePlaceDetails;
        })
    });
}

There is a problem with that solution, and it is quite ugly. There is a call inside result of another call, and I need to track another property (googlePlaceModel) in the component.

Solution

There is a LocationModel class that consists of place details:

export class LocationModel {
    placeId: string;
    latitude?: number;
    longitude?: number;
    formattedAddress?: string;
    url?: string;

    static fromGooglePlaces(object: any): LocationModel {
        const locationModel: LocationModel = {
            placeId: object.place_id,
            latitude: object.geometry.location.lat(),
            longitude: object.geometry.location.lng(),
            formattedAddress: object.formatted_address,
            url: object.url,
        };
        return locationModel;
    }
}

There is a GooglePlacesService that retrieves data from the Google API:

// part of GooglePlacesService
public locations: LocationModel[] = [];

public getLocation(id: string, subject: Subject<any>): LocationModel {
  const index = this.locations.findIndex(x => x.placeId === id);
  if (index === -1) {
    const locationModel: LocationModel = { placeId: id };
    const newIndex = this.locations.push(locationModel) - 1;

    this.getDetails(id)
      .subscribe(val => {
        this.locations[newIndex] = LocationModel.fromGooglePlaces(val);
        subject.next();
      })
    return this.locations[newIndex]
  }
  return this.locations[index];
}

Let's take a while to inspect what is happening here. The getLocation method returns details of a place with given id. First, it inspects the locations array, and if the place already is inside that array it returns it. Otherwise, it creates a LocationModel with just placeId, returns it, and calls the method that retrieves all the details (this.getDetails). It is important to notice that return this.locations[newIndex] will be executed BEFORE the code within subscribe method, so it will return almost empty model. Inside the subscribe method there is also subject.next(); which informs the caller that the object has been retrieved.

Last but not least, there is a GetLocationPipe:

// GetLocationPipe
@Pipe({
  name: 'getLocation',
  pure: false
})
export class GetLocationPipe implements PipeTransform {

  result: LocationModel;
  subject = new Subject();

  constructor(
    private googlePlacesService: GooglePlacesService, 
    private ref: ChangeDetectorRef) {
    this.subject.subscribe(() => this.ref.detectChanges())
  }

  transform(placeId: string): LocationModel {
    if (placeId == '') {
      return null;
    }
    return this.result = this.googlePlacesService.getLocation(placeId, this.subject);
  }
}

From now on it is pretty straightforward. There is the result that is retrieved from getLocation method. At the beginning, it will be almost empty as discussed earlier, but we expect to get more details from that call inside the getLocation method. Now, to know when the result is retrieved, the subject is passed inside the getLocation method. In the constructor, we subscribe to the subject and perform refresh when the subject is raised.

Theoretically, it would work without subject and our smart refresh as the pipe is NOT pure and will refresh with time. Unfortunately Angular doesn't detect the change of our result right away, and it would take something like 10 - 20 seconds to see property refresh. The subject subscription comes to help with that, by letting getLocation method inform us when the result is retrieved (it has all the details).

Usage

Finally, how it actually works in practice, here is fragment of a template:

{{(baseModel.googlePlaceId | getLocation).formattedAddress}}

It is that simple, component itself doesn't even know we are retrieving additional data. Of course, we would still have to wait for information being retrieved, I haven't done any serious tests, but there is absolutely no visible latency with this method comparing to the method presented in the "Problem" part.

Summary

I hope I have presented the problem and solution well enough. I am interested to know what to do think. Is it viable, cool and clean solution, or simply overkill and I should avoid such experiments? What are your thoughts on performance and usability?

\$\endgroup\$
  • \$\begingroup\$ I've removed the old solutions and kept the updated ones since there's only a need for one version. \$\endgroup\$ – Jamal Jun 17 '17 at 23:41
1
\$\begingroup\$

There are some problems with your implementation, first of all you're creating subscriptions without unsubscribing from them when the pipe get's destroyed or the request finishes. This could lead to some unwanted behavior.

Second you're using a class but implementing a static method and using the class only as an interface, why not creating a constructor and actually creating instances?

For the LocationModel i would go with this one:

export class GooglePlacesLocation {
    public readonly id: string;
    public readonly latitude?: number;
    public readonly longitude?: number;
    public readonly formattedAddress?: string;
    public readonly url?: string;

    constructor(placesObj: any) {
      this.id = object.place_id;
      this.latitude = object.geometry.location.lat();
      this.longitude = object.geometry.location.lng();
      this.formattedAddress = object.formatted_address;
      this.url = object.url;
    }
}

Inside your service you're using a array to cache the locations, just use a simple object, it's faster. And your the method to get the location should return an observable, to make return it inside your pipe. For the subscription just use the angular build-in async pipe.

GooglePlacesService

@Injectable()
export class GooglePlacesService {
  private locationCache: { [id: string]: GooglePlacesLocation } = {};

  public getDetails$(id: string): Observable<any> {
    // api implementation
  }

  public getLocation$(id: string): Observable<GooglePlacesLocation> {
    const cachedLocation = this.locations[id];
    if(cachedLocation) {
      return Observable.of(cachedLocation);
    } else {
      return this.getDetails(id).map(value => {
        const location = new GooglePlacesLocation(id);
        this.locations[id] = location;
        return location;
      })
    }
  }
}

And inside your pipe just return the observable:

@Pipe({
  name: 'googlePlacesLocation',
  pure: false
})
export class GooglePlacesLocationPipe implements PipeTransform {
  constructor(private googlePlacesService: GooglePlacesService) { }

  transform(id: string): Observable<GooglePlacesLocation> {
    if(id) {
      return this.googlePlacesService.getLocation$(id);
    }
    return Observable.of(null);
  }
}

Now if you're using it, you have to use it together with the async pipe.

{{ id | googlePlacesLocation | async }}

This code is currently unteste, made it up on the spot, so if you could test it and tell me if it works would be nice.

Note: The $ at the end of the method name indicates that it's returning an observable.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your response. 1. As for the LocationModel. It is being used in many places and can be retrieved from different sources. I think that in this case, static method working as a constructor is better as it clearly states from what source object is being created. 2. Using object instead of array may be better to store the data, thanks. I still can't get used to this js object like dictionary thing. 3. As for returning observable, I was trying to do this for quite some time but without success. The value was refreshing with a big delay. Maybe I will try to do it again. \$\endgroup\$ – Bielik Aug 1 '17 at 23:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.