Usage
The HTML and JavaScript tabs build the identical Grid and Form setup shown above,
declaratively via attributes or imperatively via the JS API. The ui-events
strings (e.g. "$bindary.controller.grid.saveItem") are resolved to a matching
function by the component itself, this demo happens to route through a Bindary controller,
but any globally accessible method works the same way. The .NET Backend tab shows the
server-side data source both approaches talk to.
<div id="jobComboBox"
ui-type="ComboBox"
ui-action="clone"
ui-placeholder="- Select -"
ui-allow-input="false"
ui-load-on-render="true"
ui-load-on-demand="true"
ui-ajax='{"load":{"url":"DataServer"}}'
ui-events='{ "onPreLoadItemList": "$bindary.controller.grid.preLoadJobComboBox" }'>
</div>
<div id="companyComboBox"
ui-type="ComboBox"
ui-action="clone"
ui-placeholder="- Select -"
ui-allow-input="false"
ui-load-on-render="true"
ui-load-on-demand="true"
ui-ajax='{"load":{"url":"DataServer"}}'
ui-events='{ "onPreLoadItemList": "$bindary.controller.grid.preLoadCompanyComboBox" }'>
</div>
<div id="demoDataPager" ui-type="DataPager" ui-action="clone" ui-page-size="20"></div>
<div id="grid"
ui-type="Grid"
ui-observing="true"
ui-item-template-id="rowDetail"
ui-row-selection="1"
ui-preserve-total-width="true"
ui-enable-data-pager="true"
ui-data-pager-id="demoDataPager"
ui-fixed-columns="0"
ui-column-sorting="1"
ui-enable-filter-row="true"
ui-row-expansion="1"
ui-ajax='{"load":{"url":"DataServer"}}'
ui-events='{
"onPreLoadItemList": "$bindary.controller.grid.preLoadItemList",
"onItemExpand": "$bindary.controller.grid.expandItem",
"onPostRender": "$bindary.controller.grid.updateSelectionColumn"
}'
ui-column-groups='[
{ "id": "group0", "headerText": "PERSONAL INFO" },
{ "id": "group1", "headerText": "COMPANY INFO" }
]'
ui-columns='[
{
"id": "name", "columnGroupId": "group0", "headerText": "NAME", "width": "180px",
"filterMenuOptions": 1089, "filterMenuSelectedOption": 1024
},
{
"id": "birthDate", "columnGroupId": "group0", "headerText": "BIRTHDATE",
"width": "220px", "filterInputType": 3, "filterMenuOptions": 41
},
{
"id": "gender", "columnGroupId": "group0", "headerText": "GENDER",
"width": "140px"
},
{
"id": "job", "columnGroupId": "group1", "headerText": "JOB TITLE", "width": "180px",
"filterMenuOptions": 1, "filterMenuSelectedOption": 0, "filterInputType": 2,
"filterInputId": "jobComboBox"
},
{
"id": "companyName", "columnGroupId": "group1", "headerText": "COMPANY",
"width": "180px", "filterMenuOptions": 1, "filterMenuSelectedOption": 0,
"filterInputType": 2, "filterInputId": "companyComboBox"
},
{
"id": "country", "columnGroupId": "group1", "headerText": "COUNTRY",
"width": "140px"
}
]'>
<div ui-template="Column_name">{text}</div>
<div ui-template="Column_birthDate">{birthDate}</div>
<div ui-template="Column_gender">{gender}</div>
<div ui-template="Column_job">{job}</div>
<div ui-template="Column_companyName">{companyName}</div>
<div ui-template="Column_country">{country}</div>
<div ui-template="rowDetail">
<div b-context="$.appData.components.grid.expandedItem">
<cui-form id="rowEditForm"
ui-auto-data-bind-fields="true"
ui-bind-to-custom-model="true"
ui-observing="true"
ui-has-data-bind-root-context="true"
ui-labels="{{labels}}"
ui-field-sets='[{"type":"FieldSet","id":"rowFieldSet","fields":[
{
"type":"Field", "name":"name", "bindingKey":"text",
"label":"Name (read-only)", "readOnly":true, "width": "50%"
},
{
"type":"Field", "name":"birthDate",
"bindingKey":"attributes.birthDate",
"label":"Birth date", "inputType":8, "required":true,
"width": "50%", "lineBreak": true
},
{
"type":"Field", "name":"gender", "bindingKey":"attributes.gender",
"label":"Gender", "inputType":3, "inlineOptions":true,
"required":true, "width": "50%", "lineBreak": true,
"options":[
{"type": "FieldOption", "value":"Female","label":"Female"},
{"type": "FieldOption", "value":"Male","label":"Male"}
]
},
{
"type":"Field", "name":"job", "bindingKey":"attributes.job",
"label":"Job title", "placeholder":"- Select -", "inputType":7,
"inputId":"jobComboBox", "required":true,a
"width": "50%", "lineBreak": true
},
{
"type":"Field", "name":"companyName",
"bindingKey":"attributes.companyName",
"label":"Company", "placeholder":"- Select -", "inputType":7,
"inputId":"companyComboBox", "required":true,a
"width": "50%", "lineBreak": true
}
]}]'
ui-events='{"onSubmit":"$bindary.controller.grid.saveItem"}'></cui-form>
</div>
</div>
</div>
The functions referenced above by name live on this demo's Bindary controller:
$bindary.controller.grid =
{
preLoadItemList: (grid, args) =>
{
grid.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadGridData')
);
},
preLoadJobComboBox: (comboBox, args) =>
{
comboBox.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadJobData')
);
},
preLoadCompanyComboBox: (comboBox, args) =>
{
comboBox.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadCompanyData')
);
},
expandItem: (grid, args) =>
{
$bindary.appData.components.grid.expandedItem = args.item;
$bindary.appData.components.grid.expandedItem.labels =
$bindary.appData.components.grid.expandedItem.labels || {};
$bindary.appData.components.grid.expandedItem.labels.headerTitle = args.item.text;
let form = args.td.querySelector('cui-form');
form.setAttribute('ui-labels', '{{labels}}');
},
updateSelectionColumn: (grid) =>
{
const hasCheckboxColumn = grid.columns[0] && grid.columns[0].checkBox === true;
if (grid.rowSelection > 0 && !hasCheckboxColumn)
{
grid.columns.unshift(new componyx.UI.Grid.Column({ checkBox: true }));
grid.render();
}
else if (grid.rowSelection == 0 && hasCheckboxColumn)
{
grid.columns.shift();
grid.render();
}
},
saveItem: async (form) =>
{
const grid = $UI.store.demoGrid,
item = $bindary.appData.components.grid.expandedItem;
await $bindary.dataSendAsync(item, 'Components.GridController.SaveItem');
grid.renderItem(item);
grid.collapseItem(item.id);
}
};
// Job and Company comboboxes, these exist only to be cloned into the
// row-edit Form's job/company fields via inputId, they are never rendered
// on their own (see ui-action="clone" in the HTML tab for the declarative
// equivalent).
const jobCombo = $UI.createComponent(componyx.UI.ComboBox, {
id: 'jobComboBox',
placeholder: '- Select -',
allowInput: false,
loadOnRender: true,
loadOnDemand: true,
containerElement: document.body
});
jobCombo.ajax.load.url = 'DataServer';
jobCombo.events.onPreLoadItemList.add(function ()
{
jobCombo.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadJobData')
);
});
const companyCombo = $UI.createComponent(componyx.UI.ComboBox, {
id: 'companyComboBox',
placeholder: '- Select -',
allowInput: false,
loadOnRender: true,
loadOnDemand: true,
containerElement: document.body
});
companyCombo.ajax.load.url = 'DataServer';
companyCombo.events.onPreLoadItemList.add(function ()
{
companyCombo.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadCompanyData')
);
});
// Data pager and grid
const pager = $UI.createComponent(componyx.UI.DataPager, {
id: 'demoDataPager',
pageSize: 20,
containerElement: document.body
});
const grid = $UI.createComponent(componyx.UI.Grid, {
id: 'grid',
itemTemplateId: 'rowDetail',
observing: true,
preserveTotalWidth: true,
enableDataPager: true,
dataPagerId: pager.id,
rowSelection: 1,
fixedColumns: 3, // Name, plus the selection checkbox and row-expansion columns
columnSorting: 1,
enableFilterRow: true,
rowExpansion: 1,
containerElement: document.body
});
// Wire up grid events
grid.ajax.load.url = 'DataServer';
grid.events.onPreLoadItemList.add(function ()
{
grid.ajax.load.customParameters = window.JSON.stringify(
window.createDataServerMsg('Components.GridController.LoadGridData')
);
});
grid.events.onItemExpand.add(function (grid, args)
{
$bindary.appData.components.grid.expandedItem = args.item;
$bindary.appData.components.grid.expandedItem.labels =
$bindary.appData.components.grid.expandedItem.labels || {};
$bindary.appData.components.grid.expandedItem.labels.headerTitle = args.item.text;
let form = args.td.querySelector('cui-form');
form.setAttribute('ui-labels', '{{labels}}');
});
// Row selection adds/removes a checkbox column automatically
grid.events.onPostRender.add(function (grid)
{
const hasCheckboxColumn = grid.columns[0] && grid.columns[0].checkBox === true;
if (grid.rowSelection > 0 && !hasCheckboxColumn)
{
grid.columns.unshift(new componyx.UI.Grid.Column({ checkBox: true }));
grid.render();
}
else if (grid.rowSelection == 0 && hasCheckboxColumn)
{
grid.columns.shift();
grid.render();
}
});
// Column groups and columns
grid.columnGroups.push(new componyx.UI.Grid.ColumnGroup({
id: 'group0',
headerText: 'PERSONAL INFO'
}));
grid.columnGroups.push(new componyx.UI.Grid.ColumnGroup({
id: 'group1',
headerText: 'COMPANY INFO'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'name',
columnGroupId: 'group0',
headerText: 'NAME',
width: '180px',
filterMenuOptions: 1089,
filterMenuSelectedOption: 1024,
columnTemplate: '{text}'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'birthDate',
columnGroupId: 'group0',
headerText: 'BIRTHDATE',
width: '220px',
filterInputType: 3,
filterMenuOptions: 41,
columnTemplate: '{birthDate}'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'gender',
columnGroupId: 'group0',
headerText: 'GENDER',
width: '140px',
columnTemplate: '{gender}'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'job',
columnGroupId: 'group1',
headerText: 'JOB TITLE',
width: '180px',
filterInputType: 2,
filterMenuOptions: 1,
filterMenuSelectedOption: 0,
filterInputId: 'jobComboBox',
columnTemplate: '{job}'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'companyName',
columnGroupId: 'group1',
headerText: 'COMPANY',
width: '180px',
filterInputType: 2,
filterMenuOptions: 1,
filterMenuSelectedOption: 0,
filterInputId: 'companyComboBox',
columnTemplate: '{companyName}'
}));
grid.columns.push(new componyx.UI.Grid.Column({
id: 'country',
columnGroupId: 'group1',
headerText: 'COUNTRY',
width: '140px',
columnTemplate: '{country}'
}));
// Render the grid
grid.addTemplate('rowDetail', buildRowDetailTemplate(), false);
grid.show();
// Row-edit Form, rendered inside the grid's expanded row
function buildRowDetailTemplate()
{
const container = document.createElement('div');
container.setAttribute('b-context', '$.appData.components.grid.expandedItem');
const form = $UI.createComponent(componyx.UI.Form, {
id: 'rowEditForm',
autoDataBindFields: true,
bindToCustomModel: true,
observing: true,
hasDataBindRootContext: true,
fieldSets: [
new componyx.UI.Form.FieldSet({
id: 'rowFieldSet',
fields: [
new componyx.UI.Form.Field({
name: 'name',
bindingKey: 'text',
label: 'Name (read-only)',
readOnly: true,
width: '50%'
}),
new componyx.UI.Form.Field({
name: 'birthDate',
bindingKey: 'attributes.birthDate',
label: 'Birth date',
inputType: 8,
required: true,
width: '50%',
lineBreak: true
}),
new componyx.UI.Form.Field({
name: 'gender',
bindingKey: 'attributes.gender',
label: 'Gender',
inputType: 3,
inlineOptions: true,
required: true,
width: '50%',
lineBreak: true,
options: [
new componyx.UI.Form.FieldOption({ value: 'Female', label: 'Female' }),
new componyx.UI.Form.FieldOption({ value: 'Male', label: 'Male' })
]
}),
new componyx.UI.Form.Field({
name: 'job',
bindingKey: 'attributes.job',
label: 'Job title',
placeholder: '- Select -',
inputType: 7,
inputId: 'jobComboBox',
required: true,
width: '50%',
lineBreak: true
}),
new componyx.UI.Form.Field({
name: 'companyName',
bindingKey: 'attributes.companyName',
label: 'Company',
placeholder: '- Select -',
inputType: 7,
inputId: 'companyComboBox',
required: true,
width: '50%',
lineBreak: true
})
]
})
],
events: {
onSubmit: async function (form)
{
const item = $bindary.appData.components.grid.expandedItem;
await $bindary.dataSendAsync(item, 'Components.GridController.SaveItem');
grid.renderItem(item);
grid.collapseItem(item.id);
}
},
containerElement: container
});
form.show();
return container;
}
private const int PageSize = 20;
private const string DateFilterFormat = "MM-dd-yyyy";
private static readonly string[] DateFilterFormats = { "MM-dd-yyyy", "MM/dd/yyyy", "yyyy-MM-dd" };
private static readonly Dictionary<string, Func<Person, object>> SortSelectors = new()
{
["name"] = p => p.Name,
["birthDate"] = p => p.BirthDate,
["gender"] = p => p.Gender.ToString(),
["job"] = p => p.Job,
["companyName"] = p => p.Company.Name,
["country"] = p => p.Company.Country
};
private static readonly Dictionary<string, Func<Person, GridColumnFilter, bool>> FilterMatchers = new()
{
["name"] = (p, f) => MatchesText(p.Name, f),
["birthDate"] = (p, f) => MatchesDate(p.BirthDate, f),
["gender"] = (p, f) => MatchesText(p.Gender.ToString(), f),
["job"] = (p, f) => MatchesText(p.Job, f),
["companyName"] = (p, f) => MatchesText(p.Company.Name, f),
["country"] = (p, f) => MatchesText(p.Company.Country, f)
};
public static Task LoadJobData(Client client, DataRequest request) =>
LoadComboData(client, request, Global.DemoData.Jobs);
public static Task LoadCompanyData(Client client, DataRequest request) =>
LoadComboData(client, request, Global.DemoData.Companies.Select(c => c.Name));
private static async Task LoadComboData(
Client client, DataRequest request, IEnumerable<string> values)
{
var itemList = new ItemList<ComboBoxItem>();
foreach (var value in values)
itemList.Add(new ComboBoxItem { Id = value, Text = value });
var result = new ComboBoxAjaxResult(itemList);
await Server.Send(client, result.Serialize(), request.XhrRequestId);
}
public static async Task LoadGridData(Client client, DataRequest request)
{
GridRequest gridRequest = JsonConvert.DeserializeObject<GridRequest>(
request.Message,
new JsonSerializerSettings { DateFormatString = DateFilterFormat });
ItemList<GridItem> itemList = new ItemList<GridItem>();
AjaxResult<GridItem> result = new AjaxResult<GridItem>(itemList);
IEnumerable<Person> filtered = Global.DemoData.Persons;
if (gridRequest.Filter != null)
{
foreach (var columnFilter in gridRequest.Filter)
{
if (!FilterMatchers.TryGetValue(columnFilter.Key, out var matcher))
continue;
filtered = filtered.Where(p => matcher(p, columnFilter.Value));
}
}
IOrderedEnumerable<Person> sorted = null;
if (gridRequest.SortOrder != null)
{
foreach (var sort in gridRequest.SortOrder)
{
if (!SortSelectors.TryGetValue(sort.Key, out var selector))
continue;
bool descending = sort.Value > 0;
sorted = (sorted == null)
? (descending ? filtered.OrderByDescending(selector) : filtered.OrderBy(selector))
: (descending ? sorted.ThenByDescending(selector) : sorted.ThenBy(selector));
}
}
if (sorted != null)
filtered = sorted;
int pageIndex = gridRequest.PageIndex ?? 1;
foreach (var person in filtered.Skip((pageIndex - 1) * PageSize).Take(PageSize))
itemList.Add(CreateItem(person));
result.TotalItemCount = filtered.Count();
await Server.Send(client, result.Serialize(), request.XhrRequestId);
}
private static GridItem CreateItem(Person person)
{
GridItem item = new GridItem();
Dictionary<string, object> attributes = new Dictionary<string, object>();
item.Id = person.Id.ToString();
item.Value = item.Id;
item.Text = person.Name;
attributes.Add("birthDate", person.BirthDate.ToString(DateFilterFormat));
attributes.Add("gender", person.Gender.ToString());
attributes.Add("job", person.Job);
attributes.Add("companyName", person.Company.Name);
attributes.Add("country", person.Company.Country);
item.Attributes = new Attributes(attributes);
return item;
}
public static async Task SaveItem(Client client, DataRequest request)
{
GridItem item = JsonConvert.DeserializeObject<GridItem>(request.Data);
Person person = Global.DemoData.Persons.FirstOrDefault(p => p.Id == int.Parse(item.Id));
person.BirthDate = DateTime.ParseExact(
item.Attributes["birthDate"], DateFilterFormats, CultureInfo.InvariantCulture, DateTimeStyles.None);
person.Gender = Enum.Parse<Gender>(item.Attributes["gender"]);
person.Job = item.Attributes["job"];
person.Company = new Company
{
Name = item.Attributes["companyName"],
Country = Global.DemoData.Companies
.Where(c => c.Name == item.Attributes["companyName"])
.FirstOrDefault().Country
};
var data = UI.Json.Utility.Serialize(new { callbackId = request.CallbackId });
await Server.Send(client, data, request.XhrRequestId);
}
private static bool MatchesText(string fieldValue, GridColumnFilter filter)
{
if (filter?.Value == null || filter.Value.Length == 0)
return true;
switch (filter.Type)
{
case "empty":
return string.IsNullOrEmpty(fieldValue);
case "notempty":
return !string.IsNullOrEmpty(fieldValue);
case "in":
return filter.Value.Any(v => string.Equals(fieldValue, v?.ToString(), StringComparison.OrdinalIgnoreCase));
case "notin":
return !filter.Value.Any(v => string.Equals(fieldValue, v?.ToString(), StringComparison.OrdinalIgnoreCase));
}
string value = filter.Value[0]?.ToString() ?? string.Empty;
return filter.Type switch
{
"equalto" => string.Equals(fieldValue, value, StringComparison.OrdinalIgnoreCase),
"notequalto" => !string.Equals(fieldValue, value, StringComparison.OrdinalIgnoreCase),
"startswith" => fieldValue.StartsWith(value, StringComparison.OrdinalIgnoreCase),
"notstartswith" => !fieldValue.StartsWith(value, StringComparison.OrdinalIgnoreCase),
"endswith" => fieldValue.EndsWith(value, StringComparison.OrdinalIgnoreCase),
"notendswith" => !fieldValue.EndsWith(value, StringComparison.OrdinalIgnoreCase),
"contains" => fieldValue.Contains(value, StringComparison.OrdinalIgnoreCase),
"notcontains" => !fieldValue.Contains(value, StringComparison.OrdinalIgnoreCase),
_ => true
};
}
private static bool MatchesDate(DateTime fieldValue, GridColumnFilter filter)
{
if (filter?.Value == null || filter.Value.Length == 0)
return true;
if (!DateTime.TryParseExact(filter.Value[0]?.ToString(), DateFilterFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkDate))
return true;
return filter.Type switch
{
"equalto" => fieldValue.Date == checkDate.Date,
"greaterthanorequalto" => fieldValue.Date >= checkDate.Date,
"lessthanorequalto" => fieldValue.Date <= checkDate.Date,
_ => true
};
}
onItemExpand points the shared expandedItem context at the row
being expanded. The Form binds directly to that context, so changes update the
shared data live. onSubmit then sends the changes to the server, calls
grid.renderItem() to refresh the row and collapses it. See
Form for the full set of field types and validation options
available beyond what's shown here.