Table control

Table Control is a PHUI class used to create tables that list data. It is useful when you want to list a body of entities (fewer than 100) with multiple properties, or when a data set is too large to display all at once.

Table Control offers several helpful features:

  • Actions on a single entity
  • Actions on multiple entities at the same time
  • Filtering on a specific field or fields
  • Sorting on a specific field or fields

Create a table control

The render of a Table Control is handled by the browser’s JavaScript engine. The page can be as simple as the following:

1<div>content</div>
2<div>title-bar</div>
3
4<h3>title Device Management</h3>
5<div>workspace</div>
6<div id="devices-table"></div>

The table appends to the devices-table div. Obtain the context of the table using JavaScript:

1var context = document.getElementById('devices-table');

Table Control creates a table in the context once you provide additional data about how you want the table to behave. You define this in an object called tableSeed.

For example, to create a table representing a list of horses — where each horse is an entity and you want to show its name, color, and age — create your tableSeed object as follows:

1var tableSeed = {
2 fields: {
3 name: {
4 displayName: 'Name'
5 },
6 color: {
7 displayName: 'Color',
8 },
9 age: {
10 displayname: 'Age',
11 }
12 }
13};

This is the simplest valid tableSeed object. A tableSeed must have a fields property where you define the properties to display as columns. At minimum, specify a displayName — a human-readable description of the field. The key for each field maps to the properties of the horse objects.

Now construct the table:

1var horseTable = new PHUI.Table(context, tableSeed);

You should see a table with columns but no rows. Keep the horseTable instance — you will need it later.

Add entities

Add the first entity

Add static data to your table by creating your first horse as a plain JavaScript object:

1var myFirstHorseJohn = {
2 name: 'John',
3 color: 'Green',
4 age: '42'
5};

Add the horse to the table with the pushEntity method:

1horseTable.pushEntity(myFirstHorseJohn);

Once pushed, the table immediately creates the row, mapping name, color, and age to the fields defined in tableSeed.

Table methods

There are three basic methods for adding entities to a table:

MethodDescription
myTable.pushEntity(entity)Add a single entity to the table.
myTable.concatEntities(entities)Add an array of entities to the table.
myTable.setEntities(entities)Reset the table to display a new array of entities.

Additional methods exposed by Table Control:

MethodDescription
myTable.getEntities()Returns an array of all objects the table currently knows about.
myTable.getMeta()Returns an object with numEntities, a filter object, and a sorter object.
myTable.remove()Removes the table from the DOM.

Table Control automatically handles sorting and filtering as you add entities. Even when concatenating horses, they appear in the appropriate row based on the current sorter and are hidden if filtered. You can concatenate an array of entities at once:

1var newHorses = [
2 {
3 name: 'Sally',
4 age: 24,
5 breed: 'Mustang',
6 gender: 'Female'
7 },
8 {
9 name: 'Rebecca'
10 }
11];
12horseTable.concatEntities(newHorses);

Note that the object properties do not need to match the fields exactly — Rebecca has no age, and Sally has a breed and gender that are not defined as table fields.

Update entities

When an object is added to a table, it inherits a Table Control super class. Table Control stores objects by reference and handles view updates behind the scenes. This means you can modify the original object and Table Control will update the table accordingly.

You can also use table.getEntities() to retrieve all entity objects.

To remove John from the table:

1myFirstHorseJohn.removeFromTable();

The object still exists; the table simply no longer tracks it. The removeFromTable method is added to every object passed through pushEntity, concatEntities, or setEntities.

You can also update an entity property by reassignment, and it takes effect in the table immediately:

1// Note that you still have newHorses.
2var sally = newHorses[0];
3
4sally.color = 'black';
5// Color updates from "N/A" to "black" in the cell immediately.
6
7setTimeout(function() {
8 sally.color = 'white';
9 // Color updates in the view immediately again.
10}, 5000);

Sort and filter

Enable sorting and filtering by setting canSort and canFilter to true on specific fields in your tableSeed:

1var tableSeed = {
2 fields: {
3 name: {
4 displayName: 'Name',
5 canSort: true,
6 canFilter: true
7 },
8 color: {
9 displayName: 'Color',
10 canSort: false,
11 canFilter: false
12 },
13 age: {
14 displayname: 'Age',
15 canSort: true,
16 canFilter: false
17 }
18 }
19};

If no field is marked as defaultSort, the first field marked canSort is used as the default sorter.

To configure a specific field as the default sorter:

1var tableSeed = {
2 age: {
3 displayname: 'Age',
4 canSort: true,
5 defaultSort: -1,
6 canFilter: false
7 }
8};

The defaultSort property accepts 1 (ascending) or -1 (descending).

Define row actions

The rowActions function operates at the entity level and allows users to perform an action on a single entity. For example, to allow editing and selling horses:

1var sellHorse = function(horse, event) {
2 if (window.confirm('Are you sure you want to sell ' + horse.name + '?')) {
3 horse.removeFromTable();
4 }
5};
6
7var tableSeed = {
8 fields: {
9 name: {
10 displayName: 'Name',
11 canSort: true,
12 canFilter: true
13 },
14 color: {
15 displayName: 'Color',
16 canSort: false,
17 canFilter: false
18 },
19 age: {
20 displayname: 'Age',
21 canSort: true,
22 canFilter: false
23 }
24 },
25 rowActions: {
26 edit: {
27 action: function(horse, event) {
28 // ...open modal to get newColor from user...
29 horse.color = 'newColor';
30 },
31 class: 'edit',
32 primary: false,
33 disabled: false
34 },
35 sell: {
36 action: sellHorse,
37 class: 'sell',
38 primary: true,
39 disabled: false
40 }
41 }
42};

The class property is a CSS class set on the button for the action, which appears in a new cell on the right-hand side of the row. Each action’s action property is a function that receives the entity itself.

The second parameter is the DOM event from the button click, as you would normally expect from onclick actions in JavaScript. The primary property specifies which action triggers on row click. At most, one primary action may be specified. If primary is not explicitly set, the first action is primary by default.

Define batch actions

To allow users to perform the same action on multiple rows simultaneously, define batchActions in your tableSeed:

1var brandHorses = function(horses) {
2 for (var i = 0; i < horses.length; i++) {
3 horses[i].color = 'red'; // reflected in the table immediately
4 }
5};
6
7var tableSeed = {
8 batchActions: {
9 'Brand Horses': {
10 action: brandHorses,
11 class: 'brand',
12 disabled: false
13 }
14 }
15};

Table Control creates the interface in the DOM. Users can then check multiple rows and select the Brand Horses button at the bottom of their viewport.

Select entities

To programmatically select certain entities — for example, pre-checking rows for horses the user already owns — set the selected flag on the entity:

1var someHorse;
2someHorse.selected = true;

This causes the horse to appear with its checkbox already checked when pushed to the table.

The selected flag behaves like other entity properties and can be reset in real time:

1setTimeout(function() {
2 horsesTable.getEntities()[0].selected = true;
3}, 5000);

After five seconds, the first horse becomes selected instantly.

Update action properties and override actions

Row actions or batch actions often need to change at runtime — for example, disabling an action for specific entities or setting a processing class during an API call. Each entity object has an actions property, which you can set in two ways:

  1. Define it yourself when adding entities via pushEntities, concatEntities, or setEntities.
  2. Let Table Control set it automatically.

If you allow Table Control to set the actions property, all horses will have it assigned with disabled: false by default (for example, horses[i].actions.edit.disabled = false).

Regardless of how actions is set, you can modify it at runtime and changes take effect immediately:

1var currentHorses = horseTable.getEntities();
2
3// Disable the edit action on the first horse.
4currentHorses[0].actions.edit.disabled = true;
5
6// Update editHorse to show a processing icon during a server call.
7var editHorse = function(horse) {
8 horse.actions.edit.class = 'processing';
9
10 utilities.http('POST', '/horses/sell/' + horse.name)
11 .then(function() {
12 horse.actions.edit.class = 'edit';
13 });
14};

You can also replace entire actions:

1someHorse.actions.edit = {
2 action: function() {
3 console.log('edit has changed');
4 },
5 class: 'new class'
6}

Advanced techniques

Parcel data

Table Control supports chunking data via AJAX calls for lists too large to store in browser memory. To enable this, add the fetch function to your tableSeed:

1var tableSeed = {
2 parcelControl: {
3
4 fetch: function(tableData, isAppend, callback) {
5 /**
6 * @param {Object} tableData
7 * @param {number} tableData.numEntities - The number of entities currently shown.
8 * @param {Object} tableData.sorter - Used when sorting is enabled.
9 * @param {number} tableData.sorter.direction - 1 = descending, -1 = ascending.
10 * @param {string} tableData.sorter.field - The field name used for sorting.
11 * @param {Object} tableData.filter - Key-value pairs where the key is a field name
12 * and the value is a string to match.
13 * @param {boolean} isAppend - true = append data; false = replace data.
14 * @param {fetch~requestCallback} callback - Handles the returned data.
15 */
16
17 /**
18 * @callback fetch~requestCallback
19 * @param {Object[]} newEntities - The response data.
20 * @param {boolean} exhausted - false if more data is available; true when all data returned.
21 */
22
23 var options = {
24 start: isAppend ? tableData.numEntities : 0,
25 limit: 25,
26 sort: tableData.sorter,
27 entity: 'device'
28 };
29
30 if (!isEmpty(tableData.filter)) {
31 options.filter = tableData.filter;
32 }
33
34 utilities.http('POST', '/device_management/devices/filtered', {
35 options: options
36 })
37 .then(function(res) {
38 if (!res) return callback([], true);
39 if (res.total <= res.list.length) return callback(res.list || [], true);
40 return callback(res.list || [], false);
41 })
42 .catch(handleError);
43 }
44 }
45};

A fetch call is made whenever Table Control needs more entities. The table uses lazy loading (infinite scrolling) to display large amounts of data without making calls with huge payloads or overwhelming browser memory. fetch is called repeatedly as the user scrolls. If the user changes the sorter or filter, fetch is called again to reset the table data with the new parameters.

If a table uses parcelling, it must sort and filter by making calls to the server. If it does not use parcelling, Table Control handles sorting and filtering on the client. Never sort or filter data client-side that you know to be a subset of a larger server-side list.

There are two reasons fetch is called:

  • To append to the list on scroll.
  • To reset entities due to a filter or sort change.

The isAppend flag indicates which case applies. When not appending, start from 0 to ensure the list resets on a filter or sort change.

The callback requires two arguments:

ArgumentDescription
Argument 1A list of new entities to append or set on the table.
Argument 2Whether the list is exhausted (for example, numEntities >= totalEntities).

When Argument 2 is true, infinite scrolling stops calling fetch.

Style and size columns

Styling can be done with CSS class modifiers. Table Control adds a class to all <td> and <th> tags corresponding to the field name. For example, a <td> element for the Name field has the class name. This makes it possible to set the width of a specific column using CSS.