Show children in Mat Tree from dynamic data backend - javascript

I need your help about Mat Tree with dynamic Data and lazy loading.
I have found this example here from Angular https://stackblitz.com/run?file=src%2Fapp%2Ftree-dynamic-example.ts
But I have a different structure of data.
I can see the parent but unable to load the children.
The parent it is an Object itself like this.
{
"description":"Test Purpose",
"childDevices":8,
"childLocations":8,
"name":"Test",
"id":"1234556788www2"
}
Then to load more information or children for this I need to create an Get request which gives me back like this.
{
"id": "1234556788www2",
"name": "Test",
"description": "Test Purpose",
"parent": null,
"children": [
"1st Child ID",
"2nd Child ID"
],
"groupsV2": [
"group ID"
],
"usersV2": [
"userID"
],
"tenantId": "Tenant ID",
"devices": [
"device1 ID",
"device2 ID",
"device3 ID",
"device4 ID"
]
}
Now to get its children I need to iterate again through API Get so I can get information about each children and then to show on tree under the parent. I have spent a lot of hours and did not find how to show the children into the tree.
This is the JSON for each children.
{
"id": "1st Child ID",
"name": "TestLocation",
"description": "333",
"parent": "1234556788www2",
"children": [],
"groupsV2": [
],
"usersV2": [
],
"tenantId": "",
"devices": []
}
This is my HTML.
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" data-cy="locationsMatTree">
<mat-tree-node>
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
[class.hidden]="!node.visible"
[class.fe-action]="node.overallFEChildCount > 0"
[class.be-action]="node.overallFEChildCount < node.overallChildCount">
<div class="title mat-tree-node on-hover">
<button mat-icon-button matTreeNodeToggle *ngIf="node.childLocations > 0; else noLocations">
<mat-icon class="mat-icon-rtl-mirror" (click)="loadChildren(node)">
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
<ng-template #noLocations>
<button mat-icon-button>
<mat-icon class="no-sub-icon">
<span class="leaf-icon"> • </span>
</mat-icon>
</button>
</ng-template>
<div
class="location-name hide-in-percy"
(click)="loadDevices(node)"
[routerLink]="['/location/details', node.id]"
[id]="'created-' + node.name">
{{ node.name }}
</div>
<icon-button
color="primary"
*ngIf="!isDisabled"
iconName="add"
type="mat-icon-button"
class="action-button action-button-add"
(click)="onNewClicked(node)">
</icon-button>
<icon-button
color="primary"
type="mat-icon-button"
iconName="edit"
class="action-button action-button-edit"
(click)="onEditClicked(node)">
</icon-button>
<icon-button
color="warn"
type="mat-icon-button"
iconName="delete"
class="action-button action-button-delete"
[disabled]="node.action?.generated"
[matTooltipDisabled]="!node.action?.generated"
matTooltip="{{ 'RIGHT_ACTIONS_DISABLED_LEAF' | translate }}"
(click)="deleteLocation(node)">
</icon-button>
</div>
<ul [class.tree-invisible]="!treeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
<mat-progress-bar *ngIf="node.isLoading" mode="indeterminate" class="example-tree-progress-bar"></mat-progress-bar>
</mat-nested-tree-node>
</mat-tree-node>
</mat-tree>
TS Code.
private readonly subscription: Subscription = new Subscription();
dataSource = new MatTreeNestedDataSource<Location>();
hasChild = (_: number, node: Location) => !!node.childLocations && node.childLocations > 0;
this.locationService
.getLocationsForUser(this.pageIndex, this.pageSize)
.pipe(take(1))
.subscribe((response) => {
this.dataSource.data = response.body.content; // here I load all parents
this.flatLocations = this.getFlatLocations();
this.length = response.body.totalElements;
if (response.body.content && response.body.content.length > 0) {
this.location = response.body.content[0];
this.loadDevices(this.location);
}
this.subscribeToRouteParams();
});
}
loadChildren = (location: Location): any => {
this.locationService.getLocation(location.id).subscribe((res) => {
for (let i = 0; i < res.body.children.length; i++) {
this.locationService.getLocation(res.body.children[i]).subscribe((t) => {
return of(t.body); //Here I can see each child data
});
}
});
return false;
};

Your problem is currently that you are nesting subscriptions in your loadChildren function. You probably want to use switchMap and forkJoin to make the calls to this.locationService.getLocation() in parallel and output the response of each call in an array:
loadChildren = (location: Location): any => {
this.locationService.getLocation(location.id).pipe(
switchMap(res => forkJoin(
res.body.children.map(child => this.locationService.getLocation(child))
)
)
).subscribe( resultArray => {
location.childLocations = resultArray;
});
}

Related

#firebase/database: FIREBASE WARNING: Exception was thrown by user callback. TypeError: Cannot read properties of undefined (reading 'AccountStatus')

Few minutes ago, my application was running ok. Suddenly, variables start to get undefined and I get error messages. I had tried to build this app using laravel but kept having issues. Now I am working with Vue.js. These are my codes below:
service.js
import firebase from "../firebase";
// var whereis = "Deliverers.Profile";
const db = firebase.ref("/Deliverers");
class Service {
getAll() {
return db;
}
// create(tutorial) {
// return db.push(tutorial);
// }
update(key, value) {
return db.child(key).update(value);
}
// delete(key) {
// return db.child(key).remove();
// }
// deleteAll() {
// return db.remove();
// }
}
export default new Service();
user.vue
<template>
<div v-if="currentTutorial" class="edit-form">
<h4>Tutorial</h4>
<form>
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
class="form-control"
id="title"
v-model="currentTutorial.user['FullName']"
/>
</div>
<div class="form-group">
<label for="description">Description</label>
<input
type="text"
class="form-control"
id="description"
v-model="currentTutorial.user['description']"
/>
</div>
<div class="form-group">
<label><strong>Status:</strong></label>
<!-- {{ currentTutorial.status ? "Published" : "Pending" }} -->
{{ currentTutorial.status }}
</div>
</form>
<button
class="badge badge-primary mr-2"
v-if="currentTutorial.published"
#click="updatePublished(false)"
>
UnPublish
</button>
<button
v-else
class="badge badge-primary mr-2"
#click="updatePublished(true)"
>
Publish
</button>
<button class="badge badge-danger mr-2" #click="deleteTutorial">
Delete
</button>
<button type="submit" class="badge badge-success" #click="updateTutorial">
Update
</button>
<p>{{ message }}</p>
</div>
<div v-else>
<br />
<p>Please click on a Tutorial...</p>
</div>
</template>
<script>
import Service from "../services/service";
export default {
name: "tutorial",
props: ["tutorial"],
data() {
return {
currentTutorial: null,
message: "",
};
},
watch: {
tutorial: function(tutorial) {
this.currentTutorial = { ...tutorial };
this.message = "";
},
},
methods: {
updatePublished(status) {
Service.update(this.currentTutorial.key, {
published: status,
})
.then(() => {
this.currentTutorial.published = status;
this.message = "The status was updated successfully!";
})
.catch((e) => {
console.log(e);
});
},
updateTutorial() {
const data = {
title: this.currentTutorial.title,
// description: this.currentTutorial.description,
};
Service.update(this.currentTutorial.key, data)
.then(() => {
this.message = "The tutorial was updated successfully!";
})
.catch((e) => {
console.log(e);
});
},
deleteTutorial() {
Service.delete(this.currentTutorial.key)
.then(() => {
this.$emit("refreshList");
})
.catch((e) => {
console.log(e);
});
},
},
mounted() {
this.message = "";
this.currentTutorial = { ...this.tutorial }
},
};
</script>
userlist.vue
EDITED: To replace the faulty function with a working version.
<template>
<div class="list row">
<div class="col-md-6">
<h4>Tutorials List</h4>
<ul class="list-group">
<li
class="list-group-item"
:class="{ active: index == currentIndex }"
v-for="(tutorial, index) in tutorials"
:key="index"
#click="setActiveTutorial(tutorial, index)"
>
{{ tutorial.user.Email}}
</li>
</ul>
<button class="m-3 btn btn-sm btn-danger" #click="removeAllTutorials">
Remove All
</button>
</div>
<div class="col-md-6">
<div v-if="currentTutorial">
<tutorial-details
:tutorial="currentTutorial"
#refreshList="refreshList"
/>
</div>
<div v-else>
<br />
<p>Please click on a Tutorial...</p>
</div>
</div>
</div>
</template>
<script>
import Service from "../services/service";
import TutorialDetails from "./user";
export default {
name: "tutorials-list",
components: { TutorialDetails },
data() {
return {
tutorials: [],
currentTutorial: null,
currentIndex: -1
};
},
methods: {
// onDataChange() {
// snapshot => {
// let data = snapshot.val();
// let _tutorials = [];
// Object.keys(data).forEach(key => {
// _tutorials.push({
// key: key,
// username: data[key].profile.fullname,
// // text: data[key].text}
// });
// });
// this.tutorials = _tutorials;
// };
// },
//THIS METHOD WORKED!!
onDataChange(snapshot) {
let data = snapshot.val();
let _tutorials = [];
Object.keys(data).forEach(key => {
_tutorials.push({
key: key,
user: data[key].Profile,
});
});
this.tutorials = _tutorials;
},
refreshList() {
this.currentTutorial = null;
this.currentIndex = -1;
},
setActiveTutorial(tutorial, index) {
this.currentTutorial = tutorial;
this.currentIndex = index;
},
removeAllTutorials() {
Service.deleteAll()
.then(() => {
this.refreshList();
})
.catch((e) => {
console.log(e);
});
},
},
mounted() {
Service.getAll().on("value", this.onDataChange);
// Service.getAll().on("value", function(snapshot){
// var data = snapshot.val();
// for(let i in data){
// console.log(data[i].Profile.Email);
// }
// });
},
beforeDestroy() {
Service.getAll().off("value", this.onDataChange);
}
};
</script>
Console.log(data) gives this result - which is what I expect it to do:
{AccountStatus: 'Verified', Address: 'Abakaliki', BirthYear: '1999', DeliveryOn: false, Email: 'www#gmail.com', …}
userslist.vue?8b1e:73
{AccountStatus: 'Unverified', Address: 'Amaeke Ekoli Edda Afikpo South LGA Ebonyi state', BirthYear: '1985', Date: 1640599050717, DeliveryOn: false, …}
userslist.vue?8b1e:73
{AccountStatus: 'Verified', Address: '7 glibert street kpirikpiri abakalik', BirthYear: '1973', Date: 1638213915413, DeliveryOn: false, …}
console.log(data.AccountStatus) gives the right results as shown below:
Verified
Unverified
Verified
console.log("items is:\n", JSON.stringify(items, null, 2)); gives the following output
items is:
{
"1UWuDwL2WvWndqgUBweoUjeEEZk1": {
"Bank details": {
"accountName": "Samuel Ankkk",
"accountNumber": "234567890",
"bank": "wensBANK"
},
"Location": {
"latitude": "9.0691414",
"longitude": "7.4a424",
"state": "Abuja"
},
"Profile": {
"AccountStatus": "Verified",
"Address": "Area B Opp Living Faith Church ",
"BirthYear": "1984",
"DeliveryOn": false,
"Email": "hyu#gmail.com",
"From": "Federal Capital Territory",
"Fullname": "Samuel jui",
"IdImage": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/ID_Pictures%2FNational%20ID%2F1612350737409.jpg?alt=media&token=640b2544-a9af-4f86-b7d8-3d47f638f77f",
"Image": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/Profile%20Pictures%2F1612350733949.jpg?alt=media&token=617d6b6b-50bd-4898-abdc-be53e70612bd",
"Phone_number": "08062093434",
"Route": "Jibowu",
"State": "Kogi State",
"To": "Lagos State",
"carrier": "Traveller || Individual",
"routed": "Federal Capital Territory - Jibowu"
}
},
"1WAVu8OUYzN7EudSc4vs55GUGYg1": {
"Profile": {
"AccountStatus": "Verified",
"Address": "ohuru amangwu obingwa ogbohill Aba Abia state nigeria",
"BirthYear": "1994",
"Date": 1631033704355,
"DeliveryOn": false,
"Email": "kui#gmail.com",
"Fullname": "Sunday vh",
"IdImage": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/ID_Pictures%2FOthers%2F1631033692952.jpg?alt=media&token=71967d4d-4a7f-4669-a7e1-9e2968970c2f",
"Image": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/Profile%20Pictures%2F1631033678872.jpg?alt=media&token=71f2f23c-ffa7-4bb7-a82a-d16949e0971f",
"Phone_number": "09064491585",
"Route": "None",
"State": "Abia State",
"routed": "None"
}
},
"1sLWwcxZL8Sr1sBFAzuPDcl1iXs1": {
"Bank details": {
"accountName": "Ekoh Jety",
"accountNumber": "67808870",
"bank": "first Bank"
},
"Location": {
"latitude": "5.3872821",
"longitude": "7.0089u9u8"
},
"Profile": {
"AccountStatus": "Verified",
"Address": "No 2 Obuagu, Enugu",
"BirthYear": "1998",
"DeliveryOn": false,
"Email": "j.fffg#gmail.com",
"From": "Enugu State",
"Fullname": "Ekoh wwiiwC.",
"IdImage": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/ID_Pictures%2FNational%20ID%2F1609831737206.jpg?alt=media&token=8290066b-73ab-44d0-8807-c28af6592b0e",
"Image": "https://firebasestorage.googleapis.com/v0/b/lombaz-3490e.appspot.com/o/Profile%20Pictures%2F1609831730721.jpg?alt=media&token=6efe967a-58eb-4fa7-b841-81d3d7666eb1",
"Phone_number": "08100489261",
"Route": "Emene Axis (Enugu Int’l Airport)",
"State": "Enugu State",
"To": "Enugu State",
"carrier": "Traveller || Individual",
"routed": "Enugu State - Emene Axis (Enugu Int’l Airport)"
}
}
}
On the console, Console.log(data) displays fine. Then this warning and error follows:
index.esm.js?abfd:106 [2022-02-05T15:39:46.784Z] #firebase/database: FIREBASE WARNING: Exception was thrown by user callback. TypeError: Cannot read properties of undefined (reading 'AccountStatus')
at eval (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/components/userslist.vue?vue&type=script&lang=js&:84:24)
at eval (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:4345:20)
at LLRBNode.inorderTraversal (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:2697:15)
at LLRBNode.inorderTraversal (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:2696:27)
at LLRBNode.inorderTraversal (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:2698:24)
at LLRBNode.inorderTraversal (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:2696:27)
at SortedMap.inorderTraversal (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:3147:27)
at ChildrenNode.forEachChild (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:3756:35)
at DataSnapshot.forEach (webpack-internal:///./node_modules/#firebase/database/dist/index.esm.js:4344:31)
at VueComponent.onDataChange (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/components/userslist.vue?vue&type=script&lang=js&:76:13)
defaultLogHandler # index.esm.js?abfd:106
Logger.warn # index.esm.js?abfd:212
warn # index.esm.js?e947:348
eval # index.esm.js?e947:704
setTimeout (async)
exceptionGuard # index.esm.js?e947:698
EventList.raise # index.esm.js?e947:9565
EventQueue.raiseQueuedEventsMatchingPredicate_ # index.esm.js?e947:9519
EventQueue.raiseEventsForChangedPath # index.esm.js?e947:9503
Repo.onDataUpdate_ # index.esm.js?e947:12730
PersistentConnection.onDataPush_ # index.esm.js?e947:12006
PersistentConnection.onDataMessage_ # index.esm.js?e947:12000
Connection.onDataMessage_ # index.esm.js?e947:11246
Connection.onPrimaryMessageReceived_ # index.esm.js?e947:11240
eval # index.esm.js?e947:11141
WebSocketConnection.appendFrame_ # index.esm.js?e947:10727
WebSocketConnection.handleIncomingFrame # index.esm.js?e947:10772
mySock.onmessage # index.esm.js?e947:10673
index.esm.js?e947:705 Uncaught TypeError: Cannot read properties of undefined (reading 'AccountStatus')
at eval (userslist.vue?8b1e:77:1)
at eval (index.esm.js?e947:4331:1)
at LLRBNode.inorderTraversal (index.esm.js?e947:2683:1)
at LLRBNode.inorderTraversal (index.esm.js?e947:2682:1)
at LLRBNode.inorderTraversal (index.esm.js?e947:2684:1)
at LLRBNode.inorderTraversal (index.esm.js?e947:2682:1)
at SortedMap.inorderTraversal (index.esm.js?e947:3133:1)
at ChildrenNode.forEachChild (index.esm.js?e947:3742:1)
at DataSnapshot.forEach (index.esm.js?e947:4330:1)
at VueComponent.onDataChange (userslist.vue?8b1e:70:1)
eval # userslist.vue?8b1e:77
eval # index.esm.js?e947:4331
LLRBNode.inorderTraversal # index.esm.js?e947:2683
LLRBNode.inorderTraversal # index.esm.js?e947:2682
LLRBNode.inorderTraversal # index.esm.js?e947:2684
LLRBNode.inorderTraversal # index.esm.js?e947:2682
SortedMap.inorderTraversal # index.esm.js?e947:3133
ChildrenNode.forEachChild # index.esm.js?e947:3742
DataSnapshot.forEach # index.esm.js?e947:4330
onDataChange # userslist.vue?8b1e:70
onceCallback # index.esm.js?e947:4935
eval # index.esm.js?e947:4545
exceptionGuard # index.esm.js?e947:694
EventList.raise # index.esm.js?e947:9565
EventQueue.raiseQueuedEventsMatchingPredicate_ # index.esm.js?e947:9519
EventQueue.raiseEventsForChangedPath # index.esm.js?e947:9503
Repo.onDataUpdate_ # index.esm.js?e947:12730
PersistentConnection.onDataPush_ # index.esm.js?e947:12006
PersistentConnection.onDataMessage_ # index.esm.js?e947:12000
Connection.onDataMessage_ # index.esm.js?e947:11246
Connection.onPrimaryMessageReceived_ # index.esm.js?e947:11240
eval # index.esm.js?e947:11141
WebSocketConnection.appendFrame_ # index.esm.js?e947:10727
WebSocketConnection.handleIncomingFrame # index.esm.js?e947:10772
mySock.onmessage # index.esm.js?e947:10673
setTimeout (async)
exceptionGuard # index.esm.js?e947:698
EventList.raise # index.esm.js?e947:9565
EventQueue.raiseQueuedEventsMatchingPredicate_ # index.esm.js?e947:9519
EventQueue.raiseEventsForChangedPath # index.esm.js?e947:9503
Repo.onDataUpdate_ # index.esm.js?e947:12730
PersistentConnection.onDataPush_ # index.esm.js?e947:12006
PersistentConnection.onDataMessage_ # index.esm.js?e947:12000
Connection.onDataMessage_ # index.esm.js?e947:11246
Connection.onPrimaryMessageReceived_ # index.esm.js?e947:11240
eval # index.esm.js?e947:11141
WebSocketConnection.appendFrame_ # index.esm.js?e947:10727
WebSocketConnection.handleIncomingFrame # index.esm.js?e947:10772
mySock.onmessage # index.esm.js?e947:10673
Before this error, I could see a list of all the emails and clicking on each, displays other details.
Suddenly, everything went south. I have cleared cache, still nothing changed. In fact, nothing displays in Microsoft edge, no warning, nothing in console at all.The errors are seen on chrome.
Any help will be appreciated please. Thanks
Watch out for asynchronous processes
console.log(data) can have a confusing output, because what you see in the console is what the value of data is at the time you are looking, which may not have been the value at the time it was actually printed to the console.
To avoid this, I suggest changing the console.log to the following:
console.log(JSON.stringify(data,null,2));
Doing a JSON.stringify forces Javascript to evaluate the object on-the-spot and turn it into a string.
I bet you a virtual $10 bill that when you make that change, you will see that data is rubbish at that time.
Why I am confident that data is at fault
Your error message was:
TypeError: Cannot read properties of undefined (reading 'AccountStatus')
This means that you were asking Javascript to read undefined.AccountStatus. The only place you could be doing that, i.e. the only place the word "AccountStatus" exists in your code, is here:
let key = item.key;
let data = item.val().Profile;
console.log(data);
_tutorials.push({
key: key,
status: data.AccountStatus, // ☚
email: data.Email,
});
This means that data must be undefined.
So if what you were printing out looked good, that must be a later value of data.
Surely the parameter to onDataChange should be a snapshot, not an array?
onDataChange(snapshot) {
const items = snapshot.val()
console.log("items is:\n", JSON.stringify(items, null, 2));
let _tutorials = [];
items.forEach((item) => {
let key = item.key;
let data = item.Profile;
_tutorials.push({
key: key,
status: data.AccountStatus,
email: data.Email,
});
});
this.tutorials = _tutorials;
}
Solution: items is now an object, rather than an array!
You say the code was working before, i.e. you were able to run
items.forEach( )
That means items was (before you made the change to the database) an array. That way, the elements of items were the item value, which in turn have the correct properties, including .AccountStatus.
In the current state of your database, it is clear that items is not an array at all! It is an object.
An array would appear like this:
items is:
[
{
"Bank details": {
"accountName": "Samuel Ankkk",
"accountNumber": "234567890",
"bank": "wensBANK"
},
"Location": {
"latitude": "9.0691414",
"longitude": "7.4a424",
"state": "Abuja"
},
"Profile": {
"AccountStatus": "Verified",
"Address": "Area B Opp Living Faith Church ",
...
But what you are seeing is this:
items is:
{
"1UWuDwL2WvWndqgUBweoUjeEEZk1": {
"Bank details": {
"accountName": "Samuel Ankkk",
"accountNumber": "234567890",
"bank": "wensBANK"
},
"Location": {
"latitude": "9.0691414",
"longitude": "7.4a424",
"state": "Abuja"
},
"Profile": {
"AccountStatus": "Verified",
"Address": "Area B Opp Living Faith Church ",
...
In other words, an array looks like this:
[ { "key": value, "anotherKey": anotherValue},
{ "key": value2, ... },
...
]
This can be accessed with .forEach
In contrast, an object looks like this:
{
"itemKey1": { "key": value, "anotherKey": anotherValue},
"itemKey2": { "key": value2, ... }
}
It cannot be accessed by .forEach. That is the error you are seeing.
What I think happened
You unknowingly changed the structure of items in Firebase. Instead of writing an array (or equivalently an object with keys 0, 1, 2, 3, 4 etc) you are now writing a real object with string keys.
Solution
Convert the object to an array, for example like this. Change this line:
items.forEach((item) => {
to this:
Object.values(items).forEach((item) => {
This is assuming you don't need the key of the item, e.g. "1UWuDwL2WvWndqgUBweoUjeEEZk1". To me, this string looks like a Firebase User ID. If so, you probably do want to extract it. For example:
Object.keys(items).forEach(userId => {
const item = items[userId];

Dynamically change select input in Bootstrap Vue

I am trying to set some objects in a Bootstrap-Vue form select which I get via JSON.
The JSON is made up of teacher objects from the following fields:
[
{
"id": 1,
"name": "John",
"surname": "Doe",
"email": "john.doe#gmail.com"
}
]
What I'm trying to do is put the name and surname in the select list, that is the full name.
I have already managed to do this via a computed property by processing the list.
But now I want that when I select a teacher, the list of courses is filtered according to the chosen teacher.
To do this I need the teacher's email, which I can't recover, having processed the teachers to get the full name.
Consequently, I can't even update the list of courses based on the teacher chosen.
This is the code for the template:
<b-form-group
id="input-group-3"
label="Docente:"
label-for="input-3"
>
<b-form-select
v-model="teacher"
:options="teachers"
value-field="item"
text-field="fullName"
required
#change="filterCourse"
></b-form-select>
<div class="mt-3">
Selected: <strong>{{ teacher }}</strong>
</div>
</b-form-group>
This is the script code:
import { mapGetters, mapActions } from "vuex";
export default {
data() {
return {
teacher: "",
course: "",
};
},
created: function() {
this.GetActiveTeachers();
this.GetActiveCourses();
},
computed: {
...mapGetters({
ActiveTeacherList: "StateActiveTeachers",
ActiveCourseList: "StateActiveCourses",
FilteredTeacherList: "StateTeacherByCourse",
FilteredCourseList: "StateCourseByTeacher",
}),
teachers: function() {
let list = [];
this.ActiveTeacherList.forEach((element) => {
let teacher = element.name + " " + element.surname;
list.push(teacher);
});
return list;
},
},
methods: {
...mapActions([
"GetActiveTeachers",
"GetActiveCourses",
"GetCourseByTeacher",
"GetTeacherByCourse",
"AssignTeaching",
]),
async filterCourse() {
const Teacher = {
teacherEmail: "john.doe#gmail.com", // For testing purpose
};
try {
await this.GetCourseByTeacher(Teacher);
} catch {
console.log("ERROR");
}
},
async filterTeacher() {
const Course = {
title: "Programming", // For testing purpose
};
try {
await this.GetTeacherByCourse(Course);
} catch {
console.log("ERROR");
}
},
},
};
You're currently using the simplest notation that Bootstrap Vue offers for form selects, an array of strings.
I suggest you switch to use their object notation, which will allow you to specify the text (what you show in the list) separately from the value (what's sent to the select's v-model).
This way, you'll be able to access all the data of the teacher object that you need, while still being able to display only the data you'd like.
We can do this by swapping the forEach() in your teachers computed property for map():
teachers() {
return this.ActiveTeacherList.map((teacher) => ({
text: teacher.name + " " + teacher.surname,
value: teacher
}));
},
Then, all you need to do is update your filterCourse() handler to use the new syntax, eg.:
async filterCourse() {
const Teacher = {
teacherEmail: this.teacher.email,
};
try {
await this.GetCourseByTeacher(Teacher);
} catch {
console.log("ERROR");
}
},
As a final note, if you don't want or need the full object as the value, then you can mold it to be whatever you need, that's the beauty of this syntax.
For example, you want the full name and email, instead of the parts:
value: {
fullName: teacher.name + " " + teacher.surname,
email: teacher.email
}
Here's two different options you can do.
One would be to generate the <option>'s inside the select yourself, using a v-for looping over your teachers, and binding the email property to the value, and displaying the name and surname inside the option.
This will make your <b-select>'s v-model return the chosen teachers e-mail, which you can then use in your filter.
new Vue({
el: '#app',
data() {
return {
selectedTeacher: null,
activeTeachers: [{
"id": 1,
"name": "Dickerson",
"surname": "Macdonald",
"email": "dickerson.macdonald#example.com"
},
{
"id": 2,
"name": "Larsen",
"surname": "Shaw",
"email": "larsen.shaw#example.com"
},
{
"id": 3,
"name": "Geneva",
"surname": "Wilson",
"email": "geneva.wilson#example.com"
}
]
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.js"></script>
<div id="app">
<b-select v-model="selectedTeacher">
<option v-for="teacher in activeTeachers" :value="teacher.email">
{{ teacher.name }} {{ teacher.surname }}
</option>
</b-select>
{{ selectedTeacher }}
</div>
The other option would be to change your computed to return an array of objects instead of simple strings as you're currently doing.
By default <b-select> expects the properties value and text if you use an array of objects in the options prop.
Here you would bind the email for each teacher to the value, and the name and surname to the text prop.
This will make your <b-select>'s v-model return the chosen teachers e-mail, which you can then use in your filter.
Reference: https://bootstrap-vue.org/docs/components/form-select#options-property
new Vue({
el: '#app',
data() {
return {
selectedTeacher: null,
activeTeachers: [{
"id": 1,
"name": "Dickerson",
"surname": "Macdonald",
"email": "dickerson.macdonald#example.com"
},
{
"id": 2,
"name": "Larsen",
"surname": "Shaw",
"email": "larsen.shaw#example.com"
},
{
"id": 3,
"name": "Geneva",
"surname": "Wilson",
"email": "geneva.wilson#example.com"
}
]
}
},
computed: {
teacherOptions() {
return this.activeTeachers.map(teacher => ({
value: teacher.email,
text: `${teacher.name} ${teacher.surname}`
}));
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.js"></script>
<div id="app">
<b-select v-model="selectedTeacher" :options="teacherOptions"></b-select>
{{ selectedTeacher }}
</div>

How to have parent data be updated by child component with multiple values

Code below.
I think I'm missing a crucial piece here. I've been through the docs and watched the entire vue2 step by step. Everything is making sense so far but I'm stuck on what seems to be a core piece. Any help would be appreciated. If this is totally wrong, please let me know, I'm not married to any of this stuff.
Desired functionality: There is an order Vue instance and it has line items.
On order.mounted() we hit an api endpoint for the order's data, including possible existing line items. If there are existing line items, we set that order data (this.lineitems = request.body.lineitems or similar). This part works fine and I can get the order total since the orders' line items are up to date at this point.
Each line item is an editable form with a quantity and a product . If I change the quantity or product of any line item, I want the child line-item component to notify the parent component that it changed, then the parent will update its own lineitems data array with the new value, and preform a POST request with all current line item data so the server side can calculate the new line item totals (many specials, discounts, etc). This will return a full replacement array for the order's line item data, which in turn would passed down to the line items to re-render.
Problems:
The line-items components "update..." methods are feeling obviously wrong, but my biggest issue is understanding how to get the parent to update its own line items data array with the new data. for instance
​
lineitems = [
{id: 1000, quantity: 3, product: 555, total: 30.00},
{id: 1001, quantity: 2, product: 777, total: 10.00}
]
If the second line item is changed to quantity 1, how do I get the parent's lineitems data to change to this? My main problem is that I don't know how the parent is suppose to know which of its own lineitems data array need to be modified, and how to grab the data from the changed child. I assume it came in via an event, via emit, but do I now need to pass around the primary key everywhere so I can do loops and compare? What if its a new line item and there is no primary key yet?
Mentioned above, I'm using the existing line item's DB primary key as the v-for key. What if I need a "new lineitem" that appends a blank lineitem below the existing ones, or if its a new order with no primary keys. How is this normally handled.
Is there a best practice to use for props instead of my "initial..." style? I assume just using $emit directly on the v-on, but I'm not sure how to get the relevant information to get passed that way.
This seems like the exact task that VueJS is suited for and I just feel like I keep chasing my tail in the wrong direction. Thanks for the help!
LineItem
Vue.component('line-item', {
props: ["initialQuantity", "initialProduct", "total"],
data () {
return {
// There are more but limiting for example
quantity: initialQuantity,
product: initialProduct,
productOptions = [
{ id: 333, text: "Product A"},
{ id: 555, text: "Product B"},
{ id: 777, text: "Product C"},
]
}
},
updateQuantity(event) {
item = {
quantity: event.target.value,
product: this.product
}
this.$emit('update-item', item)
},
updateProduct(event) {
item = {
quantity: this.quantity,
product: event.target.value
}
this.$emit('update-item', item)
}
template: `
<input :value="quantity" type="number" #input="updateQuantity">
<select :value="product" #input="updateProduct">
<option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
</select>
Line Item Price: {{ total }}
<hr />
`
})
Order/App
var order = new Vue({
el: '#app',
data: {
orderPK: orderPK,
lineitems: []
},
mounted() {
this.fetchLineItems()
},
computed: {
total() {
// This should sum the line items, like (li.total for li in this.lineitems)
return 0.0
},
methods: {
updateOrder(item) {
// First, somehow update this.lineitems with the passed in item, then
fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems)
.then(resp => resp.json())
.then(data => {
this.lineitems = data.lineitems;
})
},
fetchLineItems() {
fetch(`domain.com/api/orders/${this.orderPK}`)
.then(resp => resp.json())
.then(data => {
this.lineitems = data.lineitems;
})
},
},
template: `
<div>
<h2 id="total">Order total: {{ total }}</h2>
<line-item v-for="item in lineitems"
#update-item="updateOrder"
:key="item.id"
:quantity="item.quantity"
:product="item.product"
:total="item.total"
></line-item>
</div>
`
})
Here's a list of problems in your attempt that would prevent it from displaying anything at all i.e.
quantity: initialQuantity, - surely you meant quantity: this.initialQuantity, ... etc for all the other such data
missing } for computed total
your line-item template is invalid - you have multiple "root" elements
And then there's some minor issues:
you want the #change handler for the select, not #input, if your code ran, you'd see the difference,
Similarly you want #change for input otherwise you'll be making fetch requests to change the items every keystroke, probably not what you'd want
So, despite all that, I've produced some working code that does all you need - mainly for my own "learning" though, to be fair :p
// ******** some dummy data and functions to emulate fetches
const products = [
{ id: 333, text: "Product A", unitPrice: 10},
{ id: 555, text: "Product B", unitPrice: 11},
{ id: 777, text: "Product C", unitPrice: 12},
];
let dummy = [
{id: 1, quantity:2, product: 333, total: 20},
{id: 2, quantity:3, product: 777, total: 36},
];
const getLineItems = () => new Promise(resolve => setTimeout(resolve, 1000, JSON.stringify({lineitems: dummy})));
const update = items => {
return new Promise(resolve => setTimeout(() => {
dummy = JSON.parse(items);
dummy.forEach(item =>
item.total = parseFloat(
(
item.quantity *
(products.find(p => p.id === item.product) || {unitPrice: 0}).unitPrice *
(item.quantity > 4 ? 0.9 : 1.0)
).toFixed(2)
)
);
let res = JSON.stringify({lineitems: dummy});
resolve(res);
}, 50));
}
//********* lineItem component
Vue.component('line-item', {
props: ["value"],
data () {
return {
productOptions: [
{ id: 333, text: "Product A"},
{ id: 555, text: "Product B"},
{ id: 777, text: "Product C"},
]
}
},
methods: {
doupdate() {
this.$emit('update-item', this.value.product);
}
},
template: `
<p>
<input v-model="value.quantity" type="number" #change="doupdate()"/>
<select v-model="value.product" #change="doupdate()">
<option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
</select>
Line Item Price: {{ '$' + value.total.toFixed(2) }}
</p>
`
})
//********* Order/App
const orderPK = '';
var order = new Vue({
el: '#app',
data: {
orderPK: orderPK,
lineitems: []
},
mounted() {
// initial load
this.fetchLineItems();
},
computed: {
carttotal() {
return this.lineitems.reduce((a, {total}) => a + total, 0)
}
},
methods: {
updateOrder(productCode) {
// only call update if the updated item has a product code
if (productCode) {
// real code would be
// fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems).then(resp => resp.json())
// dummy code is
update(JSON.stringify(this.lineitems)).then(data => JSON.parse(data))
.then(data => this.lineitems = data.lineitems);
}
},
fetchLineItems() {
// real code would be
//fetch(`domain.com/api/orders/${this.orderPK}`).then(resp => resp.json())
// dummy code is
getLineItems().then(data => JSON.parse(data))
.then(data => this.lineitems = data.lineitems);
},
addLine() {
this.lineitems.push({
id: Math.max([this.lineitems.map(({id}) => id)]) + 1,
quantity:0,
product: 0,
total: 0
});
}
},
template: `
<div>
<h2 id="total">Order: {{lineitems.length}} items, total: {{'$'+carttotal.toFixed(2)}}</h2>
<line-item v-for="(item, index) in lineitems"
:key="item.id"
v-model="lineitems[index]"
#update-item="updateOrder"
/>
<button #click="addLine()">
Add item
</button>
</div>
`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
</div>
note: there may be some inefficient code in there, please don't judge too harshly, I've only been using vuejs for a week

How to display paging results using paging id using ngx-datatable & angular?

Requirement:
Showing dynamic data using ngx-datatable and use paging using page id
Description :
I have a dynamic data where I am displaying it using the ngx-datatable in angular and here everything works but the issue I m not sure how to apply the paging using the page_id (sent to the server using post body). Here I am getting the page_id along with the API response this is 1st API call. here page_id has to be sent as a body for the very same API for getting the rest of results.
Ex: Suppose I have 20 results in the 1ST API call I will get the 10 records and a page id by using the page id how can I get the next 10 results
What I implemented:
Getting data and displaying it in table basic paging applied
Below is my code :
Result=[];
reorderable: boolean = true;
selected = [];
rows = [];
columns = [];
DataArray = [];
Results = {
"data": [
{
"_score": 0.36464313,
"_type": "data",
"_id": "abcd",
"_source": {
"filter": "data",
"information": {
"product_id": "abcd",
"creation_utctime": "1477335693653"
},
"enduser": "free"
},
"_index": "dell_laptop"
},
{
"_score": 0.36464314,
"_type": "data",
"_id": "abcde",
"_source": {
"filter": "data",
"information": {
"product_id": "abcde",
"creation_utctime": "1477335693653"
},
"enduser": "free"
},
"_index": "lenovo_laptop"
},
{
"_score": 0.36464314,
"_type": "data",
"_id": "abcdef",
"_source": {
"filter": "data",
"information": {
"product_id": "abcde",
"creation_utctime": "1477335693653"
},
"enduser": "free"
},
"_index": "lenovo_laptop"
}
],
"total": 4,
"page_id": "WpNdVJMMjlJVnJTYTFuUklB"
}
LoadInfo(){
this.DataArray = ["filter","information.product_id","information.creation_utctime","enduser"];
this.rows=[];
this.Result = this.Results['data'];
// tslint:disable-next-line: forin
for (var res in this.Result) {
var row = {};
for (var key in this.Result[res]['_source']) {
if (typeof this.Result[res]['_source'][key] === 'object') {
for (var k in this.Result[res]['_source'][key]) {
let temp = key + "." + k;
row[temp] = this.Result[res]['_source'][key][k];
}
} else {
row[key] = this.Result[res]['_source'][key]
}
row['_id'] = this.Result[res]['_id'];
}
this.rows.push(row);
}
console.log(this.rows);
}
onActivate(event) {
// console.log('Activate Event', event);
}
onSelect({ selected }) {
// console.log('Select Event', selected, this.selected);
this.selected.splice(0, this.selected.length);
this.selected.push(...selected);
}
HTML Code:
<button type="button" (click)="LoadInfo()">LoadData</button>
<div>
<ngx-datatable class="material ui" [rows]="rows" [columnMode]="'force'" [headerHeight]="50"
[footerHeight]="50" [limit]="2" [rowHeight]="'auto'" [reorderable]="reorderable" [selected]="selected"
[selectionType]="'checkbox'" [scrollbarH]="true" [sortType]="'multi'" (activate)="onActivate($event)"
(select)='onSelect($event)'>
<ngx-datatable-column [width]="30" [sortable]="true" [canAutoResize]="false" [draggable]="false"
[resizeable]="false" class="uih">
<ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn">
<input type="checkbox" [checked]="allRowsSelected" (change)="selectFn(!allRowsSelected)" />
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value" let-isSelected="isSelected"
let-onCheckboxChangeFn="onCheckboxChangeFn">
<input type="checkbox" [checked]="isSelected" (change)="onCheckboxChangeFn($event)" />
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column *ngFor="let attr of DataArray" [sortable]="true" prop={{attr}} name={{attr}}>
</ngx-datatable-column>
</ngx-datatable>
</div>
Stackblitz link: https://stackblitz.com/edit/angular-secw8u
Note: even though if there is pageid also some times after 10 records there may not be more records also
here api call is simple post request
api : https://xxxx.xxxx..com/<some method>
body: { "key1":"data1","key2":"data2","pageid":"ss"}
here in the first api call we wont send page id as after calling the first api call we will get response in that we will get the pageid and for the second api call i mean for paging then we have to use the pageid
For pagination you need to know total number of pages. Otherwise you need total number
of items along with number of items per page (to derive total number of pages). In your case
you only have a page-id which does not even say the which page you are on. The page-id
only gives you access to next page items.
This API is useful if you implement an infinite scroll feature. Otherwise you can only
implement a more button to which loads new items to table. The link
you provided in comments implements this more button feature.
So you can override the default footer of ngx-datatable to add your more button to load
more items to table.
<ngx-datatable-footer>
<ng-template ngx-datatable-footer-template
let-rowCount="rowCount"
let-pageSize="pageSize"
let-selectedCount="selectedCount"
let-curPage="curPage"
let-offset="offset"
let-isVisible="isVisible">
<div class="datatable-footer-inner selected-count">
<div class="page-count" *ngIf="selectedCount">
<span> {{selectedCount}} selected / </span> {{rowCount}} total
</div>
<div class="datatable-pager">
<ul class="pager">
<li class="pages active" role="button" aria-label="next" *ngIf="rowCount">
Next
</li>
</ul>
</div>
</div>
</ng-template>
</ngx-datatable-footer>
Stackblitz Demo

Vue - submitting dynamically created form

I am creating a form dynamically with the data that I get from the backend:
{
"title": "Contact Me",
"fields": [
{
"label": "Name",
"type": "textbox",
"required": "1"
},
{
"label": "Email",
"type": "email",
"required": "1"
},
{
"label": "Message",
"type": "textarea",
"required": "1"
},
{
"label": "Submit",
"type": "submit",
"required": null
}
]
}
In Vue the component where I am making this form looks like this:
<form #submit.prevent="submit()" class="form">
<template v-for="input in ninjaForm.fields">
<div v-if="input.type != 'submit' && input.type != 'textarea'" class="control">
<input
v-bind:value="form[input.label]"
class="input is-large"
:type="input.type"
:name="input.label.toLowerCase()"
:required="input.required == 1">
<label>{{ input.label }}</label>
</div>
<div v-if="input.type == 'textarea'" class="control">
<textarea
v-bind:value="form[input.label]"
class="textarea"
:name="input.label.toLowerCase()">
</textarea>
<label>{{ input.label }}</label>
</div>
<div v-if="input.type == 'submit'">
<button class="button is-primary">{{ input.label }}</button>
</div>
</template>
</form>
I would like to submit this data back to the backend, but I am not sure how to do that, I have tried with something like this:
data() {
return {
form: {},
};
},
methods: {
submit() {
let payload = {
headers: {
'Content-Type': 'application/json'
},
params: JSON.parse(JSON.stringify(this.form))
};
console.log(payload);
this.$backend.post('submit', null, payload)
.then(_ => {
this.response = 'Meldingen ble sendt!';
}, err => {
console.warn(err);
this.response = 'En feil oppstod under innsending av skjemaet, prøv igjen senere.';
});
}
}
But when I am doing console.log(this.form) I get an observer object, and if I do console.log(payload) I get an empty params property. What am I doing wrong and how should I fix this so that I can send form data as a params object?
I have tried with setting the form properties on created method, like this:
created() {
this.ninjaForm.fields.forEach(field => this.form[field.label.toLowerCase()] = '');
},
Which has made an object with properties that looks like this:
form: {
email:"",
message:"",
name:"",
submit:""
}
But, when I was submitting the form, the values of this properties where still empty:
v-bind:value="form[input.label.toLowerCase()]"
I have also tried with changing v-bind:value to v-model, but then I have got an error:
v-model does not support dynamic input types. Use v-if branches
instead.
Please check this thread:
https://github.com/vuejs/vue/issues/3915#issuecomment-294707727
Seems like it works with v-model. However, this doesn't work when you use a v-bind on the input type. The only workaround is to create a v-if for every possible form type. Really annoying, but there seems to be no apparent other solution.

Categories