We're Almost Done...
The JavaScript code will now generate the table code that represents the contacts (CliFou module). Not just yet.
We’ve observed that both modules utilize xhr.onreadystatechange = () => { ... }, which clearly indicates that their processing should be localized within the code of this XMLHttpRequest event. If you opt for a single definition while managing hundreds of modules or components scattered throughout your site, it could quickly become a nightmare to handle all potential behaviors.
Take, for example, the four small modules we currently have; each could exhibit both a 'browse' behavior and a 'form' behavior. That already gives us a total of eight variations. Now, considering what I have in mind, we could easily end up with a hundred different modules: a search module, a weather module, a recipe module, a geolocation module, a technology watch module, a document management module, a meeting management module, a board meeting management module, a service sheet management module, a project management module... the list is endless.
Moreover, for each of these modules, we could introduce sub-modules. For instance, the marketing suite might include a campaign management module, a mass SMS sending module, an email shot module, and so on. Managing all these different behaviors within the same xhr.onreadystatechange event simply isn’t feasible.
What we need is to develop a robust "handler" system: each module can have its own handler—a callback that is invoked at the appropriate moment. This is precisely what we’re going to implement, and I’ll show you how to do it in the simplest way possible. We’re heading towards something like this:" Feel free to let me know if you need any further adjustments!
<section>
<quitus-module id="QM_ac0dd673-82ba-4e61-b021-c5b0443b0f75" data-module="clifou" data-function="browse" data-start-recno="1" data-handler="myHandler1">blablabla</quitus-module>
<hr />
<quitus-module id="QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0" data-module="clifou" data-function="form" data-start-recno="88" data-handler="myHandler2">blablabla</quitus-module>
</section>
Notice the new attributes data-handler="myHandler1" and data-handler="myHandler2", which are defined as follows:
function myHandler1( oJSON )
{
console.log( 'WITH HANDLER 1',oJSON );
}
function myHandler2( oJSON )
{
console.log( 'WITH HANDLER 2',oJSON );
}
Here is the adaptation of connectedCallback():
connectedCallback()
/*---------------*/
{
// Get all attributes
const attributes = this.attributes;
const params = [];
// Add each attribute and its value to aan array of parameters
for ( let i = 0; i < attributes.length; i++ )
{
const attr = attributes[i];
params.push(`${encodeURIComponent(attr.name)}=${encodeURIComponent(attr.value)}`);
}
// Create the XMLHttpRequest
const xhr = new XMLHttpRequest();
// Endpoint of my service
const url = '/services/module.php';
// POST, url, async
xhr.open( 'POST',url,true );
xhr.setRequestHeader( 'Content-Type','application/x-www-form-urlencoded' );
// Handling the response
xhr.onreadystatechange = () => {
if ( xhr.readyState === 4 && xhr.status === 200 )
{
//console.log('Réponse:',xhr.responseText );
let oJSON = JSON.parse( xhr.responseText );
const handlerName = this.getAttribute('data-handler');
if ( handlerName )
{
if ( typeof window[handlerName] === 'function' )
window[handlerName]( oJSON );
else
throw new Error( `${handlerName} does not exist.` );
}
else
{
console.log( 'NO HANDLER',oJSON );
}
}
};
// Send the request with all parameters
xhr.send( params.join( '&' ) );
}
From then on, it becomes easy to isolate the response handling logic for each module (see how the handler's name is extracted from the attributes and how it is triggered if the underlying function exists: window[handlerName]( oJSON );
At this stage, I will refactor the source code to establish a stable foundation.
Test Code (https://quitus.trql.io/test-modules/)
<?php
use \trql\vaesoli\Vaesoli as v;
{ /* Load classes */
if ( ! defined( 'VAESOLI_CLASS_VERSION' ) )
require_once( "{$_SERVER['snippet-center']}/trql.vaesoli.class.php" );
} /* Load classes */
?>
<script>
function myHandler1( oJSON )
{
console.log( 'WITH HANDLER 1',oJSON );
}
function myHandler2( oJSON )
{
console.log( 'WITH HANDLER 2',oJSON );
}
class QuitusModule extends HTMLElement
{
constructor()
/*---------*/
{
super();
}
connectedCallback()
/*---------------*/
{
// Get all attributes
const attributes = this.attributes;
const params = [];
// Add each attribute and its value to aan array of parameters
for ( let i = 0; i < attributes.length; i++ )
{
const attr = attributes[i];
params.push(`${encodeURIComponent(attr.name)}=${encodeURIComponent(attr.value)}`);
}
// Create the XMLHttpRequest
const xhr = new XMLHttpRequest();
// Endpoint of my service
const url = '/services/module.php';
// POST, url, async
xhr.open( 'POST',url,true );
xhr.setRequestHeader( 'Content-Type','application/x-www-form-urlencoded' );
// Handling the response
xhr.onreadystatechange = () => {
if ( xhr.readyState === 4 && xhr.status === 200 )
{
//console.log('Réponse:',xhr.responseText );
let oJSON = JSON.parse( xhr.responseText );
const handlerName = this.getAttribute('data-handler');
if ( handlerName )
{
if ( typeof window[handlerName] === 'function' )
window[handlerName]( oJSON );
else
throw new Error( `${handlerName} does not exist.` );
}
else
{
console.log( 'NO HANDLER',oJSON );
}
}
};
// Send the request with all parameters
xhr.send( params.join( '&' ) );
}
}
// Define the custom element
customElements.define( 'quitus-module',QuitusModule );
</script>
<style>
table { width: 80%;
margin: 2em auto;
background-color: #fff;
border: 1px solid silver;
}
table th,
table td { border: 1px solid silver;
}
</style>
<section>
<quitus-module id="QM_ac0dd673-82ba-4e61-b021-c5b0443b0f75" data-module="clifou" data-function="browse" data-start-recno="1" data-handler="myHandler1">blablabla</quitus-module>
<hr />
<quitus-module id="QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0" data-module="clifou" data-function="form" data-start-recno="88" data-handler="myHandler2">blablabla</quitus-module>
</section>
Server-Side Code:
<?php
use \trql\vaesoli\Vaesoli as v;
use \trql\db\IllicoDB3\IllicoDB3 as IllicoDB3;
if ( ! defined( 'VAESOLI_CLASS_VERSION' ) )
require_once( '/home/vaesoli/snippet-center/trql.vaesoli.class.php' );
if ( ! defined( 'ILLICODB3_CLASS_VERSION' ) )
require_once( '/home/vaesoli/snippet-center/trql.illicodb3.class.php' );
if ( ! defined( 'HTTP_STATUS_CODES' ) )
require_once( '/home/vaesoli/snippet-center/trql.http-error-codes.h.php' );
$errorCode = HTTP_STATUS_CODE_NOT_IMPLEMENTED;
$fields = null;
$records = null;
$messages = null;
$DBName = 'Quitus';
$folder = '/home/vaesoli/domains/trql.io/quitus/databases/';
$t1 = microtime( true );
$oDB = new IllicoDB3();
if ( $success = $oDB->open( $DBName,$folder ) )
{
$messages[] = 'DB open';
if ( $_POST['data-module'] === 'clifou' )
{
$messages[] = 'CliFou module';
if ( $table = $oDB->contacts ?? null )
{
$messages[] = 'Contacts found';
$fields = $table->fields;
$table->recno = (int) $_POST['data-start-recno'] ?? 1;
if ( ( $operation = $_POST['data-function'] ) === 'browse' )
{
$messages[] = 'BROWSE requested';
while ( ! $table->eof() )
{
$records[] = $table->record();
$table->skip();
} /* while ( ! $table->eof() )*/
$errorCode = HTTP_STATUS_CODE_OK;
} /* if ( ( $operation = $_POST['data-function'] ) === 'browse' ) */
elseif ( ( $operation = $_POST['data-function'] ) === 'form' )
{
$messages[] = 'FORM requested';
$records[] = $table->record();
$errorCode = HTTP_STATUS_CODE_OK;
}
} /* if ( $table = $oDB->contacts ?? null ) */
} /* if ( $_POST['data-module'] === 'clifou' ) */
} /* if ( $success = $oDB->open( $DBName,$folder ) ) */
$t2 = microtime( true );
$response = [ 'prolog' => [ 'status' => $errorCode ,
'params' => $_GET ,
'data' => $_POST ,
'perf' => $t2 - $t1 ,
'debug' => $messages ,
],
'payload' => [ 'table' => 'contact' ,
'fields' => $fields ,
'records' => $records ,
]
];
echo json_encode( $response );
?>
The server-side code is poorly written, but it is simple to understand. It only handles requests related to the 'CliFou' module (if ($_POST['data-module'] === 'clifou')) and within this module, it only processes the browse and form requests (if (($operation = $_POST['data-function']) === 'browse') and (($operation = $_POST['data-function']) === 'form')).
This code is perfect (but poorly written, I repeat) for handling the two quitus modules that are listed in the test code.
Form Handling
"First, we will focus on the handling of the response for module2 because it is the simplest
<quitus-module id="QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0" data-module="clifou" data-function="form" data-start-recno="88" data-handler="myHandler2">blablabla</quitus-module>
We are indeed calling the 'form' functionality (data-function='form') and we want to edit record 88 (data-start-recno='88'). As we can see in the module, it is the handler 'myHandler2' that will receive control for the processing of the service response (data-handler='myHandler2'). How is this function 'myHandler2()' defined for now:
function myHandler2( oJSON )
{
console.log( 'WITH HANDLER 2',oJSON );
}
I will slightly modify this definition to ensure that this is indeed where changes need to be made.
function myHandler2( oJSON )
{
console.log( 'THIS IS WHERE I NEED TO MAKE SOME CHANGES',oJSON );
}
Here's the result:
So this is indeed where I need to build my form. I will indeed create one, but I will greatly simplify and shorten it because I am only interested in the principle.
Let's take a look at a small modification to the callback function myHandler2!
function myHandler2( oJSON )
{
console.log( 'THIS IS WHERE I NEED TO MAKE SOME CHANGES',oJSON );
// The ID is returned by the service to easily identify the origin of the request.
const id = oJSON?.prolog?.data?.id ?? null;
if ( id )
{
const module = document.getElementById( id );
if ( module )
{
module.innerHTML = "THIS IS WHERE A FORM MUST APPEAR";
}
}
}
To ensure that the display is somewhat successful, I added CSS classes and styles. The test code has then become:
<style>
section.modules { width: 100%;
display: block;
box-sizing: border-box;
}
quitus-module { margin: 2em;
width: calc( 100% - 4em );
padding:4em;
background-color: lightyellow;
color: black;
height: 10em;
display: block;
box-sizing: border-box;
}
table { width: 80%;
margin: 2em auto;
background-color: #fff;
border: 1px solid silver;
}
table th,
table td { border: 1px solid silver;
}
</style>
<section class="modules">
<quitus-module id="QM_ac0dd673-82ba-4e61-b021-c5b0443b0f75" class="quitus-module" data-module="clifou" data-function="browse" data-start-recno="1" data-handler="myHandler1">blablabla</quitus-module>
<hr />
<quitus-module id="QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0" class="quitus-module" data-module="clifou" data-function="form" data-start-recno="88" data-handler="myHandler2">blablabla</quitus-module>
</section>
Now let's see what our modification of myHandler2 yields:
Let's advance further and create a simplistic form:
function myHandler2( oJSON )
{
console.log( 'THIS IS WHERE I NEED TO MAKE SOME CHANGES',oJSON );
// The ID is returned by the service to easily identify the
// origin of the request.
const id = oJSON?.prolog?.data?.id ?? null
if ( id )
{
const module = document.getElementById( id );
let HTML = '';
// If module found
if ( module )
{
// Construct the HTML
HTML += `<form method="POST">`;
HTML += `<fieldset>`;
HTML += `<legend>Formulaire</legend>`;
HTML += `<li><label for="txtID">ID: </label><input type="text" name="txtID" id="txtID" value="" /></li>`;
HTML += `<li><label for="txtf_fname">First name: </label><input type="text" name="txtf_fname" id="txtf_fname" value="" /></li>`;
HTML += `<li><label for="txtf_lname">Last name: </label><input type="text" name="txtf_lname" id="txtf_lname" value="" /></li>`;
HTML += `</fieldset>`;
HTML += `</form>`;
module.innerHTML = HTML;
} /* if ( module ) */
} /* if ( id ) */
} /* End of function myHandler2( oJSON ) */
And here is the form:
This form corresponds to the HTML code that was generated, which was very straightforward. We’re going to enhance it a bit to capture the values from the fields we display.
In the prolog block, we have the return code (prolog.status). We will definitely put that to good use! In the payload block, we have the record associated with the form. Specifically, it’s an array containing only a single element. This is the data we need to extract.
By refining this form, we can ensure it not only displays information clearly but also effectively captures user input. The prolog block will help us manage the status of our operations, while the payload will provide us with the necessary data for further processing. This approach sets us up for a more dynamic and interactive experience, allowing us to build upon this foundation as we progress.
Let's start by the status and the ID of the Quitus Module:
function myHandler2( oJSON )
{
console.log( 'THIS IS WHERE I NEED TO MAKE SOME CHANGES',oJSON );
// The ID is returned by the service to easily identify the
// origin of the request.
const id = oJSON?.prolog?.data?.id ?? null
const status = oJSON?.prolog?.status ?? 403;
console.log( "STATUS CODE:",status );
if ( status === 200 )
{
console.log( "ID:",id );
// If we have found the originating module
if ( id )
{
const module = document.getElementById( id );
let HTML = '';
// If module found
if ( module )
{
// Construct the HTML
HTML += `<form method="POST">`;
HTML += `<fieldset>`;
HTML += `<legend>Formulaire</legend>`;
HTML += `<li><label for="txtID">ID: </label><input type="text" name="txtID" id="txtID" value="" /></li>`;
HTML += `<li><label for="txtf_fname">First name: </label><input type="text" name="txtf_fname" id="txtf_fname" value="" /></li>`;
HTML += `<li><label for="txtf_lname">Last name: </label><input type="text" name="txtf_lname" id="txtf_lname" value="" /></li>`;
HTML += `</fieldset>`;
HTML += `</form>`;
module.innerHTML = HTML;
} /* if ( module ) */
} /* if ( id ) */
}
} /* End of function myHandler2( oJSON ) */
It keeps working ... the status code is displayed in the console and the ID of the quitus-module is also shown in the console.
It's now time to retrieve the record and fill the input zones with their values. The only thing that matters is how I have added more fields and how I have prefilled the values:
function myHandler2( oJSON )
{
console.log( 'THIS IS WHERE I NEED TO MAKE SOME CHANGES',oJSON );
// The ID is returned by the service to easily identify the
// origin of the request.
const id = oJSON?.prolog?.data?.id ?? null
const status = oJSON?.prolog?.status ?? 403;
console.log( "STATUS CODE:",status );
if ( status === 200 )
{
console.log( "ID:",id );
// If we have found the originating module
if ( id )
{
const module = document.getElementById( id );
const record = oJSON?.payload?.records[0];
const table = oJSON?.payload?.table;
console.log ( record );
let HTML = '';
// If module found
if ( module )
{
// Construct the HTML
HTML += `<form method="POST">`;
HTML += `<fieldset>`;
HTML += `<legend>Formulaire</legend>`;
HTML += `<li><label for="txtID">ID: </label><input type="text" class="${record['id'] .field.type}" name="txtID" id="txtID" value="${record['id'].value.trim()}" /></li>`;
HTML += `<li><label for="txtf_fname">First name: </label><input type="text" class="${record['f_fname'] .field.type}" name="txtf_fname" id="txtf_fname" value="${record['f_fname'].value.trim()}" /></li>`;
HTML += `<li><label for="txtf_lname">Last name: </label><input type="text" class="${record['f_lname'] .field.type}" name="txtf_lname" id="txtf_lname" value="${record['f_lname'].value.trim()}" /></li>`;
HTML += `<li><label for="txtf_birthdate">Birthdate: </label><input type="date" class="${record['f_birthdate'].field.type}" name="txtf_birthdate" id="txtf_birthdate" value="${str2date(record['f_birthdate'].value.trim())}" /></li>`;
HTML += `<li><label for="txtf_rating">Rating: </label><input type="number" class="${record['f_rating'] .field.type}" name="txtf_rating" id="txtf_rating" value="${record['f_rating'].value}" /></li>`;
HTML += `<li><label for="txtf_rewards">Rewards: </label><input type="number" class="${record['f_rewards'] .field.type}" name="txtf_rewards" id="txtf_rewards" value="${record['f_rewards'].value}" /></li>`;
HTML += `<input type="hidden" name="illicoDB3-table-name" id="illicoDB3-table-name" value="${table}" />`;
HTML += `<input type="hidden" name="illicoDB3-table-recno" id="illicoDB3-table-recno" value="${record['recno'].value}" />`;
HTML += `</fieldset>`;
HTML += '<button type="submit" class="btn btn-primary">Submit</button>';
HTML += `</form>`;
module.innerHTML = HTML;
} /* if ( module ) */
} /* if ( id ) */
}
} /* End of function myHandler2( oJSON ) */
function str2date( value )
{
return ( `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6)}` );
}
Here's what it gives ...
For the presentation I have added a few styles:
<style>
section.modules { width: 100%;
display: block;
box-sizing: border-box;
}
quitus-module { margin: 2em;
width: calc( 100% - 4em );
padding:4em;
background-color: lightyellow;
color: black;
height: auto;
display: block;
box-sizing: border-box;
}
quitus-module form { color: black;
}
quitus-module form li { list-style-type: none;
}
quitus-module form li label { display: inline-block;
width: 8em;
}
quitus-module form li input.int,
quitus-module form li input.float { text-align: right;
}
quitus-module form fieldset { border: 1px solid red;
background-color: #fff;
}
quitus-module table { width: 80%;
margin: 2em auto;
background-color: #fff;
border: 1px solid silver;
}
quitus-module table th,
quitus-module table td { border: 1px solid silver;
}
</style>
We're going to focus on the browse functionality now, and here too, we will simplify things to highlight the principles rather than the details. So, it's 'myHandler1' that we will need to modify! Are you ready? Brace yourself now!
function myHandler1( oJSON )
{
console.log( 'WITH HANDLER 1',oJSON );
// The ID is returned by the service to easily identify the
// origin of the request.
// The ID is returned by the service to easily identify the
// origin of the request.
const id = oJSON?.prolog?.data?.id ?? null
const status = oJSON?.prolog?.status ?? 403;
console.log( "STATUS CODE:",status );
if ( status === 200 )
{
console.log( "ID:",id );
// If we have found the originating module
if ( id )
{
const module = document.getElementById( id );
const records = oJSON?.payload?.records ?? null;
const table = oJSON?.payload?.table ?? null;
let HTML = '';
// If module found
if ( module )
{
// If we have records to show
if ( records )
{
// Build the HTML table with all the records
HTML += `<table class="${table}">`;
HTML += `<caption>${table}<caption>`;
HTML += `<thead>`;
HTML += `<tr>`;
HTML += `<th class="recno"><em>RECNO</em></th>`;
HTML += `<th class="id">ID</th>`;
HTML += `<th class="fname">First name</th>`;
HTML += `<th class="lname">Last name</th>`;
HTML += `<th class="birthdate">Birthdate</th>`;
HTML += `<th class="rating">Rating</th>`;
HTML += `<th class="rewards">Rewards</th>`;
HTML += `</tr>`;
HTML += `</thead>`;
HTML += `<tbody>`;
for ( let record of records )
{
//console.log( record )
HTML += `<tr onclick="document.getElementById('${id}').setAttribute('data-recno',this.getAttribute('data-recno'))" data-recno="${record.recno.value}">`;
HTML += `<td class="recno">${record.recno.value}</td>`;
HTML += `<td class="id">${record.id.value.trim()}</td>`;
HTML += `<td class="fname">${record.f_fname.value.trim()}</td>`;
HTML += `<td class="lname">${record.f_lname.value.trim()}</td>`;
HTML += `<td class="birthdate">${str2date(record.f_birthdate.value)}</td>`;
HTML += `<td class="rating">${record.f_rating.value}</td>`;
HTML += `<td class="rewards">${record.f_rewards.value}</td>`;
HTML += `</tr>`;
} /* For each record */
HTML += `</tbody>`;
HTML += `</table>`;
}
module.innerHTML = HTML;
} /* if ( module ) */
} /* if ( id ) */
} /* if ( status === 200 ) */
} /* End of function myHandler1( oJSON ) */
The code isn’t difficult: we simply create a table with the records (a subset of fields) that we insert into the innerHTML of the module, and that’s it!
Here's what it leads to...
It’s starting to take shape. Now, what we need to do is implement a navigation mechanism. Essentially, when I move from one record to another, I want to notify the form of the record change so that it can refresh accordingly.For now, let’s establish a straightforward mechanism that everyone can easily understand. We can always refine it later with something more comprehensive and complex. Let’s start with something simple.
Additional Thoughts
This approach not only keeps things manageable but also lays a solid foundation for future enhancements. By focusing on clarity and simplicity at this stage, we ensure that everyone involved can follow along without feeling overwhelmed. Once we’ve got the basics in place, we can explore more sophisticated solutions that will enhance functionality and user experience. Let’s dive in and get started!
Click on the RECNO Column
The objective here is to intercept the click event on a row of the table, allowing us to inform the form about the change in record. We will tackle half of this process now, while the remaining part will be addressed in the upcoming Article 7d.
A Simple Approach
This task is quite straightforward. We will simply set up a onclick event during the table creation process. This onclick event will trigger a console.log() statement, which will serve as our proof that we can successfully intercept the change in record.
Future Enhancements
In Article 7d, we will go beyond just logging to the console. Instead, we will publish an event that the "Form" module will subscribe to. This way, it will be notified of any changes in records and can respond accordingly based on its own logic.
The construction of the body of the table becomes:
HTML += `<tbody>`;
for ( let record of records )
{
HTML += `<tr onclick="console.log( 'recno is ${record.recno.value}' )">`;
HTML += `<td class="recno">${record.recno.value}</td>`;
HTML += `<td class="id">${record.id.value.trim()}</td>`;
HTML += `<td class="fname">${record.f_fname.value.trim()}</td>`;
HTML += `<td class="lname">${record.f_lname.value.trim()}</td>`;
HTML += `<td class="birthdate">${str2date(record.f_birthdate.value)}</td>`;
HTML += `<td class="rating">${record.f_rating.value}</td>`;
HTML += `<td class="rewards">${record.f_rewards.value}</td>`;
HTML += `</tr>`;
} /* For each record */
HTML += `</tbody>`;
Please take note of the onclick event on the table row, which triggers a log to the console. This is a crucial element that we will need to modify—along with a few other components—to enable the publication of an event that the form can capture. By doing so, we ensure that the form remains updated and informed about any changes to the RECNO. This enhancement will facilitate better communication between the table and the form, ultimately improving user experience and functionality.
Conclusion
Phew! What a significant advancement! We have nearly resolved all outstanding issues and established a solid foundation for linking to an event publication system that other modules can subscribe to. Each of the two modules on our page has its own service handler, and we have successfully created the form and the browsing functionality. We are now capturing changes in the table position, and it seems we have a mechanism in place that will allow us to connect our event publication seamlessly.
I believe we will have everything ready to finalize in Article 7d, as anticipated. Once we have everything set up for the four core modules of our Small Business Suite, it will be time to consider adding additional modules. I already have hundreds of ideas in mind!
Oh, and just a quick note: I will be publishing the complete code in Article 7d.
See you soon, and let’s keep things uncomplicated!
Previous article - Next article