I have creating VueJs application and passing API calls using AXIOS. Within current scenario user is able to click a button, which will execute function and display list of all unique manufacturers. Within the list a button is assigned, which should let user to see all the models under the manufacturer. As of yer I am unsure how to connect to functions so when clicking on one object it will return user a filter view where models assigned to manufacturer will be showed.
Below I have displayed my code
VueJs
<div v-for="(manufacturerResponse) in manufacturerResponse ">
<p> <b>Manufacturer ID {{manufacturerResponse.manufacturerId}} </b>
<b-btn variant="warning" v-on:click="show(); getModels(response.manufactuerId);">View Models</b-btn>
</p>
</div>
AXIOS - getManufacturer, which displays only unique Manufacturers
getManufacturers () {
AXIOS.get(`/url/`)
.then(response => {
console.log(this.manufacturerResponse)
this.response = response.data
this.manufacturerResponse = []
response.data.forEach(item => {
if (!this.manufacturerResponse.some(fltr => {
return item.manufacturerId == fltr.manufacturerId
})) {
this.manufacturerResponse.push(item);
}
});
})
},
AXIOS - getModel, which displays models under Manufacturer
getModels () {
AXIOS.get(`/url/`)
.then(response => {
const id = 0;
this.testResponse = response.data.filter (kp6 => kp6.manufacturerId === this.manufacturerResponse[id].manufacturerId );
console.log(this.testResponse)
})
},
If it helps also added example how the response appears in the simple array
[
{"id":1,"manufacturerId":1,"model":"Focus"},
{"id":2,"manufacturerId":1,"model":"Transit"},
{"id":3,"manufacturerId":2,"model":"Clio"},
{"id":4,"manufacturerId":3,"model":"Niva"},
{"id":5,"manufacturerId":3,"model":"Yaris"},
]
In template you have below:
v-on:click="show(); getModels(response.manufactuerId);"
But it should be:
v-on:click="show(); getModels(manufacturerResponse.manufacturerId);"
since manufacturerResponse.manufacturerId is the id you are currently displaying and the button click should get the models for that id.
getModels() would receive that param like getModels(manufacturerId) then use that to filter as below:
this.testResponse = response.data.filter (kp6 => kp6.manufacturerId === manufacturerId);
The show() method should be setup to accept a parameter of response.manufactuerId
So ...
v-on:click="show(response.manufacturerId);"
Now... inside your Vue instance
you will need to make sure the method for show looks something like this...
show(manufacturerId){
this.getModels(manufacturerId) // This will call the getModels method on the Vue instance and pass in the manufacturerId that you provided via your click event
}
You can probably just bypass the show method and just have the click event call getModels directly and pass in the manufacturerId directly.
Related
I am calling an API-service which returns an Observable - containing an array of elements.
apiMethod(input: Input): Observable<ResultElement[]>
From this I have been choosing the first element of the array, subscribing to that. Then used that element to route to another page like this:
this.apiService
.apiMethod(input)
.pipe(map((results) => results[0])
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
This works just fine.
Problem is, I do not want to just use the first element, I want a MatDialog, or other similar to pop up, and give the user option of which element to choose, and THEN route to the correct one.
If the list only contain one element though, the dialog should not show, and the user should be routed immediately.
I have tried to open a dialog in the .pipe(map()) function, but the subscribe() happens before I get answer from the user, causing it to fail. And I am not sure if that even is the correct approach. How would any of you solve this problem?
Edit
Ended up doing partly what #BizzyBob suggested:
Changing map to switchmap in the API-call, making it this way:
this.apiService
.apiMethod(input)
.pipe(switchMap((results) => this.mapToSingle(results)
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
With the mapToSingle(ResultElement[]) being like this:
private mapToSingle(results: ResultElement[]): Observable<ResultElement> {
if (result.length === 1){
return of(results[0]);
}
const dialogConfig = new MatDialogConfig<ResultElement[]>();
dialogConfig.data = results;
const dialogRef = this.dialog.open(ResultDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
I would create a DialogComponent that takes in the list of choices as an input, and emits the chosen item when it's closed.
Then, create a helper method (maybe call it promptUser) that simply returns an observable that emits the selected value:
this.apiService.apiMethod(input)
.pipe(
switchMap(results => results.length > 1
? this.promptUser(results)
: of(results[0])
)
)
.subscribe(
result => this.router.navigate('elements/', result.id)
);
Here we simply use switchMap to return an observable that emits the proper item. If the length is greater than 1, we return the helper method that displays the dialog and emits the chosen item, else just emit the first (only) item. Notice that we wrapped plain value with of since within switchMap, we need to return observable.
In either case, the desired item is emitted and received by your subscribe callback.
Two possible options:
Having a subject for the selected result that is "nexted" either by user input or a side effect of getting an api result with one element.
Keeping track of an overall state of the component and responding appropriately whenever a selectedResult is set in the state.
The example below is a sketch of using an Observable to keep track of the component's state.
There are two input streams into the state, the results from the api and the user input for the selected result.
Each stream is converted into a reducer function that will modify the overall state.
The UI should subscribe to this state via an async pipe, showing the modal when appropriate, and updating updating state from events via the Subjects.
The redirection should come as an effect to the change of the state when selectedResult has a value.
readonly getResultsSubject = new Subject<MyInput>();
readonly resultSelectedSubject = new Subject<ResultType>();
private readonly apiResults$ = this.getResultsSubjects.pipe(
switchMap((input) => this.apiMethod(input))
);
readonly state = combineLatest([
this.apiResults$.pipe(map(results => (s) => results.length === 1
? { ...s, results, selectedResult: x[0], showModal: false }
: { ...s, results, showModal: results.length > 1 })),
this.resultSelectedSubject.pipe(map(selectedResult => (s) => ({ ...s, selectedResult })))
]).pipe(
scan((s, reducer) => reducer(s), { }),
shareReplay(1)
);
ngOnInit() {
this.state.pipe(
filter(x => !!x.selectedResult)
).subscribe(x => this.router.navigate('elements/', x.selectedResult.id));
}
I've been using this pattern a lot lately. It makes it pretty easy the number of actions and properties of the state grow.
I would solve it using the following method:
Get the data with your subscribe (without the pipe). And save this data in the component variable
options: any;
this.apiService
.apiMethod(input)
.subscribe(
(result) => {
if (result.length === 1) {
this.router.navigate([result[0]]);
return;
}
options = result;
}
)
with an ngIf on the modal (conditional of the length of the array of options > 0 display the component with the different choices when the data is received
<modal-component *ngIf="options.length > 0"></modal-component>
when the user (click) on an option inside your modal, use the router to redirect.
html
<div (click)="redirect(value)">option 1</div>
ts
redirect(value) {
this.router.navigate([value]);
}
That would be the most straight forward
I'm trying to build a simple app that lets the user type a name of a movie in a search bar, and get a list of all the movies related to that name (from an external public API).
I have a problem with the actual state updating.
If a user will type "Star", the list will show just movies with "Sta". So if the user would like to see the actual list of "Star" movies, he'd need to type "Star " (with an extra char to update the previous state).
In other words, the search query is one char behind the State.
How should it be written in React Native?
state = {
query: "",
data: []
};
searchUpdate = e => {
let query = this.state.query;
this.setState({ query: e }, () => {
if (query.length > 2) {
this.searchQuery(query.toLowerCase());
}
});
};
searchQuery = async query => {
try {
const get = await fetch(`${API.URL}/?s=${query}&${API.KEY}`);
const get2 = await get.json();
const data = get2.Search; // .Search is to get the actual array from the json
this.setState({ data });
} catch (err) {
console.log(err);
}
};
You don't have to rely on state for the query, just get the value from the event in the change handler
searchUpdate = e => {
if(e.target.value.length > 2) {
this.searchQuery(e.target.value)
}
};
You could keep state updated as well if you need to in order to maintain the value of the input correctly, but you don't need it for the search.
However, to answer what you're problem is, you are getting the value of state.query from the previous state. The first line of your searchUpdate function is getting the value of your query from the current state, which doesn't yet contain the updated value that triggered the searchUpdate function.
I don't prefer to send api call every change of letters. You should send API just when user stop typing and this can achieved by debounce function from lodash
debounce-lodash
this is the best practise and best for user and server instead of sending 10 requests in long phases
the next thing You get the value from previous state you should do API call after changing state as
const changeStateQuery = query => {
this.setState({query}, () => {
//call api call after already changing state
})
}
In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations
I am developing a table component and the data for the table component is to be populated on basis of three dropdown values. I need to pass in all three values to the API to get the desired response. I can achieve it using nested subscribes, which is a very bad way. but any change calls the API multiple times. How can I fix it? Most examples I found are for getting only the final subscribe value but in my case, I need all three. Any advice to achieve using tap and flatMap?
Please advice.
this._data.currentGroup.subscribe(bg => {
this.bg = bg;
this._data.currentUnit.subscribe(bu => {
this.bu = bu;
this._data.currentFunction.subscribe(jf => {
this.jf = jf;
this.apiService.getFunctionalData(this.bg, this.bu, this.jf)
.subscribe(
(data) => {
console.log(data)
}
);
});
});
});
This is what I did.
this._data.currentGroup.pipe(
tap(bg => this.bg = bg),
flatMap(bu => this._data.currentUnit.pipe(
tap(bu => this.bu = bu),
flatMap(jf => this._data.currentFunction.pipe(
tap(jf => this.jf = jf)
))
))
).subscribe();
This is a sample example of my dataService. I initialize my dataservice in the table component's constructor as _data.
changeGroup(bg: string) {
this.changeGroupData.next(bg);
}
private changeGroupData = new BehaviorSubject<string>('');
currentChangeGroup = this.changeGroupData.asObservable();
You can use combineLatest to combine the three Observables and then subscribe to all of them at once. It will emit a new value as soon as one of the three Observables changes.
combineLatest(this._data.currentGroup,
this._data.currentUnit,
this._data.currentFunction).subscribe(([bg, bu, jf]) => {
// do stuff
});
For an example, have a look at this stackblitz demo I created.
UPDATE
If I change my delete function to this:
function deleteResNote(reservation, note_idx) {
realm.write(() => {
reservation.notes.splice(note_idx, 1)
})
}
Everything works fine. No errors, and the UI updates accordingly.
Why do I need to delete my notes through my reservation object? Why doesn't realm allow me to delete my Notes objects directly?
My app deals with Tasks, which have Reservations, which have Notes. I'm working on the ability to delete a note.
My notes render in a ListView. When the cell is tapped, I pass the Note object (Realm Object) through to my deletion method, and ultimately call:
function deleteResNote(noteObj) {
realm.write(() => {
realm.delete(noteObj)
})
}
After this delete, this method triggers the following error:
Error: Accessing object of type Notes which has been deleted
The component which holds my ListView is set up to listen for the realm change event, and the same method used to render for the first time is used again to re-render the updated notes. I think it's important to point out that in this method, Notes are accessed through the parent Task object.
handleReservationNotes = taskId => {
let dataBlob = {}
let sectionIDs = []
let rowIDs = []
let reservations = []
const details = Tasks.getTask(taskId)
Object.values(details.reservations).forEach((reservation, i) => {
reservations.push({ id: reservation.id, lead: reservation.lead_name })
// manage our sections
sectionIDs.push(reservation.id)
dataBlob[reservation.id] = reservation.lead_name
// manage our rows
rowIDs[i] = []
Object.values(reservation.notes).forEach((note, j) => {
rowIDs[i].push(j)
dataBlob[`${reservation.id}:${j}`] = note
})
})
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
reservations,
selectedReservationId:reservations[0].id,
})
}
It seems as though I'm properly deleting the note from the database, but my Reservation object seems to be still holding on to it.
How can I successfully delete a Note while updating all the appropriate relationships?