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?