UI component for DataGrids provides UI component for DataGrids
They are located in the keeper bundle, that can be included like so:
<use:bundle path="keeper:bundle"/>
See Usage Samples in the demo repository
A simple data grid declaration would look like so:
<ui:grid url="@action('users.list', inject('params', []))" namespace="main">
<grid:cell.text name="id" label="#"/>
<grid:cell.text name="firstName" label="First Name"/>
<grid:cell.text name="lastName" label="Last Name"/>
</ui:grid>
DataGrids are described in a declarative way. The code is not iterated in php/stempler phase, but later in the JavaScript phase, meaning you can't use php conditions for cell renders inside declarations. If you need a conditional renderer for cell, you are expected to write one in JavaScript.
Columns should be defined prioritizing semantics over column names. Typically most columns would match a sort key. For
example, if you have a data row with the first and the last name and a user, you might want to have a column name
with firstName
and lastName
joined.
<grid:cell.link name="name" label="Name" url="@action('users.edit', ['user' => '{id}'])" sort="true">
{firstName} {lastName}
</grid:cell.link>
<ui:grid
url="/some/url"
method="GET"
id="my-grid"
namespace="foo"
capture-forms="['form1','form2']"
capture-filters="['filter1','filter2']"
paginate-options="[10,20,30]"
actions-title=""
actions-label="Actions"
actions-kind=""
actions-icon="cog"
actions-size="sm"
actions-class=""
actions-cell-class="">
</ui:grid>
Parameter | Required | Default | Description |
---|---|---|---|
url | yes | - | Url that implements DataGrid API |
method | no | GET | Http method to use, GET or POST |
id | no | [auto generated] | Id of datagrid to use |
namespace | no | [empty] | A prefix for field names that is used in the datagrid filters serialization. It's used when multiple datagrids are present on page. I.e. if namespace="foo" filter value bar=1 will end up as "foo-bar=1" in URL. It's developer's responsibility to use namespaces if multiple datagrids are present on page. Otherwise the behavior is unpredictable |
capture-forms | no | [empty] | JSON array of strings. It attaches forms by their ids to datagrid as a filter fields source. It can be used for filters that are visually separated from datagrids, i.e. in the nav panel or sidebar. It's used internally to attach the filter defined with <ui:filter> |
capture-filters | no | [empty] | JSON array of strings. It attaches the instances of filter toggle buttons |
paginate-options | no | [empty] | JSON array of numbers. It options for the default paginator |
action-* | no | [as in sample] | A set of parameters that is used to generate the Actions button and the corresponding column |
Limitation: All the filter forms should produce a flat object map without files. Nesting and/or arrays are not supported at the moment.
Grid:filter component attaches the filter and/or search forms directly to the grid. Internally it works with the 'capture-forms' parameter that technically allows to attach any number of forms to the datagrid.
<ui:grid url="@action('users.list', inject('params', []))" namespace="main">
<grid:filter search="true" immediate="300" buttons="true">
<form:input name="firstName" label="First Name" value="" size="6" required="true"/>
<form:input name="lastName" label="Last Name" value="" size="6" required="true"/>
<form:input name="email" label="Email" value="" required="true"/>
</grid:filter>
</ui:grid>
Parameter | Required | Default | Description |
---|---|---|---|
search | no | false | Renders a search input in the top right corner |
search-name | no | "search" | The name of the search field |
fields | no | - | JSON array of strings. If you want the filter button to indicate the number of active filters correctly, list all the form fields names as a JSON array of strings. For the example above that would be fields="['firstName','lastName','email']" . If it's not specified, the filter button does not indicate if the filter values are active or not |
refresh | no | false | Renders a refresh button to trigger data refresh |
immediate | no | - | If it's specified, the search input will have no "Submit" button and the search will be performed as user types with the debounce value equivalent to this param value in milliseconds. |
[tag body] | no | - | Insides of tag are used as an additional filter form shown in the modal window if it's specified |
buttons | no | false | If they're specified as true, the default "Clear" and "Apply" buttons will append to the tag body |
Note, the filter form is a separate form to the "search input" form and its reset button has effect only on the filter form.
<ui:grid url="@action('users.list', inject('params', []))" namespace="main">
<grid:filter search="true"/>
</ui:grid>
<ui:grid url="@action('users.list', inject('params', []))" namespace="main">
<grid:filter search="true" immediate="300" buttons="true">
<form:input name="firstName" label="First Name" value="" size="6" required="true"/>
<form:input name="lastName" label="Last Name" value="" size="6" required="true"/>
<form:input name="email" label="Email" value="" required="true"/>
</grid:filter>
</ui:grid>
Lots of cells allow to use templates using row fields as variables.
The used template system is handlebars templates. Typically field accepting templates have
pre-processors that convert {
to {{
, so if you want to output something like {{firstName}} {{lastName}}
, you pass
it in as {firstName} {lastName}
<grid:cell.text
name="user"
label="User Name"
/>
Directly outputs a field in a cell
Parameter | Required | Default | Description |
---|---|---|---|
name | yes | - | A semantic column name which is matching the field from the server data |
label | yes | - | A column label |
sort | no | - | Enables sorting for a column |
sort-default | no | - | Supply "true" to make this column a defaultly sorted column. Only one column should have this |
sort-dir | no | 'asc' | A default sort direction, 'asc' or 'desc' |
<grid:cell.link
name="user"
title="Edit User {firstName}"
body="{fistName}"
href="/edit/{id}"
/>
Outputs a link
Parameter | Required | Default | Description |
---|---|---|---|
name | yes | - | A semantic column name which is matching the field from the server data. It should match the sorter name if the sort is used. Content is the field itself as default, but it can be customized with the 'body' parameter to output another field |
label | yes | - | A column label |
href | yes | - | A template to use for the link url |
title | no | - | A template to use for the link title text |
body | no | - | A template to use for the link body. Instead of specifying the 'body' attribute, it possible to use the tag body |
sort | no | - | Enables sorting for a column |
sort-default | no | - | Supply "true" to make this column a defaultly sorted column. Only one column should have this |
sort-dir | no | 'asc' | A default sort direction, 'asc' or 'desc' |
<grid:cell.link
name="created"
label="Created At"
format="LLL dd, yyyy HH:mm"
sort="true"
sort-dir="desc"
sort-default="true"
/>
Outputs a date
Parameter | Required | Default | Description |
---|---|---|---|
name | yes | - | A semantic column name which is matching the field from the server data. It accepts ISO dates with a timezone. |
label | yes | - | A column label |
format | no | LLL dd, yyyy HH:mm | A date format. See Luxon docs for available tokens |
sort | no | - | Enables sorting for a column |
sort-default | no | - | Supply "true" to make this column a defaultly sorted column. Only one column should have this. |
sort-dir | no | 'asc' | A default sort direction, 'asc' or 'desc' |
A separate kind of tags that are provided are action tags. Using any of them adds the 'actions' column with a button with a drwondown list of actions. Each action's tag appends an action to that dropdown.
To customize the look of the button, use action-*
parameters of ui:grid
<grid:action.link
href="edit/{id}"
template="{id}"
label="Edit"
title="Edit {firstName}"
icon="edit"
target="_blank"
/>
Outputs a link
Parameter | Required | Default | Description |
---|---|---|---|
href | yes | - | A template to use for link url |
title | no | - | A template to use for the link title text |
target | no | _self | Specifies the target of the link |
template | no | - | A template to use for the link body. Instead of specifying the 'template' attribute, the tag body can be used |
label | no | - | Text to use for the link text if the template param is not used. It supports templates if it's passed as unescaped value, i.e. label = "{!! '{{variable}}' !!}" |
icon | no | - | A name to use for the link icon if the template param is not used. It uses Font Awesome icons. It supports templates if it's passed as unescaped value, i.e. icon = "{!! '{{variable}}' !!}" |
<grid:action.action
href="edit/{id}"
template="{id}"
label="Edit"
title="Edit {firstName}"
icon="edit"
method="POST"
data="{ foo: 1}"
refresh="true"
confirm="true"
confirm-title="true"
confirm-ok="true"
confirm-cancel="true"
/>
Makes a request to server
Parameter | Required | Default | Description |
---|---|---|---|
href | yes | - | A template to use for action url |
method | no | GET | A method name to use |
title | no | - | A template to use for the action title text |
template | no | - | A template to use for the link body. Instead of specifying the 'template' attribute, it's possible to use the tag body |
label | no | - | Text to use for the link text if the template param is not used. It supports templates if it's passed as unescaped value, i.e. label = "{!! '{{variable}}' !!}" |
icon | no | - | A name to use for the link icon if the template param is not used. It uses Font Awesome icons. It supports templates if it's passed as unescaped value, i.e. icon = "{!! '{{variable}}' !!}" |
body | no | - | JSON String to attach as a request body. It supports templates if it's passed as unescaped value, i.e. data = "{!! '{ "id": {{variable}} }' !!}" |
refresh | no | - | A boolean value which is indicating if a success response from the server should trigger a datagrid refresh |
condition | no | - | A field name that contains boolean variable indicating if that action should be shown. Alternatively there could be a handlebars template that should return '' for a false value or anything else for a positive value in case more complex logic is needed |
toastError | no | - | A template for the toast message to show on an action error |
toastSuccess | no | - | A template for the toast message to show on an action success |
confirm | no | - | If it's specified, specify confirm-title, confirm-ok and confirm-cancel. It will show a confirm dialog before executing an action. confirm specifies handlebars template for confirm dialog body |
confirm-title | no | - | Specifies the handlebars template for the confirm dialog title |
confirm-ok | no | - | Specifies the handlebars template for the confirmation dialog of the positive result button text |
confirm-cancel | no | - | Specifies the handlebars template for the confirmation dialog of the negative result button text |
<grid:action.delete
href="edit/{id}"
/>
It's the same as 'grid:action' but with predefined confirmation texts and a danger class. Typically only href
is
needed for that action type.
<grid:cell.template
name="user"
label="User Name"
body="<i class='fa fa-user'></i> {firstName} {lastName}"
/>
Outputs custom template
Parameter | Required | Default | Description |
---|---|---|---|
name | yes | - | A semantic column name which is matching the field from the server data. It should match the sorter name if the sort is used. The content is the field itself as default, but it can be customized with the 'body' parameter to output another field |
label | yes | - | A column label |
body | no | - | A template to use for the body. Instead of specifying the 'body' attribute, it's possible to use the tag body |
sort | no | - | Enables sorting for a column |
sort-default | no | - | Supply "true" to make this column a defaultly sorted column. Only one column should have this |
sort-dir | no | 'asc' | A default sort direction, 'asc' or 'desc' |
<grid:cell.render
name="roles"
label="Roles"
renderer="roles"
/>
It uses a custom function to render the cell
Parameter | Required | Default | Description |
---|---|---|---|
name | yes | - | A Semantic column name matching the field from server the data. It should match the sorter name if the sort is used. The content is the field itself as default, but it can be customized with the 'renderer' parameter to output another field |
label | yes | - | A column label |
renderer | no | - | A Name of a pre-registered function that is able to render a cell |
sort | no | - | Enables sorting for a column |
sort-default | no | - | Supply "true" to make this column a defaultly sorted column. Only one column should have this |
sort-dir | no | 'asc' | A default sort direction, 'asc' or 'desc' |
To define a custom function for the datagrid, add a script tag before the toolkit declarations. If you see 'renderer not found' error, most likely you've outputted the renderer too late in the code.
<stack:push name="scripts" unique-id="datagrid-roles-renderer">
<script type="text/javascript">
window.SFToolkit_tools_datagrid = window.SFToolkit_tools_datagrid || {};
window.SFToolkit_tools_datagrid['roles'] = function () {
return function (roles) {
return roles.map(function (role) {
return '<span class="badge badge-primary mr-1">' + role.toUpperCase() + '</span>'
}).join('');
}
};
</script>
</stack:push>
The function returned by the render function should be CellRenderFunction and it accepts a lot of useful params allowing to customize the render in a really flexible way.
DataGrid is based on the JavaScript library and can be initialized with JavaScript
The JavaScript declaration allows for more flexibility, i.e. you can have multiple actions columns, batch actions and selections, custom renderers directly in the code without the need to pre-define them.
See DataGrid Source Code for more details.
<div class="sf-table">
<div class="js-sf-datagrid">
@declare(syntax=off)
<script type="text/javascript" role="sf-options">
(function () {
return {
"id": "custom",
"url": "/keeper/users/list",
"namespace": "custom",
"method": "GET",
"ui": {
"headerCellClassName": {"actions": "text-right"},
"cellClassName": {"actions": "text-right py-2", "created": "text-nowrap"}
},
"paginator": {"limitOptions": [10, 20, 50, 100]},
"sort": "created",
"columns": [{"id": "name", "title": "Name", "sortDir": "asc"}, {"id": "actions2", "title": " "}, {
"id": "email",
"title": "Email",
"sortDir": "asc"
}, {"id": "created", "title": "Created At", "sortDir": "desc"}, {
"id": "roles",
"title": "Roles",
"sortDir": null
}, {"id": "id", "title": "ID", "sortDir": null}, {"id": "actions", "title": " "}],
"selectable": {
"type": "multiple",
"id": "id"
},
"renderers": {
"cells": {
"name": {
"name": "link",
"arguments": {
"title": "",
"body": "{{firstName}} {{lastName}}",
"href": "\/keeper\/users\/{{id}}"
}
},
"email": {
"name": "link",
"arguments": {"title": "", "body": "{{email}}", "href": "mailto:{{email}}"}
},
"created": {"name": "dateFormat", "arguments": ["LLL dd, yyyy HH:mm"]},
"roles": {"name": "roles", "arguments": []},
"id": {"name": "template", "arguments": ["{{id}}"]},
"actions": {
"name": "actions",
"arguments": {
"kind": "",
"size": "sm",
"className": "",
"icon": "cog",
"label": "Actions",
"actions": [{
"type": "href",
"url": "\/keeper\/users\/{{id}}",
"label": "Edit",
"target": null,
"icon": "edit",
"template": ""
}, {
"type": "action",
"url": "\/keeper\/users\/{{id}}",
"method": "DELETE",
"label": "Delete",
"icon": "trash",
"template": "<span class=\"text-danger\"><i class=\"fa fw fa-trash\"><\/i> Delete<\/span>",
"condition": null,
"data": [],
"refresh": true,
"confirm": {
"body": "Are you sure to delete this entry?",
"title": "Confirmation Required",
"confirm": "Delete",
"confirmKind": "danger",
"cancel": "Cancel"
},
"toastSuccess": "<i class=\"fa fa-check-circle\"><\/i> {{message}}\n ",
"toastError": "<i class=\"fa fa-exclamation\"><\/i> {{error}}\n "
}]
}
},
"actions2": {
"name": "actions",
"arguments": {
"kind": "",
"size": "sm",
"className": "",
"icon": "cog",
"label": "Actions 2",
"actions": [{
"type": "href",
"url": "\/keeper\/users\/{{id}}",
"label": "Edit",
"target": null,
"icon": "edit",
"template": ""
}, {
"type": "action",
"url": "\/keeper\/users\/{{id}}",
"method": "DELETE",
"label": "Delete",
"icon": "trash",
"template": "<span class=\"text-danger\"><i class=\"fa fw fa-trash\"><\/i> Delete<\/span>",
"condition": null,
"data": [],
"refresh": true,
"confirm": {
"body": "Are you sure to delete this entry?",
"title": "Confirmation Required",
"confirm": "Delete",
"confirmKind": "danger",
"cancel": "Cancel"
},
"toastSuccess": "<i class=\"fa fa-check-circle\"><\/i> {{message}}\n ",
"toastError": "<i class=\"fa fa-exclamation\"><\/i> {{error}}\n "
}]
}
}
},
"actions": {
"delete": {
renderAs: "<div class='btn btn-danger'>Delete</div>",
onClick: function (state, grid) {
console.log(state, grid);
}
}
}
}
};
});
</script>
@declare(syntax=on)
</div>
</div>