Implementing Material Table with api call Angular - node.js

I have been developing an application with Angular 2 and it has admittedly been a struggle doing so as a new developer. I've managed quite a bit so far but I do need some assistance. I'm including a plunkr that I'm using for reference to get a Material Table with pagination, filter and sort, but this example, as well as all of the other examples on material.angular.io show an example with a database that's essentially hardcoded/generated in the component class. I have a service that calls the api for an SQL query and I'd like to populate the table in the example with that, however my attempts so far have been miserable failures and I think I've overwhelmed myself in the process.
By request I can post my component code, but I fear that I've gutted/revised it beyond the point of any use. But until then below is the plunkr with what I want to implement, and the service class I'd like to use to populate the data table in place of the plunkr's database and data source.
Please let me know if you can help, you'd be saving me a huge headache.
https://plnkr.co/edit/EU3BBlViWpPf2NJW4PXx?p=preview
My service
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class RcgqueueService {
constructor(private http: Http) { }
populateRCGQueue() {
return this.http.get('/api/rcgqueue').map(res => res.json());
}
}
And my current pathetic attempt at the component code
import { Component, ElementRef, ViewChild, OnInit, forwardRef } from '#angular/core';
import { DataSource, SelectionModel } from '#angular/cdk/collections';
import { MatPaginator, MatSort, MatTable } from '#angular/material';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/debounceTime';
import { RcgqueueService } from './rcgqueue.service';
#Component({
selector: 'app-rcgqueue',
templateUrl: './rcgqueue.component.html',
styleUrls: ['./rcgqueue.component.css']
})
export class RcgqueueComponent implements OnInit {
isDataAvailable = false;
displayedColumns = ['changeId', 'changeTitle', 'dateSubmitted', 'changeSponsor', 'changeDescription'];
dataChange: BehaviorSubject<ChangeData[]> = new BehaviorSubject<ChangeData[]>([]);
get data(): ChangeData[] { return this.dataChange.value; }
dataSource: ExampleDataSource | null;
#ViewChild(MatPaginator) paginator: MatPaginator;
#ViewChild(forwardRef(() => MatSort)) sort: MatSort;
#ViewChild('filter') filter: ElementRef;
constructor(private rcgservice: RcgqueueService) {
}
populateRCGQueue() {
this.rcgservice.populateRCGQueue().subscribe(rcgitems => {
this.dataChange = rcgitems;
this.isDataAvailable = true;
})
}
ngOnInit() {
this.populateRCGQueue();
this.dataSource = new ExampleDataSource(this, this.paginator, this.sort);
Observable.fromEvent(this.filter.nativeElement, 'keyup')
.debounceTime(150)
.distinctUntilChanged()
.subscribe(() => {
if (!this.dataSource) { return; }
this.dataSource.filter = this.filter.nativeElement.value;
});
}
}
export interface ChangeData {
ChangeId: string;
ChangeTitle: string;
DateSubmitted: string;
ChangeSponsor: string;
ChangeDescription: string;
}
/**
* Data source to provide what data should be rendered in the table. Note that the data source
* can retrieve its data in any way. In this case, the data source is provided a reference
* to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
* the underlying data. Instead, it only needs to take the data and send the table exactly what
* should be rendered.
*/
export class ExampleDataSource extends DataSource<any> {
_filterChange = new BehaviorSubject('');
get filter(): string { return this._filterChange.value; }
set filter(filter: string) { this._filterChange.next(filter); }
dataChange: BehaviorSubject<ChangeData[]> = new BehaviorSubject<ChangeData[]>([]);
get data(): ChangeData[] { return this.dataChange.value; }
filteredData: ChangeData[] = [];
renderedData: ChangeData[] = [];
constructor(private rcgcomponent: RcgqueueComponent,
private _paginator: MatPaginator,
private _sort: MatSort) {
super();
this._filterChange.subscribe(() => this._paginator.pageIndex = 0);
}
/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<ChangeData[]> {
// Listen for any changes in the base data, sorting, filtering, or pagination
const displayDataChanges = [
this.rcgcomponent.dataChange,
this._sort.sortChange,
this._filterChange,
this._paginator.page,
];
return Observable.merge(...displayDataChanges).map(() => {
// Filter data
this.filteredData = this.rcgcomponent.data.slice().filter((item: ChangeData) => {
const searchStr = (item.ChangeDescription + item.ChangeSponsor).toLowerCase();
return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
});
// Sort filtered data
const sortedData = this.sortData(this.filteredData.slice());
// Grab the page's slice of the filtered sorted data.
const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
this.renderedData = sortedData.splice(startIndex, this._paginator.pageSize);
return this.renderedData;
});
}
disconnect() { }
/** Returns a sorted copy of the database data. */
sortData(data: ChangeData[]): ChangeData[] {
if (!this._sort.active || this._sort.direction === '') { return data; }
return data.sort((a, b) => {
let propertyA: number | string = '';
let propertyB: number | string = '';
switch (this._sort.active) {
case 'changeId': [propertyA, propertyB] = [a.ChangeId, b.ChangeId]; break;
case 'changeTitle': [propertyA, propertyB] = [a.ChangeTitle, b.ChangeTitle]; break;
case 'dateSubmitted': [propertyA, propertyB] = [a.DateSubmitted, b.DateSubmitted]; break;
case 'changeSponsor': [propertyA, propertyB] = [a.ChangeSponsor, b.ChangeSponsor]; break;
case 'changeDescription': [propertyA, propertyB] = [a.ChangeDescription, b.ChangeDescription]; break;
}
const valueA = isNaN(+propertyA) ? propertyA : +propertyA;
const valueB = isNaN(+propertyB) ? propertyB : +propertyB;
return (valueA < valueB ? -1 : 1) * (this._sort.direction === 'asc' ? 1 : -1);
});
}
}
I've removed anything having to do with selection from the table from the plunkr code, what is left here is where I am currently. I'm very sorry if this ends up more hindering than helpful.
Oh and this may be helpful, my api.js on the server side with the query I'm using. (The only one so far)
const express = require('express');
const async = require('async');
const jwt = require('jsonwebtoken');
const shape = require('shape-json');
const router = express.Router();
var sql = require('mssql/msnodesqlv8');
var config = {
driver: 'msnodesqlv8',
connectionString: 'Driver=SQL Server Native Client 11.0;Server=localhost;Database=Change;Trusted_Connection=yes;',
}
var conn = new sql.ConnectionPool(config);
conn.connect().then(function() {
log("Change Governance Database Connection opened");
}).catch(function (err) {
console.error(new Date() + " - Issue connecting to the MS SQL database.", err);
});
router.get('/', (req, res) => {
res.send('api works');
});
router.get('/rcgqueue', (req, res) => {
new sql.Request(conn)
.query('SELECT ChangeId, ChangeTitle, DateSubmitted, ChangeSponsor, ChangeDescription FROM dbo.ChangeEvaluationForm;')
.then(function(recordset) {
log("Successful query request for RCG Records.");
res.send(recordset.recordset);
}).catch(function(err) {
log(err);
res.send("Issue querying database!");
});
});
/********************************/
/* Functions */
/********************************/
// Log lines with date/time for server
function log(msg) {
console.log(new Date() + " - " + msg);
};
module.exports = router;
EDIT: Adding template and errors.
Template
<!-- Issues
https://github.com/angular/angular/issues/16614
https://github.com/angular/angular/issues/17572 -->
<div *ngIf="isDataAvailable">
<div class="example-container mat-elevation-z8">
<div class="example-header">
<md-input-container floatPlaceholder="never">
<input mdInput #filter placeholder="Filter">
</md-input-container>
</div>
<md-table #table [dataSource]="dataSource" mdSort>
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- ChangeId Column -->
<ng-container cdkColumnDef="changeId">
<md-header-cell *cdkHeaderCellDef md-sort-header> ChangeId </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.ChangeId}} </md-cell>
</ng-container>
<!-- ChangeTitle Column -->
<ng-container cdkColumnDef="changeTitle">
<md-header-cell *cdkHeaderCellDef md-sort-header> Change Title </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.ChangeTitle}}% </md-cell>
</ng-container>
<!-- DateSubmitted -->
<ng-container cdkColumnDef="dateSubmitted">
<md-header-cell *cdkHeaderCellDef md-sort-header> Date Submitted </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.DateSubmitted}} </md-cell>
</ng-container>
<!-- ChangeSponsor -->
<ng-container cdkColumnDef="changeSponsor">
<md-header-cell *cdkHeaderCellDef md-sort-header> Change Sponsor </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.ChangeSponsor}} </md-cell>
</ng-container>
<!-- ChangeDescription -->
<ng-container cdkColumnDef="changeDescription">
<md-header-cell *cdkHeaderCellDef md-sort-header> Change Description </md-header-cell>
<md-cell *cdkCellDef="let row"> {{row.ChangeDescription}} </md-cell>
</ng-container>
<md-header-row *cdkHeaderRowDef="displayedColumns"></md-header-row>
<md-row *cdkRowDef="let row; columns: displayedColumns;">
</md-row>
</md-table>
<div class="example-no-results" [style.display]="dataSource.renderedData.length == 0 ? '' : 'none'">
No changes found matching filter.
</div>
<md-paginator #paginator [length]="dataSource.filteredData.length" [pageIndex]="0" [pageSize]="25" [pageSizeOptions]="[5, 10, 25, 100]">
</md-paginator>
</div>
</div>
And finally a screenshot of my errors

The reason why you are getting
Cannot set property pageIndex of undefined
is that you wrap table in *ngIf and by the time you pass #ViewChilds to DataSource class they are not initialized yet.
I solved it by calling initialization after getting data:
this.rcgservice.populateRCGQueue().subscribe(rcgitems => {
this.dataChange.next(rcgitems);
this.isDataAvailable = true;
this.cdRef.detectChanges(); // make sure that all ViewChilds were initialized
this.initSource();
Another mistake is that you assign data to BehaviourSubject
this.rcgservice.populateRCGQueue().subscribe(rcgitems => {
this.dataChange = rcgitems;
it should be:
this.dataChange.next(rcgitems);
I also added some safe navigation operators to your template:
[length]="dataSource?.filteredData.length"
and
[style.display]="dataSource?.renderedData.length == 0 ? '' : 'none'"
Plunker Example
If we don't use ngIf then we do not need ChangeDetectorRef anymore
Plunker Example

Related

LitElement initialize State from Properties without default value

I'm trying out Lit and I'm having trouble figuring out how to initialize a state field from props. Here's a basic contrived pagination component:
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
This fails in render when using this._numPages (throws RangeError for the map because it's NaN), but is fine with numPages. It seems like if the state is using public properties that have defaults, it works, but otherwise it fails. I think this has to do with the fact that when the element is created it doesn't have props yet, so the initial first render doesn't have a value. But what is the right pattern to achieve this then in that case? The documentation here says "In some cases, internal reactive state may be initialized from public properties—for example, if there is a expensive transformation between the user-visible property and the internal state." (https://lit.dev/docs/components/properties/#public-properties-and-internal-state).
For completeness, here's a snippet of output of tsc:
import { __decorate } from "tslib";
import { html } from 'lit';
import { localized, msg } from '#lit/localize';
import { property } from 'lit/decorators/property.js';
import { state } from 'lit/decorators/state.js';
import { BaseElement } from '../BaseElement.js';
let Pagination = class Pagination extends BaseElement {
constructor() {
super(...arguments);
this.selected = 0;
this._selected = this.selected;
this.pageSize = 10;
this._numPages = Math.ceil(this.total / this.pageSize);
}
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(this._selected, this._numPages);
return html `
<ul class="pagination">
<li>${msg("Previous", { id: 'pagination.previous', desc: 'Previous button in pagination' })}</li>
${Array.from(Array(numPages).keys()).map((e, i) => html `<li class=${this.selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", { id: 'pagination.next', desc: 'Previous button in pagination' })}</li>
</ul>
`;
}
};
Pagination.styles = [BaseElement.styles];
__decorate([
property({ type: Number })
], Pagination.prototype, "selected", void 0);
__decorate([
state()
], Pagination.prototype, "_selected", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "total", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "pageSize", void 0);
__decorate([
state()
], Pagination.prototype, "_numPages", void 0);
Pagination = __decorate([
localized()
], Pagination);
export { Pagination };
The #property decorator only declares the property for any lit-element component. To initialize the property with an default value, you need to declare it in Constructor.
I am not clear on your issue, what i understand that _numPages is giving error. So you can declare and initialize it as follow.
Below is your modified code.
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#property({type: Number})
_numPages: number;
constructor() {
super();
this._numPages = 10
}
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
Assigning the values in the class fields feels risky. I would need to review this, but I think this will not use the instance values so it might have unintended side-effects. I would wait until the instance is available (like the constructor), but, setting it in the constructor is also too soon as the property does not yet have the value from the component, rather it has the initial value from the class-field.
So we need to delay this even further.
I've solved this by using the firstUpdated lifecycle hook:
#customElement("my-component")
export class MyComponent extends LitElement {
#property()
myProperty: string = "";
#state()
_internalState: string = "";
override async firstUpdated() {
// avoid unnecessary calls by waiting for any pending updated to complete.
await this.updateComplete;
this._internalState = this.myProperty;
}
...
}

Populating nativescript radList view with Data from Webservice

I have a RadList view which am trying to populate with data from a web service. The data returned from the service is in JSON format. So am trying to create a service which i will use to get the information by passing the respective URL. Below are the codes:-
interface class (cps-admin.interface.ts)
export class AddTenantInterface {
tenant_id: string;
tenant_names: string;
tenant_contact: string;
tenant_email: string;
starting_date: string;
rent_amount: string;
initial_water_reading: string;
water_unit_cost: string;
}
the service (CPSAdminService.ts)
import { Injectable } from "#angular/core";
import { AddTenantInterface } from "../interfaces/cps-admin.interface";
import { Observable } from "rxjs";
import { HttpClient, HttpHeaders, HttpResponse } from "#angular/common/http";
#Injectable()
export class CPSAdminService {
public _fetchTenantListUrl: string = "http://192.168.137.1/cps/fetchTenantsObj.php"; // fetch tenants list api url
constructor(private _http: HttpClient) {}
fetchTenantsList() {
let headers = this.createRequestHeader();
return this._http.get(this._fetchTenantListUrl, { headers: headers });
}
private createRequestHeader() {
// set headers here e.g.
let headers = new HttpHeaders({
"AuthKey": "my-key",
"AuthToken": "my-token",
"Content-Type": "application/json",
});
return headers;
}
}
Here is the tenants-component.ts
import { Component, OnInit, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy} from "#angular/core";
import { AddTenantInterface } from "./../../interfaces/cps-admin.interface";
import { CPSAdminService } from "./../../services/cps-admin.service";
import * as ApplicationSettings from "application-settings";
import { ObservableArray } from "tns-core-modules/data/observable-array";
import { RadListViewComponent } from "nativescript-ui-listview/angular";
import { ListViewLinearLayout, ListViewEventData, RadListView, LoadOnDemandListViewEventData } from "nativescript-ui-listview";
import { setTimeout } from "tns-core-modules/timer";
import { TextField } from "ui/text-field";
#Component({
selector: "tenants-list",
moduleId: module.id,
templateUrl: "./tenants-list.component.html",
styleUrls: ["./tenants-list.css"]
})
export class TenantListComponent implements OnInit {
public rentItems: ObservableArray<AddTenantInterface>;
private _sourceDataItems: ObservableArray<AddTenantInterface>;
private layout: ListViewLinearLayout;
public searchPaymentsList: string;
private _myFilteringFunc: (item: any) => any;
#ViewChild("myListView") myListViewComponent: RadListViewComponent;
constructor(private _cPSAdminService: CPSAdminService, private _changeDetectionRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.layout = new ListViewLinearLayout();
this.layout.scrollDirection = "Vertical";
this.initDataItems();
this._changeDetectionRef.detectChanges();
this.rentItems = new ObservableArray<AddTenantInterface>();
this.addMoreItemsFromSource(6);
}
get myFilteringFunc(): (item: any) => any {
return this._myFilteringFunc;
}
set myFilteringFunc(value: (item: any) => any) {
this._myFilteringFunc = value;
}
public onTextChanged(args) {
let searchBar = <TextField>args.object;
let listView = this.myListViewComponent.listView;
this.myFilteringFunc = (item: AddTenantInterface) => {
return item.tenant_names.includes(searchBar.text) || item.starting_date.includes(searchBar.text);
};
if (!listView.filteringFunction) {
listView.filteringFunction = this.myFilteringFunc;
} else {
listView.filteringFunction = undefined;
}
}
get rentDataItems(): ObservableArray<AddTenantInterface> {
return this.rentItems;
}
public addMoreItemsFromSource(chunkSize: number) {
let newItems = this._sourceDataItems.splice(0, chunkSize);
this.rentDataItems.push(newItems);
}
public onLoadMoreItemsRequested(args: LoadOnDemandListViewEventData) {
const that = new WeakRef(this);
if (this._sourceDataItems.length > 0) {
setTimeout(function () {
const listView: RadListView = args.object;
that.get().addMoreItemsFromSource(2);
listView.notifyLoadOnDemandFinished();
}, 1000);
} else {
args.returnValue = false;
}
}
// ===== **PROBLEM IS HERE**
private initDataItems() {
this._sourceDataItems = new ObservableArray(this._cPSAdminService.fetchTenantsList());
}
}
Note where i have marked "PROBLEM IS HERE", Here is the error image clip
I just don't what the problem. When i place "any" as the method return in fetchTenantsList() like so fetchTenantsList(): any , the error disappears but nothing is displayed in list.
And when i hard code the data there like below, it works;
tenantData: AddTenantInterface[] = [
{
"tenant_id":"542948",
"tenant_names":"Jane Doe",
"tenant_contact":"0787916686",
"tenant_email":"jane.doe#ymail.com",
"starting_date":"2004-08-09",
"rent_amount":"850000",
"initial_water_reading":"100",
"water_unit_cost":"250"
},
{
"tenant_id":"575550",
"tenant_names":"Emily Clarke",
"tenant_contact":"07752654868",
"tenant_email":"emily.clarke#gmail.com",
"starting_date":"2007-07-04",
"rent_amount":"700000",
"initial_water_reading":"400",
"water_unit_cost":"250"
},
{
"tenant_id":"422031",
"tenant_names":"John Doe",
"tenant_contact":"0772485364",
"tenant_email":"john.doe#ymail.com",
"starting_date":"2008-12-14",
"rent_amount":"700000",
"initial_water_reading":"120",
"water_unit_cost":"250"
}
];
fetchTenantsList(): AddTenantInterface[] {
return this.tenantData;
}
Here is my component html:
<GridLayout class="page page-content custom_font_family m-5" rows="50, *">
<StackLayout class="input-field" row="0">
<TextField
hint="search..."
[(ngModel)]='searchPaymentsList'
secure="false"
returnKeyType="done"
(textChange)="onTextChanged($event)"
autocorrect="false"
autocapitalizationType="allCharacters"
focus="onFocus"
blur="onBlur"
class="input input-border"
color="navy"
textFieldHintColor="#bfbfbf"></TextField>
</StackLayout>
<GridLayout tkExampleTitle tkToggleNavButton row="1" rows="*">
<RadListView [items]="rentDataItems" loadOnDemandMode="Manual" (loadMoreDataRequested)="onLoadMoreItemsRequested($event)" [filteringFunction]="myFilteringFunc" #myListView row="0">
<ng-template tkListItemTemplate let-item="item" let-i="index" let-odd="odd" let-even="even">
<StackLayout [class.odd]="odd" [class.even]="even" class="list-group-item p-y-10 m-y-2 t-17 p-x-5">
<Label [text]='item.starting_date | date: "d-M-y"'></Label>
<Label [text]='item.tenant_id + ". "'></Label>
<Label [text]='item.tenant_names'></Label>
<Label [text]='item.tenant_contact'></Label>
<Label [text]='item.tenant_email'></Label>
<Label [text]='item.rent_amount | currency:"UG ":"Ug. ": "3.1-1"'></Label>
</StackLayout>
<!--</GridLayout>-->
</ng-template>
<ng-template tkListViewHeader>
<GridLayout class="header" rows="*" columns="30, auto, auto, auto, auto, auto">
<Label row="0" col="0" text='Date'></Label>
<Label row="0" col="1" text='No '></Label>
<Label row="0" col="2" text='Names'></Label>
<Label row="0" col="3" text='Contact'></Label>
<Label row="0" col="4" text='Email'></Label>
<Label row="0" col="5" text='Rent'></Label>
</GridLayout>
</ng-template>
<ListViewLinearLayout *tkIfIOS tkListViewLayout itemHeight="120"></ListViewLinearLayout>
<div *tkIfIOS>
<GridLayout *tkListLoadOnDemandTemplate class="loadOnDemandItemGridLayout">
<Label text="Load More" horizontalAlignment="center" verticalAlignment="center"></Label>
</GridLayout>
</div>
</RadListView>
</GridLayout>
</GridLayout>
Any help is appreciated.
this._cPSAdminService.fetchTenantsList()
This will return an Observable that emits the result from server when the request is completed. You can't simply pass it to Observable Array.
It must be something like this,
this._cPSAdminService.fetchTenantsList()
.subscribe((result) => {
this._sourceDataItems = new ObservableArray(result);
});
Alright my bad. I was supposed to remove
this.addMoreItemsFromSource(6) from ngOnInit() and put it inside the subscribe.
Here is the solution.
import { Component, OnInit, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy} from "#angular/core";
import { AddTenantInterface } from "./../../interfaces/cps-admin.interface";
import { CPSAdminService } from "./../../services/cps-admin.service";
import * as ApplicationSettings from "application-settings";
import { ObservableArray } from "tns-core-modules/data/observable-array";
import { RadListViewComponent } from "nativescript-ui-listview/angular";
import { ListViewLinearLayout, ListViewEventData, RadListView, LoadOnDemandListViewEventData } from "nativescript-ui-listview";
import { setTimeout } from "tns-core-modules/timer";
import { TextField } from "ui/text-field";
#Component({
selector: "tenants-list",
moduleId: module.id,
templateUrl: "./tenants-list.component.html",
styleUrls: ["./tenants-list.css"]
})
export class TenantListComponent implements OnInit {
public rentItems: ObservableArray<AddTenantInterface>;
private _sourceDataItems: ObservableArray<AddTenantInterface>;
private layout: ListViewLinearLayout;
public searchPaymentsList: string;
private _myFilteringFunc: (item: any) => any;
#ViewChild("myListView") myListViewComponent: RadListViewComponent;
constructor(private _cPSAdminService: CPSAdminService, private _changeDetectionRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.layout = new ListViewLinearLayout();
this.layout.scrollDirection = "Vertical";
this.initDataItems();
this._changeDetectionRef.detectChanges();
this.rentItems = new ObservableArray<AddTenantInterface>();
//this.addMoreItemsFromSource(6); // remove this and put it inthe initDataItems method
}
get myFilteringFunc(): (item: any) => any {
return this._myFilteringFunc;
}
set myFilteringFunc(value: (item: any) => any) {
this._myFilteringFunc = value;
}
public onTextChanged(args) {
let searchBar = <TextField>args.object;
let listView = this.myListViewComponent.listView;
this.myFilteringFunc = (item: AddTenantInterface) => {
return item.tenant_names.includes(searchBar.text) || item.starting_date.includes(searchBar.text);
};
if (!listView.filteringFunction) {
listView.filteringFunction = this.myFilteringFunc;
} else {
listView.filteringFunction = undefined;
}
}
get rentDataItems(): ObservableArray<AddTenantInterface> {
return this.rentItems;
}
public addMoreItemsFromSource(chunkSize: number) {
let newItems = this._sourceDataItems.splice(0, chunkSize);
this.rentDataItems.push(newItems);
}
public onLoadMoreItemsRequested(args: LoadOnDemandListViewEventData) {
const that = new WeakRef(this);
if (this._sourceDataItems.length > 0) {
setTimeout(function () {
const listView: RadListView = args.object;
that.get().addMoreItemsFromSource(2);
listView.notifyLoadOnDemandFinished();
}, 1000);
} else {
args.returnValue = false;
}
}
public initDataItems() {
//this._sourceDataItems = new ObservableArray(this._cPSAdminService.fetchTenantsList());
this._cPSAdminService.fetchTenantsList()
.subscribe( (result) => {
this._sourceDataItems = new ObservableArray(result);
this.addMoreItemsFromSource(6); // put it here
},
error => {
console.log("Error: ", error);
});
}
}
Thanks #Manoj

React Virtualized - Table Example - How do I make it work?

I've been struggling to make this react virtualized table example work & seriously starting to doubt my sanity. I've created a react app and I'm just trying to render the example table inside App.js with this:
class App extends Component {
render() {
var data = [1,2,3,4,5,6,7,8,9,10];
return (
<TableExample
list={data}
/>
);
}
}
React keeps saying list isn't defined - it seems obvious I'm not getting the data into the component the right way. I haven't been able to understand the example code, what props need to be passed in and what they should be named. Sorry for the stupid question but I've been stuck forever not knowing where else to find an answer. The table example code is below:
/** #flow */
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import * as React from 'react';
import {
ContentBox,
ContentBoxHeader,
ContentBoxParagraph,
} from '../demo/ContentBox';
import {LabeledInput, InputRow} from '../demo/LabeledInput';
import AutoSizer from '../AutoSizer';
import Column from './Column';
import Table from './Table';
import SortDirection from './SortDirection';
import SortIndicator from './SortIndicator';
import styles from './Table.example.css';
export default class TableExample extends React.PureComponent {
static contextTypes = {
list: PropTypes.instanceOf(Immutable.List).isRequired,
};
constructor(props, context) {
super(props, context);
const sortBy = 'index';
const sortDirection = SortDirection.ASC;
const sortedList = this._sortList({sortBy, sortDirection});
this.state = {
disableHeader: false,
headerHeight: 30,
height: 270,
hideIndexRow: false,
overscanRowCount: 10,
rowHeight: 40,
rowCount: 1000,
scrollToIndex: undefined,
sortBy,
sortDirection,
sortedList,
useDynamicRowHeight: false,
};
this._getRowHeight = this._getRowHeight.bind(this);
this._headerRenderer = this._headerRenderer.bind(this);
this._noRowsRenderer = this._noRowsRenderer.bind(this);
this._onRowCountChange = this._onRowCountChange.bind(this);
this._onScrollToRowChange = this._onScrollToRowChange.bind(this);
this._rowClassName = this._rowClassName.bind(this);
this._sort = this._sort.bind(this);
}
render() {
const {
disableHeader,
headerHeight,
height,
hideIndexRow,
overscanRowCount,
rowHeight,
rowCount,
scrollToIndex,
sortBy,
sortDirection,
sortedList,
useDynamicRowHeight,
} = this.state;
const rowGetter = ({index}) => this._getDatum(sortedList, index);
return (
<ContentBox>
<ContentBoxHeader
text="Table"
sourceLink="https://github.com/bvaughn/react-virtualized/blob/master/source/Table/Table.example.js"
docsLink="https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md"
/>
<ContentBoxParagraph>
The table layout below is created with flexboxes. This allows it to
have a fixed header and scrollable body content. It also makes use of{' '}
<code>Grid</code> for windowing table content so that large lists are
rendered efficiently. Adjust its configurable properties below to see
how it reacts.
</ContentBoxParagraph>
<ContentBoxParagraph>
<label className={styles.checkboxLabel}>
<input
aria-label="Use dynamic row heights?"
checked={useDynamicRowHeight}
className={styles.checkbox}
type="checkbox"
onChange={event =>
this._updateUseDynamicRowHeight(event.target.checked)
}
/>
Use dynamic row heights?
</label>
<label className={styles.checkboxLabel}>
<input
aria-label="Hide index?"
checked={hideIndexRow}
className={styles.checkbox}
type="checkbox"
onChange={event =>
this.setState({hideIndexRow: event.target.checked})
}
/>
Hide index?
</label>
<label className={styles.checkboxLabel}>
<input
aria-label="Hide header?"
checked={disableHeader}
className={styles.checkbox}
type="checkbox"
onChange={event =>
this.setState({disableHeader: event.target.checked})
}
/>
Hide header?
</label>
</ContentBoxParagraph>
<InputRow>
<LabeledInput
label="Num rows"
name="rowCount"
onChange={this._onRowCountChange}
value={rowCount}
/>
<LabeledInput
label="Scroll to"
name="onScrollToRow"
placeholder="Index..."
onChange={this._onScrollToRowChange}
value={scrollToIndex || ''}
/>
<LabeledInput
label="List height"
name="height"
onChange={event =>
this.setState({height: parseInt(event.target.value, 10) || 1})
}
value={height}
/>
<LabeledInput
disabled={useDynamicRowHeight}
label="Row height"
name="rowHeight"
onChange={event =>
this.setState({
rowHeight: parseInt(event.target.value, 10) || 1,
})
}
value={rowHeight}
/>
<LabeledInput
label="Header height"
name="headerHeight"
onChange={event =>
this.setState({
headerHeight: parseInt(event.target.value, 10) || 1,
})
}
value={headerHeight}
/>
<LabeledInput
label="Overscan"
name="overscanRowCount"
onChange={event =>
this.setState({
overscanRowCount: parseInt(event.target.value, 10) || 0,
})
}
value={overscanRowCount}
/>
</InputRow>
<div>
<AutoSizer disableHeight>
{({width}) => (
<Table
ref="Table"
disableHeader={disableHeader}
headerClassName={styles.headerColumn}
headerHeight={headerHeight}
height={height}
noRowsRenderer={this._noRowsRenderer}
overscanRowCount={overscanRowCount}
rowClassName={this._rowClassName}
rowHeight={useDynamicRowHeight ? this._getRowHeight : rowHeight}
rowGetter={rowGetter}
rowCount={rowCount}
scrollToIndex={scrollToIndex}
sort={this._sort}
sortBy={sortBy}
sortDirection={sortDirection}
width={width}>
{!hideIndexRow && (
<Column
label="Index"
cellDataGetter={({rowData}) => rowData.index}
dataKey="index"
disableSort={!this._isSortEnabled()}
width={60}
/>
)}
<Column
dataKey="name"
disableSort={!this._isSortEnabled()}
headerRenderer={this._headerRenderer}
width={90}
/>
<Column
width={210}
disableSort
label="The description label is really long so that it will be truncated"
dataKey="random"
className={styles.exampleColumn}
cellRenderer={({cellData}) => cellData}
flexGrow={1}
/>
</Table>
)}
</AutoSizer>
</div>
</ContentBox>
);
}
_getDatum(list, index) {
return list.get(index % list.size);
}
_getRowHeight({index}) {
const {list} = this.context;
return this._getDatum(list, index).size;
}
_headerRenderer({dataKey, sortBy, sortDirection}) {
return (
<div>
Full Name
{sortBy === dataKey && <SortIndicator sortDirection={sortDirection} />}
</div>
);
}
_isSortEnabled() {
const {list} = this.context;
const {rowCount} = this.state;
return rowCount <= list.size;
}
_noRowsRenderer() {
return <div className={styles.noRows}>No rows</div>;
}
_onRowCountChange(event) {
const rowCount = parseInt(event.target.value, 10) || 0;
this.setState({rowCount});
}
_onScrollToRowChange(event) {
const {rowCount} = this.state;
let scrollToIndex = Math.min(
rowCount - 1,
parseInt(event.target.value, 10),
);
if (isNaN(scrollToIndex)) {
scrollToIndex = undefined;
}
this.setState({scrollToIndex});
}
_rowClassName({index}) {
if (index < 0) {
return styles.headerRow;
} else {
return index % 2 === 0 ? styles.evenRow : styles.oddRow;
}
}
_sort({sortBy, sortDirection}) {
const sortedList = this._sortList({sortBy, sortDirection});
this.setState({sortBy, sortDirection, sortedList});
}
_sortList({sortBy, sortDirection}) {
const {list} = this.context;
return list
.sortBy(item => item[sortBy])
.update(
list => (sortDirection === SortDirection.DESC ? list.reverse() : list),
);
}
_updateUseDynamicRowHeight(value) {
this.setState({
useDynamicRowHeight: value,
});
}
}
Looking at some previous questions, it seems that the example is using some components that are not included inside dist package. Thats probably why you are getting undefined error.
Here is the most basic example of Tables in react virtulized:
import React from 'react';
import ReactDOM from 'react-dom';
import { Column, Table } from 'react-virtualized';
import 'react-virtualized/styles.css'; // only needs to be imported once
// Table data as an array of objects
const list = [
{ name: 'Brian Vaughn', description: 'Software engineer' }
// And so on...
];
// Render your table
ReactDOM.render(
<Table
width={300}
height={300}
headerHeight={20}
rowHeight={30}
rowCount={list.length}
rowGetter={({ index }) => list[index]}
>
<Column
label='Name'
dataKey='name'
width={100}
/>
<Column
width={200}
label='Description'
dataKey='description'
/>
</Table>,
document.getElementById('example')
);
When learning new library, its always best to start with the most simple example then expand on it. Here is the link to the full table docs.
The issue with the example is that in order for contextTypes to work, the parent component needs to define corresponding contextTypes and a getChildContext function.
In the parent component:
class App extends React.Component {
static childContextTypes = {
list: PropTypes.instanceOf(Immutable.List).isRequired
};
getChildContext() {
return {
list
};
}
render() {
return <TableExample />;
}
}
Mentioned in this issue; see lines 48-53 and 68-76 of the react-virtualized demo Application.js.

Get uid and iterate over an array of objects using Angular4 and Fireabse

I am building a 1-1 chat using Angular4 and Firebase and I am pretty new to Angular.
In order to initiate a conversation, I am trying to display all available users form '/users' subcollection. So, I need to get user/{user.uid}/username.
This is my chat.component.ts:
import { Component, OnInit } from '#angular/core';
import {AngularFireDatabase, FirebaseListObservable} from 'angularfire2/database';
import { AngularFireAuth } from 'angularfire2/auth';
import { UserSessionService } from '../_services/user-session.service';
#Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit {
items: FirebaseListObservable<any[]>;
other_users: FirebaseListObservable<any[]>;
user_id: any;
from: any;
msgVal: string = '';
constructor(public afAuth: AngularFireAuth, public af: AngularFireDatabase, public logged_user: UserSessionService ){ }
ngOnInit() {
this.from= this.logged_user.getFirebaseUid();
this.user_id= this.logged_user.getFirebaseUid();
this.items = this.af.list('/personalMessages', {
query: { limitToLast: 5 }
});
this.other_users= this.af.list('/users');
}
findChat(){
this.other_users= this.other_users;
this.user_id = this.user_id;
}
chatSend(theirMessage: string) {
this.items.push({ text: theirMessage, from: this.logged_user.getFirebaseUid(), isRead: false, timestamp: + new Date() });
this.msgVal = '';
this.user_id = this.user_id;
this.other_users= this.other_users;
}
}
And this is my chat.component.html:
<div class="users-chat-container" *ngFor="let other_user of other_users| async">
<div id="circle" style="background-color:pink;">
</div>
<br/> {{other_user.username}}
</div>
<div class="chat-container" *ngFor="let item of items | async">
<div id="circle" style="background-image:url( http://www.ics.forth.gr/mobile/female.png);">
</div>
<br/> {{item.from}}
<p>{{item.text}}</p>
</div>
<input type="text" id="message" placeholder="Type a message..." (keyup.enter)="chatSend($event.target.value)" [(ngModel)]="msgVal" />
How can I iterate over the array of objects I get from '/users' collection? Thank you! :)
you need use ForkJoin. ForkJoin will take users list as input and fire parallel request for all users list
try some thing like this
this.af.list('/users')
.mergeMap((users) => {
if (users.length > 0) {
return Observable.forkJoin(
users.map((user) => this.af.database
.object(`user/${user.$uid}/username`)
.first()
),
(...values) => { // here you can assign username
users.forEach((user, index) => { user.username = values[index]; });
return users;
}
);
}
return Observable.of([]);
});
more info about forkJoin
https://www.learnrxjs.io/operators/combination/forkjoin.html
You need an array for *ngFor. With object.keys you can create an array of the objects. In the example below I have done this with players from a group, coming as objects from firebase.
private getPlayersPerGroup(group: Group) {
this.playersInGroup = [];
if (group["players"]) {
Object.keys(group["players"]).forEach((key) => {
this.groupService.getPlayerById(key).then((player) => {
this.playersInGroup.push(player);
});
});
}
}

Undefined index after loadMoreRows is called

I have the following example implementing the InfiniteLoader with a Table which is setting the Table rowCount to a known large number (count of logs in the db) and the InfiniteLoader rowCount to the size of the batch of logs I fetch. I need this so that the user knows how much data there is based on the scroll heigth. Otherwise, he would have to scroll to the end and see if more logs are loaded. Could be that I'm misusing the two rowCount props, but whenever I scroll fast to an index close to the end, where data is not loaded yet, data is undefined in the getRowClassName function. I assumed the loadMoreRows would get called in this case.
import React = require('react');
import _ = require('lodash');
import Immutable = require('immutable');
import Api = require('./Api');
const STATUS_LOADING = 1,
STATUS_LOADED = 2,
LOG_LIMIT = 200;
interface Props {
logEntries: Immutable.List<Immutable.Map<string, any>>;
}
interface State {
logEntries?: Immutable.List<Immutable.Map<string, any>>;
count?: number;
loadedRowsMap?: any;
}
class LogViewer extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
logEntries: props.logEntries,
count: 0,
loadedRowsMap: {}
};
}
render() {
return {this.renderLoader()};
}
private renderLoader() {
const {logEntries, count} = this.state;
return (
<InfiniteLoader isRowLoaded={this.isRowLoaded.bind(this)}
loadMoreRows={this.loadMoreRows.bind(this)}
minimumBatchSize={LOG_LIMIT}
rowCount={logEntries.size} >
{
({onRowsRendered, registerChild}) => (
<AutoSizer disableHeight>
{
({width}) => (
<Table headerHeight={20}
height={400}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={count}
className='log-entries'
gridClassName='grid'
rowClassName={this.getRowClassName.bind(this)}
headerStyle={{ fontSize: 15 }}
rowGetter={({index}) => logEntries.get(index)}
rowHeight={50}
width={width} >
<Column label='Name'
key='name'
dataKey='name'
width={200} />
</Table>
)
}
</AutoSizer>
)
}
</InfiniteLoader>
);
}
private getRowClassName({index}) {
const {logEntries} = this.state;
if(index > -1) {
const data = logEntries.get(index);
return `log-entry ${data.get('name').toLowerCase()}`;
}
return '';
}
private isRowLoaded({index}) {
const {loadedRowsMap} = this.state;
return !!loadedRowsMap[index];
}
private loadMoreRows({startIndex, stopIndex}) {
const {loadedRowsMap, level, logEntries} = this.state;
_.range(startIndex, stopIndex).forEach(i => {
loadedRowsMap[i] = STATUS_LOADING;
});
this.setState({ loadedRowsMap });
const offset = Math.floor((startIndex + 1) / LOG_LIMIT);
return Api.logs(LOG_LIMIT, offset)
.then(({body: [count, logs]}) => {
_.range(startIndex, stopIndex).forEach(i => {
loadedRowsMap[i] = STATUS_LOADED;
});
const newLogs = logEntries.toJS().concat(logs);
this.setState({
count,
logEntries: Immutable.fromJS(newLogs)
});
});
}
};
Could be that I'm misusing the two rowCount props
You should pass the same rowCount value to both InfiniteLoader and Table. It should either be the total size of all of your data on the server (as shown here) or the size of your local data +1 to allow loading more when a user scrolls near the end (as shown here).
whenever I scroll fast to an index close to the end, where data is not loaded yet, data is undefined in the getRowClassName function. I assumed the loadMoreRows would get called in this case.
loadMoreRows does get called- but it's async. react-virtualized won't block user scrolling until data loads. Your getRowClassName function needs to handle the fact that a user may scroll faster than your lazy-loaded data is able to load. You can show a different "load in progress" UI for unloaded rows if you'd like.

Resources