Please, I am having issues working with some async data on angular which comes from my API. I’ve spent some time trying to figure out how to scale through, but I still get stuck.
Scenario
When on edit mode of a patient form, I need to call my centre service to get all available centres from db. When the data is returned, I need to process the data to check which centres a patient belong to, then use this on the html. But I see that the component renders before data is received. This is because, when I click save button to check the data, I see the data there. But in the method where I need to write some logic, when I try to inspect the data returned from the API, it remains undefined.
NB: I can’t use a resolver in this case because, I’m not using a router link to navigate to the page.
I’ve tried to use an async pipe to conditionally check and render the html only if I receive the data which was one solution that worked for someone else. But this seem not to work in my case as i still get undefined on the variable which is inside a method, and where I need to process the data returned before showing my component/html.
Goal
The goal is to first get all centres first before initializing the reactive form, so that i can handle the data on the getPatientCentres() method. I intend to use the data gotten from the API to pre-populate an array when creating the form.
Done other steps and research but the solution doesn’t seem to solve my case.
Any help or logic on how to proceed would be highly appreciated.
Here is my TS code
export class Patient2Component implements OnInit {
formTitle: string;
patientForm: FormGroup;
centreList: ICentre[] = [];
loadedData: boolean = false;
patient: IPatient;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private fb: FormBuilder,
private centreService: CentreService,
) { }
ngOnInit() {
this.getCentres();
this.initCentreForm();
this.checkParamsForEditAction();
}
initCentreForm() {
this.patientForm = this.fb.group({
id: [null],
firstName: ['', Validators.required],
lastName: ['', Validators.required],
centres: [this.centreList]
});
}
getCentres() {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
});
}
checkParamsForEditAction() {
this.activatedRoute.data.subscribe(data => {
this.patient = data['patient'];
if (this.patient) {
this.formTitle = 'Edit Patient';
this.getPatientCentres(this.patient);
this.assignValuesToControl(this.patient);
}
});
}
assignValuesToControl(patient: IPatient) {
this.patientForm.patchValue({
id: patient.id,
firstName: patient.firstName || '',
lastName: patient.lastName || '',
});
}
getPatientCentres(patient: IPatient) {
const patientCentres = patient.patientCentres;
/**Here, the centreList is undefined since data has not returned yet
* And i need this data for processing.
*/
console.log(this.centreList);
}
save() {
/**Here, i can see the data */
console.log(this.centreList);
}
Try this
in ngOnInit
ngOnInit() {
this.initCentreForm();
this.getCentres(this.checkParamsForEditAction);
}
getCenters Method
getCentres(callBack?) {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
if(callBack) {
callBack();
}
});
}
you can also use forkJoin, or async await
getCentres(callBack?) {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
//Now call your function directly
this.checkParamsForEditAction();
});
}
Call your function after the get centers is loaded
Order of calling
this.initCentreForm();
this.getCentres();
Related
In my next.js app i'm using the swr hook since it provides cache and real time updates, and that is quite great for my project ( facebook clone ), but, there's a problem.
The problem, is that, in my publications, i fetch them along with getStaticProps, and i just map the array and everything great, but, when i do an action, like, liking a post, or commenting a post, i mutate the cache, and i thought that what that does, is to ask the server if the information that is in the cache is right.
But, what it really does, is that, it makes another API call, and, the problem with that, is that, if i like a publication, after the call to the API to make sure everything is right in the cache, if there are 30 new publications, they will appear in the screen, and i don't want that, i want the pubs the user on screen requested at the beggining, imagine comenting on a post, then there are 50 new post so you lost the post where you commented...
Let me show you a litle bit of my code.
First, let me show you my posts interfaces
// Publication representation
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm making the call to get all posts
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<PublicationsHomeHero>
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
And, for example, this is how i create a comment, i update cache, make the call to the API, then mutate to see if data is right
// Create comment
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
try {
mutate(
`${process.env.URL}/api/publication`,
(allPubs: Ipublication[]) => {
const currentPub = allPubs.find(f => f === publication);
const updatePub = allPubs.map(pub =>
pub._id === currentPub._id
? {
...currentPub,
comments: [
{
body: commentBody,
createdAt: new Date().toISOString(),
identifier: userAuth.user.id,
name: userAuth.user.name
},
...currentPub.comments
]
}
: pub
);
return updatePub;
},
false
);
await createComment(
{ identifier: userAuth.user.id, body: commentBody },
publication._id
);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
mutate(`${process.env.URL}/api/publication`);
}
};
Now, after creating the comment, as i already mentioned, it makes another call to the API, and if there are new posts or whatever, it will appear in the screen, and i just want to keep the posts i have or add new ones if i'm the one that created them.
So, let's say that i will like a post
Everything is great and fast, but, after making sure data is right, another post will appear because another user created it
Is there a way to make sure data is right without making another call to the API that will add more posts to the screen ?
I'm new to this swr hook, so, hope you can help me and thanks for your time !
Upate
There's a way to update cache without needing to refetch
many POST APIs will just return the updated data directly, so we don’t need to revalidate again. Here’s an example showing the “local mutate - request - update” usage:
mutate('/api/user', newUser, false) // use `false` to mutate without revalidation
mutate('/api/user', updateUser(newUser)) // `updateUser` is a Promise of the request,
// which returns the updated document
But, i don't kwow how should i change my code to implement this, any ideas !?
If you want to update the cache, and make sure everything is right, withouth having to make another call to the API, this seems like working
Change this
await createComment(
{ identifier: userAuth.user.id, body: commentBody },
publication._id
);
mutate(`${process.env.URL}/api/publication`);
For this
mutate(
`${process.env.URL}/api/publication`,
async (allPublications: Ipublication[]) => {
const { data } = await like(
{ identifier: userAuth.user.id },
publication._id
);
const updatePub = allPublications.map(pub =>
pub._id === data._id ? { ...data, likes: data.likes } : pub
);
return updatePub;
},
false
);
What you are doing there, is to update cache with the data that you'll receive data from the API's action, and you put it in the cache, but, you have to put false as well so it doesn't revalidate again, i tried it out and it's working, don't know if i'll have problems with it in the future, but for knowm it works great !
I'm trying to write a website with angular and firebase.
The site works so that different users can be created (I added auth.service.ts)
And any registered user will be able to add a contact to the firestore.
The problem: I'm unable to get the added contact information to appear on the screen.
The error I keep getting in the console is:
core.js:6241 ERROR FirebaseError: Function CollectionReference.doc() cannot be called with an empty path.
my firestore design:
collection name "users"-> containing document for each user->each user has "contacts" collection-> each contact containing the email,name,phone contacts attributes.
The following code is from the crud.service.ts file:
export class CrudService {
constructor(private authservice: AuthService, public fireservices:AngularFirestore) { }
//create_NewContact adds to the 'contacts' collection in firebase, the contact that the user entered as input
create_NewContact(Record)
{
return this.fireservices.collection('users').doc(this.authservice.currentUserId).collection('contacts').add(Record);
}
//get_AllContacts gets the 'contacts' collection from firebase
get_AllContacts()
{
return this.fireservices.collection('users').doc(this.authservice.currentUserId).collection('contacts').snapshotChanges();
}
.
.
.
}
The following code is from the contact-list.component.ts file:
export class ContactListComponent implements OnInit {
contact: any;
contactName: string;
contactEmail: string;
contactPhone: string;
message:string;
constructor(private authservice: AuthService,public crudservice:CrudService) { }
ngOnInit() {
this.crudservice.get_AllContacts().subscribe(data => {
this.contact = data.map(c => {
return {
id: c.payload.doc.id,
isEdit: false,
name: c.payload.doc.data()['name'],
email: c.payload.doc.data()['email'],
phone: c.payload.doc.data()['phone'],
};
})
console.log(this.contact);
});
}
/*CreateRecord() will fire after the user press the "Create Contact" btn*/
CreateRecord()
{
//The function stores within the relevant fields in "Record" variable, the user's input
let Record = {};
Record['name'] = this.contactName;
Record['email'] = this.contactEmail;
Record['phone'] = this.contactPhone;
//create_NewContact is defined in crud.service.ts file
this.crudservice.create_NewContact(Record).then(res => {
this.contactName = "";
this.contactEmail = "";
this.contactPhone = "";
console.log(res);
this.message = "Contact data save done";
}).catch(error => {
console.log(error);
})
}
.
.
.
}
Do you have ideas how to fix this problem? Thank you!
The error message indicates that the value passed to the doc() method is an empty string.
The problem most probably comes from the fact that, at the moment you call this method, this.authservice.currentUserId is null. currentUserId is probably null because the authservice object has not finished initializing: see the doc for more details.
You should either use the onAuthStateChanged observer or check that this.authservice.currentUserId is not null.
I have a function to get rates from products, so lets say I have one product with two rates. So my product has two rates. Then, when I get those rates I must get the prices attached to my product. So for each rate I have to look for its prices.
The next code below explains this:
this.loadProductInfo = true; // bool to load data in my form
// First of all, I get rates from API
// const rates = this._http....
// Now, for each rate I must search If my product/products have a price:
this.rates.forEach((rate, index, arr) => {
this._glbGetPricesForProduct.getPrice(params).subscribe(response => {
if (!arr[index + 1]) {
this.initForm();
this.loadProductInfo = false;
}
})
});
The variable loadProductInfo it loads content in my form, so in my html I have:
<form *ngIf="!loadProductInfo"></form>
But form it still give me error: could not find control name.
But if I do this instead, it works correctlly:
setTimeout(() => {
this.initForm();
this.loadProductInfo = false;
}, 2000);
So what I want its to say my form to wait until I have all code loaded and then after it, load its contents. But instead it cant find the control because it loads before code. Any help I really appreciate it.
The main mistake I see there is that you are looping over async data which may not be there when your code execute the for each loop (your rates).
I would build an observable with your rates as a source:
...
$rates: Observable<any> = this._http.get(...);
rates.pipe(
mergeMap((rates) => {
const priceByRates: Observable<any>[] = rates.map((rate, index, arr) => this._glbGetPricesForProduct.getPrice(params));
return combineLatest(pricesByRates); // if getPrice complete right away, use forkJoin() instead
})
).subscribe(res => {
// No need to check for the last item, all rates have been checked for possible price
this.initForm();
this.loadProductInfo = false;
});
...
This implementation should wait for your api calls to resolve before printing your form.
Since you are hiding the entire form, it may be better to just move the API call into a resolver so that the page does not render until the data is ready.
Here is a minimal StackBlitz showcasing this behavior: https://stackblitz.com/edit/angular-ivy-4beuww
Component
In your component, include an ActivatedRoute parameter via DI.
#Component(/*omitted for brevity*/)
export class MyComponent {
constructor(private route: ActivatedRoute) {
// note: 'data' is whatever you label your resolver prop in your routing setup
route.data.subscribe(resolved => {
if ("data" in resolved) this.resolveData = resolved["data"];
});
}
}
Route Setup
And in your router setup you would have the following:
const routes: Routes = [
{
path: 'my-route-path',
component: MyComponent,
resolve: {
data: MyResolver
}
}
];
Resolver
Finally, your resolver would make your API call utilizing your service:
#Injectable({providedIn: 'root'})
export class MyResolver() implements Resolve<T> {
constructor(private service: MyService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | any {
return this.service.myRequest();
}
}
The final result will be that your view will not be rendered until your data is ready.
Currently, I have a modal material dialog window that asks the user to input a number and then hit search. On search, it fetches data from api call and gets back a response object. I want to use the response object to populate a new page (edit form).
My question is, how can I past the data, particularly the number the user entered on the material dialog component to another component, so that it can fetch the api call results or how can I pass my response object to my edit from from dialog?
E.g.
Here's my search function:
search(searchNumber) {
if (this.selectedOption === 'Bill Number') {
this._transactionService.findExistingTransactionByBillNumber('U001', searchNumber)
.subscribe(data => this.transactionResponse = data);
console.log(JSON.stringify(this.transactionResponse));
this.router.navigate(['/edit-transaction-portal']);
} else {
this._transactionService.findExistingTransactionByTransactionNumber('U001', searchNumber)
.subscribe(data => this.transactionResponse = data);
console.log(JSON.stringify(this.transactionResponse));
this.router.navigate(['/edit-transaction-portal']);
}
}
I want to be able to either 1) pass the response object I get here or pass the searchNumber the user entered, so that I can do a lookup within my edit form component. I need to pass in either one from this component to my new component that I navigate to.
EDIT: Accepted solution shows how to add query params to this.router.navigate() and how to retrieve it by subscribing to activateRoute, a different approach than the one identified in the other SO post.
You can pass the number (bill/transaction)
this.router.navigate(['/edit-transaction-portal'], { queryParams: { bill: 'U001' } });
this.router.navigate(['/edit-transaction-portal'], { queryParams: { transaction: 'U001' } });
then in your component(edit-transaction-portal) hit the api to get the data. In component you should include ActivatedRoute in constructor. It will be something like:
isBill: boolean;
isTransaction: boolean;
number: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.sub = this.route
.queryParams
.subscribe(params => {
this.isBill = params['bill'] != undefined;
this.isTransaction = params['transaction'] != undefined;
this.number = this.isBill ? params['bill'] : params['transaction'];
// Call API here
});
}
My question is, how can I past the data, particularly the number the
user entered on the material dialog component to another component
You can pass it throw material dialog component. Inject dialogRef to you component which opened in the dialog:
constructor(
public dialogRef: MatDialogRef<SomeComponent>,
#Inject(MAT_DIALOG_DATA) public data: any,
) { }
After the submitting data, you can pass any data to component which opened this dialog, by closing the dialog:
onSubmit() {
this.service.postProduct(this.contract, this.product)
.subscribe(resp => {
this.dialogRef.close(resp);
});
}
And in your Parent component, who opened this dialog can get this passed data by subscribing to afterClosed() observable:
Parent.component.ts:
openDialog(id) {
const dialogRef = this.dialog.open(SomeComponent, {
data: { id: anyData}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// do something...
}
});
}
Would I pass the data object in dialog.open()? How would I retrieve it
from there?
Look at openDialog() above. It has data property, that you can send to dialog components. And in the opened component inject MAT_DIALOG_DATA as this:
#Inject(MAT_DIALOG_DATA) public data: any,
to access passed data object as shown code above
Official docs[sharing-data-with-the-dialog-component]
if you want to pass data which the help of routing you have to define route which takes value as part of rout like as below
const appRoutes: Routes = [
{ path: 'hero/:id', component: HeroDetailComponent },];
it will from code side
gotoHeroes(hero: Hero) {
let heroId = hero ? hero.id : null;
// Pass along the hero id if available
// so that the HeroList component can select that hero.
// Include a junk 'foo' property for fun.
this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}
Read : https://angular.io/guide/router#router-imports
If you want to pass data between two component then there is #Input and #Output property concept in angular which allows you to pass data between components.
#Input() - this type of property allows you to pass data from parent to child component.
Output() - this type of property allows you to pass data from child to parent component.
Other way to do it is make use of Service as use the same instance of service between component.
Read : 3 ways to communicate between Angular components
I'm stuck trying to figure out how to write a flux store and action that works in just fetching data from my express API using altjs
import $ from 'jquery';
const utils = {
myProfile: () => {
return $.ajax({
url: '/myProfile',
type: 'GET'
});
}
};
This is how I believe I should write my GET request for just grabbing a user's profile (which should return a json with user info).
then for my store :
import UserActions from 'actions/UserActions';
import alt from 'altInstance';
class UserStore {
constructor() {
this.userProfile = [];
this.on('init', this.bootstrap);
this.on('bootstrap', this.bootstrap);
this.bindListeners({
fetchUserProfile: UserActions.FETCHUSERPROFILE,
});
}
fetchUserProfile(profile) {
this.userProfile = profile;
}
}
export default alt.createStore(UserStore, 'UserStore');
However the action is where i'm the most clueless
import alt from 'altInstance';
import UserWebAPIUtils from 'utils/UserWebAPIUtils';
fetchProfile(){
this.dispatch();
UserWebAPIUtils.getProfile()
//what do we do with it to let our store know we have the data?
});
}
}
}
All im trying to do, is grab data from the server, tell my store we've recieved the data and fill the userprofile array with the data from our api, and the messenger for telling our store is through a dispatcher which belongs to 'actions' correct? I've looked at a lot of tutorials but I still dont feel very confident on how I am thinking about this. What if I wanted to update data through a POST request what would that be like?
Looking through altjs doc it seems like they recommend doing the async operations from actions. I prefer this approach as well because it keeps stores synchronous and easy to understand. Based on their example
LocationAction
LocationsFetcher.fetch()
.then((locations) => {
// we can access other actions within our action through `this.actions`
this.actions.updateLocations(locations);
})
.catch((errorMessage) => {
this.actions.locationsFailed(errorMessage);
});
Basically they are fetching the information and then triggering 2 actions depending on the result of the request which the store is listening on to.
LocationStore
this.bindListeners({
handleUpdateLocations: LocationActions.UPDATE_LOCATIONS,
handleFetchLocations: LocationActions.FETCH_LOCATIONS,
handleLocationsFailed: LocationActions.LOCATIONS_FAILED
});
When the store receives a handleUpdateLocations action which happens when the fetcher returns successfully. The store will update itself with new data and dispatch
handleUpdateLocations(locations) {
this.locations = locations;
this.errorMessage = null;
}
With your code you can do something similar. The fetch user profile will be triggered when data is originally requested. Here I am setting user profile to be [] which is your original init value but you can set it to anything to indicate data is being loaded. I then added 2 more methods, handleFetchUserProfileComplete and handleFetchUserProfileError which get called depending on if your fetch was successful or not. The code below is a rough idea of what you should have.
constructor() {
this.userProfile = [];
this.on('init', this.bootstrap);
this.on('bootstrap', this.bootstrap);
this.bindListeners({
handleFetchUserProfile: UserActions.FETCH_USER_PROFILE,
handleFetchUserProfileComplete: UserActions.FETCH_USER_PROFILE_COMPLETE,
handleFetchUserProfileError: UserActions.FETCH_USER_PROFILE_ERROR,
});
}
fetchUserProfile() {
this.userProfile = [];
}
handleFetchUserProfileComplete(profile) {
this.userProfile = profile;
}
handleFetchUserProfileError(error) {
this.error= error;
}
export default alt.createStore(UserStore, 'UserStore');
The only thing left is to trigger these 2 actions depending on the result of your fetch request in your action code
fetchUserProfile(){
this.dispatch();
UserWebAPIUtils.getProfile().then((data) => {
//what do we do with it to let our store know we have the data?
this.actions.fetchUserProfileComplete(data)
})
.catch((errorMessage) => {
this.actions.locationsFailed(errorMessage);
});
}
fetchUserProfileComplete(profile) {
this.dispatch(profile);
}
fetchUserProfileError(error) {
this.dispatch(error);
}