Originally in my Vue component I had a series of nested if statements that would go through the JSON data to determine whether a text input should be displayed or a select based on a has_selectable_value option being true (select display) or false (text input display), and if it was a select then loop through the data and output associated options.
I have been able to change that to a computed statement which almost does everything I need it to do apart from one little thing which is to display the select options.
Here is the relevant part of the Vue Code:
<template v-else-if="searchtype == 9">
<select v-for="service in selectableServices" class="form-control" v-model="searchvalue" required>
<option value="">Select A Location</option>
<option v-for="sl in selectableLocations" :value="sl.location_id">{{sl.name}}</option>
</select>
<input v-for="service in nonSelectableServices" class="form-control" v-model="searchvalue" placeholder="Enter Search Value" required>
</template>
The current computed functions:
services: function () {
var ret = []
this.countries.forEach(function(country) {
country.states.forEach(function(state) {
state.services.forEach(function(service) {
ret.push(service)
});
});
});
return ret;
},
selectableServices: function () {
return this.services.filter(service => service.id == this.service && service.has_selectable_location);
},
nonSelectableServices: function () {
return this.services.filter(service => service.id == this.service && !service.has_selectable_location);
},
selectableLocations: function () {
// Filter one more level down
return this.selectableServices.map(service => service.selectablelocations);
},
This is the JSON data structure I am working with as well (I cut it back to the relevant parts for this part of the code):
[
{
"id": 1,
"name": "Country Name",
"states": [
{
"id": 1,
"name": "State Name",
"services": [
{
"id": 1,
"name": "Service Name",
"has_selectable_location": 1,
"selectablelocations": [
{
"id": 1,
"name": "Selectable Location A",
},
]
}
]
}
]
}
]
Using a Vue plugin for Chrome I can see that the computed function selectableLocations loads an array containing the individual locations, but the existing v-for statement isn't able to function correctly. Instead I still need to go down one more level which I can do by adding an extra v-for loop like so:
<template v-for="selectableLocationsList in selectableLocations" >
<option v-for="sl in selectableLocationsList" :value="sl.location_id">{{sl.name}}</option>
</template>
Everything displays correctly, but I am not sure if this is best practice as I was hoping to do as much of this in a computed function as possible and ideally only require a single v-for statement. But if it's not possible like that I understand and I can leave it as is.
Thank you in advance.
Edit: After more testing and research I have come up with this code that works as I had desired:
var formArray = []
var locationsArray = this.servicesArray.filter(service => service.id == this.service);
locationsArray.map(service => service.selectablelocations);
locationsArray.forEach(function(selectableLocations) {
selectableLocations.selectablelocations.forEach(function(location) {
formArray.push(location)
});
});
return formArray;
Is there a way I can refactor this further and make it a bit cleaner?
Solely considering the code you posted after the Edit , the code can be refactored this way:
let formArray = [];
formArray = this.servicesArray
.filter(service => service.id == this.service)
.map(service => service.selectablelocations)
.reduce((prev, curr) => prev.concat(curr))
return formArray
Note that the map you used doesn't do anything as Array.prototype.map only returns a new array, but you didn't assign it to anything.
Related
I am using VUEJS to build a single page application. I have a single file component that gets some data from an API (using Axios) and uses v-for to render out a list like so:
<template>
<div>
<ul>
<li v-for="division in divisions" :key="division.id_PK">
{{ division.c6code }} - XXX
</li>
</ul>
</div>
</template>
props: {
country: {
type: Object,
required: true
}
},
data() {
return {
divisions: [],
},
methods: {
async getDivisions() {
const divisions = await APIService.getDivisions(
this.country.igpeSID_FK
)
this.divisions = divisions
},
}
An example of the API data source looks like /getDivisions/800001:
{
"status": 200,
"data": [
{
"id_PK": 205891,
"igpeSID_FK": 800001,
"c6code": "AB-CDE"
},
{
"id_PK": 205890,
"igpeSID_FK": 800001,
"c6code": "FG-HIJ"
},
{
"id_PK": 205889,
"igpeSID_FK": 800001,
"c6code": "KL-MNO"
},
{
"id_PK": 205888,
"igpeSID_FK": 800001,
"c6code": "PQ-RST"
},
{
"id_PK": 205887,
"igpeSID_FK": 800001,
"c6code": "WX-YZA"
}
]
}
The rendered UI looks like so:
Now, there is another API endpoint that contains additional data that I need to put in place of the "XXX" placeholder shown above. That additional data source has a property called name and contains the actual division name which was not included in the /getDivisions API (only the associated property id_PK is provided).
Question: How can I get this name for each division?
An example of this endpoint that contains the additional data is: /getDivisionNameById/{id_PK} where id_PK is the parameter I need to pass to it from the getDivisions data shown above. So, for example, if I pass 205891 to the /getDivisionNameById/205891 I get back data that looks like so:
Example /getDivisionNamesById/205891:
{
"status": 200,
"data": [
{
"P_uniqueID": 16919,
"isoGeoDivisionEntry_FK": 205891,
"name": "ZABUL",
"lang": "fa"
},
{
"P_uniqueID": 16918,
"isoGeoDivisionEntry_FK": 205891,
"name": "ZABUL",
"lang": "ps"
}
]
}
I am thinking that I need to create a function that somehow creates a new array of names that I could then loop through in yet another v-for in my original template like so:
<li v-for="division in divisions" :key="division.id_PK">
{{ division.c6code }} - <span v-for="divName in divisionNames" :key="division.id_PK">{{divName.name}}</span>
</li>
async getDivisionNameById(id_PK) {
const name = await APIService.getDivisionNameById(id_PK)
this.divNames.push(name)
}
Obviously, I don't know what I am doing there...
Codesandbox with data:
https://codesandbox.io/s/intelligent-wind-21w35?file=/src/getDivisionNamesById.json
You have to query the data first before rendering the content.
You could fetch all the data in onMounted hook.
Thats a good place for a computed property.
Edit this example as needed depending on how you call your APIs.
You could call both API endpoints in the mounted lifecycle-hook of the component. Included this with a simple timeout to simulate the data coming in at different times.
About the secondary API call(s) where you get the name-data: As you've already said, loop through the IDs and make the API calls. In this case you'd need to wait for the result of your first API call and then use those ID's it returns to make the second API call(s). Maybe this will help you with the await/asyc/then stuff:
How to use async/await in Vue.js?
<template>
<div class="hello">
<ul>
<li v-for="(division, index) in fullList" :key="index">
{{ division }}
</li>
</ul>
</div>
</template>
<script>
import divisions from "#/getDivisionsEntries.json";
import divisionNames from "#/getDivisionNamesById.json";
export default {
name: "DivisionsList",
data() {
return {
divisions: divisions.data,
divisionNames: null,
};
},
mounted() {
setTimeout(() => {
this.divisions = divisions.data;
}, 1000);
setTimeout(() => {
this.divisionNames = divisionNames.data;
}, 3000);
},
computed: {
fullList: function () {
let combinedArr = [];
for (var divi of this.divisions) {
var divNameData = this.divisionNames?.find(
(x) => x.isoGeoDivisionEntry_FK === divi.id_PK
);
if (divNameData !== undefined) {
combinedArr.push(`${divi.c6code} - ${divNameData.name}`);
}
}
return combinedArr;
},
},
};
</script>
The following example gives me a blank screen (jsfiddle here). Even the parts which have nothing to do with the loop are not being rendered.
HTML:
<div id="app">
<button #click="objectFromApi">
run objectFromApi function
</button>
<div
v-for="obj in myObject[0].results"
:key="obj.id"
>
<p>
{{ obj.message }}
</p>
</div>
</div>
JavaScript:
new Vue({
el: "#app",
data: {
myObject: []
},
methods: {
objectFromApi: function(){
this.myObject.push(
{
"count": 5,
"results": [
{
"id": 1,
"message": "object 1"
},
{
"id": 2,
"message": "object 2"
}
]
}
)
}
},
//created() {
// this.objectFromApi()
//}
})
Nevertheless it does work if:
1.) Either using objectFromApi function directly in the created life cycle hook (what I don't want!)
created() {
this.objectFromApi()
}
2.) Or (without the use of created life cycle hook) if I go directly into the nested results array and spread the objects out like this (what I also don't want!)
this.myObject.push(
...{
"count": 5,
"next": "http://127.0.0.1:8000/api/someurl/?page=2",
"previous": null,
"results": [
{
"id": 1,
"message": "object 1"
},
{
"id": 2,
"message": "object 2"
}
]
}.results
)
When using option 2.) of course the v-for loop has to look different:
v-for="obj in myObject" instead of v-for="obj in myObject[0].results"
What is wrong with my initial example?
When the component is first rendering the array myObject will be empty.
During rendering it attempts this:
<div
v-for="obj in myObject[0].results"
:key="obj.id"
>
The value of myObject[0] will be undefined. Attempting to access the results property of undefined will result in an error. This error will cause rendering to fail. Nothing will be shown, even the parts that didn't fail.
There are various ways to fix this problem. You could prepopulate the data with suitable empty properties:
data: {
myObject: [
{
results: []
}
]
}
Alternatively, as you've noted, you could change the loop to use v-for="obj in myObject", changing objectFromApi accordingly to only store the results array in myObject. Even if you don't want that exact change some similar change is probably a good idea because the [0] part strongly suggests you've got a problem with your data model. The key thing here is that it avoids trying to access nested objects that don't exist. The use of the spread operator in your second example is largely irrelevant.
Or you could skip the loop in the template:
<template v-if="myObject[0]">
<div
v-for="obj in myObject[0].results"
:key="obj.id"
>
...
</div>
</template>
Sorry, the title isn't worded very well. I'm using a category choose to choose a category from an API. I currently get the list of categories, filter through their names, and display them in the category chooser. When the user clicks submit, I want the to parse through the API and find the id associated with that category name. Here's an example output from the API:
{
"_id": "5c2fde414502d923ceafaa30",
"title": "Category 2",
"description": "My second category, testing 123",
"createdAt": "2019-01-04T22:29:21.047Z",
"updatedAt": "2019-01-04T22:29:21.047Z",
"__v": 0
},
Here's the code I use for the Category Chooser:
JS:
$.getJSON("http://localhost:2672/categories", function (json) {
$('#category-chooser').empty();
$('#category-chooser').append($('<option>').text("Choose a Category"));
$.each(json, function (i, obj) {
$('#category-chooser').append($('<option>').text(obj.title));
});
});
HTML
<select id="category-chooser" class="form-control" name="category">
<option selected="selected">blank</option>
</select>
If you store the json returned from getJSON somewhere outside the callback, your submit button would fire off something like below:
function getCategoryId(){
const categoryChooser = document.getElementById('category-chooser');
const categorySelected = categoryChooser.value;
json.forEach(entry => {
if(entry.title === categorySelected){
return entry["_id"];
}
});
}
I’ve been experimenting with this GitHub repo via a course on Lynda.com (https://github.com/planetoftheweb/learnangular) by Ray Villalobos -- it functions similarly to a basic web app that I’m hoping to build, but I’ve recently hit a bit of a road block.
In that repo linked above, in app/component.app.ts, is the following array:
var ARTISTS: Artist[] = [
{
"name": "Barot Bellingham",
"shortname": "Barot_Bellingham",
"reknown": "Royal Academy of Painting and Sculpture",
"bio": "Some bio here."
},
// etc...
]
This array is filtered by a pipe as seen in app/pipe.search.ts:
export class SearchPipe implements PipeTransform {
transform(pipeData, pipeModifier) {
return pipeData.filter((eachItem) => {
return eachItem['name'].toLowerCase().includes(pipeModifier.toLowerCase()) ||
eachItem['reknown'].toLowerCase().includes(pipeModifier.toLowerCase());
});
}
}
Here's the filter input:
<input class="search-input" [(ngModel)]="field1Filter" placeholder="type in search term here" (click)="showArtist(item); field1Filter=''">
And the code for the filter results:
<ul class="artistlist cf" *ngIf="field1Filter">
<li class="artistlist-item cf"
(click)="showArtist(item);"
*ngFor="let item of (artists | search: field1Filter)">
<artist-item class="content" [artist]=item></artist-item>
</li>
</ul>
<artist-details *ngIf="currentArtist" [artist]="currentArtist"></artist-details>
This all works perfectly, however, in my project, I would need to include three nested arrays, and have the ability to filter based upon the values in those arrays. A sample of the kind of array I need will look something like this:
var ARTISTS: Artist[] = [
{
"name": "Barot Bellingham",
"shortname": "Barot_Bellingham",
"reknown": "Royal Academy of Painting and Sculpture",
"bio": "Some bio here...",
"friends": [
"James",
"Harry",
"Bob",
"Liz",
"Kate",
"Jesse"
],
"emails": [
"bb#this.com",
"aa#this.com"
],
"car": [
"honda",
"scion",
"aston martin"
]
},
// etc...
]
Therefore, I hope to filter by “Harry,” and only display objects that contain “harry” in either “name,” “reknown,” “friends,” "emails," or "cars." Is this possible, and if so, how can I edit the pipe filter to do this? Thank you!!
(I'm pretty green at angular and JS in general, so I want to apologize in advance if I’ve used incorrect terminology or overlooked/misunderstood something basic.)
I deleted my prior answer because it was more confusing than helpful. I pasted example code without applying it to your variables/properties/objects and it was misleading. Let's try again:
export class SearchPipe implements PipeTransform {
transform(pipeData, pipeModifier) {
pipeModifier = pipeModifier ? pipeModifier.toLowerCase() : null;
return pipeModifier ? pipeData.filter(eachItem => {
eachItem['name'].toLowerCase().indexOf(pipeModifier) !== -1 ||
eachItem['reknown'].toLowerCase().indexOf(pipeModifier !== -1) : pipeData;
});
}
}
The first line of code in the transform method ensures that the modifier passed in is also lowercase so that the compare always compares lower case values. It also has a null check to ensure it does not try to lowercase it if it is null.
The second line of code also uses the "?" syntax to handle the case of a null pipeModifier.
I changed includes to indexOf. Includes checks arrays. Are these items, such as eachItem['name'], an array?
That should be closer.
NOTE: Without a provided plunker ... I did not check the syntax or correct execution of this code.
I have two json:
all_users
"all_users":
{
"4":{
"user_id":4,
"user_name":"Miranda"
},
"7":{
"user_id":7,
"user_name":"seconduser"
}
And tickets
"tickets":
[{
"ticket_id" : 12,
"created_by" : 7,
"assigned_to": 6
}]
Now, from the json tickets, I need to search who created a ticket, i.e created_by. But, since this is id, I am not able to search it directly using name.
After doing my bit of research, I implemented this:
<input id="created-by-input" type="text" ng-model="search.created_by" placeholder="Created by" typeahead="user.user_id as user.user_name for user in transformationFunction(all_users, $viewValue) | filter:{user_name:$viewValue}" class="form-control">
And the scope:
$scope.transformationFunction = function(object) {
var newArray = [];
for(var key in object) {
newArray.push({user_id: key, user_name: object[key].user_name});
}
return newArray;
console.log(newArray)
};
However, when I am searching, I get all the relevant users when I start typing. Also, when I click on them, the search filter works and shows me the result. But, when I click on the users in the dropdown while searching, the text field shows me the ID and not the user name.
For example: I start typing Mira, the dropdown shows Miranda, when I click on it, the text field shows me 4.
What am I missing out in here??
If you would store your users as an array
e.g.
$scope.users = [{
id: 1,
name: "Fancyname"
},
{
id: 2,
name: "Fancyname2"
}]
You could use the .map function like this:
var index = $scope.users.map(function(x) {return x.id; }).indexOf(idYourAreLookingFor);
var user= $scope.users[index];