Reputation: 145
I'm developing a web app where multiple components share similar functions. In order to avoid code duplication in the different components, I created a service to host these functions.
However, I'm unsure how to manage the shared functions' subscriptions since they are not part of each component anymore and still require to be subscribed to in the service. Ideally I wouldn't be subscribing at all in the service and only subscribe in the component, but the nature of the function and its purpose doesn't allow me to do so. These are AngularFire functions nested one in another. The purpose of this structure is to navigate through the different levels of a DB and dynamically populate an object. This object is then used to generate the headers section of a master table. The number of levels (columns and stacked rows) depends on the data that is present in the DB depending on the DB node that is being queried. This is not my favorite way of handling this so if you think of a cleaner way to do it, by all means. Keep in mind that the actual code is somewhat more complex than the example below.
For example, the following object is generated by this code and its corresponding header section is the one displayed in the following image :
object = {
title: 'PW100',
sections: {
section1: {
title: 'HIGH PRESSURE ROTOR',
sections: {
section1: {
title: 'HP TURBINE',
sections : {
section1: {title: 'ASS'},
section2: {title: 'GRD'},
section3: {title: 'BAL'}
}
},
section2: {
title: 'HP ROTOR',
sections: {
section1: {title: 'ASS'},
section2: {title: 'BAL'}
}
}
}
},
section2: {
title: 'LOW PRESSURE ROTOR',
sections: {
section1: {
title: 'LP TURBINE',
sections : {
section1: {title: 'ASS'},
section2: {title: 'RIV'},
section3: {title: 'GRD'},
section4: {title: 'BAL'}
}
},
section2: {
title: 'LP ROTOR',
sections: {
section1: {title: 'ASS'},
section2: {title: 'BAL'}
}
}
}
},
section3: {
title: 'POWER TURBINE PACK',
sections: {
section1: {
title: '3RD STAGE TURBINE',
sections : {
section1: {title: 'ASS'},
section2: {title: 'RIV'},
section3: {title: 'GRD'},
section4: {title: 'BAL'}
}
},
section2: {
title: '4TH STAGE TURBINE',
sections : {
section1: {title: 'ASS'},
section2: {title: 'RIV'},
section3: {title: 'GRD'},
section4: {title: 'BAL'}
}
},
section3: {
title: 'PT PACK',
sections: {
section1: {title: 'ASS'},
section2: {title: 'BAL'}
}
}
}
}
}
};
Components A and B used to have the following structure :
export class ComponentXXClass implements OnInit, OnDestroy {
// Variable used to unsubscribe in ngOnDestroy()
$unsubscribe = new Subject();
constructor(private afs: AngularFirestore) {}
ngOnInit(): void {
this.functionA().then(result => {
// Use promise's result (which is an object) to generate the table's headers section
});
}
ngOnDestroy(): void {
this.$unsubscribe.next();
this.$unsubscribe.complete();
}
functionA() {
const objectToPopulateOnEachLevel = {};
return new Promise((resolve) => {
this.afs.collection('collectionName')
.snapshotChanges()
.pipe(takeUntil(this.$unsubscribe))
.subscribe(level1VariableRequiredToAccessLevel2 => {
level1VariableRequiredToAccessLevel2.forEach(level1Var => {
// Store relevant information found at this level of the query
objectToPopulateOnEachLevel['level1'] = level1Var.payload.doc.data()['variableOfInterest'];
this.afs.collection(level1Var.payload.doc.ref.path + '/path1')
.snapshotChanges()
.pipe(takeUntil(this.$unsubscribe))
.subscribe(level2Variable => {
level2Variable.forEach(level2Var => {
// Store relevant information found at this level of the query
objectToPopulateOnEachLevel['level2'] = level2Var.payload.doc.data()['variableOfInterest'];
});
resolve(objectToPopulateOnEachLevel);
});
});
});
});
}
}
Service :
export class Service {
// Variable used to unsubscribe in components' ngOnDestroy()
$unsubscribe = new Subject();
constructor(private afs: AngularFirestore) {}
functionA(parentPath: string, childPath: string) {
const objectToPopulateOnEachLevel = {};
return new Promise((resolve) => {
this.afs.collection(parentPath)
.snapshotChanges()
.pipe(takeUntil(this.$unsubscribe))
.subscribe(level1VariableRequiredToAccessLevel2 => {
level1VariableRequiredToAccessLevel2.forEach(level1Var => {
// Store relevant information found at this level of the query
objectToPopulateOnEachLevel['level1'] = level1Var.payload.doc.data()['variableOfInterest'];
this.afs.collection(level1Var.payload.doc.ref.path + '/' + childPath)
.snapshotChanges()
.pipe(takeUntil(this.$unsubscribe))
.subscribe(level2Variable => {
level2Variable.forEach(level2Var => {
// Store relevant information found at this level of the query
objectToPopulateOnEachLevel['level2'] = level2Var.payload.doc.data()['variableOfInterest'];
});
resolve(objectToPopulateOnEachLevel);
});
});
});
});
}
}
Components A and B :
export class ComponentXXClass implements OnInit, OnDestroy {
constructor(private _service: Service) {}
ngOnInit(): void {
this._service.functionA('parentPathString', 'childPathString').then(result => {
// Use promise's result (which is an object) to generate the table's headers section
});
}
ngOnDestroy(): void {
this._service.$unsubscribe.next();
this._service.$unsubscribe.complete();
}
}
This works, but not entirely. I mean the data gets displayed correctly, but it seems as though eventhough I have not exited component A (for example), the subscription doesn't seem to be alive. While in component A, I tried to update a variable in the DB at the node that is being subscribed to in the service. The expected behaviour is to have that subscription fire and emit a new value to component A. For example, in the example above (object and its corresponding headers displayed in the image), if I go in the DB and change "HIGH PRESSURE ROTOR" to "NO PRESSURE ROTOR", I expect the subscription to fire in the service, fetch that change by re-creating the object in the promise, and re-emitting it to component A who will then re-render the table headers. However, this doesn't happen. I also tried to remove the "unsubscription" portion to see if the way I handled the unsubscription was the cause. The result was the same.
Any thoughts?
Thanks
Upvotes: 0
Views: 325
Reputation: 186
You have to use observables to manage the state of the data readonly objectPopulatedSubject = new BehaviorSubject({})
also you have to create a property to access to the observable readonly objectPopulated$ = objectPopulatedSubject.asObservable()
and also you have to create a method to update the subject and emit a new value for your subscriptions updateObjectPopulated(data: YourType) { this.objectPopulatedSubject.next(data)}
and in your components you should subscribe to objectPopulated$.
Also to manage the subscription you should to follow the next instructions:
Create a subscription property objectPopulatedSubscription: Subscription;
then in your ngOnInit this.objectPopulatedSubscription = this.yourservice.objectPopulated$.subscribe(data => {//put here your logic})
and also in your ngOnDestroy put this.objectPopulatedSubscription.unsubscribe()
Upvotes: 0