Showing posts with label Ag-Grid. Show all posts
Showing posts with label Ag-Grid. Show all posts

Tuesday, February 11, 2020

AG-Grid Tutorial With Asp.Net Core Razor Pages - Part 3




Prerequisites:
1. Check previous tutorial, part 2
2. Check documentation for Ag-Grid, I will link it under the right point
3. App is online: https://ag-grid3.zoltanhalasz.net/
4. Repository for this tutorial: https://drive.google.com/open?id=1WAbKlYsg3lpbfwE-NfYOmHFFtYPOck_f
5. Intermediate Javascript and Asp.Net Core Razor Pages

As a continuation of my study for this grid system, I checked the following options:

1. Row selection, and removal of rows using Ajax
https://www.ag-grid.com/javascript-grid-selection/

The goal here is to select the rows and remove them from the list and the database table.
The selection is described in above link and implemented in the javascript part of the index page.

    function deleteSelectedRows() {
        var selectedNodes = gridOptions.api.getSelectedNodes()
        var selectedData = selectedNodes.map(function (node) { return node.data })
     
        if (selectedData) {
        postData('./api/InvoiceModels', selectedData)
            .then((data) => {
                console.log("response", data); // JSON data parsed by `response.json()` call
            });
        }


In order to do this, I created an api controller, for the InvoiceModel class and placed it in the Controllers Folder. Then rewrote the post routing, which takes a list of invoicemodel as an input, and then deletes them from the table:

        // POST: api/InvoiceModels
        [HttpPost]
        public async Task<ActionResult<List<InvoiceModel>>> PostInvoiceModel(List<InvoiceModel> invoiceModels)
        {
            if (invoiceModels==null) return NoContent();
            foreach (var model in invoiceModels)
            {
                var myinvoiceModel = await _context.InvoiceTable.FindAsync(model.ID);
                if (myinvoiceModel != null)
                {
                    _context.InvoiceTable.Remove(myinvoiceModel);
                }
           
            }

            await _context.SaveChangesAsync();

            return await _context.InvoiceTable.ToListAsync();
        }

There is a Fetch Command  in the index page which communicates with this Api route :
 async function postData(url = '', data = {}) {
        console.log(JSON.stringify(data), url); 

        await fetch(url, {
            method: 'POST', // *GET, POST, PUT, DELETE, etc.
            headers: {   
                'Content-Type': 'application/json',       
            },           
            body: JSON.stringify(data) // body data type must match "Content-Type" header
        }).then(function (response) {
            console.log(response.json());       
        }).then(function (response) {
            agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
                gridOptions.api.setRowData(data);
            });
        }).catch(() => console.log("err"));
    }

2. Filtering Options
please study: https://www.ag-grid.com/javascript-grid-filter-provided-simple/
a. with filter set to false the filter is invisible for that column
b. for numeric filters, we can use the below (actually this will be with a range)
        {
            headerName: "Amount", field: "Amount",
            filter: 'agNumberColumnFilter', filterParams: {
                filterOptions: ['inRange']
            }

        },
c. for the category, a text filter will be used:
        {
            headerName: "CostCategory", field: "CostCategory", checkboxSelection: true,
                filter: 'agTextColumnFilter', filterParams: {
                        defaultOption: 'startsWith',
                        resetButton: true,
                        applyButton: true,
                        debounceMs: 200
                }

        },

3. Data import
a. I saved the import.xlsx file under wwwroot/uploads, having the following structure
(below data is tab delimited)
ID InvoiceNumber Amount CostCategory Period
1 1 500.00 Utilities 2019_11
2 2 121.69 Telephone 2019_12
3 3 342.61 Services 2019_11
4 4 733.21 Consultancy 2019_11
5 5 107.79 Raw materials 2019_10
6 6 161.44 Raw materials 2019_11
7 7 334.48 Raw materials 2019_11
8 8 504.63 Services 2019_11
9 8 869.44 Services 2019_11
10 9 401.57 Services 2019_11

b. I checked the upload documentation and took their example https://www.ag-grid.com/example-excel-import/

c. please see the implementation of point b, it is almost copy-paste, just adding my columns instead:

After pushing the import button, the grid is filled in with the values from import.xlsx placed in wwwroot/uploads.

result will be:


The contents of the import page:

@page
@model DataTables.ImportModel
@{
    ViewData["Title"] = "Import";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>Import from pre-uploaded excel file (import.xlsx)</h1>

<script src="https://unpkg.com/xlsx-style@0.8.13/dist/xlsx.full.min.js"></script>
<script src="~/js/ag-grid-community.min.js"></script>
<div class="container">
    <div class="row">
        <button onclick="importExcel()" class="btn-outline-info">Import Excel</button>
    </div>
    <div class="row">
        <div id="myGrid" style="height: 500px;width:650px;" class="ag-theme-balham-dark"></div>
    </div> 
</div>
    <script>
        // XMLHttpRequest in promise format
        function makeRequest(method, url, success, error) {
            var httpRequest = new XMLHttpRequest();

            httpRequest.open("GET", url, true);
            httpRequest.responseType = "arraybuffer";

            httpRequest.open(method, url);
            httpRequest.onload = function () {
                success(httpRequest.response);
            };
            httpRequest.onerror = function () {
                error(httpRequest.response);
            };
            httpRequest.send();
        }

        // read the raw data and convert it to a XLSX workbook
        function convertDataToWorkbook(data) {
            /* convert data to binary string */
            console.log(data);
            var data = new Uint8Array(data);
            var arr = new Array();

            for (var i = 0; i !== data.length; ++i) {
                arr[i] = String.fromCharCode(data[i]);
            }

            var bstr = arr.join("");

            return XLSX.read(bstr, { type: "binary" });
        }

        // pull out the values we're after, converting it into an array of rowData

        function populateGrid(workbook) {
            // our data is in the first sheet
            //console.log(workbook);
            var firstSheetName = workbook.SheetNames[0];
            var worksheet = workbook.Sheets[firstSheetName];

            // we expect the following columns to be present
            var columns = {
                'A': 'ID',
                'B': 'InvoiceNumber',
                'C': 'Amount',
                'D': 'CostCategory',
                'E': 'Period'
            };

            var rowData = [];

            // start at the 2nd row - the first row are the headers
            var rowIndex = 2;

            // iterate over the worksheet pulling out the columns we're expecting
            while (worksheet['A' + rowIndex]) {
                var row = {};
                Object.keys(columns).forEach(function (column) {
                    row[columns[column]] = worksheet[column + rowIndex].w;
                });

                rowData.push(row);
                console.log(row);
                rowIndex++;
            }

            // finally, set the imported rowData into the grid
            gridOptions.api.setRowData(rowData);
        }

        function importExcel() {
            makeRequest('GET',
                '/uploads/import.xlsx',
                // success
                function (data) {
                    var workbook = convertDataToWorkbook(data);
                    populateGrid(workbook);
                },
                // error
                function (error) {
                    throw error;
                }
            );
        }

        // specify the columns
        var columnDefs = [
            { headerName: "ID", field: "ID", width: 50 },
            { headerName: "InvoiceNumber", field: "InvoiceNumber", width: 80 },
            { headerName: "Amount", field: "Amount", width: 100 },
            { headerName: "CostCategory", field: "CostCategory" },
            { headerName: "Period", field: "Period" },
        ];

        // no row data to begin with
        var rowData = [];

        // let the grid know which columns and what data to use
        var gridOptions = {
            columnDefs: columnDefs,
            rowData: rowData,
            onGridReady: function () {
                gridOptions.api.sizeColumnsToFit();
            }
        };

        // wait for the document to be loaded, otherwise
        // ag-Grid will not find the div in the document.
        document.addEventListener("DOMContentLoaded", function () {
            console.log('before create grid');
            // lookup the container we want the Grid to use
            var eGridDiv = document.querySelector('#myGrid');
            console.log(eGridDiv);
            // create the grid passing in the div to use together with the columns & data we want to use
            new agGrid.Grid(eGridDiv, gridOptions);
        });

    </script>






Monday, February 3, 2020

AG-Grid Tutorial With Asp.Net Core Razor Pages - Part 2

Continuing the AG Grid series, I added some other features to my previous post. I cleared the repo a little bit, removing some unnecessary files and dependencies, and also adding the ag-grid specific js and css files to the local folder, instead of accessing them from the internet. So, I apologize because of the quality of the previous code.

My observation until now is that DataTables.js as a free JavaScript grid is an easier solution versus Ag-Grid. Later on I will try some enterprise features of Ag-Grid, maybe this will compensate for that.

Preliminary steps:
1. check the previous tutorial: https://mydev-journey.blogspot.com/2020/01/ag-grid-tutorial-with-aspnet-core-razor.html
1. application is live under: https://ag-grid2.zoltanhalasz.net/
2. zipped(cleaned) repo of the application: https://drive.google.com/open?id=10A0as_DTC94ve_oVtDF2uFW1cX19k4J7


How the app will look like:



The steps of the tutorial are:

1. please remove the unnecessary files from the wwwroot (if you are using the part 1 repo).

2. check their official community resource from github, cloning their repo https://github.com/ag-grid/ag-grid/tree/master/community-modules/all-modules/dist

a. take the file ag-grid-community.min.js and copy it under wwwroot/js folder
(or ag-grid-community.noStyle.js)

b. the same, for the CSS files, taken from the Styles folder in the repo above
copy the them in the Css folder, and then reference them from the Layout file:
    <link rel="stylesheet" href="~/css/ag-grid.css">
    <link rel="stylesheet" href="~/css/ag-theme-balham.css">

3. the whole source of the index page will be:

<script src="~/js/ag-grid-community.min.js"></script>
<h1>Hello from ag-grid - Part 2</h1>

<div>
    <div id="mybutton">
        <button onclick="onExportCsv()" class="btn-info">Export Csv</button>
    </div>
    <div id="myGrid" style="height: 450px;width:900px;" class="ag-theme-balham"></div>
</div>


<script type="text/javascript" charset="utf-8">

    var deleteRenderer = function(params) {
        var eDiv = document.createElement('div');
        eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
        var eButton = eDiv.querySelectorAll('.btn-del')[0];

        eButton.addEventListener('click', function() {         
            console.log('will be deleted, invoice no:', params.data.ID);
            window.location.href = './Delete?id='+params.data.ID;
        });

        return eDiv;
    }
    var editRenderer = function (params) {
        var eDiv = document.createElement('div');
        eDiv.innerHTML = '<span class="my-css-class"><button class="btn-edit btn-warning btn-xs">Edit</button></span>';
        var eButton = eDiv.querySelectorAll('.btn-edit')[0];

        eButton.addEventListener('click', function() {         
            console.log('will be edited, invoice no:', params.data.ID);
            window.location.href = './Edit?id='+params.data.ID;
        });

        return eDiv;
    }

    var detailsRenderer = function (params) {
        var eDiv = document.createElement('div');
        eDiv.innerHTML = '<span class="my-css-class"><button class="btn-details btn-info btn-xs">Details</button></span>';
        var eButton = eDiv.querySelectorAll('.btn-details')[0];

        eButton.addEventListener('click', function() {         
            console.log('will be info, invoice no:', params.data.ID);
                  window.location.href = './Detail?id='+params.data.ID;
        });

        return eDiv;
    }
    // specify the columns
    var columnDefs = [
        { headerName: "InvoiceNumber", field: "InvoiceNumber" },
        { headerName: "Amount", field: "Amount" },
        { headerName: "CostCategory", field: "CostCategory" },
        { headerName: "Period", field: "Period" },
        { headerName: "Delete", field: null, cellRenderer: deleteRenderer },
        { headerName: "Edit", field: null, cellRenderer: editRenderer },
        { headerName: "Details", field: null, cellRenderer: detailsRenderer },
    ];

    // let the grid know which columns to use
    var gridOptions = {
        columnDefs: columnDefs,
        defaultColDef: {
            sortable: true,
            filter: true,
            width: 120,

        },
        rowHeight : 35,
        pagination: true,
        paginationPageSize: 10,     
    };

    // lookup the container we want the Grid to use
    var eGridDiv = document.querySelector('#myGrid');



    function getParams() {
        return {
            suppressQuotes: null,
            columnSeparator: null,
            customHeader: null,
            customFooter: null
        };
    }

    function onExportCsv() {
        var params = getParams();
         gridOptions.api.exportDataAsCsv(params);
    }
 
    // create the grid passing in the div to use together with the columns & data we want to use
    new agGrid.Grid(eGridDiv, gridOptions);

    agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
        gridOptions.api.setRowData(data);
    });

</script>

5. pagination
- this is solved using the gridOptions object, setting the properties:
        pagination: true,
        paginationPageSize: 10,   

6. Csv export (basic)
- this is managed using the button with id = "mybutton", and writing its onclick event function:
    function onExportCsv() {
        var params = getParams();
         gridOptions.api.exportDataAsCsv(params);
    }

for the params object, I returned the basic features for a csv export using getParams function.

See more detailed explanation about csv export on their page: https://www.ag-grid.com/javascript-grid-csv/

7. height and width on the rows/columns
     width: 120, // in defaultColDef properties
     rowHeight: 35, // in gridOptions properties

8. cell rendering (basic)
for example, in the columnDef for Delete
      { headerName: "Delete", field: null, cellRenderer: deleteRenderer },
deleteRenderer will be a function generating html for the delete button


    var deleteRenderer = function(params) {
        var eDiv = document.createElement('div');
        eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
        var eButton = eDiv.querySelectorAll('.btn-del')[0];

        eButton.addEventListener('click', function() {         
            console.log('will be deleted, invoice no:', params.data.ID);
            window.location.href = './Delete?id='+params.data.ID;
        });

        return eDiv;
    }

9. Detail, Edit, Delete Razor pages added for complete CRUD, as per cell rendering links suggest.
This is achieved using Entity Framework applied on the In-Memory database.

Saturday, January 25, 2020

AG-Grid Tutorial With Asp.Net Core Razor Pages - Part 1


Besides DataTables.js, I always wanted to check Ag-Grid, which is another great Javascript Grid solution for the front-end. As my apps are from the business world, and have many tables, I wanted to see how AG-Grid would look like in Asp.Net Razor pages.


I quickly checked their Javascript documentation, and prepared a short tutorial, firstly for myself.
The material is based on the existing Datatables tutorial, from my previous posts. It uses an in-memory database and context, injected in the page model.

Please check the following, before starting the tutorial:
1. general introduction: https://www.ag-grid.com/javascript-grid/
2. see previous tutorial, which this one builds upon: https://mydev-journey.blogspot.com/2019/12/datatablejs-tutorial-for-net-core-razor_11.html
3. working example of the tutorial: https://ag-grid1.zoltanhalasz.net/
4. code repo for the tutorial:(zipped) https://drive.google.com/open?id=1BXYkn5RwHnC4w8USCeIBCV0lsDGi-qFh

Explanation of the project:
a. Base class for the application, identical with datatable tutorial above
 public class InvoiceModel
    {
        [JsonProperty(PropertyName = "ID")]
        public int ID { get; set; }

        [JsonProperty(PropertyName = "InvoiceNumber")]
        public int InvoiceNumber { get; set; }

        [JsonProperty(PropertyName = "Amount")]
        public double Amount { get; set; }
        [JsonProperty(PropertyName = "CostCategory")]
        public string CostCategory { get; set; }
        [JsonProperty(PropertyName = "Period")]
        public string Period { get; set; }
     

    }
b. Context for in-memory database, idem

 public class InvoiceContext : DbContext
    {
        public InvoiceContext(DbContextOptions<InvoiceContext> options)
            : base(options)
        {
        }

        public DbSet<InvoiceModel> InvoiceTable { get; set; }
    }

c. In the Pages/Shared folder, we will edit the _Layout page
insert the following stylesheet links specific to AG-grid, just before </head>
    <link rel="stylesheet" href="https://unpkg.com/ag-grid-community/dist/styles/ag-grid.css">
    <link rel="stylesheet" href="https://unpkg.com/ag-grid-community/dist/styles/ag-theme-balham.css">

d. The index page, will have the following methods:

 public class IndexModel : PageModel
    {
        private InvoiceContext _context;

        public List<InvoiceModel> InvoiceList;
        public IndexModel(InvoiceContext context)
        {
            _context = context;
        }

        // this will populate the page, if you want to show the table using the list (with foreach)
        public async Task<IActionResult> OnGet()
        {
            InvoiceList = _context.InvoiceTable.ToList();
            return Page();
        }

        //method to provide list in json format, for the ag-grid
        public JsonResult OnGetArrayData()
        {
            InvoiceList = _context.InvoiceTable.ToList();

            return new JsonResult(InvoiceList);
        }


    }

e. the html razor file, will contain the javascript code, based on the tutorial from ag-grid page
@page
@model IndexModel
@{
    Layout = "_Layout";
}

@*//script used to load the grid*@
<script src="https://unpkg.com/ag-grid-community/dist/ag-grid-community.min.noStyle.js"></script>
<h1>Hello from ag-grid!</h1>

<div id="myGrid" style="height: 600px;width:800px;" class="ag-theme-balham"></div>

<script type="text/javascript" charset="utf-8">
    // specify the columns
    var columnDefs = [
        { headerName: "InvoiceNumber", field: "InvoiceNumber" },
        { headerName: "Amount", field: "Amount" },
        { headerName: "CostCategory", field: "CostCategory" },
        { headerName: "Period", field: "Period" },
    ];

    // let the grid know which columns to use
    var gridOptions = {
        columnDefs: columnDefs,
        defaultColDef: {
            sortable: true,
            filter: true
        },
        rowClassRules: {
        // row style function
            'bigexpense-warning': function(params) {
                var numExpense = params.data.Amount;
                return  numExpense > 20 && numExpense <= 50;
            },
            // row style expression
            'bigexpense-breach': 'data.Amount > 50',
            'bigexpense-ok': 'data.Amount <=20'
        }
    };

    // lookup the container we want the Grid to use
    var eGridDiv = document.querySelector('#myGrid');

    // create the grid passing in the div to use together with the columns & data we want to use
    new agGrid.Grid(eGridDiv, gridOptions);

    agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
        gridOptions.api.setRowData(data);
    });

</script>

f. create the gridformat.css file in the folder wwwroot/css
include the following in this file
.ag-theme-balham .bigexpense-warning {
    background-color: sandybrown !important;
}

.ag-theme-balham .bigexpense-breach {
    background-color: lightcoral !important;
}

.ag-theme-balham .bigexpense-ok {
    background-color: mediumseagreen !important;
}

- in order to be able to populate the rowClassRules with valid css formatting classes
- include the reference to gridformat.css in the _Layout file, just above the ag-grid css links

g. some description of the javascript in the index.cshtml

Grid is marked by id:

<div id="myGrid" style="height: 600px;width:800px;" class="ag-theme-balham"></div>
Column headers are defined by array:
 var columnDefs = [
        { headerName: "InvoiceNumber", field: "InvoiceNumber" },
        { headerName: "Amount", field: "Amount" },
        { headerName: "CostCategory", field: "CostCategory" },
        { headerName: "Period", field: "Period" },
    ];
Sorting and filtering (basic) is setup by:
defaultColDef: {
            sortable: true,
            filter: true
        },

Double click on the row headers will sort the column.
There is a basic text filter included.

The formatting of rows, where the invoice Amount is between certain values
 rowClassRules: {
        // row style function
            'bigexpense-warning': function(params) {
                var numExpense = params.data.Amount;
                return  numExpense > 20 && numExpense <= 50;
            },
            // row style expression
            'bigexpense-breach': 'data.Amount > 50',
            'bigexpense-ok': 'data.Amount <=20'
        }

If you check the simpleHttpRequest function it's called on the handler defined in the pagemodel of the index page:

'./Index?handler=ArrayData'

The final result will be: