- 21 Mar 2024
-
DarkLight
-
PDF
Table Control
- Updated on 21 Mar 2024
-
DarkLight
-
PDF
Table Control is a PHUI class used to create tables that list data. It is useful where you want to list a body of entities (less than 100 entities) with multiple properties or where a data set is too large to display all at once.
Table Control offers several helpful features, including:
- 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.
This guide describes how to create Table Control in an Itential Automation Platform (IAP) page.
Creating a Table Control
The render of a Table Control is handled by the browser's JavaScript engine. The IAP page can be as simple as the following.
<div>content</div>
<div>title-bar</div>
<h3>title Device Management</h3>
<div>workspace</div>
<div id="devices-table"></div>
The table will append to the devices-table
div
.
Obtain the context of the table using JavaScript.
var context = document.getElementById('devices-table');
Table Control will create a table in the context once you give it additional data about how you want the table to act. You can define this in an object called tableSeed
.
Suppose you want to create a table that represents a list of horses; each horse will be an entity. You represent entities internally as objects which may have a variety of properties. For each horse, you want to show their name, color, and age in the table.
Create your tableSeed
object as shown below.
var tableSeed = {
fields: {
name: {
displayName: 'Name'
},
color: {
displayName: 'Color',
},
age: {
displayname: 'Age',
}
}
};
This is the simplest valid tableSeed
object. A tableSeed
must have a fields property where you define the properties you want to display as columns. At the very least, it is recommended you specify a displayName
, which is a human-readable description of the field. The key for each field will map to the properties of the horse objects.
Now construct the table.
var horseTable = new PHUI.Table(context, tableSeed);
You should see a table with columns but no rows in it yet. Keep the horseTable
instance; it will be necessary later.
Adding Entities
Add the First Entity
Now you may add some static data to your table. Create your first horse, represented as a plain JavaScript object:
var myFirstHorseJohn = {
name: 'John',
color: 'Green',
age: '42'
};
Add your first horse to the table with the pushEntity
method.
horseTable.pushEntity(myFirstHorseJohn);
Once the pushEntity
method has pushed myFirstHorse
to the table, the table should immediately create the row. It knows to map name, color, and age to the corresponding fields that you defined in tableSeed
.
Table Methods
There are three basic methods to statically add entities to a table.
Method | Description |
---|---|
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. |
Other Table Methods
In addition to the previous three methods for adding entities, Table Control exposes the following methods.
Method | Description |
---|---|
myTable.getEntities() |
Returns an array of all the objects it currently knows about. |
myTable.getMeta() |
Gives an object with numEntities , a filter object, and a sorter object (explained later). |
myTable.remove() |
Remove the table from the DOM. |
Table Control will automatically handle sorting and filtering as you add entities. Even though the method is concatenating the horses, they will still be in the appropriate row based on the current sorter and will be hidden if filtered. You may also concatenate an array of entities at once.
var newHorses = [
{
name: 'Sally',
age: 24,
breed: 'Mustang',
gender: 'Female'
},
{
name: 'Rebecca'
}
];
horseTable.concatEntities(newHorses);
Notice that the properties in the objects do not map exactly to the fields you specified earlier. Rebecca
does not have an age, and Sally
is a female Mustang
although the table properties do not have a breed
or a gender
property.
Updating Entities
When an object is added to a table, the object inherits a Table Control super class. Table Control stores objects by reference and handles view updates behind the scenes. This allows you to make modifications on the original object, and Table Control will make the appropriate table updates.
You can also use table.getEntities()
to get all the entity objects that you created.
Suppose you want to remove John
.
myFirstHorseJohn.removeFromTable();
Table Control modifies the existing entity object. This way you can perform operations using the objects you originally passed it, or alternatively you can modify the entities from a call to getEntities
. They are one and the same.
You have removed myFirstHorseJohn
from the table. You still have the object; the table just forgot about it. The table added the removeFromTable
method to the myFirstHorseJohn
object when it was added to the table. Every object added to a table in calls to pushEntity
, concatEntities
, or setEntities
adds removeFromTable
. You can also change the property of an existing entity by reassignment, and it will take effect on the entire table.
// Note that you still have newHorses.
var sally = newHorses[0];
sally.color = 'black';
// Color will update from "N/A" to "black" in the cell in the table immediately.
setTimeout(function() {
sally.color = 'white';
// Color will update in the view immediately again.
}, 5000);
Sorting and Filtering
You can sort and filter by setting canSort
and canFilter
to true
on specific fields in your tableSeed
object.
Create a new table with the following tableSeed
.
var tableSeed = {
fields: {
name: {
displayName: 'Name',
canSort: true,
canFilter: true
},
color: {
displayName: 'Color',
canSort: false,
canFilter: false
},
age: {
displayname: 'Age',
canSort: true,
canFilter: false
}
}
};
This will add filtering and sorting only on the specified fields.
If no field is marked as defaultSort
, then the first field marked as canSort
will be used as the default sorter.
You can configure a specific field to be the default sorter using the following syntax.
var tableSeed = {
age: {
displayname: 'Age',
canSort: true,
defaultSort: -1
canFilter: false
}
};
The defaultSort
property can be set to a value of 1 or -1. If configured with a value of 1, default sorting will be in ascending order. If configured with a value of -1, default sorting will be in descending order.
Defining Row Actions
The rowActions
function is performed at the entity level and allows the user to perform an action on a single entity in your table. Suppose you want the user to be able to edit horses and sell horses. You can define these row actions as shown below.
var sellHorse = function(horse, event) {
if (window.confirm('Are you sure you want to sell ' + horse.name + '?')) {
horse.removeFromTable();
}
};
var tableSeed = {
fields: {
name: {
displayName: 'Name',
canSort: true,
canFilter: true
},
color: {
displayName: 'Color',
canSort: false,
canFilter: false
},
age: {
displayname: 'Age',
canSort: true,
canFilter: false
}
},
rowActions: {
edit: {
action: function(horse, event) {
// ...open modal to get newColor from user...
horse.color = 'newColor';
},
class: 'edit',
primary: false,
disabled: false
},
sell: {
action: sellHorse,
class: 'sell',
primary: true,
disabled: false
}
}
};
The class will be a CSS class set on the button for the action, which will appear in a new cell on the right hand side of the column. Each action has an action
property which is a function that takes the entity itself.
The second parameter is the DOM event from the button click, which you would normally expect from onclick
actions in JavaScript. The primary
property specifies which action will be triggered on row click. At most, one primary action may be specified. If primary is not explicit for all fields, then the first action is primary by default.
Defining Batch Actions
Sometimes, you may want to allow the user to perform the same action on multiple rows simultaneously. Suppose you want the user to be able to specify they have branded many horses at once. You can define this batchAction
in your tableSeed
with the following.
var brandHorses = function(horses) {
for (var i = 0; i < horses.length; i++) {
horse.color = 'red'; // this is reflected in the table immediately
}
};
var tableSeed = {
batchActions: {
'Brand Horses': {
action: brandHorses,
class: 'brand',
disabled: false
}
}
};
Table Control will create the interface in the DOM. The user can then check many rows and select the "Brand Horses" button at the bottom of their viewport.
Selecting Entities
Sometimes, you want to programmatically select certain entities. For example, suppose you want users to see all horses including those they do not own, but you want the ones they do own to be preselected, so that when the page loads some of the checkboxes are already checked. You can do that by setting the selected
flag on the entity.
var someHorse.selected = true;
This will cause the horse, when pushed to the table, to come with the checkbox already checked.
The selected
flag behaves like other entity properties that can be reset in real time. For example, suppose you want to check the first entity of a table after waiting five seconds.
setTimeout(function() {
horsesTable.getEntities()[0].selected = true;
}, 5000);
After five seconds, the first horse that was pushed will become selected instantly.
Updating Action Properties and Action Overriding
Row actions or batch actions will often need to change during the run-time of your application. For example, you may want to set an action state to disabled for some entities but not others, or you may want to set the class to processing
for the duration of a call. Each entity object should have an actions
property. There are two ways to set the actions
property:
- Define the property yourself as you add entities using
pushEntities
,concatEntities
, orsetEntities
. - Let Table Control do it for you.
If you choose the first option, it will allow you to specify the action state as you add entities. Moreover, you can actually specify whether an entity has an action disabled server-side.
If you allow Table Control to set the actions property, all horses will have the property assigned horses[i].actions.edit.disabled = false
.
Regardless of which way is used to set the actions
property, entity objects have an actions
property, which can be modified during run-time and the changes will take effect in the table the same as they do with updating properties.
var currentHorses = horseTable.getEntities();
// I want to disable the edit actions on the first horse I added.
currentHorses[0].actions.edit.disabled = true;
// I want to update editHorse so it makes a call to the server to process
// the "transaction". The call takes a while so I want to set a processing
// icon on the button.
var editHorse = function(horse) {
horse.actions.edit.class = 'processing';
utilities.http('POST', '/horses/sell/' + horse.name)
.then(function() {
horse.actions.edit.class = 'edit';
}
};
You can also change entire actions.
someHorse.actions.edit = {
action: function() {
console.log('edit has changed');
},
class: 'new class'
}
Advanced Techniques
Parcelling Data
Table Control supports chunking data by making AJAX calls for lists that would otherwise be too large to store in the browser's primary memory. To do so, you must add the fetch
function to your tableSeed
.
The following is an example of filtering on a device list from an existing program.
var tableSeed = {
parcelControl: {
fetch: function(tableData, isAppend, callback) {
/**
* @param {Object} tableData
* @param {number} tableData.numEntities - The number of entities (table rows) currently
* shown in the browser.
* @param {Object} tableData.sorter - Used when sorting is enabled. Passed through
* to external API call as an options property.
* @param {number} tableData.sorter.direction - If value is 1, sorts in descending order.
* If value is -1, sorts in ascending order.
* @param {string} tableData.sorter.field - The field's name used for sorting.
* @param {Object} tableData.filter - Used when filter is enabled on the table. Passed
* through to external API call as an options property.
* When a filter is applied to the table, this
* parameter contains key-value pairs where the
* key is a field name and the value is a string on
* which to match.
* @param {boolean} isAppend - When true, tells the table that it should append data
* with the next set of incoming data.
* When false, tells the table that it should replace data
* with the next set of incoming data.
* @param {fetch~requestCallback} callback - The callback function that handles the
* returned data.
*/
/**
* @callback fetch~requestCallback
* @param {Object[]} newEntities - The response data.
* @param {boolean} exhausted - Set to false if more data is available.
* Set to true when all data has been returned.
*/
var options = {
start: isAppend ? tableData.numEntities : 0,
limit: 25,
sort: tableData.sorter,
entity: 'device'
};
if (!isEmpty(tableData.filter)) {
options.filter = tableData.filter;
}
utilities.http('POST', '/device_management/devices/filtered', {
options: options
})
.then(function(res) {
if (!res) return callback([], true);
if (res.total <= res.list.length) return callback(res.list || [], true);
return callback(res.list || [], false);
})
.catch(handleError);
}
}
};
A fetch
call is made whenever Table Control decides it needs more entities. The table's parcel control uses lazy loading (also known as infinite scrolling) to display large amounts of data to the user without making calls with huge payloads or inundating browser memory. It will call fetch
repeatedly to add data to the table as the user scrolls. If the user changes the sorter or the filter, then fetch will be also called to reset the entire table data with the new sorted and filtered data. From there, the user will then be able to infinite scroll with the sort and filter applied.
If a table uses parcelling, then it must sort and filter by making calls to the server. If it does not use parcelling, then Table Control will always handle sorting and filtering on its own by executing the logic on the page. The client should never sort and filter data that it gets from the server if it knows that data to be a subset of a larger list.
Essentially, there are two reasons fetch will be called.
- To append to the list on scroll.
- To reset entities due to a filter or sort change.
This is why the isAppend
flag is used. It is true if fetch is called to append to the list, and false if fetch is called to reset entities due to a filter or sort change. This is important because your call needs a starting point (also known as cursor, position, etc.) from which to start the chunk. If not appending, it should start at 0 again to ensure the list starts over on a filter or sort change.
The callback allows the fetch call to perform asynchronous calls. This requires two arguments: an array and a boolean value.
Argument | Description |
---|---|
Argument 1 | A list of new entities to append (or set) to the table. |
Argument 2 | Whether or not the list is exhausted, e.g., numEntities >= totalEntities . |
If Argument 2 is set to true, the infinite scroll will stop trying to call fetch repeatedly.
Styling and Sizing
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 would have the class name
. This makes it possible to set the width of a specific column.