MEAN-Stack - Cannot read property 'map' of undefined - sql

I'm developing a WebApp with MEANStack, using Sequelize to access SQL Databases. Unfortunately I get the following error on the client's side: core.js:1673 ERROR TypeError: Cannot read property 'map' of undefined
at MapSubscriber.project (tables.service.ts:39)
"Line 39" of the error is applicationsTables: tableData.applicationsTables.map(table => {
Here's how the DT on the server side looks like:
Data Table on the Browser - Server Side
And here's how the error on the client's side looks like:
Error Messages on the Chrome developers' tools view
Here's my code
tables-list.component.html
<mat-spinner *ngIf="isLoading"></mat-spinner>
<h1 class="mat-body-2">Process List </h1>
<mat-accordion multi="true" *ngIf="userIsAuthenticated && !isLoading">
<mat-expansion-panel>
<mat-expansion-panel-header>
Process List
</mat-expansion-panel-header>
<table mat-table [dataSource]="processTables" matSort class="mat-elevation-z8" *ngIf="userIsAuthenticated">
<!-- ProcessName Column -->
<ng-container matColumnDef="ProcessName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ProcessName </th>
<td mat-cell *matCellDef="let element"> {{element.ProcessName}} </td>
</ng-container>
<!-- PackageVersion Column -->
<ng-container matColumnDef="PackageVersion">
<th mat-header-cell *matHeaderCellDef mat-sort-header> PackageVersion </th>
<td mat-cell *matCellDef="let element"> {{element.PackageVersion}} </td>
</ng-container>
<!-- RobotType Column -->
<ng-container matColumnDef="RobotType">
<th mat-header-cell *matHeaderCellDef mat-sort-header> RobotType </th>
<td mat-cell *matCellDef="let element"> {{element.RobotType}} </td>
</ng-container>
<!-- PackagePath Column -->
<ng-container matColumnDef="PackagePath">
<th mat-header-cell *matHeaderCellDef mat-sort-header> PackagePath </th>
<td mat-cell *matCellDef="let element"> {{element.PackagePath}} </td>
</ng-container>
<!-- CreationTime Column -->
<ng-container matColumnDef="CreationTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> CreationTime </th>
<td mat-cell *matCellDef="let element"> {{element.CreationTime}} </td>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="Status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Status </th>
<td mat-cell *matCellDef="let element"> {{element.Status}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedprocessTablesColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedprocessTablesColumns;"></tr>
</table>
</mat-expansion-panel>
</mat-accordion>
<br> <h1 class="mat-body-2">Applications List </h1>
tables-list.component.ts:
import { Component, OnInit, OnDestroy } from "#angular/core";
import { Table, ApplicationsTable } from "./tables.model";
import { PageEvent } from "#angular/material";
import { Subscription } from "rxjs";
import { TablesService } from "./tables.service";
import { AuthService } from "../auth/auth.service";
#Component({
// We load the component via routing and therefore we do not need a selector
selector: "app-tables",
templateUrl: "./tables-list.component.html",
styleUrls: ["./tables-list.component.css"]
}) // Turn class into component by adding #Component Decorator
export class TableListComponent implements OnInit, OnDestroy {
processTables: Table[] = [];
applicationsTables: ApplicationsTable[] = [];
isLoading = false;
totalTables = 0;
tablesPerPage = 5;
currentPage = 1;
pageSizeOptions = [1, 2, 5, 10];
displayedprocessTablesColumns: string[] = ["ProcessName", "PackageVersion", "RobotType", "PackagePath", "CreationTime", "Status" ];
userIsAuthenticated = false;
userId: string;
isAdmin: boolean;
private tablesSub: Subscription;
private authStatusSub: Subscription;
constructor(
public tablesService: TablesService,
private authService: AuthService
) {}
ngOnInit() {
this.isLoading = true;
this.tablesService.getTables(this.tablesPerPage, this.currentPage);
this.userId = this.authService.getUserId();
this.tablesSub = this.tablesService
.getTableUpdateListener()
.subscribe((tableData: { processTables: Table[]; applicationsTables: ApplicationsTable[]; tableCount: number }) => {
this.isLoading = false;
this.totalTables = tableData.tableCount;
this.processTables = tableData.processTables;
this.applicationsTables = tableData.applicationsTables;
console.log(tableData.applicationsTables);
});
this.userIsAuthenticated = this.authService.getIsAuth();
// console.log("Is authenticated: " + this.userIsAuthenticated);
this.authStatusSub = this.authService
.getAuthStatusListener()
.subscribe(isAuthenticated => {
this.userIsAuthenticated = isAuthenticated;
});
}
onChangedPage(pageData: PageEvent) {
this.isLoading = true;
this.currentPage = pageData.pageIndex + 1;
this.tablesPerPage = pageData.pageSize;
this.tablesService.getTables(this.tablesPerPage, this.currentPage);
}
onLogout() {
this.authService.logout();
}
ngOnDestroy() {
this.tablesSub.unsubscribe();
this.authStatusSub.unsubscribe();
}
}
Tables.service.ts:
import { Injectable } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { Subject } from "rxjs";
import { map } from "rxjs/operators";
import { Router } from "#angular/router";
import { environment } from "../../environments/environment";
import { Table, ApplicationsTable } from "./tables.model";
const BACKEND_URL = environment.apiUrl + "/tables/";
#Injectable({ providedIn: "root" })
export class TablesService {
private processTables: Table[] = [];
private applicationsTables: ApplicationsTable[] = [];
private tablesUpdated = new Subject<{ processTables: Table[]; applicationsTables: ApplicationsTable[]; tableCount: number }>();
constructor(private http: HttpClient, private router: Router) {}
getTables(tablesPerPage: number, currentPage: number) {
const queryParams = `?pagesize=${tablesPerPage}&page=${currentPage}`;
this.http
.get<{ processTables: Table[]; applicationsTables: ApplicationsTable[]; maxTables: number }>(
BACKEND_URL + queryParams
)
.pipe(
map((tableData: { processTables: Table[]; applicationsTables: ApplicationsTable[]; maxTables: number }) => {
return {
processTables: tableData.processTables.map(table => {
return {
ProcessName: table.ProcessName,
PackageVersion: table.PackageVersion,
RobotType: table.RobotType,
PackagePath: table.PackagePath,
CreationTime: table.CreationTime,
Status: table.Status
};
}),
applicationsTables: tableData.applicationsTables.map(table => {
return {
ProcessName: table.ProcessName,
PackageVersion: table.PackageVersion,
WorkflowsBelongingToProcess: table.WorkflowsBelongingToProcess,
ApplicationsBelongingToWorkflow: table.ApplicationsBelongingToWorkflow
};
}),
maxTables: tableData.maxTables
};
})
)
.subscribe(transformedTablesData => {
this.processTables = transformedTablesData.processTables;
this.tablesUpdated.next({
processTables: [...this.processTables],
applicationsTables: [...this.applicationsTables],
tableCount: transformedTablesData.maxTables
});
});
}
getTableUpdateListener() {
return this.tablesUpdated.asObservable();
}
getTable(id: string) {
return this.http.get<{
ProcessName: string;
PackageVersion: string;
RobotType: string;
PackagePath: string;
CreationTime: string;
Status: string;
}>(BACKEND_URL + id);
}
}
Tables\model.ts:
export interface Table {
ProcessName: string;
PackageVersion: string;
RobotType: string;
PackagePath: string;
CreationTime: string;
Status: string;
}
export interface ApplicationsTable {
ProcessName: string;
PackageVersion: string;
WorkflowsBelongingToProcess: string;
ApplicationsBelongingToWorkflow: string;
}
Backend\models\tables.js:
Backend\controllers\tables.js:
const sequelize = require("../sequelize");
exports.getProcessTables = (req, res) => {
sequelize.query("SELECT * FROM dbo.Process", { type: sequelize.QueryTypes.SELECT})
.then(fetchedtables => {
res.status(200).json({
message: "Process table fetched from the server",
processTables: fetchedtables,
maxProcessTables: fetchedtables.length
});
});
};
exports.getApplicationsTables = (req, res) => {
sequelize.query("SELECT * FROM dbo.Applications", { type: sequelize.QueryTypes.SELECT})
.then(fetchedtables => {
res.status(200).json({
message: "Applications Table fetched from the server",
applicationTables: fetchedtables,
maxApplicationsTables: fetchedtables.length
});
});
};
Backend\routes\tables.js:
const express = require("express");
const TableController = require("../controllers/tables")
const router = express.Router({ mergeParams: true });
router.get("", TableController.getProcessTables);
router.get("", TableController.getApplicationsTables);
module.exports = router;
How can I fix it?
Many Thanks
Gennaro

You're returning an object which has a property 'retrievedTables' from the server but on the client you are trying to access 'tables' which doesn't exist.
You can fix this in either Backend\controllers\tables.js or Tables.service.ts. To fix it on the server, just change retrievedTables: tables to tables: tables so the client is getting the fields it expects. If you were to fix it on the client instead you would need to reference retrievedTables rather than tables and update your types accordingly. You're also not sending a maxTables from the server so you will want to add that. Perhaps, maxTables: tables.length.
You also need to make sure you correctly reference the property names. On the server you are sending table properties with an uppercase first letter and on the client you are reading properties with a lowercase first letter which leads to it being undefined.
Be careful with type definitions too. In this case, you are saying that the tables property of the return object of the this.http.get is of type any and then try to call its map method. Only certain types have a map method so be more specific about what you are expecting. Even specifying the type as an array of any types is better as you then guarantee the map method:
this.http
.get<{ tables: any[]; maxTables: number }>(
BACKEND_URL + queryParams
)
That could still be improved further by specifying a specific type rather than any, especially as you go on to use its properties. The better type would be the same one used for the return type of getTable which can be defined as an interface for reuse.
interface TableInstance {
processName: string;
packageVersion: string;
robotType: string;
packagePath: string;
creationTime: string;
status: string;
}
Then, you would define the type returned from get as below.
.get<{ tables: TableInstance[]; maxTables: number }>(
You can set the type of the map function too, which will work instead of doing the above, which directly addresses the problematic line from your error message.
.pipe(
map((tableData: { tables: TableInstance[]; maxTables: number }) => {
return {

Related

Error in Vuex Computed property was assigned to but it has no setter

I am trying to display a list of Invites to groups.
Sometimes this component displays as expected, and sometimes not at all. This error returns:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Computed property "groupInvites" was assigned to but it has no setter.
found in
---> <InviteList> at src/components/Invites/InviteList.vue
<UserProfile> at src/views/UserProfile.vue
<App> at src/components/App.vue
<Root>
Here is the it component generating the error:
<template>
<div class="">
<h4 mt-10 class="text-warning">Your Current Invites:</h4>
<table class="table">
<thead>
<tr>
<th>Group Name</th>
<th>Invited By</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(invite, i) in groupInvites" :key="`${i}-${invite.id} `">
<td>
<router-link :to="`/groups/${invite.group_id}`" class="lightbox">
{{ invite.group_name }}
</router-link>
</td>
<td>{{ invite.sender_id }}</td>
<td>{{ moment(invite.created_at).strftime("%A, %d %b %Y %l:%M %p") }}</td>
<td scope="row">
<a class="btn btn-success mr-3" #click="acceptInvite(invite)">
Join Group
</a>
<a flat color="grey" #click="deleteInvite(invite.id)">
<i class="fa fa-trash " ></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import moment from 'moment-strftime';
import InvitesService from '../../services/InvitesService';
import UsersService from '../../services/UsersService';
export default {
name: "InviteList",
components: {
// NewInvite
},
props: {
// user: {
// type: Object,
// required: true
// },
},
computed: {
user() {
return this.$store.state.auth.user
},
groupInvites() {
return this.$store.state.invites.groupInvites;
}
},
mounted() {
this.getInvites();
},
methods: {
async getInvites () {
console.log('in invitelist, getting invites for user: ', this.user.id)
this.groupInvites = await this.$store.dispatch("getGroupInvites", this.user);
},
async getUser (id) {
this.sender = await UsersService.getUserById({
id: id
});
},
deleteInvite: async function (id) {
if(confirm("Do you really want to reject this invite?")) {
let response = await InvitesService.deleteInvite(id)
if (response.status === 200) {
console.log('In Invite.vue, invite deleted, about to emit');
this.$emit('invite-deleted');
}
}
this.getInvites();
},
async acceptInvite(invite) {
let result = await InvitesService.acceptInvite(invite.invitation_code)
this.$emit("membership-created");
console.log('this is the membership created: ', result.data)
// window.analytics.track('Group Joined', {
// title: this.group.name,
// user: this.$store.getters.user.username
// });
this.getInvites();
},
moment: function (datetime) {
return moment(datetime);
}
}
};
</script>
Separately, here is the store module:
import InvitesService from '#/services/InvitesService'
export const state = {
groupInvites: []
}
export const mutations = {
setGroupInvites(state, groupInvites) {
state.groupInvites = groupInvites;
}
}
export const actions = {
getGroupInvites({ commit }, user) {
console.log('in store. getting invites, for user: ', user.id)
InvitesService.getAllUserInvitation(user.id)
.then(resp => {
console.log('in store, getGroupInvites,this is groupInvites: ', resp.data);
commit('setGroupInvites', resp.data);
});
}
}
export const getters = {
}
Incidentally, getGroupInvites is being called twice. here are the console.logs:
in invitelist, getting invites for user: 9
invites.js?d00b:16 in store. getting invites, for user: 9
InvitesService.js?109c:10 in service getting all user invites for user: 9
invites.js?d00b:16 in store. getting invites, for user: undefined
InvitesService.js?109c:10 in service getting all user invites for user: undefined
notice user is undefined on the second go around.
It is possible to assign a value to a computed if you've defined it using a computed setter, but you haven't and probably don't need to. So this line is wrong and throws the error because it tries to do that:
this.groupInvites = await this.$store.dispatch("getGroupInvites", this.user);
But it's ok because this.groupInvites already gets its value reactively from the same state that getGroupInvites action populates anyway (state.groupInvites) so it's also unnecessary. Change that line to:
this.$store.dispatch("getGroupInvites", this.user);
and allow the computed to update itself.

sorting does not works with Virtual scrolling custom data source

Angular 8 brings really cool feature to implement virtual scroll. They have provide example here regarding usage of it. Although, its really difficult implement with mat-table.
I have been working on to implement virtual scroll on mat-table and found that, we need to have our own custom data source for that. I have create new data source by implementing DataSource class but what I found is that, I loose the sort and other features due to custom data source.
app.component.html
<cdk-virtual-scroll-viewport class="example-viewport">
<table class="table" mat-table #table matSort [dataSource]="dataSource" [multiTemplateDataRows]="true">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> id </th>
<td mat-cell *matCellDef="let element"> {{element.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name</th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<ng-container matColumnDef="age">
<th mat-header-cell *matHeaderCellDef> Age</th>
<td mat-cell *matCellDef="let element"> {{element.age}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<!-- cdkVirtualScrollViewport uses transform: translateY to correct for all elements that are removed.
We will use a plcaholder row in the table instead because the translate causes problems with the sticky header -->
<tr [style.height.px]="placeholderHeight" mat-row *matRowDef="let row; let index = index; columns: []; when: placeholderWhen"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</cdk-virtual-scroll-viewport>
app.component.ts
import { Component, OnInit, ViewChild, AfterViewInit } from "#angular/core";
import {
CdkVirtualScrollViewport,
FixedSizeVirtualScrollStrategy,
VIRTUAL_SCROLL_STRATEGY
} from "#angular/cdk/scrolling";
import { CollectionViewer } from "#angular/cdk/collections";
import { MatSort } from "#angular/material";
// import { GridTableDataSource } from './gridTable.datasource'
import { GridTableDataSource } from "./gridTableM.datasource";
const ROW_HEIGHT = 48;
/**
* Virtual Scroll Strategy
*/
export class CustomVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
constructor() {
super(ROW_HEIGHT, 1000, 2000);
}
attach(viewport: CdkVirtualScrollViewport): void {
this.onDataLengthChanged();
}
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
providers: [
{ provide: VIRTUAL_SCROLL_STRATEGY, useClass: CustomVirtualScrollStrategy }
]
})
export class AppComponent implements OnInit, AfterViewInit {
placeholderHeight = 0;
displayedColumns: string[] = ["id", "name", "age"];
dataSource: GridTableDataSource<any>;
rows = Array(200)
.fill(0)
.map((x, i) => {
return { name: "name" + i, id: i, age: 27 };
});
itemSize = 48;
#ViewChild(CdkVirtualScrollViewport, { static: true })
viewport: CdkVirtualScrollViewport;
#ViewChild(MatSort, { static: false }) sort: MatSort;
constructor() {}
ngOnInit() {
this.dataSource = new GridTableDataSource(
this.rows,
this.viewport,
this.itemSize
);
this.dataSource.offsetChange.subscribe(offset => {
this.placeholderHeight = offset;
});
this.dataSource.data = this.rows;
// debugger;
// this.dataSource.sort = this.sort; // this.sort is null here
}
ngAfterViewInit() {
debugger;
this.dataSource.sort = this.sort; // this.sort is null here as well
}
placeholderWhen(index: number, _: any) {
return index == 0;
}
}
To implement sorting and other feature, I just copied code from MatTableDataSource. but somehow, this.sort is always null in any method. I couldn't find reason for this. Can anyone explain this behavior?
Here is stackblitz to play with.
it doesn't work.
angular team doesn't support this
https://github.com/angular/components/issues/19003

VueJS method returns promise. How to display data in template

VueJS front uses axios to retrieve data from Express API. Data returns as promise. I can't figure out how to display it in the template. The goal is to add this data to be displayed within a leaderboard, so I am doing a first query to get the username and the score(which is a sum) and then a second to get the data on the last single activity by each user.
Here is the code.
Leaderboard component:
<template>
<table class="ui celled table">
<thead>
<tr><th colspan="4">Leaderboard for {{ currentGroup.name }}</th></tr>
<tr>
<th>Username</th>
<th>Last Action</th>
<th>Date</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr v-for="(leader) in leaderboard" :key="leader.users_id">
<td>
<h4 class="ui image header">
<img v-if="leader.avatar_url" :src="leader.avatar_url" class="ui mini rounded image">
<v-gravatar v-else :email="leader.email" class="ui mini rounded image" />
<div class="content">
{{leader.username}}
</div>
</h4>
</td>
<td>{{ lastActByUser(leader.users_id).deed }}</td>
<td></td>
<!-- <td>{{ lastAct(leader.id).deed }}</td>
<td>{{ moment(lastAct(leader.id).created_at).strftime("%A, %d %b %Y %l:%M %p") }}</td> -->
<td>{{leader.count}}</td>
</tr>
</tbody>
</table>
</template>
<script>
import moment from 'moment-strftime'
import _ from 'lodash'
import ReportsService from '#/services/ReportsService'
import ActsService from '#/services/ActsService'
export default {
name: "Leaderboard",
data() {
return {
}
},
computed: {
leaderboard () {
return this.$store.getters.leaderboard;
},
currentGroup () {
return this.$store.getters.currentGroup;
}
// ,
// lastAct (userId) {
// return _.orderBy(this.actsByUser(userId), 'created_at')[0];
// }
},
mounted () {
this.getLeaderboard();
},
methods: {
getLeaderboard: async function () {
console.log('currentGroup: ', this.$store.getters.currentGroup)
this.$store.dispatch("updateLeaderboard", this.$store.getters.currentGroup);
},
moment: function (datetime) {
return moment(datetime);
}
,
async lastActByUser (leader_id) {
console.log('getting last act for user')
const response = await ActsService.fetchLastActByUser ({
userId: leader_id
});
this.deed = response.data.deed
this.created_at = response.data.created_at
console.log('lastAct response: ', response.data)
}
}
};
</script>
here is ActsService:
import Api from '#/services/Api'
export default {
fetchActs () {
return Api().get('acts')
},
...
fetchLastActByUser (params) {
console.log('calling the last act service with userId: ', params.userId)
return Api().post('acts/last_by_user', params)
},
}
here is the Express route:
const express = require('express');
const User = require('../models/User');
const auth = require('../middlewares/authenticate');
const Act = require('../models/Act');
let router = express.Router();
router.get('/', async (req, res) => {
const acts = await Act
.query().eager('user');
res.json(acts);
});
...
// GET last act for specified user
router.post('/last_by_user', async (req, res, next) => {
const lastAct = await Act
.query()
.findOne({
users_id: req.body.userId
});
console.log('lastAct on server side is: ', lastAct)
res.json(lastAct);
})
module.exports = router;

:class not updating when clicked in vuejs v-for

I want to add a css class to a item in v-for when the td in clicked
<template>
<div>
<h1>Forces ()</h1>
<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>
<section v-if="loading">
<p>loading...</p>
</section>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th
#click="orderByName = !orderByName">Forces</th>
</tr>
<th>Delete</th>
</thead>
<tbody>
<tr v-for="(force, index) in forces" #click="completeTask(force)" :class="{completed: force.done}" v-bind:key="index">
<td>
<router-link :to="{ name: 'ListForce', params: { name: force.id } }">Link</router-link>
</td>
<th>{{ force.done }}</th>
<th>{{ force.name }}</th>
<th><button v-on:click="removeElement(index)">remove</button></th>
</tr>
</tbody>
</table>
<div>
</div>
</div>
</template>
<script>
import {APIService} from '../APIService';
const apiService = new APIService();
const _ = require('lodash');
export default {
name: 'ListForces',
components: {
},
data() {
return {
question: '',
forces: [{
done: null,
id: null,
name: null
}],
errored: false,
loading: true,
orderByName: false,
};
},
methods: {
getForces(){
apiService.getForces().then((data, error) => {
this.forces = data;
this.forces.map(function(e){
e.done = false;
});
this.loading= false;
console.log(this.forces)
});
},
completeTask(force) {
force.done = ! force.done;
console.log(force.done);
},
removeElement: function (index) {
this.forces.splice(index, 1);
}
},
computed: {
/* forcesOrdered() {
return this.orderByName ? _.orderBy(this.forces, 'name', 'desc') : this.forces;
}*/
},
mounted() {
this.getForces();
},
}
</script>
<style>
.completed {
text-decoration: line-through;
}
</style>
I want a line to go through the items when clicked, but the dom doesn't update. If I go into the vue tab in chrome I can see the force.done changes for each item but only if I click out of the object and click back into it. I'm not to sure why the state isn't updating as it's done so when I have used an click and a css bind before.
I've tried to make minimal changes to get this working:
// import {APIService} from '../APIService';
// const apiService = new APIService();
// const _ = require('lodash');
const apiService = {
getForces () {
return Promise.resolve([
{ id: 1, name: 'Red' },
{ id: 2, name: 'Green' },
{ id: 3, name: 'Blue' }
])
}
}
new Vue({
el: '#app',
name: 'ListForces',
components: {
},
data() {
return {
question: '',
forces: [{
done: null,
id: null,
name: null
}],
errored: false,
loading: true,
orderByName: false,
};
},
methods: {
getForces(){
apiService.getForces().then((data, error) => {
for (const force of data) {
force.done = false;
}
this.forces = data;
this.loading= false;
console.log(this.forces)
});
},
completeTask(force) {
force.done = ! force.done;
console.log(force.done);
},
removeElement: function (index) {
this.forces.splice(index, 1);
}
},
mounted() {
this.getForces();
}
})
.completed {
text-decoration: line-through;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<div>
<h1>Forces ()</h1>
<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>
<section v-if="loading">
<p>loading...</p>
</section>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th
#click="orderByName = !orderByName">Forces</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(force, index) in forces" #click="completeTask(force)" :class="{completed: force.done}" v-bind:key="index">
<th>{{ force.done }}</th>
<th>{{ force.name }}</th>
<th><button v-on:click="removeElement(index)">remove</button></th>
</tr>
</tbody>
</table>
<div>
</div>
</div>
</div>
The key problem was here:
this.forces = data;
this.forces.map(function(e){
e.done = false;
});
The problem here is that the property done is being added to the data after it has been made reactive. The reactivity observers get added as soon as the line this.forces = data runs. Adding done after that counts as adding a new property, which won't work.
It's also a misuse of map so I've switched it to a for/of loop instead.
for (const force of data) {
force.done = false;
}
this.forces = data; // <- data becomes reactive here, including 'done'
On an unrelated note I've tweaked the template to move the Delete header inside the top row.
Make sure force.done is set to false in data before initialization so it is reactive.
data() {
return {
force:{
done: false;
}
}
}
If force exists but has no done member set, you can use Vue.set instead of = to create a reactive value after initialization.
Vue.set(this.force, 'done', true);

MEANStack Angular Table - Sorting and Pagination Functionalities

I'm developing a WebApp with MEANStack, currently working on the Front End with Angular. My App simply reads SQL Data Tables and displays them on a browser in the form of Angular Tables.
Here's how it looks like:
Data Table Display MEANStack App
Unfortunately the Sort functionality does not work. Any idea why? Furthermore, how can I implement the "scroll left and right" functionality, given that my tables are a bit large?
Here's my code (Please do not hesitate to ask for more code):
tables-list.component.html
<mat-spinner *ngIf="isLoading"></mat-spinner>
<h1 class="mat-body-2">Process List </h1>
<mat-accordion multi="true" *ngIf="userIsAuthenticated && !isLoading">
<mat-expansion-panel>
<mat-expansion-panel-header>
Process List
</mat-expansion-panel-header>
<table mat-table [dataSource]="processTables" matSort class="mat-elevation-z8" *ngIf="userIsAuthenticated">
<!-- ProcessID Column -->
<ng-container matColumnDef="ProcessID">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ProcessID </th>
<td mat-cell *matCellDef="let element"> {{element.ProcessID}} </td>
</ng-container>
<!-- ProcessName Column -->
<ng-container matColumnDef="ProcessName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ProcessName </th>
<td mat-cell *matCellDef="let element"> {{element.ProcessName}} </td>
</ng-container>
<!-- PackageVersion Column -->
<ng-container matColumnDef="PackageVersion">
<th mat-header-cell *matHeaderCellDef mat-sort-header> PackageVersion </th>
<td mat-cell *matCellDef="let element"> {{element.PackageVersion}} </td>
</ng-container>
<!-- RobotType Column -->
<ng-container matColumnDef="RobotType">
<th mat-header-cell *matHeaderCellDef mat-sort-header> RobotType </th>
<td mat-cell *matCellDef="let element"> {{element.RobotType}} </td>
</ng-container>
<!-- PackagePath Column -->
<ng-container matColumnDef="PackagePath">
<th mat-header-cell *matHeaderCellDef mat-sort-header> PackagePath </th>
<td mat-cell *matCellDef="let element"> {{element.PackagePath}} </td>
</ng-container>
<!-- CreationTime Column -->
<ng-container matColumnDef="CreationTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> CreationTime </th>
<td mat-cell *matCellDef="let element"> {{element.CreationTime}} </td>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="Status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Status </th>
<td mat-cell *matCellDef="let element"> {{element.Status}} </td>
</ng-container>
<!-- Header and Row Declarations -->
<tr mat-header-row *matHeaderRowDef="displayedprocessTablesColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedprocessTablesColumns;"></tr>
</table>
</mat-expansion-panel>
</mat-accordion>
<br> <h1 class="mat-body-2">Applications List </h1>
<mat-accordion multi="true" *ngIf="userIsAuthenticated && !isLoading">
<mat-expansion-panel>
<mat-expansion-panel-header>
Applications List
</mat-expansion-panel-header>
<table mat-table [dataSource]="applicationsTables" matSort class="mat-elevation-z8" *ngIf="userIsAuthenticated">
<!-- ProcessName Column -->
<ng-container matColumnDef="ProcessName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ProcessName </th>
<td mat-cell *matCellDef="let element"> {{element.ProcessName}} </td>
</ng-container>
<!-- PackageVersion Column -->
<ng-container matColumnDef="PackageVersion">
<th mat-header-cell *matHeaderCellDef mat-sort-header> PackageVersion </th>
<td mat-cell *matCellDef="let element"> {{element.PackageVersion}} </td>
</ng-container>
<!-- WorkflowsBelongingToProcess Column -->
<ng-container matColumnDef="WorkflowsBelongingToProcess">
<th mat-header-cell *matHeaderCellDef mat-sort-header> WorkflowsBelongingToProcess </th>
<td mat-cell *matCellDef="let element"> {{element.WorkflowsBelongingToProcess}} </td>
</ng-container>
<!-- ApplicationsBelongingToWorkflow Column -->
<ng-container matColumnDef="ApplicationsBelongingToWorkflow">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ApplicationsBelongingToWorkflow </th>
<td mat-cell *matCellDef="let element"> {{element.ApplicationsBelongingToWorkflow}} </td>
</ng-container>
<!-- Header and Row Declarations -->
<tr mat-header-row *matHeaderRowDef="displayedApplicationsTablesColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedApplicationsTablesColumns;"></tr>
</table>
</mat-expansion-panel>
</mat-accordion>
tables.module.ts
import { NgModule } from "#angular/core";
import { ReactiveFormsModule } from "#angular/forms";
import { CommonModule } from "#angular/common";
import { RouterModule } from "#angular/router";
import { TableListComponent } from "./tables-list.component";
import { AngularMaterialModule } from "../angular-material.module";
#NgModule({
declarations: [TableListComponent],
imports: [
CommonModule,
ReactiveFormsModule,
AngularMaterialModule,
RouterModule
]
})
export class TablesModule {}
angular-material.module.ts
import { NgModule } from "#angular/core";
import {
MatInputModule,
MatCardModule,
MatButtonModule,
MatToolbarModule,
MatExpansionModule,
MatProgressSpinnerModule,
MatPaginatorModule,
MatDialogModule,
MatSelectModule,
MatListModule,
MatIconModule,
MatTableModule,
MatSortModule,
MatTabsModule,
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonToggleModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatGridListModule,
MatDividerModule,
MatMenuModule,
MatNativeDateModule,
MatProgressBarModule,
MatRadioModule,
MatRippleModule,
} from "#angular/material";
import { CdkTableModule } from "#angular/cdk/table";
import { CdkTreeModule } from "#angular/cdk/tree";
#NgModule({
exports: [
MatInputModule,
MatCardModule,
MatButtonModule,
MatToolbarModule,
MatExpansionModule,
MatProgressSpinnerModule,
MatPaginatorModule,
MatDialogModule,
MatSelectModule,
MatListModule,
MatIconModule,
MatTableModule,
MatSortModule,
MatTabsModule,
CdkTableModule,
CdkTreeModule,
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonToggleModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatDividerModule,
MatGridListModule,
MatMenuModule,
MatNativeDateModule,
MatProgressBarModule,
MatRadioModule,
MatRippleModule,
]
})
export class AngularMaterialModule {}
tables-list.component.ts:
import { Component, OnInit, OnDestroy, ViewChild } from "#angular/core";
import { ProcessTable, ApplicationsTable } from "./tables.model";
import { PageEvent, MatTableDataSource, MatSort } from "#angular/material";
import { Subscription } from "rxjs";
import { TablesService } from "./tables.service";
import { AuthService } from "../auth/auth.service";
#Component({
// We load the component via routing and therefore we do not need a selector
selector: "app-tables",
templateUrl: "./tables-list.component.html",
styleUrls: ["./tables-list.component.css"]
}) // Turn class into component by adding #Component Decorator
export class TableListComponent implements OnInit, OnDestroy {
processTables: ProcessTable[] = [];
applicationsTables: ApplicationsTable[] = [];
isLoading = false;
totalTables = 0;
tablesPerPage = 5;
currentPage = 1;
pageSizeOptions = [1, 2, 5, 10];
dataSource = new MatTableDataSource(this.processTables);
#ViewChild(MatSort) sort: MatSort;
displayedprocessTablesColumns: string[] = [
"ProcessID",
"ProcessName",
"PackageVersion",
"RobotType",
"PackagePath",
"CreationTime",
"Status"
];
displayedApplicationsTablesColumns: string[] = [
"ProcessName",
"PackageVersion",
"WorkflowsBelongingToProcess",
"ApplicationsBelongingToWorkflow"
];
userIsAuthenticated = false;
userId: string;
isAdmin: boolean;
private tablesSub: Subscription;
private authStatusSub: Subscription;
constructor(
public tablesService: TablesService,
private authService: AuthService
) {}
ngOnInit() {
this.isLoading = true;
this.dataSource.sort = this.sort;
this.tablesService.getProcessTables(this.tablesPerPage, this.currentPage);
this.userId = this.authService.getUserId();
this.tablesSub = this.tablesService
.getTableUpdateListener()
.subscribe((tableData: { processTables: ProcessTable[]; applicationsTables: ApplicationsTable[]; tableCount: number }) => {
this.isLoading = false;
this.totalTables = tableData.tableCount;
this.processTables = tableData.processTables;
this.applicationsTables = tableData.applicationsTables;
});
this.userIsAuthenticated = this.authService.getIsAuth();
// console.log("Is authenticated: " + this.userIsAuthenticated);
this.authStatusSub = this.authService
.getAuthStatusListener()
.subscribe(isAuthenticated => {
this.userIsAuthenticated = isAuthenticated;
});
}
onLogout() {
this.authService.logout();
}
ngOnDestroy() {
this.tablesSub.unsubscribe();
this.authStatusSub.unsubscribe();
}
}
Tables.service.ts:
import { Injectable } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { Subject } from "rxjs";
import { map } from "rxjs/operators";
import { Router } from "#angular/router";
import { environment } from "../../environments/environment";
import { ProcessTable, ApplicationsTable } from "./tables.model";
const BACKEND_URL = environment.apiUrl + "/tables/";
#Injectable({ providedIn: "root" })
export class TablesService {
private processTables: ProcessTable[] = [];
private applicationsTables: ApplicationsTable[] = [];
private tablesUpdated = new Subject<{ processTables: ProcessTable[]; applicationsTables: ApplicationsTable[]; tableCount: number }>();
constructor(private http: HttpClient, private router: Router) {}
getProcessTables(tablesPerPage: number, currentPage: number) {
const queryParams = `?pagesize=${tablesPerPage}&page=${currentPage}`;
this.http
.get<{ processTables: ProcessTable[]; applicationsTables: ApplicationsTable[]; maxTables: number }>(
BACKEND_URL + queryParams
)
.pipe(
map((tableData: { processTables: ProcessTable[]; applicationsTables: ApplicationsTable[]; maxTables: number }) => {
return {
processTables: tableData.processTables.processTables.map(table => {
return {
ProcessID: table.ProcessID
ProcessName: table.ProcessName,
PackageVersion: table.PackageVersion,
RobotType: table.RobotType,
PackagePath: table.PackagePath,
CreationTime: table.CreationTime,
Status: table.Status
};
}),
applicationsTables: tableData.applicationsTables.applicationsTables.map(table => {
return {
ProcessName: table.ProcessName,
PackageVersion: table.PackageVersion,
WorkflowsBelongingToProcess: table.WorkflowsBelongingToProcess,
ApplicationsBelongingToWorkflow: table.ApplicationsBelongingToWorkflow
};
}),
maxTables: tableData.maxTables
};
})
)
.subscribe(transformedTablesData => {
this.processTables = transformedTablesData.processTables;
this.applicationsTables = transformedTablesData.applicationsTables;
this.tablesUpdated.next({
processTables: [...this.processTables],
applicationsTables: [...this.applicationsTables],
tableCount: transformedTablesData.maxTables
});
});
}
getTableUpdateListener() {
return this.tablesUpdated.asObservable();
}
}
Tables\model.ts:
export interface Table {
ProcessID: string;
ProcessName: string;
PackageVersion: string;
RobotType: string;
PackagePath: string;
CreationTime: string;
Status: string;
}
export interface ApplicationsTable {
ProcessName: string;
PackageVersion: string;
WorkflowsBelongingToProcess: string;
ApplicationsBelongingToWorkflow: string;
}
Backend\controllers\tables.js:
const sequelize = require("../sequelize");
const getProcessTables = (req, res) => {
return sequelize
.query("SELECT * FROM dbo.Process", { type: sequelize.QueryTypes.SELECT })
.then(fetchedtables => {
return {
message: "Process table fetched from the server",
processTables: fetchedtables,
maxProcessTables: fetchedtables.length
};
});
};
const getApplicationsTables = (req, res) => {
return sequelize
.query("SELECT * FROM dbo.Applications", {
type: sequelize.QueryTypes.SELECT
})
.then(fetchedtables => {
return {
message: "Applications Table fetched from the server",
applicationsTables: fetchedtables,
maxApplicationsTables: fetchedtables.length
};
});
};
exports.getTables = (req, res) => {
return Promise.all([
getApplicationsTables(req, res),
getProcessTables(req, res)
]).then(tables => {
res.status(200).json({
applicationsTables: tables[0],
processTables: tables[1]
});
});
};
Backend\routes\tables.js:
const express = require("express");
const TableController = require("../controllers/tables")
const router = express.Router({ mergeParams: true });
router.get("", TableController.getTables);
module.exports = router;
How can I fix it?
Many Thanks
Gennaro
I believe you are using Angular Material 7.x.x. Can we assign datasource to MatTableSource()? It has more rich properties to encapsulate the sorting, pagination,...
Now that you are using any instance of processTables object. Sorting or pagination cannot be linked. Please use this example https://stackblitz.com/angular/pakljarjylj?file=app%2Ftable-sorting-example.ts
tables-list.component.ts:
import { Component, OnInit, OnDestroy } from "#angular/core";
import { ProcessTable, ApplicationsTable } from "./tables.model";
import { PageEvent } from "#angular/material";
import { Subscription } from "rxjs";
import { TablesService } from "./tables.service";
import { AuthService } from "../auth/auth.service";
#Component({
// We load the component via routing and therefore we do not need a selector
selector: "app-tables",
templateUrl: "./tables-list.component.html",
styleUrls: ["./tables-list.component.css"]
}) // Turn class into component by adding #Component Decorator
export class TableListComponent implements OnInit, OnDestroy {
// ....other lines
dataSource = new MatTableDataSource(processTables);
// ....other lines
#ViewChild(MatSort) sort: MatSort;
// ....other lines
ngOnInit() {
this.dataSource.sort = this.sort;
}
// ....other lines
}