Grid Filtering

Filter grid using parent controls


The grid can be filtered by adding parent controls to it, when a parent control triggers the change event, the grid (child) will reload and the parent values will be sent as parameters to the data function (or url in this case).
GridDemo/Filtering.cshtml
<div class="bar" id="bar1">
@Html.Awe().TextBox("txtPerson").Placeholder("search for person ...").CssClass("searchtxt")
@Html.Awe().TextBox("txtFood").Placeholder("search for food ...").CssClass("searchtxt")
@(Html.Awe().DropdownList(
new DropdownListOpt
{
Name = "selCountry",
Url = Url.Action("GetCountries", "Data")
}))
</div>

<script>
$(function () {
var key = "Grid1-Flt";
var cont = $('#bar1');

// load filters
var data = {};
var sval = sessionStorage[key];
var data = sval ? JSON.parse(sval) : {};
cont.find('input[name]').each(function () {
var el = $(this);
var name = el.attr('name');
var val = data[name];
if(val != null){
el.val(val).trigger('change');
}
});

// save filter
cont.on('change', function (e) {
var el = $(e.target);
var name = el.attr('name');
if (!name) return;
data[name] = el.val();
sessionStorage[key] = JSON.stringify(data);
});
});
</script>

@(Html.Awe().Grid("Grid1")
.Mod(o => o.Main())
.Url(Url.Action("GetItems", "LunchGrid"))
.Columns(
new Column { Bind = "Id", Width = 75, Groupable = false, Resizable = false },
new Column { Bind = "Person" },
new Column { Bind = "Food.Name", ClientFormatFunc = "site.imgFood", MinWidth = 200 },
new Column { Bind = "Country.Name", Header = "Country" },
new Column { Bind = "Date", Width = 120 }.Mod(o => o.Autohide()),
new Column { Bind = "Location" }.Mod(o => o.Autohide()),
new Column { Bind = "Chef.FirstName,Chef.LastName", Prop = "ChefName", Header = "Chef" })
.Reorderable()
.Resizable()
.Height(350)
.Parent("txtPerson", "person")
.Parent("txtFood", "food")
.Parent("selCountry", "country"))
Awesome/Grid/LunchGridController.cs
public class LunchGridController : Controller
{
public IActionResult GetItems(GridParams g, int? country, string person, string food)
{
var query = Db.Lunches.AsQueryable();

if (person != null) query = query.Where(o => o.Person.ToLower().Contains(person.ToLower()));
if (food != null) query = query.Where(o => o.Food.Name.ToLower().Contains(food.ToLower()));
if (country.HasValue) query = query.Where(o => o.Country.Id == country);

var gmb = new GridModelBuilder<Lunch>(query.AsQueryable(), g);

gmb.KeyProp = o => o.Id;

// grid row model
// columns bound to Food.Name by default will display rowModel.FoodName
// or specify in the view Column.Prop ="FooName", or ClientFormat = ".(FoodName)"
gmb.Map = o => new
{
o.Id,
o.Person,
FoodName = o.Food.Name,
FoodPic = o.Food.Pic,
o.Location,
o.Price,
Date = o.Date.ToShortDateString(),
CountryName = o.Country.Name,
ChefName = o.Chef.FullName
};

return Json(gmb.Build());
}

public IActionResult GetItemsEFExample(GridParams g, int? country, string person, string food)
{
var query = Db.Lunches.AsQueryable();

// Filter - custom extension in demo code
// equivalent to .Where(o => o.Food.Name.Contains(food) || ...)

query = query
.Filter(food, o => o.Food.Name)
.Filter(person, o => o.Person);

if (country.HasValue) query = query.Where(o => o.Country.Id == country);

var gmb = new GridModelBuilder<Lunch>(query, g);
gmb.KeyProp = o => o.Id;
gmb.Map = o => new
{
o.Id,
o.Person,
FoodName = o.Food.Name,
FoodPic = o.Food.Pic,
o.Location,
o.Price,
Date = o.Date.ToShortDateString(),
CountryName = o.Country.Name,
ChefName = o.Chef.FullName
};

return Json(gmb.Build());
}
}

Using a popupForm and the grid api to filter the grid. The search criteria is built inside the popup, and when the Ok button is clicked, the post function executes, this is where the grid api.load function is called.
Note: we used aweui to initialize the popupForm, and a formBuilder util class to build its content (just like in the aweui grid filtering demo); an alternative would be to use the PopupForm helper and instead of the formBuilder we could set the Url to point to an action that returns a partial view with the search criteria form.
GridDemo/Filtering.cshtml
@using Column = Omu.AwesomeMvc.Column
@{
ViewData["Title"] = "Grid Filtering";
}
<h2>Filter grid using parent controls</h2>

@*begin*@
<div class="bar" id="bar1">
@Html.Awe().TextBox("txtPerson").Placeholder("search for person ...").CssClass("searchtxt")
@Html.Awe().TextBox("txtFood").Placeholder("search for food ...").CssClass("searchtxt")
@(Html.Awe().DropdownList(
new DropdownListOpt
{
Name = "selCountry",
Url = Url.Action("GetCountries", "Data")
}))
</div>

<script>
$(function () {
var key = "Grid1-Flt";
var cont = $('#bar1');

// load filters
var data = {};
var sval = sessionStorage[key];
var data = sval ? JSON.parse(sval) : {};
cont.find('input[name]').each(function () {
var el = $(this);
var name = el.attr('name');
var val = data[name];
if(val != null){
el.val(val).trigger('change');
}
});

// save filter
cont.on('change', function (e) {
var el = $(e.target);
var name = el.attr('name');
if (!name) return;
data[name] = el.val();
sessionStorage[key] = JSON.stringify(data);
});
});
</script>

@(Html.Awe().Grid("Grid1")
.Mod(o => o.Main())
.Url(Url.Action("GetItems", "LunchGrid"))
.Columns(
new Column { Bind = "Id", Width = 75, Groupable = false, Resizable = false },
new Column { Bind = "Person" },
new Column { Bind = "Food.Name", ClientFormatFunc = "site.imgFood", MinWidth = 200 },
new Column { Bind = "Country.Name", Header = "Country" },
new Column { Bind = "Date", Width = 120 }.Mod(o => o.Autohide()),
new Column { Bind = "Location" }.Mod(o => o.Autohide()),
new Column { Bind = "Chef.FirstName,Chef.LastName", Prop = "ChefName", Header = "Chef" })
.Reorderable()
.Resizable()
.Height(350)
.Parent("txtPerson", "person")
.Parent("txtFood", "food")
.Parent("selCountry", "country"))
@*end*@
<br />
@(Html.Awe().Tabs()
.Add("description",
@<text>
The grid can be filtered by adding parent controls to it, when a parent control triggers the <code>change</code> event,
the grid (child) will reload and the parent values will be sent as parameters to the data function (or url in this case).
</text>, "expl")
.Add("view", Html.Source("GridDemo/Filtering.cshtml"))
.Add("controller", Html.Code("Awesome/Grid/LunchGridController.cs")))

<br />
<div class="tabs code">
<div class="expl" data-caption="description">
Using a popupForm and the grid api to filter the grid.
The search criteria is built inside the popup, and when the Ok button is clicked,
the post function executes, this is where the grid <code>api.load</code> function is called.
<br />
Note: we used aweui to initialize the popupForm, and a formBuilder util class to build its content (just like in the aweui grid filtering demo);
an alternative would be to use the PopupForm helper and instead of the formBuilder we could set the <code>Url</code> to point to an action that returns a partial view with the search criteria form.
</div>
<div data-caption="view">
@Html.Source("GridDemo/Filtering.cshtml", "popupfilter")
</div>
<div data-caption="controller">
@Html.Code("Awesome/DataController.cs").Action("LunchGridFilter")
</div>
</div>

<div class="example">
<h2>Filter by all columns</h2>
@*beginallfilter*@
<div class="bar">
@Html.Awe().TextBox("txtsearch").Placeholder("search ...").CssClass("searchtxt")
<span class="hint">&nbsp; you can search multiple columns at the same time (try 'pizza tavern')</span>
</div>

@(Html.Awe().Grid("GridClientData")
.DataFunc("loadGridClientData")
.Height(350)
.Mod(o => o.Main())
.Resizable()
.Reorderable()
.Parent("txtsearch", "search", false)
.Columns(
new Column { Bind = "Id", Width = 75, Groupable = false, Resizable = false },
new Column { Bind = "Person" },
new Column { Bind = "FoodName", Header = "Food" },
new Column { Bind = "Location" },
new Column { Bind = "Date", Width = 120 },
new Column { Bind = "CountryName", Header = "Country" },
new Column { Bind = "ChefName", Header = "Chef" }))
<script>
function loadGridClientData(sgp) {
// cache storage used by this demo (cstorg.js), it will load data on first call
return $.when(cstorg.get('@Url.Action("GetLunches", "Data")')).then(function(lunches) {
return getGridClientData(sgp, lunches);
});
}

function getGridClientData(sgp, lunches) {
var where = awef.where, contains = awef.scont, loop = awef.loop;
var gp = aweUtils.getGridParams(sgp);
var list = lunches;

if (gp.search) {
var words = gp.search.toLowerCase().split(" ");

list = where(lunches, function (o) {
var matches = 0;
loop(words, function (w) {
if (contains(o.FoodName, w) || contains(o.Person, w) || contains(o.Location, w) || contains(o.CountryName, w)
|| contains(o.DateStr, w)
|| contains(o.ChefName, w)) matches++;
});

return matches === words.length;
});
}

function map(o) { return { Id: o.Id, Person: o.Person, FoodName: o.FoodName, Location: o.Location,
Date: o.DateStr, CountryName: o.CountryName, ChefName: o.ChefName }; };

return aweUtils.gridModelBuilder(
{
key:"Id",
gp: gp,
items: list,
map:map,
// replace default group header value for Date column
getHeaderVal:{ Date: function(o){ return o.DateStr; } }
});
}

$(function() {
$('#txtsearch').keyup(function() {
$('#GridClientData').data('api').load();
});
});
</script>
@*endallfilter*@
<br />
<div class="tabs code">
<div class="expl" data-caption="description">
Filtering by multiple columns at once from one textbox, you can type multiple keywords separated by space and the grid will get filtered.
<br />
The grid data is loaded once from the server after it is stored in the 'lunches' variable.
The function defined in <code>DataFunc</code> can either return the grid model or a promise which will return the grid model later, and in this case on the first load we return a promise ($.get).
</div>
<div data-caption="view">
@Html.Source("GridDemo/Filtering.cshtml", "allfilter")
</div>
</div>
</div>

<div class="example">
@await Html.PartialAsync("Demos/GridFilterOutside")
</div>
Awesome/DataController.cs
public IActionResult LunchGridFilter(
GridParams g,
int? country,
int? chef,
DateTime? date,
string person = "",
string food = "",
string location = "")
{
var list = Db.Lunches.Where(o => o.Food.Name.Contains(food)
&& o.Person.Contains(person)
&& o.Location.Contains(location))
.AsQueryable();

if (country.HasValue) list = list.Where(o => o.Country.Id == country);
if (chef.HasValue) list = list.Where(o => o.Chef.Id == chef);
if (date.HasValue) list = list.Where(o => o.Date == date);

return Json(new GridModelBuilder<Lunch>(list, g)
{
KeyProp = o => o.Id,// needed for Entity Framework | nesting | tree | api
Map = o => new
{
o.Id,
o.Person,
FoodName = o.Food.Name,
FoodPic = o.Food.Pic,
o.Location,
o.Price,
Date = o.Date.ToShortDateString(),
CountryName = o.Country.Name,
ChefName = o.Chef.FullName
}
}.Build());
}

Filter by all columns

  you can search multiple columns at the same time (try 'pizza tavern')

Filtering by multiple columns at once from one textbox, you can type multiple keywords separated by space and the grid will get filtered.
The grid data is loaded once from the server after it is stored in the 'lunches' variable. The function defined in DataFunc can either return the grid model or a promise which will return the grid model later, and in this case on the first load we return a promise ($.get).
GridDemo/Filtering.cshtml
<div class="bar">
@Html.Awe().TextBox("txtsearch").Placeholder("search ...").CssClass("searchtxt")
<span class="hint">&nbsp; you can search multiple columns at the same time (try 'pizza tavern')</span>
</div>

@(Html.Awe().Grid("GridClientData")
.DataFunc("loadGridClientData")
.Height(350)
.Mod(o => o.Main())
.Resizable()
.Reorderable()
.Parent("txtsearch", "search", false)
.Columns(
new Column { Bind = "Id", Width = 75, Groupable = false, Resizable = false },
new Column { Bind = "Person" },
new Column { Bind = "FoodName", Header = "Food" },
new Column { Bind = "Location" },
new Column { Bind = "Date", Width = 120 },
new Column { Bind = "CountryName", Header = "Country" },
new Column { Bind = "ChefName", Header = "Chef" }))
<script>
function loadGridClientData(sgp) {
// cache storage used by this demo (cstorg.js), it will load data on first call
return $.when(cstorg.get('@Url.Action("GetLunches", "Data")')).then(function(lunches) {
return getGridClientData(sgp, lunches);
});
}

function getGridClientData(sgp, lunches) {
var where = awef.where, contains = awef.scont, loop = awef.loop;
var gp = aweUtils.getGridParams(sgp);
var list = lunches;

if (gp.search) {
var words = gp.search.toLowerCase().split(" ");

list = where(lunches, function (o) {
var matches = 0;
loop(words, function (w) {
if (contains(o.FoodName, w) || contains(o.Person, w) || contains(o.Location, w) || contains(o.CountryName, w)
|| contains(o.DateStr, w)
|| contains(o.ChefName, w)) matches++;
});

return matches === words.length;
});
}

function map(o) { return { Id: o.Id, Person: o.Person, FoodName: o.FoodName, Location: o.Location,
Date: o.DateStr, CountryName: o.CountryName, ChefName: o.ChefName }; };

return aweUtils.gridModelBuilder(
{
key:"Id",
gp: gp,
items: list,
map:map,
// replace default group header value for Date column
getHeaderVal:{ Date: function(o){ return o.DateStr; } }
});
}

$(function() {
$('#txtsearch').keyup(function() {
$('#GridClientData').data('api').load();
});
});
</script>

Grid outside filter row and custom render


Reusing the same data source (url) as for the Grid with filter row (server side data), except in this demo we're using the controls outside of the grid to filter the demo.
Just like in the filter row demos the filter controls get their data from the grid model.
Additionally there is filter persistence, so after page refresh the grid will have the same filters applied.
And there's a custom item render mod applied so you can switch between cards view and rows.
Shared/Demos/GridFilterOutside.cshtml
@{
var pref = "frowOut";
var gridId = "GridFrowOut";
}
<div id="outfrow" class="frowpnl awe-il" style="margin-right: 1em">
@*context will add a prefix to all awesome editors html ids*@
@using (Html.Awe().BeginContext(pref))
{
@*html input name matches filter rule name*@
<label>
Name:<br />
@Html.Awe().TextBox("Name").ClearButton().Placeholder("name")
</label>
<label>
Chef:<br />
@Html.Awe().DropdownList(new DropdownListOpt { Name = "Chef", ClearBtn = true, DataFunc = "filterData('Chef')"})
</label>
<label>
Year:<br />
@Html.Awe().DropdownList(new DropdownListOpt { Name = "Date", ClearBtn = true, DataFunc = "filterData('Date')"})
</label>
<label>
Meals:<br />
@(Html.Awe().Multicheck(new MulticheckOpt { Name = "Meals", ClearBtn = true, DataFunc = "filterData('Meals')" }))
</label>
<label>
Organic:<br />
@Html.Awe().DropdownList(new DropdownListOpt { Name = "Organic", ClearBtn = true, DataFunc = "filterData('Organic')"})
</label>
<label style="margin-right: 1em;">
<span> </span><br />
<button type="button" id="btnClearFilter" class="awe-btn">Clear filters</button>
</label>
}

<label class="nonflt">
<span>Show items as:</span> <br />
@Html.Awe().Toggle(new ToggleOpt{ Name = "itemsType", No = "Rows", Yes = "Custom", Width = "7em" })
</label>
</div>

@(Html.Awe().Grid(gridId)
.Url(Url.Action("DinnersFilterGrid", "GridFilterRowServerSideData"))
.Height(350)
.Reorderable()
.Mod(o => o.Main().Custom("outsideFilter"))
.Columns(
new Column { Bind = "Id", Width = 75, Groupable = false },
new Column { Bind = "Name" },
new Column { Bind = "Chef.FirstName,Chef.LastName", Prop = "ChefName", Header = "Chef" },
new Column { Bind = "Date" },
new Column { Prop = "Meals", Header = "Meals", Grow = 2 },
new Column { Bind = "Organic" }))
<script>

// get data for filter editor from grid model
function filterData(name) {
return function () {
var o = $('#@gridId').data('o');
return awem.frowData(o, name);
}
}

// outside filter row custom mod
function outsideFilter(o) {
var g = o.v;
var fcont = $('#outfrow');
var opt = { model: {} };
o.fltopt = opt;

// reload each filter control when grid loads
g.on('aweload', function () {
$('#outfrow .awe-val').each(function () {
var api = $(this).data('api');
if (api && api.load) {
api.load()
}
});
});

// apply filters on control change
fcont.on('change', function (e) {
opt.inp = fcont.find('input').not('.nonflt input');
// instead of opt.inp we could set opt.cont = fcont; but this will also include the itemsType input
// and the grid would reload when we change the items type also

awem.loadgflt(o);
});

$('#btnClearFilter').on('click', function () {
fcont.find('.awe-val').not('.nonflt input').each(function () {
var it = $(this).val('');
var api = it.data('api');
api && api.render && api.render();
// call api.render instead of change to load the grid only once
});

opt.inp = fcont.find('input').not('.nonflt input');

awem.loadgflt(o);
});

// keep same filter editors values after page refresh

var fkey = 'persFout' + o.id;
var storage = sessionStorage;
var pref = '@pref';

g.on('awefinit', function () {
var fopt = storage[fkey];
if (fopt) {
fopt = JSON.parse(fopt, function (key, val) {
if (val && val.length > 0 && val[0] === '[') {
return JSON.parse(val);
}

return val;
});

if (fopt.model) {
o.fltopt.model = fopt.model;
o.fltopt.order = fopt.order;

// set persisted model filter params
var res = awef.serlObj(fopt.model);
res = res.concat(awef.serlArr(fopt.order, 'forder'));
o.fparams = res;
var model = fopt.model;

g.one('aweload', function () {
for (var prop in model) {
var editor = $('#' + pref + prop);
if (editor.length) {
editor.val(awef.sval(model[prop]));
if (editor.closest('.awe-txt-field')) {
editor.data('api').render();
}
}
}
});
}
}

g.on('aweload', function (e) {
if ($(e.target).is(g)) {
fopt = o.fltopt;
storage[fkey] = JSON.stringify({ model: fopt.model, order: fopt.order });
}
});
});
}
</script>
<script>
$(function () {
// rows/custom render switch
$('#itemsType').on('change', function () {
changeItms($('#@gridId').data('o'), $(this).val() === 'true');
});
});

// switch between rows and custom items render
function changeItms(o, custom) {
var gridMinHeight = 350;
var api = o.api;

if (!o.initRen) {
o.initRen = {
ncon: api.ncon,
ghead: api.ghead,
renderItem: api.renderItem
};
}

var custview = {
// node content add wrap for padding
ncon: function (p) {
if (!p.lvl) return p.ren();
return '<div class="gridTempAutoCont" style="padding-left:' + p.lvl + 'em;" >' + p.ren() + '</div>';
},

// group header content
ghead: function (g) {
return api.ceb() + g.c;
},

renderItem: function (opt) {
let { attr, item, ceb, content, indent } = opt;
const columns = o.columns;

if (!content) {
content = '';
for (let col of columns) {
if (api.isColHid(col)) continue;
const header = (col.Header ? col.Header + ': ' : '');
content += `<div class="elabel">${header}</div>${aweUtils.gcvw(api, col, opt)}</br>`;
}
}

if (ceb) {
attr.class += ' cardhead';
attr.style = `margin-left:${indent}em;`;
} else {
attr.class += ' itcard';
}

return `<div ${awef.objToAttrStr(attr)}>${content}</div>`;
},
};

var tableCon = '<table class="awe-table"><colgroup></colgroup><tbody class="awe-tbody awe-itc"></tbody></table>';
var itmCon = '<div class="awe-itc cardsViewGrid"></div>';

if (custom) {
o.v.find('.awe-tablw').html(itmCon);
api = $.extend(api, custview);
// ignore columns width for grid content
o.syncon = 0;

// no alt rows
o.alt = 0;

// min height
o.h = 0;
o.mh = gridMinHeight;
} else {
o.v.find('.awe-tablw').html(tableCon);
api = $.extend(api, o.initRen);
o.syncon = 1;
o.alt = 1;
o.h = gridMinHeight;
}

o.api.initLayout();
o.api.render();
}
</script>

<style>
.frowpnl {
padding: 1em 0;
}

.frowpnl label {
vertical-align: middle;
max-width: 20em;
}
</style>
Demos/Grid/GridFilterRowServerSideDataController.cs
public IActionResult DinnersFilterGrid(GridParams g, DinnerFilterPrm filter)
{
var query = Db.Dinners.AsQueryable();

var filterBuilder = new FilterBuilder<Dinner>();
filterBuilder.Query = query;
filterBuilder.Filter = filter;


filterBuilder
.StringFilter("Name")
.Add("Date", new FilterRule<Dinner>
{
Query = o => o.Date.Year == filter.Date,

GetData = async q =>
{
var list = new List<KeyContent>
{
new KeyContent("", "all years")
};

var years = (q.Select(o => o.Date.Year).Distinct().ToArray()).OrderBy(o => o);

list.AddRange(AweUtil.ToKeyContent(years));

return list.ToArray();
}
})
.Add("Chef", new FilterRule<Dinner>()
{
Query = o => o.Chef.Id == filter.Chef,
GetData = async q =>
{
return (q.Select(o => o.Chef).Distinct().ToArray())
.Select(o => new KeyContent(o.Id, o.FullName));
}
})
.Add("Meals", new FilterRule<Dinner>
{
QueryFunc = () =>
{
var ids = filter.Meals;
var count = ids.Count();
return itm => itm.Meals.Count(m => ids.Contains(m.Id)) == count;
},

GetData = async q =>
{
return (q.SelectMany(o => o.Meals).Distinct().ToArray())
.OrderBy(o => o.Id)
.Select(o => new KeyContent(o.Id, o.Name));
},

// get data after querying this time, to filter the meals items as well
SelfQuery = true
})
.Add("Organic", new FilterRule<Dinner>
{
Query = o => o.Organic == filter.Organic,

GetData = async q =>
{
var list = new List<KeyContent> { new KeyContent("", "all") };
list.AddRange(q.Select(o => o.Organic).Distinct()
.Select(o => new KeyContent(o, o ? "Yes" : "No")));

return list.ToArray();
}
});

// apply rules present in forder (touched by the user)
query = filterBuilder.ApplyAsync().Result;

var gmb = new GridModelBuilder<Dinner>(query, g)
{
KeyProp = o => o.Id,
Map = DinnerMapToGridModel,
Tag = filterBuilder.GetGridData()
};

return Json(gmb.Build());
}



Comments