Skip to the content.

Data widgets

Pwoli provides a set of widgets that can be used to display data.

ListView and GridView can be used to display a list or table of data records providing features like pagination, sorting and filtering.

ListView

The ListView widget is used to display data from a data provider. Each data model is rendered using the specified itemView. Since it provides features such as pagination, sorting and filtering out of the box, it is handy both to display information to end user and to create data managing UI.

A typical usage is as follows:

import { ListView,ActiveDataProvider } from 'pwoli';

let dataProvider = new ActiveDataProvider({
    query: {},
    pagination: {
        pageSize: 20,
    },
});
let list = new ListView({
    dataProvider: dataProvider,
    itemView: '_post',
});

//In the view file:
<%- list.render() %>

The _post view file could contain the following:

// The class `Html` should be passed to the view while rendering it
<div class="post">
    <h2><%- Html.encode(model.title) %></h2>

    <%- model.text %>
</div>

In the view file above, the current data model is available as model. Additionally the following variables are available:

If you need to pass additional data to each view, you can use the viewParams property to pass key value pairs like the following:

new ListView({
    dataProvider: dataProvider,
    itemView: '_post',
    viewParams: [
        fullView: true,
        context: 'main-page',
        // ...
    ],
});

These are then also available as variables in the view.

GridView

Data grid or GridView is one of the most powerful Pwoli widgets. It is extremely useful if you need to quickly build the admin section of the system. It takes data from a data provider and renders each row using a set of GridView.columns() presenting data in the form of a table.

Each row of the table represents the data of a single data item, and a column usually represents an attribute of the item (some columns may correspond to complex expressions of attributes or static text).

The minimal code needed to use GridView is as follows:

import { GridView, ActiveDataProvider } from 'pwoli';

let dataProvider = new ActiveDataProvider({
    query: { where: { status: 1 } },
    pagination: {
        pageSize: 20,
    },
});
let grid = GridView.{
    dataProvider: dataProvider,
});

//In the view file:
<%- grid.render() %>

The above code first creates a data provider and then uses GridView to display every attribute in every row taken from the data provider. The displayed table is equipped with sorting and pagination functionality out of the box.

Grid columns

The columns of the grid table are configured in terms of Column classes, which are configured in the GridView.columns() property of GridView configuration. Depending on column type and settings these are able to present data differently. The default class is DataColumn, which represents a model attribute and can be sorted and filtered by.

let grid = GridView({
    dataProvider: dataProvider,
    columns: [
        { class: 'SerialColumn' },
        // Simple columns defined by the data contained in dataProvider.
        // Data from the model's column will be used.
        'id',
        'username',
        // More complex one.
        {
            class: 'DataColumn', // can be omitted, as it is the default
            value: function (data) {
                return data.name; // data['name'] for array data, e.g. using ArrayDataProvider.
            },
        },
    ],
});

Note that if the GridView.columns() part of the configuration isn’t specified, Pwoli tries to show all possible columns of the data provider’s model.

Column classes

Grid columns could be customized by using different column classes:

let grid = new GridView({
    dataProvider: dataProvider,
    columns:
        {
            class: 'SerialColumn', // <-- here
            // you may configure additional properties here
        }}),

In addition to column classes provided by Pwoli that we’ll review below, you can create your own column classes.

Each column class extends from Column so that there are some common options you can set while configuring grid columns.

You may specify various container HTML options by passing arrays to:

Data column

DataColumn is used for displaying and sorting data. It is the default column type so the specifying class could be omitted when using it:

let grid = new GridView({
    columns: {
        {
            attribute: 'name',
        },
        {
            attribute: 'birthday',
        },
        created_at:datetime, // shortcut format
        {
            label: 'Education',
            attribute: 'education',
            filter: [0: 'Elementary', 1: 'Secondary', 2: 'Higher'},
            filterInputOptions: {prompt: 'All educations', class: 'form-control', id: null}
        },
    });

For configuring data columns there is also a shortcut format which is described in the API documentation for GridView.columns().

Use filter and filterInputOptionsto control HTML for the filter input.

By default, column headers are rendered by Sort.link(). It could be adjusted using header. To change header text you should set label like in the example above. By default the label will be populated from data model. For more details see getHeaderCellLabel.

Action column

ActionColumn displays action buttons such as update or delete for each row.

let grid = new GridView({
    dataProvider: dataProvider,
    columns: {
        class: 'ActionColumn',
        // you may configure additional properties here
    },
});

Available properties you can configure are:

Checkbox column

CheckboxColumn displays a column of checkboxes.

To add a CheckboxColumn to the GridView, add it to the GridView.columns() configuration as follows:

let grid = new GridView([
    id: 'grid',
    dataProvider: dataProvider,
    columns:
        {
            class: 'CheckboxColumn',
            // you may configure additional properties here
        },
    }))

Users may click on the checkboxes to select rows of the grid. The selected rows may be obtained by calling the following JavaScript code:

var keys = $('#grid').yiiGridView('getSelectedRows');
// keys is an array consisting of the keys associated with the selected rows

Serial column

SerialColumn renders row numbers starting with 1 and going forward.

Usage is as simple as the following:

let grid = new GridView({
    dataProvider: dataProvider,
    columns:
        {class: 'SerialColumn'}, // <-- here
        // ...

Filtering data

For filtering data, the GridView needs a filterModel that represents the search criteria which is usually taken from the filter fields in the GridView table. Pwoli declares a search() method in the IORMAdapter that will return the data provider with an adjusted query that processes the search criteria.

So each Model you extend from Pwoli’s base Model has this default search method built-in.

You can use this function in the controller to get the dataProvider for the GridView:

let filterModel = new Post();
let dataProvider = filterModel.search(DataHelper.parseUrl(request.url));

return this.render('myview', {
    dataProvider: dataProvider,
    filterModel: filterModel,
});

And in the view you then assign the dataProvider and filterModel to the GridView:

let grid = new GridView({
    dataProvider: dataProvider,
    filterModel: filterModel,
    columns: [
        // ...
    ],
});

Separate filter form

Most of the time using GridView header filters is enough, but in case you need a separate filter form, you can easily add it as well. You can create partial view _search.ejs with the following contents:

let form = new ActiveForm({
    action: 'index',
    method: 'get',
});

//In the view:
//pass ActiveForm, Html classes too when rendering this view..
<div class="post-search">
    <%- await form.begin(); %>

    <%- await form.field(model, 'title') %>

    <%- await form.field(model, 'creation_date') %>

    <div class="form-group">
        <%- Html.submitButton('Search', {class: 'btn btn-primary'}) %>
        <%- Html.submitButton('Reset', {class: 'btn btn-default'}) %>
    </div>

    <%- await form.end(); %>
</div>

and include it in the main view like so:

let searchView = await Pwoli.view.render('/_search.ejs', { form, model, Html }, false); //the last argument `false` indicates that this view should be rendered partially without layouts.

Separate filter form is useful when you need to filter by fields, that are not displayed in GridView or for special filtering conditions, like date range. For filtering by date range we can add non DB attributes createdFrom and createdTo to the search model:

class Post extends Model
{
    /**
     * @var string
     */
    public createdFrom;

    /**
     * @var string
     */
    public createdTo;
}

Extend query conditions in the search() method like so:

query.where = {
    ...query.where,
    creation_date: { [Op.gte]: this.createdFrom },
    creation_date: { [Op.lte]: this.createdTo },
};

And add the representative fields to the filter form:

<%- await form.field(model, 'creationFrom') %>

<%- await form.field(model, 'creationTo') %>

Working with model relations

When displaying active records in a GridView you might encounter the case where you display values of related columns such as the post author’s name instead of just his id. You do this by defining the attribute name in GridView.columns() as author.name when the Post model has a relation named author and the author model has an attribute name. The GridView will then display the name of the author but sorting and filtering are not enabled by default. You have to adjust the filterModel that has been introduced in the last section to add this functionality.

To enable sorting on a related column you have to include(in the case of Sequelize) the related table and add the sorting rule to the Sort component of the data provider:

let query = { where: { status: 1 }};
let dataProvider = new ActiveDataProvider({
    query: query,
});

// join with relation `author` that is a relation to the table `users`
// and set the table alias to be `author`
query = { ...query, include: [{ model: Author, as: 'author' }]};

// enable sorting for the related column
let sort = dataProvider.getSort();
sort.attributes['author.name'] = {
    asc: {'author', 'name', 'asc'},
    desc: {'author', 'name', 'desc'},
};
dataProvider.setSort(sort);
// ...

You can see this same thing working in this Boilerplate: ()

For filtering with relations, you just need to override the search() as it needs to have the additional logic for filtering with foreign columns:

Assume there’s a relation event(also assume that this relation is included in the query of DataProvider) for Post model and it has a field title. So, in the Post model:

class Post extends Model{
    ...
    public search(params) {
        let provider = super.search.call(this, params); // calling the default implementation of search
        console.log('params', params, this.getFormName())
        for (const param in params[this.getFormName()]) {
            if (['event.title'].includes(param)) {
                provider.query.where[`$${param}$`] = { [Op.like]: `%${params[this.getFormName()]['event.title']}%` };
                this[param] = params[this.getFormName()][param]; // for setting this searched value back into the filterModel for showing in the filter field of GridView
            }
        }
        return provider;
    }
    ...
}

You can see this same thing working in this Boilerplate: ()

When specifying the Sort.defaultOrder() for sorting, you need to use the relation name instead of the alias:

dataProvider.sort.defaultOrder = { 'author.name': 'asc' };

Multiple GridViews on one page

You can use more than one GridView on a single page but some additional configuration is needed so that they do not interfere with each other.

When using multiple instances of GridView you have to configure different parameter names for the generated sort and pagination links so that each GridView has its own individual sorting and pagination.

You do so by setting the Sort.sortParam() and Pagination.sortParam() of the dataProvider’s sort and pagination instances.

Assume we want to list the Post and User models for which we have already prepared two data providers in userProvider and postProvider:

import { GridView } from 'pwoli';

userProvider.pagination.pageParam = 'user-page';
userProvider.sort.sortParam = 'user-sort';

postProvider.pagination.pageParam = 'post-page';
postProvider.sort.sortParam = 'post-sort';

let userGrid = new GridView.({
    dataProvider: userProvider,
});

let postGrid = new GridView({
    dataProvider: postProvider,
]);

Pjax integration

By default all the Widgets(including GridView and ListView) extended from the base Widget class are Pjax enabled.

This means that any operations like clicking any link, submitting any form inside the Widget(For eg., in GridView - filtering, sorting and pagination) will trigger an AJAX request and the Widget will get reloaded without getting the whole page reloaded.

Important:- Please note that if you are using Pjax, you should detect if the request is triggered by Pjax and render the partial view containing only the widget in the following way:

if (req.headers['x-requested-with'] === 'XMLHttpRequest')
    // If it's a Pjax request
    content = await Pwoli.view.render('/_grid.ejs', { grid, company: new Company() }, false);
// else render the full view
else content = await Pwoli.view.render('/index.ejs', { grid, company: new Company() });

The partial view should only contain the widget rendering code and it can be included in the main view like:

Partial view file: _grid.ejs

<%- await grid.render() %>

Main view file: index.ejs

<%- include('_grid_') %>
OR
<%- partial('_grid_') %>

This is to make sure that if it’s a Pjax request, its response should only contain the required HTML to be replaced in the page. If it contains any extra HTML, that too will get shown which is unnecessary and affects the design.

This implementation is available in all of our Sample Apps here so that you can seee how they are wired up.

You can disable Pjax by setting Widget.enablePjax to false.