LitElement initialize State from Properties without default value - lit-element

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;
}
...
}

Related

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

Laravel Excel upload and progressbar

I have a website where I can upload a .xlsx file which contains some rows of information for my database. I read the documentation for laravel-excel but it looks like it only works with progress bar if you use the console method; which I don't.
I currently just use a plain HTML upload form, no ajax yet.
But to create this progress bar for this I need to convert it to ajax, which is no hassle, that I can do.
But how would I create the progress bar when uploading the file and iterating through each row in the Excel file?
This is the controller and method where the upload gets done:
/**
* Import companies
*
* #param Import $request
* #return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function postImport(Import $request)
{
# Import using Import class
Excel::import(new CompaniesImport, $request->file('file'));
return redirect(route('dashboard.companies.index.get'))->with('success', 'Import successfull!');
}
And this is the import file:
public function model(array $row)
{
# Don't create or validate on empty rows
# Bad workaround
# TODO: better solution
if (!array_filter($row)) {
return null;
}
# Create company
$company = new Company;
$company->crn = $row['crn'];
$company->name = $row['name'];
$company->email = $row['email'];
$company->phone = $row['phone'];
$company->website = (!empty($row['website'])) ? Helper::addScheme($row['website']) : '';
$company->save();
# Everything empty.. delete address
if (!empty($row['country']) || !empty($row['state']) || !empty($row['postal']) || !empty($row['address']) || !empty($row['zip'])) {
# Create address
$address = new CompanyAddress;
$address->company_id = $company->id;
$address->country = $row['country'];
$address->state = $row['state'];
$address->postal = $row['postal'];
$address->address = $row['address'];
$address->zip = $row['zip'];
$address->save();
# Attach
$company->addresses()->save($address);
}
return $company;
}
I know this is not much at this point. I just need some help figuring out how I would create this progress bar, because I'm pretty stuck.
My thought is to create a ajax upload form though, but from there I don't know.
Just an idea, but you could use the Laravel session to store the total_row_count and processed_row_count during the import execution. Then, you could create a separate AJAX call on a setInterval() to poll those session values (e.g., once per second). This would allow you to calculate your progress as processed_row_count / total_row_count, and output to a visual progress bar. – matticustard
Putting #matticustard comment into practice. Below is just sample of how things could be implemented, and maybe there are areas to improve.
1. Routes
import route to initialize Excel import.
import-status route will be used to get latest import status
Route::post('import', [ProductController::class, 'import']);
Route::get('import-status', [ProductController::class, 'status']);
2. Controller
import action will validate uploaded file, and pass $id to ProductsImport class. As it will be queued and run in the background, there is no access to current session. We will use cache in the background. It will be good idea to generate more randomized $id if more concurrent imports will be processed, for now just unix date to keep simple.
You currently cannot queue xls imports. PhpSpreadsheet's Xls reader contains some non-utf8 characters, which makes it impossible to queue.
XLS imports could not be queued
public function import()
{
request()->validate([
'file' => ['required', 'mimes:xlsx'],
]);
$id = now()->unix()
session([ 'import' => $id ]);
Excel::queueImport(new ProductsImport($id), request()->file('file')->store('temp'));
return redirect()->back();
}
Get latest import status from cache, passing $id from session.
public function status()
{
$id = session('import');
return response([
'started' => filled(cache("start_date_$id")),
'finished' => filled(cache("end_date_$id")),
'current_row' => (int) cache("current_row_$id"),
'total_rows' => (int) cache("total_rows_$id"),
]);
}
3. Import class
Using WithEvents BeforeImport we set total rows of our excel file to the cache. Using onRow we set currently processing row to the cache. And AfterReset clear all the data.
<?php
namespace App\Imports;
use App\Models\Product;
use Maatwebsite\Excel\Row;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Concerns\WithEvents;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class ProductsImport implements OnEachRow, WithEvents, WithChunkReading, ShouldQueue
{
public $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function chunkSize(): int
{
return 100;
}
public function registerEvents(): array
{
return [
BeforeImport::class => function (BeforeImport $event) {
$totalRows = $event->getReader()->getTotalRows();
if (filled($totalRows)) {
cache()->forever("total_rows_{$this->id}", array_values($totalRows)[0]);
cache()->forever("start_date_{$this->id}", now()->unix());
}
},
AfterImport::class => function (AfterImport $event) {
cache(["end_date_{$this->id}" => now()], now()->addMinute());
cache()->forget("total_rows_{$this->id}");
cache()->forget("start_date_{$this->id}");
cache()->forget("current_row_{$this->id}");
},
];
}
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
$row = array_map('trim', $row->toArray());
cache()->forever("current_row_{$this->id}", $rowIndex);
// sleep(0.2);
Product::create([ ... ]);
}
}
4. Front end
On the front-end side this is just sample how things could be handled. Here I used vuejs, ant-design-vue and lodash.
After uploading file handleChange method is called
On successful upload trackProgress method is called for the first time
trackProgress method is recursive function, calling itself on complete
with lodash _.debounce method we can prevent calling it too much
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
<template>
<a-modal
title="Upload excel"
v-model="visible"
cancel-text="Close"
ok-text="Confirm"
:closable="false"
:maskClosable="false"
destroyOnClose
>
<a-upload-dragger
name="file"
:multiple="false"
:showUploadList="false"
:action="`/import`"
#change="handleChange"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">Click to upload</p>
</a-upload-dragger>
<a-progress class="mt-5" :percent="progress" :show-info="false" />
<div class="text-right mt-1">{{ this.current_row }} / {{ this.total_rows }}</div>
<template slot="footer">
<a-button #click="close">Close</a-button>
</template>
</a-modal>
</template>
<script>
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
</script>

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

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.

Jest/Jasmine .toContain() fails even though matching values are present

I'm writing a simple React application with a Button component, which looks like this:
import React, { Component } from 'react';
// shim to find stuff
Array.prototype.contains = function (needle) {
for (var i = 0; i < this.length; i++) {
if (this[i] == needle) return true;
}
return false;
};
class Button extends Component {
propTypes: {
text: React.PropTypes.string.isRequired,
modifiers: React.PropTypes.array
}
render() {
return(
<span className={this.displayModifiers()}>{this.props.text}</span>
);
}
displayModifiers() {
const modifiers = this.props.modifiers || ["default"];
if (modifiers.contains("default") ||
modifiers.contains("danger") ||
modifiers.contains("success")) {
// do nothing
} else {
// add default
modifiers.push("defualt");
}
var classNames = "btn"
for (var i = 0; i < modifiers.length; i++) {
classNames += " btn-" + modifiers[i]
}
return(classNames);
}
}
export default Button;
I then wrote this to test it:
it("contains the correct bootstrap classes", () => {
expect(mount(<Button modifiers={["flat"]}/>).html()).toContain("<span class=\"btn btn-flat btn-default\"></span>");
});
That code should pass, but I receive the following error message:
expect(string).toContain(value)
Expected string:
"<span class=\"btn btn-flat btn-defualt\"></span>"
To contain value:
"<span class=\"btn btn-flat btn-default\"></span>"
at Object.it (src\__tests__\Button.test.js:42:293)
Any ideas why this is not passing?
From the docs:
Use .toContain when you want to check that an item is in a list.
To test strings you should use toBe or toEqual
it("contains the correct bootstrap classes", () => {
expect(mount(<Button modifiers={["flat"]}/>).html()).toBe("<span class=\"btn btn-flat btn-default\"></span>");
});
But there is a better way of testing the output rendered components: snapshots.
it("contains the correct bootstrap classes", () => {
expect(mount(<Button modifiers={["flat"]}/>).html()).toMatchSnapshot();
});
Note that you will need enzymeToJson for snapshot testing using enzyme.

Resources