Web Core and Tabulator

No problem. Take a look at the video I posted and pickout a specific item and I can do a write-up about it. There are a lot of things going on in that video, so the more specific your topic choice is, the more useful my write-up can be, as it will be more focused.

This looks very interesting. When I use tables being populated from a webbroker app I have usually used DataTables | Table plug-in for jQuery) which is incredibly flexible. I was about to start looking at how I could use this with WebCore/XData, but might now look at Tabulator. Thanks Andrew.

It would be good if we could start wrapping these things as webcore components.

I don't really have an opinion on jQuery so whether a tool is built with or without it isn't much of a concern. I suppose I'd opt for one without it, all else being equal. I guess I've been around long enough to see standards come and go. And there's a pattern - something becomes wildly successful and soon enough there's a faction that rises up to say you don't need it. NoSQL comes to mind :roll_eyes:

In this case I looked very briefly at a few data table-type projects, but once Tabulator came up on the list it was pretty obvious that it was a good match for what I was after. And I'm just barely getting started with it! Still fiddling with the UI for filters. There's a bunch of menu stuff I didn't show (like picking the visible columns in the table) and sorting and so on. Pretty robust all around I'd say.

Is there anything in the video that you were curious about? I was going to write a more exhaustive post but got distracted and thought it would be 100 pages before I was even close to being done. So I thought maybe splitting it up would be good. Maybe I'll write a book in the same style as these Dr. Flick books :thinking:

For example, in my previous posts about getting data from XData into Web Core I wrote about how I setup the StreamFormat for different things like CSV, FireDAC, JSON and so on. Turns out that the JSON that comes out of that (not the FireDAC version but the simpler "batch" version) can be dumped directly into Tabulator as-is and it does a pretty good job of figuring out everything on its own.

Andrew.

2 Likes

Also, it would be handy if there was a wrapper but there are so many points of interaction it is hard to even imagine where to begin with that. Maybe that's actually more of a justfication for having a wrapper after all - just figuring out what all the events are, for example, is a daunting task.

1 Like

A small sample program which ties up xdata tabulator and the events will be good enough as a start. No need to reinvent the wheel. Can build based on that

Thanks

Alright, here's how the actual connection happens.

In this post I covered how I go about getting data from XData into Web Core in a variety of different formats that might be useful for different purposes. I was initially interested in the FireDAC variation as it brings with it a list of field defintions that are helpful in recreating the dataset locally within Web Core that matches the original dataset created on the XData server. The idea was to simply use that communications mechanism as a means to transport the dataset into the local application.

But another approach, that I use here with Tabulator, is to get the simpler JSON variant and load that into Tabulator. In the post above, you can see how the fields are defined in Tabulator. When you import JSON, any matching fields are linked to Tabulator through this mechanism. As mentioned, it is much more pleasant if your JSON contains an ID column first that is a simple ordering of the records, but not necessarily a requirement.

The step to connect the two is then something like this.

TDM1.LoadTabulator(Endpoint: String; TabulatorTable: String);
var
  Client:    TXDataWebClient;
  Response:  TXDataClientResponse;
  TableData: WideString;
begin

  // Assuming Endpoint is your XData endpoint and there is one parameter for StreamFormat
  // as per my previous example, and it is going to return a simple JSON.
  // TabulatorTable name would be something like "#gridSample" from the previous example.

  Client := TXDataWebClient.Create(nil);
  Client.Connection := //Connection to your XData Server
  Response := await(Client.RawInvokeAsync(Endpoint, ['JSON'])); 

  TableData := string(Response.Result);

  asm
    var table = Tabluator.findTable(TabulatorTable)[0];
    table.replaceData(JSON.parse(TableData));
  end;

  Client.Free;
  PreventCompilerHint(TableData):
end;

That's really all there is to it. Much more can be done in terms of error-checking or making the LoadTabulator function work with a wider array of endpoints with different parameters and so on, but the gist of it is the same. Maybe this can be improved to skip the JSON > String > JSON translation that happens, but this works as-is pretty well, even with many thousands of records. I think in the video one of the tables has nearly 3,000 records and it isn't really much of a delay at all.

1 Like

Thanks. Just one more question . How to you save the data back to XData

Saving data back is a bit more complex and depends largely on the level of detail that you want to work at and how often you want to perform that operation. In my case, It is at the field level, so whenever someone changes a value in a cell, the change is written back to XData right away. Perhaps overkill or perhaps too much traffic depending on the nature of your data. I also don't use Aurelius in my project unfortunately, so on the XData side, I have service functions that implement writing the change and reporting on whether that change was successful. I think Aurelius has automatic CRUD endpoints, so it might be easier to implement in that environment.

So to start with, Tabulator has callback functions for notifying about changes. In this case, it is the cellEdited function. There are other callback functions for indicating whether editing is even allowed for a particular cell, whether it is in the process of being edited and so on. But for now, the main callback function is this one. It is added at the same time as the other callback functions when the table is first created. And as before, I pass a paremeter to tell me what table is being updated.

asm
  this.tabSample = new Tabulator("#gridSample", {
    columns: [
      {title: "ID",   field: "ID",   width:     70, visible: false },
      {title: "Col1", field: "ONE",  minWidth: 125, widthGrow: 1, editor: "input" },
      {title: "Col2", field: "TWO",  minWidth: 125, widthGrow: 1, editor: "number" }
    ]
  });
  this.tabSample.on("cellEdited", function(cell){
    pas.CoreDataModule.DM1.TabCellSave(101,cell.getRow().getPosition(true),cell.getColumn().getElement());
  });
end;

The implementation of TabCellSave is then responsible for contacting the XData server and perofrming the update, and if it isn't successful, reverting the change in Tabulator. On the XData side, more as a way to not go crazy, I've set it up such that I pass a JSON value containing all the fields in the table that are potentially to be updated, and it performs an update or insert to update the values. The old values are also passed in case it is for an update operation and you're changing one of the values of the primary key (for example).

So the same function gets called for each field that is edited, and will get called multiple times in succession if multiple fields are edited for the same row. It returns either "Success" indicating that the values were written successfully to the database, or an error of some kind that can optionally be displayed or logged. In the case of an error (anything other than "Success") the changes made in Tabulator are reverted back to the original values loaded into it. Here is the Web Core side. The XData side is just like the other service operations for retrieving data, but instead perform an update or an insert using the same mechanism.

procedure TDM1.TabCellSave(Grid, Row: Integer; Column:String);
var
  Client:   TXDataWebClient;
  Response: TXDataClientResponse;
  Service: String;
  Operation: String;
  TableName: String;
  Parameter: Integer;
  ParameterJSON: TJSONObject;

  updatedrow: Integer;

  fld_ONE: String;
  fld_TWO: Integer;

  fld_OLD_ONE: String;
  fld_OLD_TWO: Integer;
begin

  // Different tables may be looking for different fields to update and different XData services
  // to call to perform the update functions.  This can be generallized to some degree.

  if (Grid = 101) then
  begin
    TableName := '#gridSample';
    Service := 'XData.ServiceName';
    Operation := 'UPDATE';

    asm 
      var table = Tabulator.findTable(TableName)[0];
      var updatedow = table.getRowFromPosition(Row,true);
      if (updatedrow !== null) {
        fld_ONE     = updatedrow.getCell(table.getColumn('ONE')).getValue();
        fld_TWO     = updatedrow.getCell(table.getColumn('TWO')).getValue();
        fld_OLD_ONE = updatedrow.getCell(table.getColumn('ONE')).getValue();
        fld_OLD_TWO = updatedrow.getCell(table.getColumn('TWO')).getInitialValue();
      };
    end;

    ParameterJSON := TJSONObject.Create;
    ParameterJSON.AddPair('ONE', fld_ONE );
    ParameterJSON.AddPair('ONE', fld_ONE );
    ParameterJSON.AddPair('OLD_ONE', fld_OLD_ONE );
    ParameterJSON.AddPair('OLD_TWO', fld_OLD_TWO );

    Client            := TXDataWebClient.Create(nil);
    Client.Connection := DM1.CarnivalConn;

    try
      Response := await(Client.RawInvokeAsync(Service,[Operation, ParameterJSON.ToString]));
    except on E: Exception do
      begin
        // something happened with XData call
      end;
    end;

    if (string(TJSObject(Response.Result)['value']) <> 'Success') then
    begin
      // Update operation failed so maybe user is notified explicitly
      // But regardless, the data in Tabulator is reverted to its pre-edited state

      asm
        var table = Tabulator.findTable(TableName)[0];
        var updatedrow = table.getRowFromPosition(Row,true);
        if (updatedrow !== null) {
          updatedrow.getCell(table.getColumn('ONE')).restoreInitialValue();
          updatedrow.getCell(table.getColumn('TWO')).restoreInitialValue();
         };
      end;
    end;

    ParameterJSON.Free;
    Client.Free;
  end;
end

This could no doubt be refined a bit with more error handling or visual updates for the user while the operation is being performed. This is very quick though, in the ballpark of 100ms so the user is likely not going to notice as they're busy editing away.

1 Like

Thanks. Got the general Idea. Will play around with these ideas

Glad to hear, let me know if you want any other examples of anything.

Another Tabulator tidbit.

When setting up the control that contains the Tabulator, be extra sure to set its ElementID to something. Altough it appears to work without this being set, it behaves oddly when trying to rearrange columns or change column widths. Not sure if the default IDs that get assigned aren't recognized by Tabulator for some reason, or what the issue is, but simply assigning them fixed the problem.

Another Tabulator Tidbit - Column Visibility Toggle.

Say you want to add a little popup menu to the column headers of a Tabulator table that allow the user to select what columns are visible. Not a problem. There is actually an example buried deep in their page of examples on how to do this here.

Rather trivial to do though. Basically just add a function ahead of the table definition, and add the extra element headerMenu:headerMenu to the column definitions. So one of the samples from above might look like this.

asm
  var headerMenu = function(){
    var menu = [];
    var columns = this.getColumns();

    for(let column of columns){

        //create checkbox element using font awesome icons
        let icon = document.createElement("i");
        icon.classList.add("fas");
        icon.classList.add(column.isVisible() ? "fa-check-square" : "fa-square");

        //build label
        let label = document.createElement("span");
        let title = document.createElement("span");

        title.textContent = " " + column.getDefinition().title;

        label.appendChild(icon);
        label.appendChild(title);

        //create menu item
        menu.push({
            label:label,
            action:function(e){
                //prevent menu closing
                e.stopPropagation();

                //toggle current column visibility
                column.toggle();

                //change menu item icon
                if(column.isVisible()){
                    icon.classList.remove("fa-square");
                    icon.classList.add("fa-check-square");
                }else{
                    icon.classList.remove("fa-check-square");
                    icon.classList.add("fa-square");
                }
            }
        });
    }
   return menu;
  };

  this.tabSample = new Tabulator("#gridSample", {
    columns: [
      {title: "ID",   field: "ID",   headerMenu:headerMenu, width:     70, visible: false },
      {title: "Col1", field: "ONE",  headerMenu:headerMenu, minWidth: 125, widthGrow: 1, editor: "input" },
      {title: "Col2", field: "TWO",  headerMenu:headerMenu, minWidth: 125, widthGrow: 1, editor: "number" }
    ]
  });

I'm not entirely sure how to define the headerMenu globally, but simply copying and pasting that block of code (or a condensed version without the comments and whitespace) before each table defintion and then updating the column definitions to include the headerMenu:headerMenu element does the trick. It is also quite easy to adjust the styling for the popup menu and its elements.

Oh, actually, not hard. Just include that block of code before the default options (see original post) and then you can also define the extra element globally as well:

    Tabulator.defaultOptions.columnDefaults = {minWidth: 50, headerMenu:headerMenu};

And voila! Column visibility across all Tabulator tables in your project!

Very useful information. Thanks a lot.

You're welcome! Please do post any questions that come up as there are a lot of other little things that didn't get covered here as far as further integrating TMS Web Core with Tabulator.

Hi Andrew,

I'm just starting to look at using Tabulator. Would you have a test project that loads a json array?

Regards,

Ken

Sure!

Here's a project with just a TWebButton and a TWebHTMLDiv (to hold the table). Click the button to load the data. I've included a bit of extra stuff to help you get started, showing an example of text fields, number fields, and a date field that has its format changed for display in the table.

Here's what the code looks like. When the table is created, its "instance variable" is assigned to a Delphi form variable so we don't have to look it up when we want to access it again, but that's an option as well if you'd rather not use Form variables in this fashion. Handy, though.

unit Unit1;

interface

uses
  System.SysUtils,
  System.Classes,

  JS,
  Web,

  WEBLib.Graphics,
  WEBLib.Controls,
  WEBLib.Forms,
  WEBLib.Dialogs,
  Vcl.Controls,
  WEBLib.WebCtrls,
  Vcl.StdCtrls,
  WEBLib.StdCtrls;

type
  TForm1 = class(TWebForm)
    WebButton1: TWebButton;
    WebHTMLDiv1: TWebHTMLDiv;
    procedure WebFormCreate(Sender: TObject);
    procedure WebButton1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  tabExample: JSValue;

implementation

{$R *.dfm}

procedure TForm1.WebButton1Click(Sender: TObject);
var
  DataArray: TJSArray;
begin

  // Get yer JSON however you like
  DataArray := TJSArray(TJSJSON.parseObject(
    '['+
      '{"NAME":"Albet Einstein", "DOB":"1879-03-14", "ROLE":"Patent Office Worker", "POINTS":10},'+
      '{"NAME":"Marie Curie",    "DOB":"1867-11-07", "ROLE":"Chemist / Physicist",  "POINTS":20},'+
      '{"NAME":"Sigmund Freud",  "DOB":"1856-05-06", "ROLE":"Psychoanalyst",        "POINTS":5 }' +
    ']'
  ));

  asm
    this.tabExample.replaceData(DataArray);
  end;

end;

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  // Define an empty table, attach it to the TWebHTML component that has an ElementID of 'tabExample'
  asm
    this.tabExample = new Tabulator("#tabExample", {
      layout: "fitColumns",
      columns: [
        {field: "NAME",   title: "Name",     minWidth: 100                   },
        {field: "DOB",    title: "Birthday", width:    125, resizable: false,
          formatter:"datetime", formatterParams:{inputFormat:"yyyy-MM-dd", outputFormat:"yyyy-MMM-dd", invalidPlaceholder:"(invalid date)" }},
        {field: "ROLE",   title: "Role",     minWidth: 100                   },
        {field: "POINTS", title: "Points",   width:     75, resizable: false }
      ]
    });
  end;

end;

end.

The column definitions can be adjusted, and there are options where you don't even need to define the columns if you're so inclined. the "layout: fitColumns" kinda does what you might think, stretching the columns that don't have a fixed width to fill the width of the table. That's why some columns have a "width" and some have a "minWidth" defined. But we're just scraping the surface here.

Here's what it looks like after clicking the button.

This is pretty much what it looks like by default. I added rounded corners (WebHTMLDiv1.ElementClassName = rounded) but the rest of it is the default Tabulator look, including the header sort arrows, etc.

Getting JSON into Tabulator is done with the replaceData function, though there are other options here if the table already has data that you want to retain. If you want to load the data immediately, it can be added as an option to the table definition, or it can be called in an "onTableBuilt" event so it only gets added after the table is built. If you just try to add the data after the asm block in WebFormCreate, the table won't have been created yet so it will likely not work as expected.

Here's the project.
TabulatorExample.zip (56.5 KB)

There are about a zillion directions you can move on to from here. I plan to do a pretty extensive set of blog posts about Tabulator, but we still have a few prerequisite JS libraries to cover first. The next one is actually about Luxon, which is what handles the datetime conversions for Tabulator in this example.

Thanks Andrew

Keep the questions coming! There is a lot that Tabulator can do all on its own with a little nudge here and there. It is also quite feasible to edit data in the table via all kinds of mechanisms, so depending on what you're trying to do, there's almost certainly something available to help. And interacting with a Tabulator table from within TMS WEB Core is also super-easy once you get past the first few steps.

What is the best way to change the default colors?

Oh sure, start with the easy questions :grinning:

For starters, I'd direct you to the Tabulator theme documentation, where you can see a few links for themes that you can add by adding a CSS link, for Bootstrap 5 or something like that.
Tabulator Themes

eg: /dist/css/tabulator_midnight.min.css

Beyond that, or buried within that, is the idea that this is 100% HTML and CSS and not a <canvas> tag to be found anywhere at all (yay!) so you can actually customize anything you like by overriding CSS styles with new values. If you already have a theme for your project, it is likely you can add (admittedly probably at least a dozen) CSS overrides to get everything just the way you want. If you're familiar with SCSS, that's also a good option, but likely well beyond what you need if you're just making a few changes.

There are specific things you can change with some of the options when the table is created. Like if you wanted different arrows for sorting (you can use FontAwesome icons here), or different placeholder text (what appears when no data is loaded). Icons can also be included in the header titles, and drop-down menus (for selecting columns or filtering) can also be added, that use the same kinds of CSS.

There are also options that control row selection, whether you want row selection at all, one row at a time, multiple selections, and that sort of thing.

So I guess the question I should've asked at the outset was... What kinds of changes do you want to make, specifically?