I use Angular Material table pagination for server side data retrieval (page by page) and my API method is working and returns necessary parameters e.g. total records. However, although I pass the length parameter to the paginator, the prev and next buttons are not enabled (I think length is set the record size of per page that is 5, instead of 15 that is the real length). Here is the methods I use:
.html:
<mat-paginator
#paginator
[pageSizeOptions]="paginationSizes" // "[5,10,15,20]"
[pageSize]="defaultPageSize" // 5
showFirstLastButtons
[length] = "resultsLength" // it should be 16
[pageIndex]="0"
(page)="onPageFired($event)">
</mat-paginator>
I defined the necessary #Input and EventEmitter definitions and the necessary methods are called from base component (paginator).
ngOnInit() {
this.getPublishers();
}
getEmployee(): void {
this.service.getByPagination(1, 5, '1==1') // I iitially set pageNumber=1 and pageSize=5
.subscribe((data) => {
this.resultsLength = data.totalCount; // it returns 16
this.tableData = data.items; // data.items.length = 5, but setting resultsLength = 16 is enough I think
});
}
//I use a different method for paging, but I think I wiil use the getEmployee after making pagination work
//this method is triggered by onPageFired()
onPagingAction(event: any) {
const index = event.pageIndex + 1;
const size = event.pageSize;
this.service.getByPagination(index, size, '1==1')
.subscribe((data) => {
this.pageSizeTest = data.totalCount;
this.tableData = data.items;
});
}
The first page is loaded with 5 records after page refresh, but the next button is disabled. I think the problem may caused from setting length property as 5, but not sure.
Related
I am building an app that will fetch data from a websocket, and the size of entity will increase overtime. I want to remove the old items when the size exceeds maximum. I currently use selectTotal$ to get the current size of the entity and if it exceeds maximum, then I will call removeMany(ids) to delete the old items. However, I don't think this is a good way, because the selectTotal$ will be triggered again. Should I do this in reducer when upserting items to the entity?
My current implementation:
this.store.selectTotal$
.pipe(
filter((t) => t > MAX_COUNT),
switchMapTo(this.store.ids$)
)
.subscribe((ids) => {
const removals = ids.slice(MAX_COUNT);
this.store.removeMany(removals);
});
You can write an effect, that will listen to the websocket push but it won't save the data directly. Instead it will check for the current state, calculate a new one and then it'll dispatch an action to actually save the data to the store.
Something like this:
someEffect$ = createEffect(() =>
this.acitons$.pipe(
ofType(processDataPushAction),
withLatestFrom(this.store.selectAll), // Get all entities currently in store
map(([<action.payload>, allData]) => {
let calcResult; //
if (allData.length > MAX_COUNT) {
// splice and assign to calcResult
} else {
calcResult = <action.payload>;
}
return saveDataAction({ calcResult });
})
)
)
I have a component which have a role as a widget in a dashboard. So, I'll use an *ngFor to render as many widgets as the dashboard have. The WidgetComponent is only one and receive a part of its data by #Input() from the parent.
parent
<app-widget *ngFor="let widget of widgets"
[widget]="widget">
</app-widget>
In the child, I listen an event using NGRX Selectors:
this.store.pipe(
select(fromStore.selectPage, {widgetId: this.widgetId, page: this.paginator.currentPage}),
distinctUntilChanged() // can be used, but is unnecessary
).subscribe(rows => {
console.log(rows);
});
When I want to change the page, I dispatch a new event in my store:
ngAfterViewInit(): void {
const pages = currentPage < 3
? [1, 2, 3]
: [currentPage - 1, currentPage, currentPage + 1];
const request: PaginatedData = {
widgetId: this.widget.id,
itemsPerPage: paginator.itemsPerPage,
pages
};
this.store.dispatch(updatePagesAction({request}));
}
selector:
export const selectPage = createSelector(selectState, (state, props) => {
const table = state.widgetTables.find(x => x.widgetId === props.widgetId);
if (typeof table === 'undefined') {
return [];
}
const existingPageKey = Object.keys(table.pages).find(key => key === props.page.toString());
return existingPageKey ? table.pages[existingPageKey] : [];
});
Problem: When I dispatch an action for a widget, there will be fired the selector for all widgets which listen in same time at the store.
I need to fire the selector only for in cause widget. The problem can be that I use the same selector for all widgets?
I can not use a filter() in my widget component pipe() because even if I use something like filter(x => x.widgetId === this.widget.Id), the event will be fired and all widgets will receive again the data, even if is equals with the last value.
Ah, I know: this can be due of at every pange changed, my store return a new state (for all widgets) and so the selectors are fired for all.
Also, I have this feature stored in a service which works very well, but because the app use already ngrx in another modules, I'm thought that is better to align all data which must be saved in memory and used later, to be saved inside a ngrx store (and not using custom services).
thanks
How I would approach the problem
I think you can use a function that returns a selector instead, Try to implement like below
export const selectPageWith = ({widgetId, page}: widgetId: number, page: any) =>
createSelector(selectState, state => {
const table = state.widgetTables.find(x => x.widgetId === widgetId);
if (typeof table === 'undefined') {
return [];
}
const existingPageKey = Object.keys(table.pages).find(key => key === page.toString());
return existingPageKey ? table.pages[existingPageKey] : [];
})
Now you can use this in your component like
this.store.pipe(
select(fromStore.selectPageWith({widgetId: this.widgetId, page: this.paginator.currentPage}),
distinctUntilChanged() // can be used, but is unnecessary
).subscribe(rows => {
console.log(rows);
});
Explanation
Simply we are trying to create unique selectors for each of the widget. By creating a function that returns a selector, different parameters produces different selectors for each widget
I am trying to aggregate a list of dates from a data table, written in Angular, in a Protractor test. I'm doing the aggregation from a PageObject class that is called in the Protractor test. I know that my code is successfully grabbing the text I want, but when I try to console.log the returned array, I get an empty array. I'm still new to Javascript/Typescript, Angular, and Protractor and this may be a result of my newness to the asynchronous nature of this development environment.
Code is as follows,
The PageObject SpecMapper class with method:
import { browser, element, by } from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
row.all(by.css("td.ng-tns-c3-0")).get(2).getText().then(function(text:string) {
stringDatesArray.push(text);
});
}
});
});
return stringDatesArray;
}
}
I know it's not the prettiest code, but it's temporary place holder while my devs make me better attributes/classes/ids to grab my variables. Key things to note is that I create a string Array to hold the values I consider relevant to be returned when the method is finished.
I used WebStorm and put a breakpoint at the stringDatesArray.push(text) and return stringDatesArray lines. The first line shows that the text variable has a string variable that I'm looking for and is successfully getting pushed. I see the success in debug mode as I can see the stringDatesArray and see the values in it. The second line though, the array return, shows that the local variable stringDatesArray is empty. This is echoed in the following code when I try to console.log the array:
The Protractor run Spec class with my test in it:
import { SpecMapperPage } from "./app.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('verify sort order is desc', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
console.log(page.getImportDateSubmittedColumnValues());
});
});
});
});
I acknowledge that this code is not actively using the niceties of Protractor, there's a known issue with our app that will not be addressed for a couple of months, so I am accessing the driver directly 99% of the time.
You'll note that I call the method I posted above as the very last line in the browser.driver.sleep().then() clause, page.getImportDateSubmittedColumnValues().
I thought maybe I was running into asynchronous issues with the call being done before the page was loaded, thus I put it in the .then() clause; but learned with debugging that was not the case. This code should work once I have the array returning properly though.
The console.log is printing an empty [] array. That is synonymous with the results I saw when debugging the above method directly in the PageObject SpecMapper class. I wish to do some verification that the strings are returned properly formatted, and then I'm going to do some date order comparisons. I feel like returning an array of data retrieved from a page is not an unusual request, but I can't seem to find a good way to Google what I'm trying to do.
My apologies if I am hitting some very obvious roadblock, I'm still learning the nuances of Typescript/Angular/Protractor. Thank you for your consideration!
My attempted to used collated promises seemed promising, but fell through on execution.
My Updated PageObject SpecMapper Class
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).each(function(cell, index) {
// cell.getText().then(function(text:string) {
// console.log(text);
// });
promisesArray.push(cell.getText());
});
return protractor.promise.all(promisesArray).then(function(results) {
for(let result of results) {
stringDatesArray.push(result);
}
return stringDatesArray;
});
}
}
My Updated Spec test Using The Updated SpecMapper PO Class
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then(function(results) {
for(let value of results) {
console.log("a value is: " + value);
}
});
});
});
});
});
When I breakpoint in the PO class at the return stringDatesArray; line, I have the following variables in my differing scopes. Note that the promisesArray has 3 objects, but the results array going into the protractor.promise.all( block has 0 objects. I'm not sure what my disconnect is. :/
I think I'm running into a scopes problem that I am having issues understanding. You'll note the commented out promise resolution on the getText(), and this was my POC proving that I am getting the string values I'm expecting, so I'm not sure why it's not working in the Promise Array structure presented as a solution below.
Only other related question that I could find has to do with grabbing a particular row of a table, not specifically aggregating the data to be returned for test verification in Protractor. You can find it here if you're interested.
As you've alluded to your issue is caused by the console.log returning the value of the variable before its actually been populated.
I've taken a snippet from this answer which should allow you to solve it: Is there a way to resolve multiple promises with Protractor?
var x = element(by.id('x')).sendKeys('xxx');
var y = element(by.id('y')).sendKeys('yyy');
var z = element(by.id('z')).sendKeys('zzz');
myFun(x,y,z);
//isEnabled() is contained in the expect() function, so it'll wait for
// myFun() promise to be fulfilled
expect(element(by.id('myButton')).isEnabled()).toBe(true);
// in a common function library
function myFun(Xel,Yel,Zel) {
return protractor.promise.all([Xel,Yel,Zel]).then(function(results){
var xText = results[0];
var yText = results[1];
var zText = results[2];
});
}
So in your code it would be something like
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
promisesArray.push(row.all(by.css("td.ng-tns-c3-0")).get(2).getText());
}
});
});
return protractor.promise.all(promisesArray).then(function(results){
// In here you'll have access to the results
});
}
Theres quite a few different ways you could do it. You could process the data in that method at the end or I think you could return the array within that "then", and access it like so:
page.getImportDateSubmittedColumnValues().then((res) =>{
//And then here you will have access to the array
})
I don't do the Typescript but if you're just looking to get an array of locator texts back from your method, something resembling this should work...
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
$$('.import-component .created-date').each((cell, index) => {
cell.getText().then(text => {
stringDatesArray.push(text);
});
}).then(() => {
return stringDatesArray;
});
}
The answer ended up related to the answer posted on How do I return the response from an asynchronous call?
The final PageObject class function:
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
let promisesArray = [];
// return a promise promising that stringDatesArray will have an array of dates
return new Promise((resolve, reject) => {
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).map((cell) => {
// Gather all the getText's we want the text from
promisesArray.push(cell.getText());
}).then(() => {
protractor.promise.all(promisesArray).then((results) => {
// Resolve the getText's values and shove into array we want to return
for(let result of results) {
stringDatesArray.push(result);
}
}).then(() => {
// Set the filled array as the resolution to the returned promise
resolve(stringDatesArray);
});
});
});
}
}
The final test class:
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then((results) => {
console.log(results);
});
});
});
});
});
The biggest thing was waiting for the different calls to get done running and then waiting for the stringDataArray to be filled. That required the promise(resolve,reject) structure I found in the SO post noted above. I ended up using the lambda (()=>{}) function calls instead of declared (function(){}) for a cleaner look, the method works the same either way. None of the other proposed solutions successfully propagated the array of strings back to my test. I'm working in Typescript, with Protractor.
I have a CRUD API where the get on the entity returns both the list and the total number of items for the current filter configuration.
For example : GET /posts
{
"items": [
{
"id": 1,
"title": "post title 1"
}
...
],
"total": 9
}
How can I create an observable where the HTTP query is executed only one time but where I can subscribe to either the total value or the items list ?
I am in an Angular2 context with typescript.
I first tried this and it worked :
this.http.get('/api/posts').map(response => response.json()).subscribe(data => {
var items = data.items
var total = data.total
});
But I don't like this as I cannot use my interface for the Post entity (or I must defined an interface like that {items: Post, total: number} this is not really graceful). What I would like to do is something like this :
this.myPostService.totalObservable.subscribe(total => this.total = total)
this.myPostService.itemsObservable.subscribe(items => this.items = items)
But without triggering 2 queries.
I looked into this blog post : https://coryrylan.com/blog/angular-2-observable-data-services but I don't like the fact that you first subscribe and then call the list() method to load the list.
If you only need it to run once you can cache the value directly:
var source = this.http.get('/api/posts').map(response => response.json()).publishLast();
//unsubscribe from this when you are done
var subscription = source.connect();
Then you can expose the two sources as:
var totalObservable = source.pluck('total');
var itemsObservable = source.pluck('items');
If on the other hand you need to call these Observables multiple times, then I would recommend that you wrap the get in a triggering Observable and cache:
var source = sourceTrigger
.startWith(0) //Optionally initialize with a starting value
//This ensures that only the latest event is in flight.
.flatMapLatest(() => this.http.get('/api/posts'), (_, resp) => resp.json())
.cache(1);
//Again expose these items using `pluck`
var totalObservable = source.pluck('total');
var itemsObservable = source.pluck('items');
In the second case the sourceTrigger would be an Observable that might be connected to a button event, so that each button click triggers a new request.
I'm anxious to see an example of pagination in a flux environment, I can't wrap my mind around how that would work. I've taken a look at redux's example, but that's not really pagination, just a "load more" button. What I'm looking for is a way to paginate possibly millions of records (so you must use lazy loading).
Here are a few of the pitfalls I'm running into:
1) Someone could load page 20 without loading pages 1-19 (by clicking on a hyperlink, for example).
2) If someone edited a record inline, and then that record no longer satisfied the filter used to include in that list, we'll need to load more data to fill in the empty space left behind.
3) Monitoring props for changes to the page number, you'll need to load more data if that page hasn't been loaded yet.
I would love some examples that note how to overcome these pitfalls. Let me know if you have any suggestions. Thanks!
I added an example based on your title and the last sentence. To address your specific questions:
Of course you can load from page 20, just start from higher request params
Editing the record wouldn't change the state until committed right? So once you committed the edit change the filter would apply and your record would be removed if you edited the criteria that had been set by your filter
You would have your result set fetched further ahead as you paginated - if you clicked next it would load the next one in sequence forward, or if you clicked result start at 40 it would fetch 40-X where X is the count per fetch that you specify. The code example I found uses 10, like most applications, so you would fetch starting at 40, but would get 40 to 50.
This page. Basically Use an Event List Store to hold the data for the child objects to access, the pagination component peice itself and finally, there's the Pagination store that
"updates the current State of the active page and provide a function to calculate the number of pages available to the Pagination based on the Total amount of items and how many of those items are to be displayed per page"
I believe to implement this code you would need an api request with the query parameters such as search keywords and result set preferences. This code was designed to send an api call which returns a json response that could be broken down and presented accordingly (this example does in sets of 10.)
For another working example and there are probably many others, but off hand here is one that I know of personally. This code provided below was Posted from Adam Ellsworth and credits go to him for the code:
EventListStore.js
// requires go here ...
var events = [], // Default Event listing
total = 0, // Default number of Available Events
start = 0, // Default start
end = 9, // Default end
amt = 9; // Number of Events to list per page (0-based)
processTurnPage: function (page) {
start = (page - 1) * amt;
end = start + amt;
}
var EventListStore = assign({}, EventEmitter.prototype, {
...
getTotal: function () {
return total;
},
getStart: function () {
return start;
},
getEnd: function () {
return end;
},
getAmountPerPage: function () {
return amt;
},
...
// emitChange, addChangeListener, removeChangeListener
});
EventListStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
processTurnPage(data);
// Omitted:
// Call the API to get new event data based on our new Page params
EventListStore.emitChange();
break;
}
});
Pagination.Jsx
// requires go here ...
var LIMIT = 12; // The amount of clickable <li> to show
function getNavigation (count, per) {
/**
* This is where we build our <li /> elements. I'm omitting our code because
* there are too many ways in which pagination can be displayed, and ours
* is specific to our needs.
*
* What is returned below is just the gist of it.
*/
var pages = []; // what we'll store our JSX <li /> elements in.
var pages = PaginationStore.getTotalPageCount(count, per);
/**
* Translate our 0-based pagination data to a user-friendly representation
* by starting at 1
*/
for (var i = 1; i <= pages; i++) {
pages.push(
<li className="page" data-value={i} key={i}>
{i}
</li>
);
}
return pages;
}
var Pagination = React.createClass({
componentDidUpdate: function (prevProps, prevState) {
var self = this;
$('.page').unbind().on('click', function (e) {
var value = $(this).data('value');
if (value != self.state.page) {
EventViewActions.turnPage(value);
}
});
},
/**
* Note here that in our EventList.jsx Component we're instantiating our
* <Pagination /> component thusly:
*
* <Pagination total={this.state.total} per={this.state.amount} />
*/
render: function () {
var navigation = getNavigation(this.props.total, this.props.per);
return (
<ul>
{navigation}
</ul>
);
}
});
Pagination.jsx
// requires ...
var _page = 1; // Default page
updatePage: function (page) {
console.log('changing page: ' + _page + ' -> ' + page);
_page = page;
}
var PaginationStore = assign({}, EventEmitter.prototype, {
getPage: function () {
return _page;
},
getTotalPageCount: function (total, per) {
var pages = total - 1;
if (pages > 0) {
if (per < pages) {
return Math.ceil(pages / per);
}
return 1; // only one page of items
} else {
return 0; // no items
}
},
...
});
PaginationStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
updatePage(data)
PaginationStore.emitChange();
break;
}
});