New to Angular/Apollo/TS and this is driving me nuts, so any help is appreciated.
I am working on setting up a small app with Angular 10, Apollo, and a GraphQL API. I recently built the same thing in Vue and thought recreating the project would be a good way to pick up some Angular.
My connection to the API is working, as is my query, but I can't figure out how to map the results to an array so I can access them in my component. Using console.log inside the subscription shows the correct data is returned. console.log outside of the query on 'this' shows the query results, however they are never saved/mapped to the variable they should be set to.
Here's the code for my service:
import { Injectable } from '#angular/core';
import { Apollo } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import gql from 'graphql-tag';
const USER_SEARCH = gql`
query getUsers {
search(query: "moose", type: USER, first: 10) {
nodes {
... on User {
login
email
location
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
userCount
}
}`;
export class UserService {
loading: boolean = true;
users: [];
constructor(private apollo: Apollo) { }
getUsers(): any {
this.apollo.watchQuery<any>({
query: USER_SEARCH
})
.valueChanges
.subscribe(({ data, loading }) => {
this.loading = loading;
this.users = data.search;
});
console.log(this);
return this.users;
}
}
I can call the getUsers() function from my component, and 'this' has the service listed, and inside of it 'users' has my query results listed in it. However, console.log for this.users in the service or the component returns undefined.
I've tried about every type of example I could find, including the query examples from the apollo docs, and the example of using apollo with angular from hasura.io. Tried using a pipe and map, pluck, just valueChanges, a few different subscribes, setting a variable inside the function to assign the data value to, setting the query to variable, setting the query in ngOnInit in the component, and a few other things I'm sure I'm forgetting. Nothing seems to work. I looked into using a callback to wait for the query to return before setting the value, but my understanding is that I shouldn't have to do anything like that. I'm sure it's something dumb I'm missing or don't know about with Apollo or Angular, but I'm just not positive what it is I'm missing.
Any ideas?
this.getUsers = this.getUsers.bind(this);
within a constructor?
using setTimeout is not an ideal solution, you can directly update your component variable in subscribe callback function and do whatever you want to do with it in your template. Look at my example
getItems() {
this.apollo
.watchQuery({
query: this.getItemsQuery,
})
.valueChanges.subscribe((result: any) => {
this.items = result?.data?.items;
});
}
and in template
<mat-option *ngFor="let item of items" [value]="item.price">
{{ item.name }}
</mat-option>
Maybe not the ideal solution, so I'm still open to trying other things, but I was able to get the value set in my component by using a promise with a timer in the service, then an async await in the component.
Service
getUsers(): any {
return new Promise((resolve, reject) => {
let me = this;
this.apollo.watchQuery<any>({
query: USER_SEARCH
})
.valueChanges
.subscribe(({ data, loading }) => {
this.loading = loading;
this.users = data.search;
});
setTimeout( function() {
if(me.users !== 'undefined'){
resolve(me.users)
}
}, 1000)
})
}
Component
async getUsers(): Promise<any> {
this.users = await this.userService.getUsers();
console.log(this.users);
}
This allows this.users to be set from the service. As far as I can tell, Apollo is still running the query when Angular starts setting values, resulting in the value originally being shown as undefined, but my service having values from the query in the console. Not sure if there's a better way with Apollo or Angular to resolve this issue, but if so I'd love to hear about it.
Thanks!
Related
I am trying to set my state to the data I'm getting from my API with a GETTER in the store.
during the mounted() lifecyclehook trigger the GETTER getProducts() which looks like this:
export const getters = {
async getProducts() {
axios.get('/api/products')
.then(res => {
var data = res.data
commit('setProducts', data)
})
.catch(err => console.log(err));
}
}
In the GETTER I try to trigger a MUTATION called setProducts() which looks like this:
export const mutations = {
setProducts(state, data) {
state.products = data
}
}
But when I run this I get the error ReferenceError: commit is not defined in my console.
So obviously what goes wrong is triggering the MUTATION but after looking for 2 days straight on the internet I still couldn't find anything.
I also tried replacing commit('setProducts', data) with:
this.setProducts(data)
setProducts(data)
Which all ended with the error "TypeError: Cannot read properties of undefined (reading 'setProducts')"
If your function getProduct is defined in a Vue component, you have to access the store like this :
this.$store.commit('setProducts', data)
If your function is not defined in a Vue component but in an external javascript file, you must first import your store
import store from './fileWhereIsYourStore.js'
store.commit('setProducts', data)
If your getters export is literally the definition of your store's getters, you can use the solution of importing the store first, but you should know that it is clearly not a good practice to make commits in getters. There must be a better solution to your problem.
EDIT : To answer your comment, here's how you could do it:
// Your store module
export default {
state: {
products: []
},
mutations: {
SET_PRODUCTS(state, data) {
state.products = data
}
},
actions: {
async fetchProducts(store) {
await axios.get('/api/products')
.then(res => {
var data = res.data
store.commit('SET_PRODUCTS', data)
})
.catch(err => console.log(err));
}
}
}
Now, you can fetch products and populate your store in each of your components like this :
// A random Vue Component
<template>
</template>
<script>
export default {
async mounted() {
await this.$store.dispatch('fetchProducts')
// now you can access your products like this
console.log(this.$store.state.products)
}
}
</script>
I didn't tested this code but it should be ok.
Only actions do have commit in their context as you can see here.
Getters don't have commit.
Otherwise, you could also use mapActions (aka import { mapActions } from 'vuex'), rather than this.$store.dispatch (just a matter of style, no real difference at the end).
Refactoring your code to have an action as Julien suggested is a good solution because this is how you should be using Vuex.
Getters are usually used to have some state having a specific structure, like sorted alphabetically or alike. For common state access, use the regular state or the mapState helper.
Check this out:
import accountModule from '#/store/modules/account/account';
import otherModule from '#/store/modules/other/other';
export default new Vuex.Store({
modules: {
account: accountModule,
other: otherModule,
}
});
The data initialization in other depends on the account module because the account module has user specific settings. Suppose other.state.list depends on account.state.settings.listOrder. However, I want the data for the account module to come from the server. Which is async. So when other is trying to get set up, it can't just try to reference account.state.settings.listOrder because the response from the server may not have come back yet.
I tried exporting a promise in accountModule that resolves with the module itself. But that approach doesn't seem to work.
import accountModulePromise from '#/store/modules/account/account';
accountModulePromise.then(function (accountMoudle) {
import otherModule from '#/store/modules/other/other';
...
});
This gives me an error saying that import statements need to be top level.
The following doesn't work either:
let accountModule = await import '#/store/modules/account/account';
import otherModule from '#/store/modules/other/other';
...
It gives me an error saying that await is a reserved word. I'm confused though, because https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import says that I should be able to do it.
Your last code block didn't work because of await have to be inside async function.
Remember, the await keyword is only valid inside async functions. If
you use it outside of an async function's body, you will get a
SyntaxError.
From MDN.
You can use Dynamic Module Registration:
accountModulePromise.then(async () => {
let otherModule = await import('#/store/modules/other/other');
store.registerModule('other', otherModule.default);
});
But when you want to get state or dispatch actions you have to check whether module is registered which is pretty bad.
In my opinion it would be better if you redesign your module structure to decoupling each other. Try to move your initialize code to main.js or App.vue then dispatch actions to update module states from that.
Updates
From your last update, Another idea to decoupling your store, I think you should store your list without order and sort it only when you use. You can do this with:
Computed property:
...
computed: {
list () {
let list = this.$store.state.other.list
let order = this.$store.state.account.settings.listOrder
if (!list || !order) return []
return someSort(list, order)
}
},
beforeCreate () {
this.$store.dispatch('other/fetchList')
this.$store.dispatch('account/fetchListOrder')
}
...
Or Vuex getters:
...
getters: {
list: (state) => (order) => {
return someSort(state.list, order)
}
}
...
...
computed: {
list () {
let order = this.$store.state.account.settings.listOrder
return this.$store.getters['others/list'](order)
}
}
...
Okay, so you have two modules. One with state that is fetched from the server, the other with state that is dependent on the first, correct?
I would suggest the following approach:
Set up your modules with empty 'state' to begin with. Then create an action within accountModule to set up the state from the server. Use a getter on other to order the list. Finally, dispatch your action upon app creation.
const account = {
namespaced: true,
state: {
listOrder: ''
},
mutations: {
setListOrder (state, newListOrder) {
state.listOrder = newListOrder
}
},
actions: {
async fetchServerState (ctx) {
let result = await fetch("/path/to/server")
ctx.commit('setListOrder', result.listOrder)
// or whatever your response is, this is an example
}
}
}
const other = {
namespaced: true,
state: {
unorderedList: []
},
getters: {
list (state, getters, rootState) {
return someSort(state.unorderedList, rootState.account.listOrder);
}
}
}
within App.vue (or wherever)
created () {
this.$store.dispatch('account/fetchServerState')
}
So in normal javascript if I wanted to assign a value to a variable and then use that value outside of a function it would be done by declaring the variable first and then define it's value in the function. I'm brand new to typescript and angular so I am missing how to do this.
In the code below I am trying to get the value from a method in a service and then pass that value into my return. (I hope that makes sense). However I keep getting undefined on console.log(url) with no other errors.
emailsAPI() {
let url: any
this.apiUrlsService.urlsAPI().subscribe(
data => {
this.results = data
url = this.results.emails
}
);
console.log(url)
return this.http.get('assets/api/email_list.json')
}
api-urls service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
#Injectable()
export class ApiUrlsService {
constructor(
private http: HttpClient
) { }
urlsAPI () {
return this.http.get('assets/api/api_urls.json')
}
}
That's because you're calling async method subscribe and then trying to log the coming value before subscription is resolved. Put last two statements (console.log and return) inside the curly braces just after assigning this.results.emails to the url variable
emailsAPI(): Observable<any> {
let url: any
return this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
url = this.results.emails
// you can now access url variable
return this.http.get('assets/api/email_list.json')
});
}
As per reactive programming, this is the expected behaviour you are getting. As subscribe method is async due to which you are getting result later on when data is received. But your console log is called in sync thread so it called as soon as you are defining subscribe method. If you want the console to get printed. Put it inside your subscribe data block.
UPDATE:
As per your requirement, you should return Subject instead of Observable as Subject being data consumer as well as data producer. So it will consume data from httpget request from email and act as a producer in the method from where you called emailsAPI method.
emailsAPI(): Subject<any> {
let emailSubject:Subject = new Subject();
this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
return this.results.emails;
}).
subscribe(url=> {
this.http.get(your_email_url_from_url_received).subscribe(emailSubject);
});
return emailSubject;
}
The subject can be subscribed same as you will be doing with Observable in your calee method.
I'm trying to learn Angular 2 and am rebuilding an Angular 1 app I've made with Angular 2 using the Angular CLI. I've setup a HTTP GET request, which fires successfully, and setup a subscriber to interpret the result, and console logging in the subscriber function shows the data I expect. However, no data is being updated on the template.
I tried setting the data to an initial value, to a value in the ngOnInit, and in the subscriber function, and the initial and ngOnInit update the template accordingly. For the life of me, I can't figure out why the template won't update on the subscribe.
events: any[] = ['asdf'];
constructor(private http: Http) {
}
ngOnInit() {
this.events = ['house'];
this.getEvents().subscribe(this.processEvents);
}
getEvents(): Observable<Event[]> {
let params: URLSearchParams = new URLSearchParams();
params.set('types', this.filters.types.join(','));
params.set('dates', this.filters.dates.join(','));
return this.http
.get('//api.dexcon.local/getEvents.php', { search: params })
.map((response: Response) => {
return response.json().events;
});
}
processEvents(data: Event[]) {
this.events = ['car','bike'];
console.log(this.events);
}
The data is being displayed via an ngFor, but car and bike never show. Where have I gone wrong?
You have gone wrong with not respecting the this context of TypeScript, if you do stuff like this:
.subscribe(this.processEvents);
the context get lost onto the processEvents function.
You have to either bind it:
.subscribe(this.processEvents.bind(this));
Use an anonymous function:
.subscribe((data: Events) => {this.processEvents(data)});
Or set your method to a class property:
processEvents: Function = (data: Event[]) => {
this.events = ['car','bike'];
console.log(this.events);
}
Pick your favourite, but I like the last option, because when you use eventListeners you can easily detach them with this method.
Not really sure with what's going on with that processEvents. If you want to subscribe to your response just do:
this.getEvents()
.subscribe(data => {
this.events = data;
});
I am facing a weird issue in assigning response to a class's global variable from inside a observable. So my program logic is as follows:
Get latest playlists ID's from elastic search (i use elastic search from a type definition file). This returns me a PromiseLike to which i hook a then operator.
Inside the promise resolution, i make another http get call (i.e an observable)
In Observable subscription, i assign my global array with the response from the server.
Code is working correctly, I am getting responses as they should be but i cant assign the variable to the global one.
Here is my code:
import {Component, OnInit} from '#angular/core';
import {PlaylistService} from '../api/services'
#Component({
selector: 'app-playlists',
templateUrl: './playlists.component.html',
styleUrls: ['./playlists.component.css']
})
export class PlaylistsComponent implements OnInit {
public playlists: any[] = [];
constructor(private playlistService: PlaylistService) {
}
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
});
}
}
The listIds function is as follows:
listIds() {
return this.api.listing('playlist').then((body) => {
let hits = body.hits.hits;
return _.keys(_.groupBy(hits, '_id'));
});
}
and here is my api.listing function (elastic search client)
listing(type: string) {
let es = this.prepareES();
return es.search({
index: 'test',
_source: ["_id"],
type: type
});
}
The return type of es.search is
search(params: SearchParams): PromiseLike>;
Any ideas why i am not being able to assign value to global variable?
It looks like the promise returned by this.playlistservice.listIds() doesn't run inside Angulars zone. This is why Angular2 doesn't run change detection and doesn't recognize the change.
You can invoke change detection explicitly after the change:
constructor(private playlistService: PlaylistService, private cdRef:ChangeDetectorRef) {
...
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
this.cdRef.detectChanges();
});
}
Can you try passing
this.playlistService.listIds()
call inside your
return this.playlistService.getByIds(val)
replace val with first service call and see if your view gets updated. Just for testing purpose like
return this.playlistService.getByIds(this.playlistService.listIds())
.then((results)=>{/*rest of logic here*/});