Finlex List View (@finlexlabs/list)
Library Name: finlex-list Package Name: @finlexlabs/list Repo Name: fx-ng-components
Steps to Build & Publish Library
Package Renaming
Go to ./src/finlex-list/package.json
Rename package name from @fx-ng-components/finlex-list
to @finlexlabs/list
Build
npm run build:list
It will build finlex-confirm-dialog using ng-packagr.
The build artifacts will be stored in the dist/
directory.
Publishing
ng build:list
, go to the dist folder cd dist/finlex-list
and run npm publish
.
After building your library with - Finlex List View is a wrapper component encapsulating the functionality provided by Material Table (
mat-table
) and Material Paginator (mat-paginator
) to produce a standard list design and functionality across all finlex application. - If used together with
FinlexListAdaptor
and a service implementingfinlex-api
, it can also be used to provide standardized server-side functionality for fetching, sorting, pagination, filtering the list data. - There is a seperate
finlex-flex-list
which inherts Finlex List View logic (and renders the list a bit differently). Please refer to its separate readme if you're interested in that.
NOTE:
We are heavily using Material Table @angular/material/table so in case you're not familiar with them already, following resources will be a good start before you understand finlex-list-view and its functionality:
Background knowledge:
- Read here an intro to Angular Table Component
- Read here an intro to Angular Paginator Component.
- Read here an intro to Angular Sort Component.
(TL;DR) Simple example to use finlex-list for list view with finlexListAdaptor:
module.ts
import { FinlexListModule } from '@finlexlabs/list';
@NgModule({
imports: [ FinlexListModule ]
})
export class ProductsModule { }
controller.ts
import { FinlexTableDataSourceAdapter, COLUMN_TYPE } from '@finlexlabs/list';
constructor(
private listAdaptor: FinlexTableDataSourceAdapter,
){}
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// HTTP query param to append to each call
this.productsList.currentQuery = {
fields: [ 'name', 'created_at' ]
};
// buttons to be displayed for each row (in the last column)
this.productsList.rowActions.push(
{
label: 'open_product',
tooltipText: 'highlight info',
icon: 'pageview',
ngIf: (item) => item.status == 1,
classes: () => ['accent-fg'],
onClick: product => console.info('DO SOMETHING WHEN THIS ACTION BUTTON IS CLICKED FOR ITEM', product)
}
);
// Specify column definition for the list
this.productsList.displayedColumns = [
{
columnKey: 'name',
header: this.translate.instant('NAME_LABEL'),
type: COLUMN_TYPE.Text
},
{
columnKey: 'created_at',
header: this.translate.instant('CREATED_AT_LABEL'),
type: COLUMN_TYPE.Date
},
{
columnKey: 'status',
header: this.translate.instant('STATUS'),
type: COLUMN_TYPE.Text,
icon: {
onHover: (tender) => this.onNotifyPortfolioManagerForUnderwriterRemarks(tender?._id),
ngIf: (tender) => tender?.notify_portfolio_manager_on_underwriter_remark,
name: 'info',
tooltipText: this.translate.instant('UNDERWRITER_HAS_LEFT_A_COMMENT_FOR_YOU'),
classes: () => 'warn-fg'
},
];
view.ts
<finlex-list-view [listAdaptor]="productsList"></finlex-list-view>
(TL;DR) Simple example to use finlex-list for list view without finlexListAdaptor:
module.ts
import { FinlexListModule } from '@finlexlabs/list';
import { MatTableModule } from '@angular/material/table';
@NgModule({
imports: [ FinlexListModule, MatTableModule ]
})
export class ProductsModule { }
controller.ts
import { MatTableDataSource } from '@angular/material/table';
ngOnInit(){
// creating dummy data only
const listData = [];
for (let i = 0; i < 200; i++) {
listData.push( { id: i, name: `sample object ${i}` } )
}
// its important to create instance of MatTableDataSource
this.sampleListConfig.data = new MatTableDataSource(listData);
// the displayedColumns should be according to the data
this.sampleListConfig.displayedColumns = [
{
header: 'ID',
columnKey: 'id'
},
{
header: 'Name',
columnKey: 'name'
}
];
// only relavent if pagination is desired
this.sampleListConfig.config = {
paginate: true
}
}
view.ts
<finlex-list-view [dataSource]="sampleListConfig.data"
[displayedColumns]="sampleListConfig.displayedColumns"
[config]="sampleListConfig.config"></finlex-list-view>
Input bindings for component
-
listAdaptor
(type: FinlexTableDataSourceAdapter, required: false)
Although recommended for using server-side support, its not required. More details about this bindings will be explained below. If used, this binding alone is sufficient to render a complete list, please read more about FinlexListAdaptor
in the section below.
-
-
displayedColumns
(type: Array, required: true)
-
displayedColumns are used initialize the columns shown for mat-table
and provide customization of each column. The schema and functoinlity of each property is described as below:
Array<{
columnKey: string; --> used to fetch data in dataSource (each row object will use this key to get the value to dislplay)
header: string; --> Used to display column title in header row
translate?: Object; --> object with <key, value> to manually translate row values. It can be used for enums translation also.
classes?: Function, --> Function to be called for each row item to determine if it should display any css class.
type?: COLUMN_TYPE --> for formatting the data in the column.
modifyValue?: Function --> Function to be called for each row item to modify the actual value of each cell for that column.
}>;
We are currently using Angular's
CurrencyPipe
to format the currency column cells andDatePipe
to format the dates column cells. These pipes should normally be provided in the app.module.ts rather than finlex-list-view explicitly.
-
-
rowActions
(type: Array, required: false)
-
rowActions binding can be used with or without listAdaptor
. If used, a new column is added at the end of the table and the cell for each row shows a button for each rowAction provided. The schema and functionality for each rowAction is as follows:
Array<{
ngIf: Function, --> If provided, its called with each row object and used to decide if the action button is shown for the row or not.
onClick: Function; --> If clicked, the onClick function is triggered with row object.
icon?: string; --> If provided, the button is an icon button. The icon should be string with valid material-icon name.
label?: string, --> If icon is not provided, a label can also be provided to show as text for the button.
}>;
-
-
config
(type: object, required: false)
-
In case used, the config object is used to allow different configurations of the list-view. Each configuration and its functionality is described below:
{
selectable?: boolean, --> If each row should display a checkbox to allow selection of rows (Also see output binding selectionChanged).
paginate?: any, --> If listAdaptor is not used, paginate boolean can be used to show/hide the pagination info at the bottom of the list.
emptyValue?: string, --> If provided, this text is shown whenever a row cell is empty.
highlightCols? : number, --> This will be depricated, it was used to highlight multiple columns at the left.
highlightRows? : number --> This will be depricated, it was used to highlight multiple rows at the top
emptyListPlaceholder?: string --> Optional, can be used to display custom placeholder text when list is empty
}
-
footerRow
(type: any[], required: false)
In case we want to display a footer row with a standard layout and design, this input binding can supply the data for the footer row. It should be an array with the value at each index in the same order as displayedColumns
binding.
- *
dataSource
(type: MatTableDataSource, required: false)
In case not using listAdaptor
binding, this will become required. Its basically MatTableDataSource instance that contains the data to be shown in the list. Its basically passed on to dataSource
binding of mat-table
.
To disable a particular row, the datasource should include the property "excluded: true" for on that particular index
- *
dataReady
(type: boolean, default: true, required: false)
Only used when we want to show the list loading UI, usually its necessary when we are loading async data and while its being fetched.
- *
sortColDirection
(type: 'asc' | 'desc', default: 'asc')
Passed on to matSortDirection
directive of mat-sort
to apply client-side sort for the data shown in the browser (does not support server-side data sorting).
- *
sortColName
(type: string, required: false)
Passed on to matSortActive
directive of mat-sort
to identify the column name that is used for sorting the data.
Output bindings for component
-
selectionChanged
(type: EventEmitter)
In case the input binding config.selectable
= true, this event is emitted anytime a row is selected or deselected. Please note that the event returns entire selected rows without any prasing/formatting.
-
rowClick
(type: EventEmitter)
If provided, this event is triggered when user clicks on any of the row. Unlike rowActions
input binding, this event is not triggered when the buttons are clicked for each row, this is emitted when the user clicks anywhere on any of the row. The event returns the clicked row in row format.
-
-
pageChanged
(type: EventEmitter)
-
This event is fired when user changes the page of the list (same time when mat-paginator
fires page
event). It would only be necessary to use if we want to apply pagination manually (without listAdaptor
binding). In contrast to raw page
event from mat-paginator
, we parse the response to our own following schema:
// e is the raw event emitted by page
{
limit : e.pageSize,
skip : e.pageIndex * e.pageSize,
pageIndex : e.pageIndex
};
* FOOTNOTE: All the bindings identified with * are used internaly by the
FinlexListAdaptor
to provide server-side functionality. So if we're usinglistAdaptor
binding (which is recommended), we don't need to use these bindings. In fact, using these bindings together withlistAdaptor
binding might lead to unexpected behavior.
FinlexListAdaptor class
FinlexListAdaptor is our own custom driver for finlex-list-view
to use it together with the a service using finlex-api
functionality. It calls the endpoint (supplied to the createList
method of the class instance) to fetch the data from the service and makes subsequent calls to fetch the data again in case the sorting, pagination or filters for the list have changed.
FinlexListAdaptor Functions
If listAdaptor
binding is used for finlex-list-view
, we can also call following methods on FinlexListAdaptor
instance to manually trigger sort, pagination and filter server-side. Following are the functions/functionality available on instances of FinlexListAdaptor:
applySort
params: object with column name and -1 | 1
as value. This is called internally by finlex-list-view
if the user clicks on column header. If we wish to update sorting of the list from another user-defined behavior, we can call the function like this:
controller.ts
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// this function will update the list by making server-side call
this.productsList.applySort({
'status' : 1
})
getPageSize
Get current pagination size. params: None. we can call the function like this:
controller.ts
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
this.pageSize = this.productsList.getPageSize();
applyPagination
params: pageIndex and pageSize. This is called internally by finlex-list-view
if the user clicks 'Next Page' or 'Previous Page' or 'Page Number'. If we wish to update pagination of the list from another user-defined behavior, we can call the function like this:
controller.ts
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// this function will update the list by making server-side call
this.productsList.applyPagination({
'insurer_id' : '<<INSURER_ID>>'
})
applyFilters
params:
- filters: object with object property and value.
- customizedUrlQueryParam(optional) : This method also updates the nrowser url based on the filters argument. For example, if there is a filter based on customer_id, this will be added to the url tree like this---> .../...?customer_id=:customer_id
for complex filters such as matching elements inside an array of objects, the passed 'filters' argument will have nested objects, meaning the browser url will not be verbose, rather be something like---> .../...?some_field=%5Bbject%20Object%5D
'customizedUrlQueryParam' can be passed for a clearer meaningful browser url. For example, check expired-contracts-list filter.
This is not called internally by finlex-list-view
. We can call the function like this:
controller.ts
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// this function will update the list by making server-side call
this.productsList.applyFilters({
'insurer_id' : 'INSURER_ID'
})
FinlexListAdaptor Propertis
-
displayedColumns
|rowActions
|config
If using listAdaptor
binding, the above mentioned bindings of finlex-list-view
can be passed as properties of the FinlexListDisplayAdaptor
isntance. Example code:
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// set all config options
this.productsList.config = {
selectable: false
};
// set single config option
this.productsList.config.selectable = false;
// rowActions
this.productsList.rowActions.push(
{
label: 'open_product',
icon: 'pageview',
ngIf: (product) => product.status == ProductVersion.STATUS.ACTIVE,
onClick: product => this.router.navigate(['product-configurations', product.product_id])
}
);
// displayedColumns
this.productsList.displayedColumns = [
{
columnKey: 'name',
header: this.translate.instant('NAME_LABEL'),
type: COLUMN_TYPE.Text
},
{
columnKey: 'created_at',
header: this.translate.instant('CREATED_AT_LABEL'),
type: COLUMN_TYPE.Date
},
];
-
currentSort
|currentFilters
|currentQuery
|currentPageIndex
|currentPageSize
These properties have two purposes:
- They can be read to understand the current state of the list.
- They can be set to update the list state without triggering an immediate server-side call, in this case, the next server-call (made by whatever next user-behavior triggers a server call) will apply this state. This is specially useful when initalizing the a
FinlexListDisplay
instance (where we want to set a state of the list without triggering call each time we do so). Sample code:
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
// this will not trigger a service call immediately
this.productsList.currentQuery = {
fields: [
'name',
'created_at',
'status'
]
};
// this will not trigger a service call immediately
this.productsList.currentSort = {
'created_at' : 1
};
this.productsList.currentPageIndex = 0; // open the first page
this.productsList.currentPageSize = 10; // set page size of list
-
onListUpdated
(type: EventEmitter)
This property can be subscribed to be aware in the controller, in case necessary, when the list data is updated through a new service call. We don't have any exisitng use-case for this but it can prove useful in case we're showing the list data or state outside the list. For example:
this.productsList = this.listAdaptor.createList(this.productService.products_endpoint);
this.productsList.onListUpdated
.subscribe(newListData => {
console.info('controller knows list data is updated to', newListData);
});
Future Enhancement (suggestive):
- Improve
footerRow
binding to accept an object withdisplayedColumns
as keys and values as the value to be shown in the cell. Currently, its using an array so if thedisplayedColumns
are changed, the footerRow might show different values unless it is changed together. - Use Material to format dates in
finlex-list-view
so its the same asfinlex-form-control
. Currently we're using AngularDatePipe
which leads to different formatting string for material and Angular DatePipe.