This page shows how to get started with Wijmo's CollectionView class.
Wijmo 5 has a solid infrastructure based on a powerful data layer that is familiar to
.NET developers. The main data binding interface is the
ICollectionView
. Wijmo includes several classes that implement
ICollectionView. The most basic is the
CollectionView
class, which uses regular JavaScript arrays as data sources.
The CollectionView class implements the following interfaces:
ICollectionView: Provides current record management, custom sorting,
filtering, and grouping.
IEditableCollectionView: Provides methods for editing, adding, and
removing items.
IPagedCollectionView: Provides paging for navigating through large
numbers of items.
The CollectionView class can keep track of changes made to the data. This
feature is useful for submitting changes to the server.
Getting Started
To use the CollectionView class, start by declaring it and passing in a
regular array as the data source. Then access the view using the items property.
In this example, we show the CollectionView instance in an HTML table.
Steps for getting started with the CollectionView class in AngularJS applications:
Add references to AngularJS, Wijmo, and the Wijmo AngularJS directives.
Optionally add a reference to FlexGrid.
Add references to your app, services, filters, and directives modules.
Add a table (or FlexGrid) to the page and bind it to the CollectionView data.
Add a controller to provide data and logic.
Optionally add some CSS to customize the table's appearance.
Notes: In the Tracking Changes sample below, we use FlexGrid instead of a table,
so you can see the difference in the markup.
<div class="sGrid">
<table class="table table-condensed table-bordered">
<thead>
<tr class="active">
<th class="text-center" *ngFor="let fieldName of fieldNames">{{fieldName}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of cvGettingStarted.items">
<td class="text-center" *ngFor="let name of fieldNames">{{item[name] | globalize}}</td>
</tr>
</tbody>
</table>
</div>
// Angular
import * as wjcCore from 'wijmo/wijmo';
import { Component, EventEmitter, Input, Inject, enableProdMode, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';
import { TabsModule } from './components/AppTab';
import { GlobalizePipe } from './pipes/appPipes';
import { DataSvc } from './services/DataSvc';
'use strict';
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvGettingStarted: wjcCore.CollectionView;
fieldNames: string[];
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvGettingStarted = new wjcCore.CollectionView(dataSvc.getData(10));
this.fieldNames = dataSvc.getNames();
}
}
@NgModule({
imports: [BrowserModule, FormsModule, TabsModule],
declarations: [GlobalizePipe, AppCmp],
providers: [DataSvc],
bootstrap: [AppCmp]
})
export class AppModule {
}
enableProdMode();
// Bootstrap application with hash style navigation and global services.
platformBrowserDynamic().bootstrapModule(AppModule);;
/* set default grid height and some shadow */
.sGrid {
background-color: #fff;
box-shadow: 4px 4px 10px 0 rgba(50, 50, 50, 0.75);
height: 300px;
margin-bottom: 12px;
overflow: auto;
}
Result (live):
{{fieldName}}
{{item[name] | globalize}}
Current Record Management
Since it implements the ICollectionView interface, you can use the CollectionView
class to manage the current record.
This example shows how you can use the API provided in the CollectionView class to
change the current record by clicking a row in the grid or by clicking buttons.
We use the currentPosition property to get the position of the current record
in the collection, and use the following methods to change the current position:
moveCurrentTo(item)
moveCurrentToFirst()
moveCurrentToLast()
moveCurrentToNext()
moveCurrentToPosition(index)
moveCurrentToPrevious()
When the position changes, we use the currentChanging and currentChanged
events to track it. We can cancel the change of position in the currentChanging
event.
Click the Next button to set the next record as the current one. Click the
Previous button to set the previous record as the current one. Click the
Stop at 4th Row button to prevent the current record from being changed once
it reaches the 4th row. Click the Clear button to remove the stop and allow
the current records to be changed freely.
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvCRM: wjcCore.CollectionView;
fieldNames: string[];
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvCRM = new wjcCore.CollectionView(dataSvc.getData(10));
this.fieldNames = dataSvc.getNames();
}
// current record management
stopCurrent() {
this.cvCRM.currentChanging.addHandler(this._stopCurrentIn4th);
};
// restore to be able to change current.
reset() {
this.cvCRM.currentChanging.removeHandler(this._stopCurrentIn4th);
};
// forbid changing current when the current item is the 4th one.
private _stopCurrentIn4th(sender, e) {
// when the current is the 4rd item, stop moving.
if (sender.currentPosition === 3) {
e.cancel = true;
}
}
}
Result (live):
{{fieldName}}
{{item[name] | globalize}}
Sorting
The CollectionView class, like the one in .NET, implements the
ICollectionView interface to support sorting. To enable sorting,
add one or more
SortDescription
objects to an array and pass it to the
CollectionView.sortDescriptions property. Now you can get the sorted
results from the CollectionView.items property.
SortDescription objects are flexible, allowing you to sort data based
on a value in ascending or descending order. In the sample below, click a
property in the grid header to sort by that property in ascending order.
Click it again to sort by that property in descending order.
// Angular
import { Component, EventEmitter, Input, Inject, enableProdMode, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';
import { TabsModule } from './components/AppTab';
import { GlobalizePipe } from './pipes/appPipes';
import { DataSvc } from './services/DataSvc';
'use strict';
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvSorting: wjcCore.CollectionView;
fieldNames: string[];
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvSorting = new wjcCore.CollectionView(dataSvc.getData(10));
this.fieldNames = dataSvc.getNames();
}
// sorting
toggleSort(fieldName:string) {
// get all the sort descriptions.
var sd = this.cvSorting.sortDescriptions;
var ascending = true;
// try to find whether the field has been sorted.
if (sd.length > 0 && sd[0].property === fieldName) {
// if finded, toggle the sort order.
ascending = !sd[0].ascending;
}
// create a new SortDescription object.
var sdNew = new wjcCore.SortDescription(fieldName, ascending);
// remove any old sort descriptors and add the created one.
sd.splice(0, sd.length, sdNew);
};
// get the sort label
getSort(propName:string) {
var sd = this.cvSorting.sortDescriptions;
if (sd.length > 0 && sd[0].property === propName) {
return sd[0].ascending ? '▲' : '▼';
}
return '';
};
}
@NgModule({
imports: [BrowserModule, FormsModule, TabsModule],
declarations: [GlobalizePipe, AppCmp],
providers: [DataSvc],
bootstrap: [AppCmp]
})
export class AppModule {
}
enableProdMode();
// Bootstrap application with hash style navigation and global services.
platformBrowserDynamic().bootstrapModule(AppModule);
Result (live):
{{fieldName}}{{getSort(fieldName)}}
{{item[name] | globalize}}
Filtering
The CollectionView class, like the one in .NET, implements the ICollectionView
interface to support filtering. To enable filtering, set the CollectionView.filter
property to a function that determines which objects to include
in the view.
In this example, we create a filter for the country, and get the filter
value from the input control. When you enter a value by which to filter, the grid
refreshes and shows only the filtered data.
<div class="row-fluid well">
<input type="text" class="form-control app-pad" placeholder="Please enter characters for the country by which to filter (case-insensitive)" [(ngModel)]="filter" />
</div>
<div class="sGrid">
<table class="table table-condensed table-bordered">
<thead>
<tr class="active">
<th class="text-center" *ngFor="let fieldName of fieldNames">{{fieldName}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of cvFiltering.items">
<td class="text-center" *ngFor="let name of fieldNames">{{item[name] | globalize}}</td>
</tr>
</tbody>
</table>
</div>
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvFiltering: wjcCore.CollectionView;
fieldNames: string[];
private _toFilter: any;
private _thisFilterFunction: wjcCore.IPredicate;
private _filter: string;
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvFiltering = new wjcCore.CollectionView(dataSvc.getData(10));
this.fieldNames = dataSvc.getNames();
this._thisFilterFunction = this._filterFunction.bind(this);
}
// filtering
get filter(): string {
return this._filter;
}
set filter(value: string) {
if (this._filter != value) {
this._filter = value;
this._applyFilter();
}
}
// apply filter (applied on a 500 ms timeOut)
private _applyFilter() {
if (this._toFilter) {
clearTimeout(this._toFilter);
}
//var self = this;
this._toFilter = setTimeout(() => {
this._toFilter = null;
if (this.cvFiltering) {
var cv = this.cvFiltering;
if (cv) {
if (cv.filter != this._thisFilterFunction) {
cv.filter = this._thisFilterFunction;
} else {
cv.refresh();
}
}
}
}, 500);
}
// ICollectionView filter function
private _filterFunction(item: any): boolean {
var filter = this.filter.toLowerCase();
if (!filter) {
return true;
}
return item['country'].toLowerCase().indexOf(filter) > -1;
}
}
Result (live):
{{fieldName}}
{{item[name] | globalize}}
Grouping
The CollectionView class, like the one in .NET, implements the ICollectionView
interface to support grouping. To enable grouping, add one or more
GroupDescription
objects to an array and pass it to the CollectionView.groupDescriptions
property. You must also set the grid's showGroups property to true when creating
the grid instance, as the default value is false.
GroupDescription objects are flexible, allowing you to group data
based on value or on grouping functions.
The example below groups the collection by whichever field you select from the
list. The grid shows not only the item's content but also group information:
the group name and the average value of the amount for the group appear in the
group header row.
Notes: Selecting one item in the list adds a new instance of the
GroupDescription. Subsequent selections nest groups. If you select an item from
the list for which a GroupDescription object already exists, nothing happens.
In order to clear the group settings, select the first item in the list.
<select class="form-control" [(ngModel)]="selectedGroupOpt">
<option [ngValue]="''" >Please choose the field by which to group.</option>
<option *ngFor="let name of fieldNames" [ngValue]="name">{{name}}</option>
</select>
</div>
<div class="sGrid">
<table class="table table-condensed table-bordered">
<thead>
<tr class="active">
<th class="text-center" *ngFor="let fieldName of fieldNames">{{fieldName}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of groupItems">
<td class="active" [ngStyle]="{display:isGroupItem(item)? '':'none'}" colspan="6">
<span [ngStyle]="{display:'inline-block', width: (item.level*25) + 'px'}"></span>
<b>{{item.name | globalize}}</b> ({{item.items?.length}} items)
</td>
<td class="text-center" colspan="2" [ngStyle]="{display:isGroupItem(item)? '':'none'}" >
{{avgAmount(item)}}
</td>
<td class="text-center" *ngFor="let name of fieldNames" [ngStyle]="{display:isGroupItem(item)? 'none':''}">
{{item[name] | globalize}}
</td>
</tr>
</tbody>
</table>
</div>
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvGrouping: wjcCore.CollectionView;
fieldNames: string[];
groupItems: any;
private _selectedGroupOpt = '';
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvGrouping = new wjcCore.CollectionView(dataSvc.getData(20));
this.fieldNames = dataSvc.getNames();
this.groupItems = this.cvGrouping.items;
// update the group list
this.cvGrouping.collectionChanged.addHandler(() => {
this.groupItems = this.cvGrouping.items;
if (this.cvGrouping.groups && this.cvGrouping.groups.length > 0) {
this.groupItems = [];
for (var i = 0; i < this.cvGrouping.groups.length; i++) {
this._addGroup(this.cvGrouping.groups[i]);
}
}
});
}
// grouping
get selectedGroupOpt(): string {
return this._selectedGroupOpt;
}
set selectedGroupOpt(value: string) {
if (this._selectedGroupOpt != value) {
this._selectedGroupOpt = value;
this._applyGrouping();
}
}
private _applyGrouping() {
var gd,
fieldName = this.selectedGroupOpt;
gd = this.cvGrouping.groupDescriptions;
if (!fieldName) {
// clear all the group settings.
gd.splice(0, gd.length);
return;
}
if (this._findGroup(fieldName) >= 0) {
return;
}
if (fieldName == 'amount') {
// when grouping by amount, use ranges instead of specific values
gd.push(new wjcCore.PropertyGroupDescription(fieldName, function (item, propName) {
var value = item[propName]; // amount
if (value > 1000) return 'Large Amounts';
if (value > 100) return 'Medium Amounts';
if (value > 0) return 'Small Amounts';
return 'Negative Amounts';
}));
}
else {
// group by specific property values
gd.push(new wjcCore.PropertyGroupDescription(fieldName));
}
}
// check whether the group with the specified property name already exists.
private _findGroup(propName: string) {
var gd = this.cvGrouping.groupDescriptions;
for (var i = 0; i < gd.length; i++) {
if (gd[i].propertyName === propName) {
return i;
}
}
return -1;
}
}
The CollectionView class, like the one in .NET, implements the IEditableCollectionView
interface to support editing.
This sample shows how you can update, add, and remove items in a collection.
In the grid, select a row and press the Edit Detail button below to start
editing. When you finish editing in the popup dialog, press the OK button to
commit your updates. If you want to add a new record to the collection, press the
Add button and enter content for the item in the popup dialog. Press OK to
commit the new record. Select a row and press the Delete button to remove a
record from the collection.
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvEditing: wjcCore.CollectionView;
fieldNames: string[];
currentItem: any;
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvEditing = new wjcCore.CollectionView(dataSvc.getData(20));
this.fieldNames = dataSvc.getNames();
this.currentItem = this.cvEditing.currentItem;
// define the new item value.
this.cvEditing.newItemCreator = () => {
var item = dataSvc.getData(1)[0];
// aggregate the max value of id in the collection.
item.id = wijmo.getAggregate(wijmo.Aggregate.Max, this.cvEditing.sourceCollection, 'id') + 1;
return item;
}
// syn the scope currentItem with the collectionview.
this.cvEditing.currentChanged.addHandler(() => {
this.currentItem = this.cvEditing.currentItem;
});
}
// editing
confirmUpdate() {
// commit editing/adding
this.cvEditing.commitEdit();
this.cvEditing.commitNew();
};
cancelUpdate() {
// cancel editing or adding
this.cvEditing.cancelEdit();
this.cvEditing.cancelNew();
};
}
Result (live):
{{fieldName}}
{{item[name] | globalize}}
Edit Item
ID
Start Date
End Start
Country
Product
Color
Amount
Active
Paging
The CollectionView class, like the one in .NET, implements the
IPagedCollectionView interface to support paging.
To enable paging, set the IPagedCollectionView.pageSize
property to the number of items you want on each page, and provide a UI
for navigating the pages.
In this example, the CollectionView object is initialized to show 10 items
per page. You can customize it in the text box. We add navigation buttons, and
call IPagedCollectionView methods in the button click event. Note that we use the
pageIndex and pageCount properties to show
the current page and total number of pages. You can customize the page size in
the first text box. Leave it empty or set it to 0 to disable paging and hide
the navigation buttons.
The CollectionView class can keep track of changes made to the
data. This is useful in situations where you must submit changes
to the server. To turn on change tracking, set the trackChanges
property to true. Once you do that, the CollectionView keeps
track of any changes made to the data and exposes them in three
arrays:
itemsEdited: This list contains items that are edited using
the beginEdit and commitEdit methods.
itemsAdded: This list contains items that are added using the
addNew and commitNew methods.
itemsRemoved: This list contains items that are removed using
the remove method.
This feature is demonstrated below using a FlexGrid. The grid is bound
to a CollectionView with trackChanges set to true.
import { Component, EventEmitter, Input, Inject, enableProdMode, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';
import { WjCoreModule } from 'wijmo/wijmo.angular2.core';
import { WjGridModule } from 'wijmo/wijmo.angular2.grid';
import { WjInputModule } from 'wijmo/wijmo.angular2.input';
import { TabsModule } from './components/AppTab';
import { GlobalizePipe } from './pipes/appPipes';
import { DataSvc } from './services/DataSvc';
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html',
})
export class AppCmp {
cvTrackingChanges: wjcCore.CollectionView;
fieldNames: string[];
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvTrackingChanges = new wjcCore.CollectionView(dataSvc.getData(6));
this.fieldNames = dataSvc.getNames();
this.cvTrackingChanges.trackChanges = true;
}
}
@NgModule({
imports: [WjCoreModule, WjInputModule, WjGridModule, BrowserModule, FormsModule, TabsModule],
declarations: [GlobalizePipe, AppCmp],
providers: [DataSvc],
bootstrap: [AppCmp]
})
export class AppModule {
}
/* set default grid height and some shadow */
.sGrid {
background-color: #fff;
box-shadow: 4px 4px 10px 0 rgba(50, 50, 50, 0.75);
height: 300px;
margin-bottom: 12px;
overflow: auto;
}
/* set the record grids height and some shadow */
.tcGrid {
background-color: #fff;
box-shadow: 4px 4px 10px 0 rgba(50, 50, 50, 0.75);
height: 100px;
margin-bottom: 12px;
overflow: auto;
}
Result (live):
Change the data here
See the changes here
Items edited:
Items added:
Items removed:
Tracking changes with Customization
When you edit an item on a CollectionView with change tracking on, the
item is added to the itemdEdited collection. However, the
CollectionView doesn't keep track of the item's original values, so
if you later edit it again and restore the original values, the item will
remain in the itemdEdited collection.
But you can change that behavior if you want. This example uses the events
exposed by the CollectionView and itemsChanged classes to keep
track if the original values for each item, and to remove items from the
itemsEdited collection if the user restores the original values.
// The application root component.
@Component({
selector: 'app-cmp',
templateUrl: 'src/app.html'
})
export class AppCmp {
cvTrackingChangesExtra: wjcCore.CollectionView;
fieldNames: string[];
current: any;
constructor( @Inject(DataSvc) dataSvc: DataSvc) {
// initialize the collectionview
this.cvTrackingChangesExtra = new wjcCore.CollectionView(dataSvc.getData(6));
this.fieldNames = dataSvc.getNames();
this.cvTrackingChangesExtra.trackChanges = true;
// keep the original state of the current item in tracking Changes
this.current = this.cvTrackingChangesExtra.currentItem ? JSON.stringify(this.cvTrackingChangesExtra.currentItem) : null;
this.cvTrackingChangesExtra.currentChanged.addHandler((s, e) => {
this.current = s.currentItem ? JSON.stringify(s.currentItem) : null;
});
// keep track of the original state of edited items
var original = [];
this.cvTrackingChangesExtra.itemsEdited.collectionChanged.addHandler((s, e: any) => {
if (e.action == wjcCore.NotifyCollectionChangedAction.Add ||
e.action == wjcCore.NotifyCollectionChangedAction.Change) {
// check if we have this item's original data
var index = this.cvTrackingChangesExtra.sourceCollection.indexOf(e.item);
var found = -1;
for (var i = 0; i < original.length && found < 0; i++) {
if (original[i].index == index) {
found = i;
}
}
// if we have the item, check original value
if (found > -1) {
// if the current value is the same as the original, remove
var valueNow = JSON.stringify(e.item);
if (valueNow == original[found].value) {
original.splice(found, 1);
index = this.cvTrackingChangesExtra.itemsEdited.indexOf(e.item);
this.cvTrackingChangesExtra.itemsEdited.splice(index, 1);
}
} else { // if we don't, store it now
found = original.length;
original.push({ index: index, value: this.current });
}
}
});
}
}
/* set default grid height and some shadow */
.sGrid {
background-color: #fff;
box-shadow: 4px 4px 10px 0 rgba(50, 50, 50, 0.75);
height: 300px;
margin-bottom: 12px;
overflow: auto;
}
/* set the record grids height and some shadow */
.tcGrid {
background-color: #fff;
box-shadow: 4px 4px 10px 0 rgba(50, 50, 50, 0.75);
height: 100px;
margin-bottom: 12px;
overflow: auto;
}