ASYNergy Data Filter

This tutorial shows how to use ASYNergy to filter the contents of an HTML table and provide real-time feedback to the user without reloading the page. ASYNergy, a complement to revIgniter, is a JavaScript framework for network requests and changes on the page.

Introduction

The page consists of a table and a form with one input field.

While typing in the input field ASYNergy sends AJAX requests to filter the data of the "people" database table. Then ASYNergy replaces the HTML table with the HTML table of the response containing the filtered data.

table


 
To set up the data filter, we need the following files and a database table:

If you have not read about controllers, views and models in the User Guide you should do so before continuing.

Note: Before we begin save the following stylesheet in assets/css/asynTable.css, build the table using the table structure below, insert the provided data and store the SQLite file in application/db/. If you like you can use the tutorials.sqlite database located in the application/db folder.

Top of Page

The Stylesheet

*,
*::before,
*::after {
  box-sizing: border-box;
}

:root {
  -moz-tab-size: 4;
  tab-size: 4;
}

html {
  line-height: 1.15;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
}

body {
  color: rgb(199, 199, 199);
  font-family:
    system-ui,
    -apple-system,
    'Segoe UI',
    Roboto,
    Helvetica,
    Arial,
    sans-serif,
    'Apple Color Emoji',
    'Segoe UI Emoji';
  font-size: 16px;
}

hr {
  height: 0;
  color: inherit;
}

b,
strong {
  font-weight: bolder;
}

table {
  text-indent: 0;
  border-color: inherit;
}

button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}

button,
select {
  text-transform: none;
}

button,
[type='button'],
[type='reset'],
[type='submit'] {
  -webkit-appearance: button;
}

::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

:-moz-focusring {
  outline: 1px dotted ButtonText;
}

body,
input[type=text] {
  background-color: rgb(43, 49, 55);
}

#logo {
  display: block;
  width: 162px;
  margin-bottom: 20px;
  margin-top: 20px;
}

section.main {
  display: flex;
  align-items: center;
  flex-direction: column;
}

.formRow,
#filterList {
  display: flex;
}

.formRow {
  justify-content: flex-end;
}

.formRow,
.formRow > input {
  padding: .5em;
}

.formRow > label {
  padding: .5em .5em .5em 0;
}

.formRow > input {
  flex: 5;
}

.formRow > label {
  flex: 1;
}

input[type=text] {
  color: rgb(255, 255, 255);
}

hr {
  color: rgba(199, 199, 199, 0.5);
  margin-top: 40px;
}

body {
  padding-left: 20px;
  padding-right: 20px;
}

#filterList,
#tableWrapper,
input[type=text] {
  border: 1px solid rgb(199, 199, 199, 0.5);
  border-radius: 8px;
}

#filterList {
  flex-direction: column;
  padding: 1em 1em 4em;
}

#footer,
#filterList {
  font-size: .75em;
}

#mytable {
  width: 100%;
}

.myRow {
  color: #000;
  background-color: rgb(178, 191, 204);
}

.myAltRow {
  color: #000;
  background-color: rgb(145, 151, 157);
}

#footer,
#speed,
#noMatch {
  text-align: center;
}

#noMatch {
  color: rgb(109, 189, 0);
}

@media only screen and (min-width: 420px) {
  #filterList {
    width: 400px;
  }
}


Top of Page

The Table (SQLite)

CREATE TABLE "people" (
  "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  "name" TEXT(64) NOT NULL,
  "age" INTEGER(2) NOT NULL,
  "email" TEXT(64) NOT NULL
);

INSERT INTO "people" ("id", "name", "age", "email") VALUES
('1', 'Ali Lloyd', '32', 'ali@livecode.com'),
('2', 'Edwin Meese', '38', 'edwin@wh.gov'),
('3', 'Anthony Fauci', '74', 'anthony@nih.gov'),
('4', 'Boris Johnson', '65', 'ex-priminister@theuk.com'),
('5', 'Jonathan Edwards', '54', 'triple@jump.com'),
('6', 'Steve Jobs', '66', 'steve@apple.com'),
('7', 'Bill Gates', '72', 'bill@microsoft.com'),
('8', 'Big Bird', '46', 'bigbird@thestreet.com'),
('9', 'Kevin Miller', '36', 'kevin@livecode.com'),
('10', 'Barack Obama', '62', 'barack@theusa.com'),
('11', 'David Brent', '32', 'david@funny.com'),
('12', 'Klaus Schwab', '87', 'klaus@wef.org'),
('13', 'Steven Spielberg', '69', 'me@moviemaking.com');

Top of Page

Controller

The controller consists of two handlers commonly used in controllers and two additional ones. The first handler asynTable is named after the file itself and is called before any other handler. It loads all required libraries and helpers plus models and the database if needed. The second handler, named index, is the default handler, which is mandatory. This handler is called automatically if no other handler is specified in the URI.

Further we need a handler that triggers a database query and a handler that builds the HTML table using the query result.

We start with the basic prototype for a controller script:

<?lc

# PUT YOUR HANDLER NAMES  INTO THE GLOBAL gControllerHandlers AS A COMMA SEPARATED LIST
put "asynTable,index" into gControllerHandlers


# THE CONTROLLER HANDLER
command asynTable
  # LOAD REQUIRED LIBRAIES, MODELS, HELPERS
end asynTable

# THE DEFAULT HANDLER
command index
  -- do something here
end index



--| END OF asynTable.lc
--| Location: ./application/controllers/asynTable.lc
----------------------------------------------------------------------

Save this script as "asynTable.lc" in application/controllers. This controller is associated with an URI like this: example.com/index.lc/asynTable/ or, if you use a .htaccess file with appropriate mod_rewrite rules: example.com/asynTable/ (see revIgniter URLs).

Top of Page

The asynTable Handler

The controller handler asynTable is called first. So, this is a good place to load required helpers, libraries, models and the database.

Actually we would need to load the Asset helper to generate JavaScript and CSS location html code, but this helper is automatically loaded by the ASYNergy library to get the required ASYNergy JavaScript code which is then stored in gData["asynergyScript"]. So there is only one helper left to be loaded. The Form helper, which is used to generate the form open tag.

rigLoadHelper "form"

Since we don't want full page redraws while the user is typing in the input field, we use AJAX requests with the help of ASYNergy and therefore we load the ASYNergy library. Also, of course, we need the Table library to build the HTML table:

rigLoaderLoadLibrary "ASYNergy"
rigLoaderLoadLibrary "Table"

Now load the database. Note: If the function does not contain any information in the first parameter it will connect to the connection group specified in your database config file. For most people, this is the preferred method of use. Make sure that all these settings are correct, that gRigA["activeRecord"] is set to TRUE and that your database contains the "people" table. Add the following line to the asynTable handler:

get rigLoadDatabase()

So far we have not built the model, but we include it here anyway and write the corresponding code afterwards:

rigLoadModel "asyntablemodel"

Your asynTable handler should now look like this:

command asynTable
  # HELPERS
  rigLoadHelper "form"

  # LIBRARIES NEEDED
  rigLoaderLoadLibrary "ASYNergy"
  rigLoaderLoadLibrary "Table"

  # DATABASE
  get rigLoadDatabase("asynTable")

  # MODEL
  rigLoadModel "asyntablemodel"
end asynTable

Top of Page

The Index Handler

If no handler is specified in the URI the default handler index is called. This is the handler, which does all the work when the page is loaded the first time. First we save the page title in the global variable gData.

put "ASYNergy Data Filter" into gData["pageTitle"]

Then we add code that generates the markup for the logo:

put "logo" into tLogo["id"]
put "Logo" into tLogo["alt"]
put "162" into tLogo["width"]
put "38" into tLogo["height"]
put rigImageAsset("logo.png", tLogo, , TRUE) into gData["logo"]

Note: The fourth parameter of the rigImageAsset() function determines if asset file names should include the timestamp of last modification. Setting this parameter to true avoids issues with browser cached data whenever you modify static files like images, CSS files, JavaScript files or favicons without changing the file name.

Now let's add code that retrieves the database data and populates the HTML table.

put peopleData() into tQueryResultA
_buildTable tQueryResultA

The peopleData() function retrieves the database data. This function needs to be implemented in the model later.

The _buildTable controller handler is used to generate the HTML table that will be populated with the result of the database query.

Then we need code to generate the form's action markup and a hidden CSRF token in case CSRF protection is enabled:

put rigFormOpen("asynTable/datafilter") into gData["formOpen"]

This will generate a HTML markup like:

<form action="https://example.com/asynTable/datafilter" method="post" accept-charset="utf-8">

or, in case you enabled CSRF protection:

<form action="https://example.com/asynTable/datafilter" method="post" accept-charset="utf-8">
<input type="hidden" name="csrfTokenName" value="f349be1c-2065-4ecd-ada4-7b902d68a93f" asyn:csrf="csrfTokenName" />

The last line of the index handler loads the asynTableView view.

get rigLoadView("asynTableView")

Your index handler should now look like this:

command index
  local tQueryResultA

  # SET PAGE TITLE
  put "ASYNergy Data Filter" into gData["pageTitle"]

  # GENERATE LOGO MARKUP, NO NEED TO LOAD THE ASSET HELPER
  # IT IS LOADED BY THE ASYNERGY HELPER
  put "logo" into tLogo["id"]
  put "Logo" into tLogo["alt"]
  put "162" into tLogo["width"]
  put "38" into tLogo["height"]
  put rigImageAsset("logo.png", tLogo, , TRUE) into gData["logo"]

  # GET THE DATA AND POPULATE THE TABLE
  put peopleData() into tQueryResultA
  _buildTable tQueryResultA

  # NEEDED TO ADD A HIDDEN CSRF TOKEN
  # IN CASE CSRF PROTECTION IS ENABLED
  put rigFormOpen("asynTable/datafilter") into gData["formOpen"]

  # LOAD THE VIEW FILE
  get rigLoadView("asynTableView")
end index

Top of Page

The datafilter Handler

Now we need a handler that is called by ASYNergy while the user is typing in the input form field.

Add the following handler to the controller script. We will implement the peopleData() function and the _buildTable handler later.

command datafilter
  local tQueryResultA

  put peopleData(rigAsynElemData("datafilter")) into tQueryResultA

  if tQueryResultA <> FALSE then
    _buildTable tQueryResultA
  else
    put "<p id=" & quote & "noMatch" & quote & ">No match!</p>" into gData["peopleTable"]
  end if

  # SUBMIT THE VIEW FILE
  rigAsynRespond rigLoadView("asynFilteredTableView", TRUE)
end datafilter

The peopleData() function retrieves the filtered data by providing a search term entered by the user in the input field, which has an asyn:model attribute with the value "datafilter". If the database query returns data the datafilter handler calls the _buildTable handler, which generates the HTML table that it stores in the gData variable. If the query returns "FALSE" the datafilter handler stores an error message in the gData variable.

The rigAsynRespond handler adds the asynFilteredTableView to the JSON response data and sends it to the client. ASYNergy then replaces the current HTML enclosed by the tags of a "mutable" element whose asyn:mutable attribute has the value "datafilter". The second parameter of the rigLoadView function tells the function to return data, including merged dynamic data, as a string instead of sending it to the browser.

Note: Don't forget to add the handler name "datafilter" to the global variable gControllerHandlers at the top of the controller script.

The first line of the controller's code should now look like this:

put "asynTable,index,datafilter" into gControllerHandlers

Top of Page

The _buildTable Handler

The next handler to add to the controller script is called by the datafilter handler when the user types in the input form field.

private command _buildTable pQueryResultA
  local tResultA

  put pQueryResultA["resultarray"] into tResultA

  # STEPS TO BUILD THE TABLE:
  # STEP 1: GENERATE THE TABLE HEADING
  rigSetTableHeading pQueryResultA["fieldnames"], TRUE

  # STEP 2: FETCH A CUSTOM TABLE TEMPLATE FROM MODEL (OPTIONAL)
  rigSetTableTemplate getTableTemplate()

  # STEP 3: GENERATE THE TABLE
  put rigGenerateTable(tResultA) into gData["peopleTable"]
end _buildTable

This handler uses the database query result data to generate the heading of the HTML table, then it retrieves a custom table template from the model. Finally it generates the HTML table including the data of the "people" database table and returns it.

Top of Page

Model

The model is responsible for database related tasks. In this case our model serves two purposes: It returns data from the "people" table and it returns a custom HTML table template.

As described above, the controller calls a model function named peopleData() and a function named getTableTemplate(). We will now build a model consisting of these two functions.

We start with the basic prototype of a model script only stack:

script "asyntablemodel"


global gRigA

 
on libraryStack
  if (gRigA is not an array) and (the environment is "server") then
    put "No direct script access allowed."
    exit to top
  end if

  if the short name of the target <> the short name of me then
    pass libraryStack
  end if
end libraryStack


--| END OF asyntablemodel.livecodescript
--| Location:  ./application/models/asyntablemodel.livecodescript
----------------------------------------------------------------------

As specified in the asynTable handler of the controller script, name this file "asyntablemodel.livecodescript" and save it in application/models.

Top of Page

The peopleData Function

Add the following lines to build the peopleData() function that is used to filter the "people" table by name with a search term entered by the user:

function peopleData pSearchTerm
  local tQueryResult

  if pSearchTerm <> "" then
    rigDbLike "name", pSearchTerm
  end if

  put rigDbGet("people") into tQueryResult
  if tQueryResult["numrows"] > 0 then
    return tQueryResult
  end if

  return FALSE
end peopleData

First we set a LIKE clause to filter the database entries in the "people" table with a search term passed with the pSearchTerm parameter. Then we run the query using the rigDbGet("people") function. If the query result contains data the function returns the filtered data, otherwise "FALSE".

Top of Page

The getTableTemplate Function

Add the getTableTemplate function to the model script:

function getTableTemplate
  local tMyTemplateA

  put "<table border=" & rigQ("0") && "cellpadding=" & rigQ("4") && "cellspacing=" & rigQ("0") && "id=" & rigQ("mytable") & ">" into tMyTemplateA["tableOpen"]
  put "<tr class=" & rigQ("myRow") & ">" into tMyTemplateA["rowStart"]
  put "<tr class=" & rigQ("myAltRow") & ">" into tMyTemplateA["rowAltStart"]

  return tMyTemplateA
end getTableTemplate

This function changes the HTML table template prototype on opening tags, row tags and alternating row tags.

That's all about the model script and all that is left to do is to build the two view files.

Top of Page

View

In this tutorial, as an exception we don't break out parts like headers and footers into their own view files. Instead, we use one view that represents the complete HTML page and one that replaces the HTML table when the user types in the input field.

The Page View

Start with the complete HTML page view and save the following code in application/views as asynTableView.lc:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>[[gData["pageTitle"] ]]</title>
  
    <? return rigCssAsset("asynTable.css") ?>
  </head>

  <body>      
    <div class="container">
		
      <section class="main">
        
        [[gData["logo"] ]]

        <div id="filterList">
          [[gData["formOpen"] ]] 
            <div class="formRow">
              <label for="name">Name:</label>
              <input asyn:model.debounce.100ms="datafilter" name="name" type="text">
            </div>
          </form>

          <h3>Dynamic Result (pulled from database)</h3>

          <div asyn:mutable="datafilter" id="tableWrapper">
            [[gData["peopleTable"] ]]
            <p id="speed">
            <br>Rendered in {{g_ElapsedTime_}} seconds
            </p>
          </div>
        </div>

      </section>
    </div>
	
    <div id="footer">
      <hr>
      <p>revIgniter ASYNergy Data Filter Tutorial</p>
    
    </div>

    [[gData["asynergyScript"] ]]
  </body>
</html>

The head element is the container for the page title stored in the gData variable and the stylesheet returned by the rigCssAsset() function.

The body element includes the form, the HTML table and the ASYNergy script.

ASYNergy offers a "debounce" modifier. The asyn:model directive of the input field includes such a modifier, which in this case applies a debounce of 100ms. This means that ASYNergy will send a request at the most every 100 milliseconds. The default debounce is 150ms.

There is a div element with an asyn:mutable attribute whose value is "datafilter". This is the same value as the value of the asyn:model attribute of the input field. This means that every time the value of the input field changes i.e. the user enters something into the input field, the HTML code enclosed by the tags of the "mutable" element is replaced. In our example we swap this code with the filtered table view.

Top of Page

The Filtered Table View

This view is identical to the HTML code enclosed by the tags of the "mutable" element of the complete page view above. It includes the HTML table whose data is filtered when the user enters something in the form input field.

Save the following code as "asynFilteredTableView.lc" in application/views:

[[gData["peopleTable"] ]]
<p id="speed">
<br>Rendered in {{g_ElapsedTime_}} seconds
</p>

That's it. Now load the "asynTable" page in your browser and start typing in the form field.

Top of Page

Conclusion

This sample illustrates: