Directive unit testing fails - jestjs

I am using jest.js for testing with my angular app. here is the directive I use in html:
<textarea errorHighlighter formControlName="Url" name="Url" cols="50" rows="5"
placeholder="Enter Page URL" (ngModelChange)="pageUrlChanges($event)"></textarea>
here is my directive.ts file:
import { Directive, ElementRef, SimpleChanges, HostListener, Renderer2 } from '#angular/core';
import { NgControl } from '#angular/forms';
#Directive({
selector: '[errorHighlighter]'
})
export class ErrorHighlighterDirective {
constructor(private el: ElementRef, private control: NgControl, private renderer: Renderer2) { }
#HostListener('input') oninput() {
if (this.el.nativeElement && this.control) {
if (this.control.control.status === 'INVALID') {
this.renderer.addClass(this.el.nativeElement, 'has-err');
} else {
this.renderer.removeClass(this.el.nativeElement, 'has-err');
}
}
}
}
this is written to show the error border around the input field. I am trying to test the same like this:
import { ErrorHighlighterDirective } from './error-highlighter.directive';
import { Directive, ElementRef, SimpleChanges, HostListener, Renderer2, Component, DebugElement } from '#angular/core';
import { NgControl, FormGroup, FormsModule, FormControl, ReactiveFormsModule } from '#angular/forms';
import { TestBed, ComponentFixture } from '#angular/core/testing';
#Component({
template: `<input errorHighlighter formControlName="Url" type="text">`
})
class TestHighlighterComponent { }
describe('ErrorHighlighterDirective', () => {
let component: TestHighlighterComponent;
let fixture: ComponentFixture<TestHighlighterComponent>;
let inputEl: DebugElement;
const fg: FormGroup = new FormGroup({
'Url': new FormControl('')
});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestHighlighterComponent, ErrorHighlighterDirective],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: NgControl, useValue: fg }
]
});
fixture = TestBed.createComponent(TestHighlighterComponent);
component = fixture.componentInstance;
inputEl = fixture.debugElement.query(By.css('input'));
});
it('should create an instance', () => {
const directive = new ErrorHighlighterDirective(inputEl, fg, Renderer2);
expect(directive).toBeTruthy();
});
});
But the test not succeeds. getting error like below:
● Test suite failed to run
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
src/app/directives/error-highlighter.directive.spec.ts:33:46 - error TS2304: Cannot find name 'By'.
33 inputEl = fixture.debugElement.query(By.css('input'));
~~
src/app/directives/error-highlighter.directive.spec.ts:38:66 - error TS2345: Argument of type 'FormGroup' is not assignable to parameter of type 'NgControl'.
Type 'FormGroup' is missing the following properties from type 'NgControl': name, valueAccessor, viewToModelUpdate, control, path
38 const directive = new ErrorHighlighterDirective(inputEl, fg, Renderer2);
Any one help me to understand and fix these issue? I am not much familiar with angular test either jest.js.

You can use By.directive
e.g.
const directiveEl = fixture.debugElement.query(By.directive(MyDirective));
expect(directiveEl).not.toBeNull();
You need to import By from angular.platform-browser
import { By } from '#angular/platform-browser
You can further read here.
You can use any selector By.css that you can use with css. And a selector for a class is simply .classname.
e.g.
By.css(.classname)
or
By.css('input[type=radio]')
or
By.css('textarea')

Related

Nestjs + Swagger: How to add a custom option to #ApiProperty

Is it possible to add a custom option to #ApiProperty decorator?
import { ApiProperty } from '#nestjs/swagger';
class Animal {
#ApiProperty({
type: String,
description: 'animal name',
'x-description': 'some information' // how to add a cutom option 'x-description' ?
})
name: string;
}
I'm not sure i fully understand, but If you are talking about openapi's extensions. Take a look at this: https://github.com/nestjs/swagger/issues/195
Solution from https://github.com/nestjs/swagger/issues/195#issuecomment-526215840
import { ApiProperty } from '#nestjs/swagger';
type SwaggerDecoratorParams = Parameters<typeof ApiProperty>;
type SwaggerDecoratorMetadata = SwaggerDecoratorParams[0];
type Options = SwaggerDecoratorMetadata & DictionarySwaggerParameters;
type DictionarySwaggerParameters = { 'x-property'?: string };
export const ApiPropertyX = (params: Options) => {
return ApiProperty(params);
};

How to build server streaming via grpc and nest.js?

Need to create some "channel" to which the client can subscribe and periodically receive messages.
Within the current technology stack, I'm trying to organize something like this:
proto file:
syntax = "proto3";
package testtime;
service TimeService {
rpc GetTimeStream(Empty) returns (stream TimeStreamResponse);
}
message Empty {
}
message TimeStreamResponse {
string result = 1;
}
controller:
import { Controller } from '#nestjs/common';
import { GrpcMethod } from '#nestjs/microservices';
import moment from 'moment';
import { Observable, Subject } from 'rxjs';
const timeSubject = new Subject<{ result: string }>();
setInterval(() => {
const result = moment().format('hh:mm');
timeSubject.next({ result });
}, 5000);
#Controller()
export class TestTimeController {
#GrpcMethod('testtime.TimeService', 'GetTimeStream')
public getTimeStream(): Observable<{ result: string }> {
return timeSubject.asObservable();
}
}
when I try to call the method, I get an error:
/project/node_modules/#nestjs/microservices/server/server-grpc.js:141
this.transformToObservable(await handler).subscribe(data => callback(null, data), (err) => callback(err));
^
TypeError: callback is not a function
at SafeSubscriber._next (/project/node_modules/#nestjs/microservices/server/server-grpc.js:141:73)
at SafeSubscriber.__tryOrUnsub (/project/node_modules/rxjs/src/internal/Subscriber.ts:265:10)
at SafeSubscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:207:14)
at Subscriber._next (/project/node_modules/rxjs/src/internal/Subscriber.ts:139:22)
at Subscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:99:12)
at CatchSubscriber.Subscriber._next (/project/node_modules/rxjs/src/internal/Subscriber.ts:139:22)
at CatchSubscriber.Subscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:99:12)
at TapSubscriber._next (/project/node_modules/rxjs/src/internal/operators/tap.ts:125:22)
at TapSubscriber.Subscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:99:12)
at MergeMapSubscriber.notifyNext (/project/node_modules/rxjs/src/internal/operators/mergeMap.ts:162:22)
at SimpleInnerSubscriber._next (/project/node_modules/rxjs/src/internal/innerSubscribe.ts:30:17)
at SimpleInnerSubscriber.Subscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:99:12)
at MergeMapSubscriber.notifyNext (/project/node_modules/rxjs/src/internal/operators/mergeMap.ts:162:22)
at SimpleInnerSubscriber._next (/project/node_modules/rxjs/src/internal/innerSubscribe.ts:30:17)
at SimpleInnerSubscriber.Subscriber.next (/project/node_modules/rxjs/src/internal/Subscriber.ts:99:12)
at SwitchMapSubscriber.notifyNext (/project/node_modules/rxjs/src/internal/operators/switchMap.ts:166:24)
What am I doing wrong?

Dynamic Dependency injection with Typescript using tsyringe

I am trying to build and example to understand how the DI framework/library works, but i am encountering some problems.
I have this interface with two possible implementations:
export interface Operation {
calculate(a: number, b: number): number;
}
sub.ts
import { Operation } from "./operation.interface";
export class Sub implements Operation {
calculate(a: number, b: number): number {
return Math.abs(a - b);
}
}
sum.ts
import { Operation } from "./operation.interface";
export class Sum implements Operation {
calculate(a: number, b: number): number {
return a + b;
}
}
calculator.ts
import { Operation } from "./operation.interface";
import {injectable, inject} from "tsyringe";
#injectable()
export class Calculator {
constructor(#inject("Operation") private operation?: Operation){}
operate(a: number, b: number): number {
return this.operation.calculate(a, b);
}
}
index.ts
import "reflect-metadata";
import { container } from "tsyringe";
import { Calculator } from "./classes/calculator";
import { Sub } from "./classes/sub";
import { Sum } from "./classes/sum";
container.register("Operation", {
useClass: Sum
});
container.register("OperationSub", {
useClass: Sub
});
const calculatorSum = container.resolve(Calculator);
const result = calculatorSum.operate(4,6);
console.log(result);
// const calculatorSub = ???
is there a way where I could have two calculators with different behaviours or am I doing it completely wrong?
Since OperationSub isn’t used anywhere, it cannot affect injected Operation value.
Calculators with different dependency sets should be represented with multiple containers. Summing calculator can be considered a default implementation and use root container, or both implementations can be represented by children containers while root container remains abstract.
// common deps are registered on `container`
const sumContainer = container.createChildContainer();
const subContainer = container.createChildContainer();
sumContainer.register("Operation", { useClass: Sum });
subContainer.register("Operation", { useClass: Sub });
const calculatorSum = sumContainer.resolve(Calculator);
const calculatorSub = subContainer.resolve(Calculator);

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

Implementing Material Table with api call Angular

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

Resources