Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Basics Part 1 - Consoles, Calls, Classes, CSS, and... CAPS-LOCK?

Wednesday, November 9, 2022

Photo of Andrew Simard

In this post, we're going to revisit a few JavaScript libraries that we've already covered, with an eye to helping out new TMS WEB Core users get a more solid footing. Using JavaScript libraries in a TMS WEB Core project makes it easy to quickly build incredibly powerful and well-connected applications, but understanding how everything works together can sometimes be a challenge. And trying to take apart a demo project to extract the bits that might be of interest for your own project can sometimes be even more of a challenge without a solid foundation and some tools to help out along the way. So in this post, we'll cover some of that foundation while also trying to make it interesting for those who might be more familiar with this material.


Motivation.

A recent post about using TMS WEB Core with XData included, as a demo project, a survey for all of the TMS Software Blog readers. The post can be found here. One of the early takeaways gleaned from the survey results is that JavaScript is perhaps not all that popular among the blog readership. Not terribly surprising, given that TMS WEB Core is a product that is used within Delphi (or Visual Studio Code or Lazarus).

However, the ultimate end result of compiling a TMS WEB Core project is a complete JS/HTML/CSS web application, so being familiar with those three elements can be very helpful when it comes time to improve or debug your project. And of the three, JavaScript is likely to be the most troublesome to learn, but potentially the most powerful tool in your toolbox. So while some may consider it a necessary evil (totally understandable!) it isn't really going anywhere, so let's see what benefits we can get from it, and maybe cover a few other closely related topics along the way.


Sample Project.

For our sample project today, we're just going to have a couple of TWebButtons (ButtonUpper and ButtonLower) and a couple of TWebEdit fields (EditInput and EditOutput) to play around with. Nothing complicated at all. Clicking ButtonUpper will copy the text from EditInput into EditOutput and make it upper case. Clicking ButtonLower will copy the text from EditInput to EditOuput and make it lowercase. Just enough to show that something is happening when we interact with the elements. 

A bit later, we'll use Bootstrap and Luxon to do a few other things, so we'll include both of these libraries in our project. This can be done by adding entries to the Project.html file, or by using the Manage JavaScript Libraries function within the Delphi IDE. If you start a new TMS WEB Core project using the Bootstrap template, Bootstrap is already taken care of for you. We'll start with using the PWA template, for no particular reason, and thus we'll need to add Bootstrap ourselves. Here are the links to the CDN for the libraries we'll be using.


    <!-- Bootstrap 5.2.1-->
    <script crossorigin="anonymous" integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8" src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"></script>
    <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" rel="stylesheet"/>
    <!-- Luxon -->
    <script src="https://cdn.jsdelivr.net/npm/luxon@latest/build/global/luxon.min.js"></script>

The initial source code for our project is naturally 100% Delphi, no JavaScript is used at this point.


unit Unit1;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.StdCtrls, WEBLib.StdCtrls, Vcl.Controls;

type
  TForm1 = class(TWebForm)
    ButtonUpper: TWebButton;
    ButtonLower: TWebButton;
    EditInput: TWebEdit;
    EditOutput: TWebEdit;
    procedure ButtonUpperClick(Sender: TObject);
    procedure ButtonLowerClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ButtonLowerClick(Sender: TObject);
begin
  EditOutput.Text := Lowercase(EditInput.Text);
end;

procedure TForm1.ButtonUpperClick(Sender: TObject);
begin
  EditOutput.Text := Uppercase(EditInput.Text);
end;

end.

And here's what it looks like after clicking on the Make Uppercase button.

TMS Software Delphi  Components
Demo Application.

Clicking the buttons does exactly what is expected - the text from the top edit field is copied to the bottom, with the case adjusted depending on which button was clicked. Enough for our demo purposes today.

NOTE: In addition to dropping these four components on the form, we've set their "Name" properties to be as indicated (ButtonUpper, ButtonLower, EditInput, EditOutput), and we've also set their "ElementID" properties to have the same value. We'll cover why that is important in just a moment. It isn't important that they have the same value, just that the ElementID properties have a value.


Developer Console.

With that out of the way, let's have a look at the browser's developer console. Within every modern browser, there's a hidden suite of developer tools that can tell you nearly everything about whatever web page is currently being viewed. Mobile browsers have this as well, though sometimes it is a bit of work to get at it. We're going to be using Google Chrome's developer tools today, but everything we'll be covering works equally well on Firefox, Safari, or any number of other browsers, so long as JavaScript is enabled, naturally. Hard to remember back when that was actually a choice for users to make. Not really the case today.

For desktop browsers, usually you can get to the console just by hitting F12. By default, this will open up a panel to the right of the current web page, showing a bunch of tabs and other information. What we're interested in is the Console tab. Here's what it looks like for our project.  Not very interesting just yet.


TMS Software Delphi  Components
Demo Application Showing Developer Console.

If you're using Firefox or Safari, the developer tools might default to showing up on the bottom of the page instead of the side. And depending on your desktop settings, you might see a "dark mode" or a "light mode" variation, or any number of other slight variations of the above. The thing we're after here is just the console itself, ready to accept input.

So what is this console, exactly? Well, JavaScript is an interpreted language, not a compiled language. Or perhaps a scripting language might be more accurate - it's even in the name: JavaScript. But this has nothing to do with actual Java code of course - JavaScript and Java are very different things.

The console, then, is where JavaScript commands can be sent to the JavaScript interpreter for processing and output. The web page (in our case, a running TMS WEB Core project) is already 'running' in the interpreter, so its contents are accessible from the console. The console is always available. 

Running applications can send output directly to this console using the "console.log" command, which works in both Delphi and JavaScript. More on that in just a moment. Likely the most common use for the console is to see errors that have been generated by the web page. Often these can be ignored. Load up a major website, take a look at the console, and you're likely to see an endless stream of error messages, warnings, and likely impossible-to-decipher messages. We'll be starting from scratch, so any error messages are likely to be worth looking into in our own projects.

So what kinds of JavaScript commands can we use in the console? Well, actually, anything we want so long as it is a valid JavaScript statement. For example, we can ask it what browser it thinks it is running in. Or whether our PWA app is currently connected to the internet. Here are a few examples: "navigator.vendor", "navigator.platform", "navigator.userAgent", and "navigator.onLine".


TMS Software Delphi  Components
Issuing Console Commands.


The commands identifying the browser environment shouldn't necessarily be trusted, as there are plugins and other tools that can be used by a browser to impersonate other browsers and platforms.

The "navigator.onLine" value can be used by PWA apps to determine whether they are connected to the internet. Enabling or disabling your internet connection should see this value changed.  Or you can simulate that by using the "Network" tab elsewhere in the developer tools to disconnect the network for just this browser tab, or even simulate different network speeds. 

Here, we're just issuing JavaScript statements and the result is returned to the console. Normally, we'd be assigning the return values of these statements to variables or passing them as values to other functions. But even in this environment, there are a lot of little bits that can help. 

For example, code completion works - as you start typing a JavaScript statement, a popup window appears to help you find the right syntax or property value. You can also issue statements that assign variables and then reference those variables in subsequent statements, just like in a regular block of JavaScript code. And there is a history, just cursor up to see statements entered previously.


JavaScript Basics.

So that's all well and good, but what about JavaScript itself - the language, the syntax, and so on? Well, it is a fully-formed language in its own right, and while it isn't a huge departure for anyone familiar with Delphi or Pascal generally, it does have a few differences which will be pretty important if you're going to be looking at, or writing, any amount of JavaScript. This is a huge topic in its own right, but there are just a few things to keep in mind when getting started.

  • JavaScript is case-sensitive. Everywhere. There are perhaps a small group of Delphi developers who used Modula-2 at some point and might have preferred that Delphi went down this route. I'm normally in that camp, but at the same time having JavaScript be so lax about so many things but not this is actually really frustrating.
  • The assignment operator is just = all alone by itself. For those who think JavaScript is evil, this would be "Exhibit A".  
  • The equality comparison operator is either == or perhaps === in some cases. And !== instead of <> which is also frustrating.
  • The outermost level of an if condition must be enclosed in () always. Eg: if (a == b) 
  • The usual begin/end blocks are instead defined using curly braces {}
  • Statements can end with a semi-colon but there are monsters among us that choose not to do that - it is optional.
  • Variables must be declared and can be declared at any time before they are used (with some exceptions).
  • Types are, how shall I put it? Fluid? They can change anytime you like to whatever you like for any reason that you like.

JavaScript is not a strongly typed language. Delphi is very much a strongly typed language. This is likely the biggest area where moving back and forth can be a problem. Delphi might be expecting a value with a specific type from JavaScript, and what JavaScript sends back might not be anything resembling that type at all. This means we just have to be careful and check types a little more carefully. Also, when writing JavaScript that exists within Delphi, like in a TMS WEB Core application, we can't rely on the compiler to do as much of this kind of type-checking as we normally can in a strictly Delphi app.

While JavaScript as a language is certainly different than Delphi, it has its moments. Sometimes problematic (or hilarious - check these out), there are a lot of resources available to help. JavaScript is also a bit infamous for having obscure one-liners that can do amazing things, so we'll need to keep an eye out for those. 

Delphi Components in the Browser.

Back in our example, we can glean a bit more information from our application if we look a little deeper into our application and how it relates to HTML, CSS, and of course JavaScript. The first interesting item is that any element on the browser page that has been identified with an ID is also available in JavaScript using that ID - as a global variable of sorts.

When we filled in the ElementID property in our Delphi app, this was in effect defining variables in JavaScript that point at these elements on the page. This is also why it is critically important that the ElementID values in our Delphi objects have unique values. And that uniqueness needs to be maintained even when using popup components or when components are created at runtime.  

Delphi components, particularly the included TWeb components, are typically converted directly into HTML elements on the page and are assigned an ID that corresponds to their ElementID property. Once on the page, they are just regular HTML elements and can be accessed and manipulated like anything else on the page. For example, we can see the HTML definition of the EditInput component, and we can even change its contents.


TMS Software Delphi  Components
Accessing Delphi Components from JavaScript.

Beyond the component definitions, everything declared in the Delphi app is available, including methods, form variables, and so on. It can sometimes be difficult to figure out how to reference Delphi functions from within JavaScript as the JavaScript code is run in different contexts. 

For example, when JavaScript code is executed as part of an event handler, it might have different variables available to it pertaining to the event that might not be available otherwise. JavaScript likes to use the "this." identifier for different things at different times, so while it may work in some instances, it might very well not work well in others, particularly in the event handler situation. 

To get around that, a TMS WEB Core project typically has a "pas." identifier that can be used to drill down into the actual Delphi code. In our example, "pas.Unit1.Form1." is then the root of everything on our main form. We can then make calls to Delphi functions using this as a way to identify them. Or we can invoke methods on the HTML elements just as we would if we didn't know about the Delphi functions. The following work equally well, for example.


TMS Software Delphi  Components
Calling Delphi Methods from the Console.

This can help when it comes to troubleshooting or debugging your project, as you can see the state of everything at any particular point in time.


Console.Log and JSValue.

Back in Delphi, we can output information to the console using the "console.log" function. This function takes a single parameter of type JSValue. This, as its name implies, is a JavaScript value. Think of it perhaps as an equivalent to a Delphi variant - it can be literally anything at all. The only limitation with the call to console.log from Delphi is that the parameter itself needs to resolve to a single type. So we can pass it whatever we like, but if we want it to contain a string, any other values in the same call must be converted to a string and then appended, for example, much the same as if we were using the standard Delphi ShowMessage call. Might not seem like much, but this is a little different in JavaScript where type conversions are all more or less automatic in this kind of scenario. In any event, we can update our Delphi code with the following.


procedure TForm1.ButtonUpperClick(Sender: TObject);
begin
  console.log('Making Uppercase:');
  console.log(EditInput.ElementHandle);
  console.log('Original: '+EditInput.Text);

  EditOutput.Text := Uppercase(EditInput.Text);

  console.log('Updated: '+EditOutput.Text);
  console.log(length(EditOutput.text));
  console.log('Length: '+IntToStr(Length(EditOutput.Text)));
end;

Note that when we want to look at the HTML for a component, we're referencing the ElementHandle property.  This is equivalent to referencing the ID in the console as we were doing previously. 

TMS Software Delphi  Components
Delphi Console Calls.

Note that the console can display quite a variety of different types. Here it displays text, numbers, and HTML code with a bit of formatting.  t can also readily display arrays and JSON, even when they are very large. In some cases, it will even show images when hovering over hyperlinks or other valid image reference values. 

ASM... ...END.

We've seen how we can access our Delphi components from the console, and how we can use Delphi to send output to the console. But what about executing JavaScript code within our Delphi code directly?  

This is handled by wrapping JavaScript code in asm... ...end blocks. Some time ago, someone came up with the idea that "JavaScript is like the assembly language of the web". Not sure who gets the credit for that, but it is quite remarkable. Traditional Delphi apps get compiled down to assembly code, so any asm... ..end blocks there are actually written in x86 assembly code - no fun for anyone, honestly, no matter what they might tell you. Here, JavaScript is considerably more accessible and useful in this context. Moving between JavaScript and Delphi using this mechanism is my absolute favorite feature of TMS WEB Core. 

Adding JavaScript code is simple enough. We just wrap the code in an asm... ...end block and go about our business in JavaScript as if we didn't know about Delphi at all. If we wanted to run a bit of JavaScript code when our application first starts, we could add something to the WebFormCreate method, like the following.

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  asm
    console.log("Browser Info:");
    console.log(navigator.userAgent);
    console.log("Are we online? "+navigator.onLine);
  end;

end;


Nothing particularly challenging there, and the resulting output appears in the console. Note that in JavaScript, pretty much anything can be passed to console.log() and it will be sent to the console without issue. And as JavaScript doesn't care as much about types, you don't even have to worry about converting values to strings when appending them. Just add whatever you like. In this case, the call to navigator.onLine returns a boolean value.

TMS Software Delphi  Components JavaScript in WebFormCreate.

The code that appears within an asm... ...end block is largely passed unmodified into the final application when the TMS WEB Core project is built. Curiously, the developers of the pas2js project, used by TMS WEB Core to do the actual transpilation from Pascal to JavaScript, recommend using this mechanism as little as possible.  This is in part because there isn't any opportunity for the pas2js system to do any kind of optimization within the larger Delphi codebase - the code is already processed.

Most of the time, however, slipping between JavaScript and Delphi in this way is done to address particular issues, and often the JavaScript code is very short and highly focused. In cases where longer JavaScript passages are needed, there may be little need for any kind of optimization as this tends to be initialization code or code where the JavaScript is already optimized enough that it wouldn't matter all that much. So not something I'd get too hung up on - move back and forth as much as you like!

For example, many projects on this blog make use of Tabulator. When setting up a Tabulator table, there is usually a bunch of JavaScript code, maybe hundreds of lines in some cases. But this code is generally just a few function calls where a Tabulator table is defined, and then an array of options is passed in. So while there might be hundreds of lines of JavaScript code, there really isn't anything to optimize. Similarly, sometimes a large array might be defined in JavaScript because it is only ever used in JavaScript. 

Passing Variables Around.

When moving between Delphi and JavaScript, often there is a need to pass variables around. Here are a few tips to keep in mind.

  • JavaScript is case-sensitive, so any references shared between Delphi and JavaScript need to use the same case. This in effect adds a bit of case sensitivity in ways not particularly expected.
  • Variables can be accessed from different Delphi scopes, but be careful about using the "this." property in JavaScript.
  • Delphi really cares about types. JavaScript really does not. Keeping them on the same page is important if data is passing between them.

The general idea is that if a variable is defined in Delphi, it can be referenced by JavaScript and JavaScript can change its value. As an example, let's define a few form variables and local variables in Delphi and then see what happens when JavaScript enters the fray. 

Let's define FormVarString, FormVarInt, FormVarBool, and FormVarDateTime as form variables and LocalVarString, LocalVarInt, LocalVarBool, and LocalVarDateTime as local variables, and assign them some values. Then see what JavaScript can make of them.

unit Unit1;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.StdCtrls, WEBLib.StdCtrls, Vcl.Controls;

type
  TForm1 = class(TWebForm)
    ButtonUpper: TWebButton;
    ButtonLower: TWebButton;
    EditInput: TWebEdit;
    EditOutput: TWebEdit;
    procedure ButtonUpperClick(Sender: TObject);
    procedure ButtonLowerClick(Sender: TObject);
    procedure WebFormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    FormVarString: String;
    FormVarInt: Integer;
    FormVarBool: Boolean;
    FormVarDateTime: TDateTime;
  end;

var
  Form1: TForm1;


implementation

{$R *.dfm}

procedure TForm1.WebFormCreate(Sender: TObject);
var
  LocalVarString: String;
  LocalVarInt: Integer;
  LocalVarBool: Boolean;
  LocalVarDateTime: TDateTime;

begin

  FormVarString := 'Form Variable';
  FormVarInt := 100;
  FormVarBool := True;
  FormVarDateTime := Now;

  LocalVarString := 'Local Variable';
  LocalVarInt := 200;
  LocalVarBool := False;
  LocalVarDateTime := Now;

  asm
    console.log("Form Variables:");
    console.log("FormVarString: "+this.FormVarString);
    console.log("FormVarInt: "+this.FormVarInt);
    console.log("FormVarBool: "+this.FormVarBool);
    console.log("FormVarDateTime: "+this.FormVarDateTime);

    console.log("Local Variables:");
    console.log("LocalVarString: "+LocalVarString);
    console.log("LocalVarInt: "+LocalVarInt);
    console.log("LocalVarBool: "+LocalVarBool);
    console.log("LocalVarDateTime: "+LocalVarDateTime);
  end;

end;

end.


To reference the form variables, we use the "this." prefix in this instance. This is essentially shorthand for "pas.Unit1.Form1." and works well when we're not doing anything special in terms of JavaScript scope. Running this, the variables should all match up as expected.

TMS Software Delphi  Components
Passing Delphi Variables to JavaScript.

Internally, Delphi stores TDateTime values as a floating point number, which is exactly what is displayed in JavaScript. This can be either a minor inconvenience or a major headache depending on what you're after.

Often when we use TDateTime values, we're just after dates and times. Sounds simple enough, right? Well, sometimes we care about things like timezones, ISO 8601 formats, timezone offsets, local versus GMT conversions, and the rest of it. Check out this post on Luxon to see all the gory details. We'll revisit this a bit later, but for now, just bear in mind that Delphi and JavaScript are clearly not on the same page with respect to date and time formats, and we'll need some conversion routines to deal with it. Fortunately, they're readily available.

Getting data out of JavaScript works much the same way. Existing Delphi variables can be populated with data created in JavaScript. The only catch is that the variable has to exist ahead of time. Here are some examples.

procedure TForm1.WebFormCreate(Sender: TObject);
var
  LocalOnline: Boolean;
  LocalUserAgent: String;

begin

  asm
    LocalOnline = navigator.onLine;
    LocalUserAgent = navigator.userAgent;
  end;

  console.log('Delphi sees LocalOnline as '+BoolToStr(localonline, True));
  console.log('Delphi sees LocalUserAgent as '+localuseragent);

end;


Case sensitivity can be troublesome here. Any variations in the case may result in a project that compiles and runs without errors but will return incorrect results. For example, changing the case of "onLine" in the JavaScript code will not result in an error but will result in an incorrect value of 'false' being reported in Delphi. 

On the other hand, Javascript will generate an error if an attempt is made to assign a value to any variable that doesn't exist, and the case is important here. Delphi is more forgiving as any case can be used when referencing a variable, but it is the declaration case that is critical when referencing the value in JavaScript. So "LocalOnline" could be lowercase everywhere else, but needs to be the same as the declaration when used in JavaScript. 

TMS Software Delphi  Components
Passing JavaScript Variables to Delphi.

Also, note that JavaScript boolean values are 'true' and 'false' - all lowercase. The general convention is that false = 0 and true = 1, but that is pretty commonly accepted across most programming languages, Delphi and JavaScript included.

Passing anything else back and forth between Delphi and JavaScript, like component property values or more complex types can be tricky. Generally, the least ambiguous way to do that is to create local Delphi variables to store the properties first, and then use those variables in JavaScript, writing the values back to the component after if necessary.  Not the only way, and it really depends on what is being passed around, but this is a common approach. 

For example, if you wanted to pass the value of 'Form1.Caption" to JavaScript, it would be perhaps better to use a local variable to get the value of Form1.Caption and pass that into JavaScript. It could be referenced directly in JavaScript using "pas.Unit1.Form1.FCaption" but that is making some assumptions that might not always hold (particularly when creating forms at runtime). Using local variables tends to make this a little less messy.

Calling Delphi From JavaScript.

Passing variables around seems easy enough. And we've seen how to make method calls directly from the console.  What about calling methods directly from code? This works pretty well, with all the same caveats as variables. JavaScript is a very function-heavy environment, if there is such a thing, so calling functions, and even defining functions on-the-fly, is a pretty routine thing. 

If you're the kind of developer who likes strictly typed languages, and the thought of variables changing types makes your skin crawl, you're really not going to be happy about JavaScript functions. But this, in many respects, is a source of great flexibility in JavaScript. Functions can be defined as willy-nilly as variables are, and are often interchanged and passed around in the same way. With predictably disastrous results in some cases, sure, but with impressive efficiency in others. If you're picking up a love/hate vibe here, well, that's pretty accurate I'd say.

Calling Delphi from within JavaScript isn't much more work than accessing Delphi variables, and all the chatter about case and scope applies equally here. For example, if we wanted to initialize our app via JavaScript, we could do something like the following.

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  asm
    console.log('App Initializing');
    EditInput.value = 'This is the starting value';
    this.ButtonUpperClick();
    console.log('App Initialized');
  end;

end;


In this example, we're calling this.ButtonUpperClick() instead of the alternative ButtonUpper.click() but we end up with the same result. 

TMS Software Delphi  Components
Calling Delphi Method From JavaScript.

Any Delphi method that is defined as part of the form can be called in the same fashion, no matter whether it is tied to an element on the page or not. Passing variables works the same way as well, keeping in mind issues about types not always being the same. Often it is easier to convert everything to strings where feasible, as there are fewer headaches and troubleshooting is a little easier when you can at least look at all the variables. Just a general comment though.

If the JavaScript code being executed is part of an event handler, the value of "this." becomes linked to the event and is no longer pointing at our Delphi Form. In such cases, reverting to the "pas.Unit1.Form1." prefix works most of the time. But there are two main troublesome things that follow from this.

First, If you happen to find yourself in a scenario where the JavaScript code is in an event, and your form was created dynamically, then "pas.Unit1.Form1." might not be available either. What may help in those instances is a bit more involved.

Normally, there is at least one form or perhaps a TWebDataModule that exists with a fixed reference value. Any dynamically created form can then call this fixed form and "register" its own reference value. Any other code that needs to communicate with the dynamically created form can first make a call to the fixed form to do a lookup and determine the reference to the dynamic form. 

Sounds more complicated than it is, but it works pretty well. For example, let's say you have a main menu form that shows a list of available modules. Clicking a module opens a new instance of a form corresponding to that module.  When this new form is created, a reference to it can be added to a list in the main menu form. JavaScript calls can then be made to the main menu form, which can then in turn look up the reference to the module.

Second, you can run into the problem where the JavaScript call can be made using the "pas.Unit1.Form1." approach, but the Delphi method that you're calling might not know that it is running in your form (?!). This happened in the Survey Client code, where a JavaScript event was used to monitor for changes to the survey question checklist, for example. Updating a form variable from that Delphi function (which was invoked from a JavaScript event handler call) meant that the Delphi code also had to explicitly reference Form1 even though the method was defined as part of the form. Tricky business.

Most of the time, though, this works pretty well. Not trying to scare anyone with obscure scenarios, as they do come up, but there are solutions, just to be clear. Just a byproduct of mixing up very different environments.

Calling JavaScript From Delphi.

What about the reverse? Maybe there's a nifty bit of JavaScript code that is sitting in a JavaScript function that you'd like access to from Delphi? The main trouble here is in having Delphi be able to locate the function in the sea of JavaScript that is generated. As JavaScript functions aren't declared in Delphi directly, the call needs to be wrapped in an asm... ...end block. But the JavaScript function must be defined somewhere that can be "seen" by this block of JavaScript. 

Let's create a JavaScript function called "MixedCase" and add a new button.  The function will just capitalize the first letter of every word.  We can define the function in our WebFormCreate method, perhaps something like this.

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  asm
    function MixedCase(str) {
      return str.split(' ').map((word)=>{return word[0].toUpperCase()+word.substring(1).toLowerCase()}).join(' ');
    }
    window.MixedCase = MixedCase;
  end;

end;


This is one of those infamous JavaScript one-liners, just for fun. The split() function creates an array from a string using the passed value as the delimiter. The map() call creates a function that is run against each element of an array. In this case, the first character of the element is converted to uppercase, and substring() is used to get the rest of the word and convert it to lowercase. Finally, join() creates a string from an array using the passed value as the delimiter. Nothing too crazy but map() is likely something that might not be familiar to everyone.

The extra bit here is the last line where "window.MixedCase = MixedCase;" This seems to come up a lot and is sort of a way of turning a locally defined function into a globally accessible function. Be mindful to use a function name that doesn't collide with something else. "window" is sort of like a global scope, and assigning things here doesn't require declaring them either - notice the lack of a "var" at the beginning of that line. Messy perhaps, if one were to be thinking in Delphi terms, but not so in JavaScript. Routine even. Bordering on popular. No less crazy though.

In any event, back in Delphi, our new button can make use of this fancy JavaScript function by doing something like the following.

procedure TForm1.ButtonMiddleClick(Sender: TObject);
var
  EditText: String;
begin
  EditText := EditInput.Text;
  asm EditText = window.MixedCase(EditText); end;
  EditOutput.Text := EditText;
end;


And this gets us our desired end result. 


TMS Software Delphi  Components
Calling JavaScript Function From Delphi.

Not much more complicated than that. There can be other things that come into play. Most notably perhaps, in making calls in either direction, is that sometimes functions are asynchronous. Meaning that the function call returns immediately, but the work that it is performing continues separately. That's a bit more than we want to cover in this post, but when using other JavaScript libraries, this kind of thing comes up regularly, so something to be mindful of. 

For example, in Tabulator, you might call a function to create a new Tabulator table in the WebFormCreate method, but it isn't actually created and accessible until some time later. So a subsequent call to retrieve data from the table might encounter an error that the table isn't yet ready. 

There are similar Delphi calls where a callback method is passed as one of the parameters to the method. For example, one way to create a form involves a call that returns immediately, but a separate function is called when the form is actually available for use. 

Adding this kind of complexity to our Delphi - JavaScript calls is certainly possible, but extra care must be taken, just as is the case when using asynchronous calls in each environment natively.

Classes.

We've now covered a bit of JavaScript and even had a little peek at some of the HTML that is generated by TMS WEB Core when a project is compiled. And we don't need to use the console to look at the HTML, there's an 'Elements' tab where we can see all of the HTML that is generated, along with the CSS rules that are being applied to create the final output on the page. As this is a tiny project so far, the HTML even fits on the screen all at once. 


TMS Software Delphi  Components
Generated HTML.

This HTML is generated from the layout present in the IDE. Most components will be converted into some kind of native HTML element, like an <input> element or a <div> element or something along those lines. Some components, particularly FNC components, are converted into <canvas> elements, where the contents are drawn more directly, rather than using HTML and CSS.

A quick note about the order. Normally, the order in which Delphi components appear in the HTML isn't usually important, but If you happen to be using flex containers or button groups, it certainly can be. The order can be set manually using the ChildOrder property that is available on all of the TWeb components. In flex specifically, if you find that the ChildOrder isn't sufficient, you can explicitly set the "order" CSS attribute which will take precedence over everything else. In Bootstrap, there are "order-x" classes that can be used, but bear in mind they only define order-1 through order-5. If you have more than five elements to order, you can define more of these classes in your own CSS file. 

And another note about non-TWeb components. If you happen to be using a component that doesn't have a ChildOrder property, an ElementID property, or any of the other TWeb component properties we're covering, such as when using any of the FNC components in your TMS WEB Core project, one approach is to add them to a TWebHTMLDiv component. Using that component as a wrapper, in essence, then allows you to set those properties. The FNC or other control you're using can then be set to fill the TWebHTMLDiv component and it can be positioned as needed.

The main thing we're after when looking at HTML and CSS here is the ability to modify the UI elements in some way.  There are many ways to do this but a convenient way happens to be through the use of classes. Nearly every HTML element can be assigned any number of classes, essentially keywords that identify CSS rules to apply to the element. Classes are case-sensitive and space-delimited. Changing the list of classes assigned to an element generally brings about an immediate change in the display of the element. 

In a TMS WEB Core project, the list of classes assigned to a given component is set using the ElementClassName property. More complex components may have additional properties as well. For example, the TWebLabel component has an additional ElementLabelClassName property, for assigning a different set of classes to each of the HTML elements that are generated.

One of the benefits of adding Bootstrap to your project is that it brings an enormous array of classes that can be used to quickly add a lot of CSS to your project without much effort at all. For example, we can add a bit of color to our three buttons just by adding some of these classes.

Let's add "btn btn-daner", "btn btn-success", and "btn btn-primary" to the three ElementClassName values of the buttons, respectively.  These happen to correspond to Bootstrap's red, green, and blue styles. The first 'btn' is used to indicate that it is a button class we're after, and the second indicates what kind of styling to apply. After adjusting the buttons to be a bit bigger, the result should be something like the following.

TMS Software Delphi  Components
Bootstrap Buttons.

With almost no trouble at all, we've got some nicer-looking buttons. In the HTML code, you can see that the only real difference is the extra class appearing in the element definition. On the far right, however, there is now a ton of CSS involved in creating the page. Fortunately, this is such a common thing that it is highly optimized. To the point that you don't generally notice any changes to CSS as the effects are applied nearly instantaneously, even to very complex pages with hundreds of elements.

Much of the work of designing the UI for a TMS WEB Core project can then be addressed using traditional HTML and CSS knowledge. And a lot of this is handled just by adding or removing classes as needed. And that can be handled in either JavaScript or Delphi. Let's say for example that we want the border of the EditOutput element to reflect the function that was used to populate it. For example, if we click the "Make Uppercase" button, which is red in our example, the border of the EditOutput element should also be red.

procedure TForm1.ButtonUpperClick(Sender: TObject);
begin
  EditOutput.Text := Uppercase(EditInput.Text);
  EditOutput.ElementClassName := 'border border-3 border-danger';
end;


Here, we're using Bootstrap to set a border, set the border width to a thicker border, and then change the color to be the same as we used for that button.

TMS Software Delphi  Components
Adding Bootstrap Classes.

Looking at the HTML, the only thing that has changed is that the EditOutput element now has the new classes assigned. Simple in principle, right? Manipulating classes goes a long way toward implementing quite a lot of UI/UX work. 

Much of the time, though, classes are already assigned to an element, so we can't just replace them outright. Say, for example, we happen to be rather fond of rounded corners. Naturally, we'd want our edit fields to be rounded. And a larger font might be good as well. So let's add the classes "border border-3 border-dark rounded fs-6" to our edit fields by setting the ElementClassnName properties to have these values. 

Now, we can't just change the ElementClassName when we click on the button, as we'll end up removing these extra classes. To get around this, there are other supporting functions available. classList.remove(), classList.add(), and classList.replace() can be used to adjust the list of classes assigned. As we've got three possible colors, we'll need to be sure to remove the two that we don't want and add the one that we do want. By default, we've also set the border color to border-dark, so we'll need to remove that one as well. This time, let's have a look at ButtonLowerClick.

procedure TForm1.ButtonLowerClick(Sender: TObject);
begin
  EditOutput.Text := Lowercase(EditInput.Text);
  EditOutput.ElementHandle.classList.remove('border-danger','border-successr','border-dark');
  EditOutput.ElementHandle.classList.add('border-primary');
end;


In order to get access to these extra class-handling functions, we need to use the component's ElementHandle property. A great many other JavaScript functions can be accessed directly via Delphi in this manner without having to resort to using JavaScript directly. Using the IDE's code-completion functions makes this pretty straightforward if you're not already familiar with the syntax or even with what functions might be available.  

If you're just swapping one class for another, the classList.replace() function is a good option, such as when toggling a button color or changing a state. In this example, we've got a bit more going on, so we call classList.remove() and classList.add() separately.

TMS Software Delphi  Components
Modifying Class Elements.

However, if you're already familiar with what classes need to be changed, and you're already using JavaScript or are familiar enough with JavaScript to know what function calls to use, you can just use that. Here's the MixedCase example, where we're already in JavaScript, updated with the equivalent functionality.

procedure TForm1.ButtonMiddleClick(Sender: TObject);
var
  EditText: String;
begin
  EditText := EditInput.Text;
  asm
    EditText = window.MixedCase(EditText);
    EditOutput.classList.remove('border-danger','border-primary','border-dark');
    EditOutput.classList.add('border-success');
  end;
  EditOutput.Text := EditText;
end;


The JavaScript version can reference the element directly, without the ElementHandle reference. No code completion here, and no case typos permitted either, but otherwise, this provides the equivalent result. Personally, I tend to prefer using the JavaScript approach for this kind of thing, but that's just a preference. Ultimately I think it gets compiled to the same JavaScript code either way, at least in this trivial example. 

The fun thing is that you can move between Delphi and JavaScript as needed. Sometimes everything is done in Delphi, which is great. Or sometimes you need a bit of JavaScript for something, which is great, too. Nothing but options. And we like our options. This is why it's my favorite feature of TMS WEB Core.

So far, we've been adjusting classes by adding or removing various Bootstrap classes to get our desired effect. But Bootstrap is just a bit of shorthand, saving us from having to write our own CSS. Sometimes, though, a little extra CSS is still required when Bootstrap comes up short, when you're looking for something a little more bespoke, or when you find yourself adding so many Bootstrap classes that it might just be easier to write the CSS yourself.  It happens!

Custom CSS.

We've covered a bit of JavaScript, a bit of HTML, and a bit of using Bootstrap as shorthand for handling CSS work. We can get pretty far with just that, but at times it is handy to be able to make adjustments in CSS more directly, particularly when we want to make global changes for our particular application, or, conversely, when we want to make very specific tweaks. The middle ground is curiously not usually an issue as we can just apply classes normally to get the job done. 

Let's say, for example, that we want to add a custom border around the edit field that currently has the focus. Each browser does this already by default by adding a thicker border to the <input> element that has focus. As we've replaced the border with something thicker already, this is no longer the case. So let's add a bit of custom CSS to improve the situation.  

First, we need to add a new CSS file to our project. We can do this the same way we might add a new Delphi Unit, using the "File | New.." menu, then finding CSS Stylesheet. In my Delphi install, this is under Other/Web.

TMS Software Delphi  Components
Adding a Custom CSS Stylesheet.

In the Delphi Project Manager, this will show up as something like "Untitled1.css" or something similar.  It can be renamed to whatever you like, but preferably something short without spaces.  Let's go with "custom.css" for now.  Then, we need to add this to our Project.html file, in the <head> section.  For example, something like this.

    <link href="custom.css" rel="stylesheet"/>

Then, we can edit the contents of the CSS file just by double-clicking on it in the Project Manager window. The CSS rule we'd like to add involves creating a fancy border around an HTML <input> text field. A double-border would be super-fancy, and if we want to go overboard, a drop-shadow behind it. So let's try this out. The only thing we'll need in the CSS file is the following CSS rule.

This identifies <input> elements that are text fields and that are currently focused. Then it first applies a yellow 2px border using the box-shadow attribute (CSS colors can be expressed as a hex RGB value - #FF0 = yellow). Then an outer 1px darker border is added with the outline attribute, using the Bootstrap "dark" color. And finally, a filter attribute is added to get a blue drop-shadow blur effect. Lots of options for borders, and this is in addition to the actual border attribute that we've set up previously. Here's the CSS we're using.

input[type=text]:focus-visible {
   box-shadow: 0 0 0 2px #FF0;
   outline-offset: 2px;
   outline: 1px solid var(--bs-dark);
   filter: drop-shadow(0px 0px 4px blue);
 }

This looks like the following, drawing the double-border-plus-shadow around the edit field that currently has the focus.

TMS Software Delphi  Components
Getting Fancy With The CSS.

Normally, selecting an element in the "Elements" section of the developer tools will show you the CSS that is being used, including whatever it has pulled from your custom CSS file. In this case, though, we're applying a CSS rule to what is known as a "pseudo-class" - something that is applied to an element dynamically. To see the CSS being applied to this pseudo-class, select it at the top of the CSS section on the right. Then you can see the custom CSS that we added.  

Generally speaking, the amount of CSS that you might need or want depends on how "particular" you are about how your application appears, or how you want it to look and work as users interact with its various elements. There's naturally no end to the customization that you can do, but there are likely limits to what you should do. Having four separate borders for a text input element is probably a bit much, for example. But it all depends on your particular application. The good thing is that using CSS effectively means that you can make that one definition and it will be consistently applied everywhere. Pretty powerful, all things considered.

An Eventful Example.

When starting new TMS WEB Core projects, I find there are a number of things that I like to include. Having a custom CSS file is a given, as are a number of JavaScript libraries I've grown rather fond of. But in terms of the applications themselves, there are little touches here and there that are just nice to have. One of them is having a login/password field that will indicate whether the CAPS-LOCK key is active. What we'd like to do is just change the background of the edit component when the CAPS-LOCK key is active during text entry. And turn it off when the field doesn't have the focus. We've already covered most of what we need, but we'll need the ability to listen for JavaScript events. Here's what it looks like.

procedure TForm1.WebFormCreate(Sender: TObject);
begin
  asm
    EditInput.addEventListener("keyup", function(event) {
      if (event.getModifierState("CapsLock")) {
        EditInput.classList.add("bg-warning");
      }
      else {
        EditInput.classList.remove("bg-warning");
      }
    });
    EditInput.addEventListener("focusout", function(event) {
      EditInput.classList.remove("bg-warning");
    });
  end;
end;

This can then be duplicated for any element where this might be relevant. Usually, just a password field is sufficient, but sometimes a username field or other field makes sense as well. If you wanted a more complex indicator, a separate label on the page could be used and then the code could show or hide that label just as easily. Here, we're just using the Bootstrap class "bg-warning" which changes the background color of an element to a, well, warning color. You could just as easily define another class in your custom CSS file to change to whatever color you like, add a different border, or whatever else you might want to do in this situation.

One Last Example.

Another perhaps even more frequent item that is included in any serious project is the ability for the application to report version information about itself. In a traditional Delphi VCL app, this is a well-covered topic and there are lots of examples of code that can be used to extract this information, embedded deep in the final executable, as part of the general Windows API infrastructure. The Delphi IDE has a "Version Info" section where a lot of additional data about this kind of thing can be managed. 

For TMS WEB Core projects, though, things are a little different. What I've found to be most useful is to use the provided versioning system. This needs to be enabled in the TMS WEB Core section of the Project | Options menu.  There you'll find two entries of interest. First is the current application "Version" option, which defaults to 1.0.0. And the second is the "Auto-increment version" which defaults to "false". Turning that on is the first step, but don't forget to turn it on also if you switch from Debug to Release work - these options are specific to each build target.

TMS Software Delphi  Components
TMS WEB Core Options.

One side-effect of this change is that you'll end up with a separate JS file for your project every time you run it from the IDE. Perhaps a future version of TMS WEB Core will have a "Keep last X entries" option, but at the moment you'll have to delete these yourself. Also, depending on how you deploy your project, you might have to delete these extra copies there as well. Something to be mindful of.

But turning on this versioning is only the first part. We need to be able to get at that version number within our application, and, so far as I know, it isn't exposed anywhere by default. But we can fix that! In our Project.html file, there's a reference to a $(ProjectName) variable that contains the information we're after. You'll see it used at the top in the <head> stanza where it defines the actual JavaScript file being referenced for the project.

    <script type="text/javascript" src="$(ProjectName).js"></script>

All we're going to do is store that value in a variable that we can access later in our application. We can do that by adding a line of code at the bottom, defining ProjectName to be the value of $(ProjectName) like this.


  <script type="text/javascript">
    ProjectName="$(ProjectName)";
    rtl.run();
  </script>

Now in our application, we'll be able to access that value directly (well, directly via JavaScript).

We'd also like to get the release date for our application. In a Delphi VCL app, we can get this from the timestamp of the executable that is generated. Here, we can't do that directly, but a typical web server will report the last modified date/time for a given file that it serves up, using the "document.lastModified" value. This is directly accessible via JavaScript (you can enter it directly into the console, for example). Curiously, this is available in a set date format and apparently doesn't change with the browser or locale. And not one I'm especially fond of, so we'll need to do something about that as well. Luxon to the rescue! 

Putting it all together, we can now define a set of Delphi form variables, set them via JavaScript, and display them on the console when the app first starts. We can use the AppRelease value when we need to insert the value into a database, for example, and the AppReleaseNice version when we want it to be a little bit more friendly.

procedure TForm1.WebFormCreate(Sender: TObject);
begin

  // Retrieve Version Information
  asm
    this.AppVersion = ProjectName.replaceAll('_','.').substr(ProjectName.indexOf('_')+1);
    this.AppRelease = luxon.DateTime.fromJSDate(new Date(document.lastModified)).toISO();
    this.AppReleaseNice = luxon.DateTime.fromJSDate(new Date(document.lastModified)).toFormat('yyyy-MMM-dd HH:mm');
  end;

  // Output Version Information
  console.log('Fancy Application Started.');
  console.log('Version: '+AppVersion);
  console.log('Release: '+AppReleaseNice);

end;


And with that, we've got our version information displayed.

TMS Software Delphi  Components
Application Version Information.


Off and Running.

And with that, we've got all the basics covered for starting the next great TMS WEB Core project. A little bit of JavaScript, HTML, and CSS can go a long way toward a polished finished product. And while these are all major topics in their own right, and quite a departure from Delphi norms, developing great applications for the web would be difficult without at least a passing knowledge of each. Hopefully, we've covered at least a few things today that might be new or interesting. And the stage is set to build on this foundation considerably in future posts. 

Please let me know what you think in the comments below. 

Related Posts:
Basics Part 1: Consoles, Calls, Classes, CSS, and... CAPS-LOCK?
Basics Part 2: Files, Fonts, Flex, and FNC
Basics Part 3: Attributes, Animation, Async, and Await
Basics Part 4: Images, Icons, iFrames, and Initialization
Basics Part 5: d, div, debugger, Debug, and Deployment
Basics Part 6: Links, Lists, Layouts, and Let


Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.



Andrew Simard




This blog post has received 12 comments.


1. Wednesday, November 9, 2022 at 1:49:22 PM

Hello Andrew.
Thank you for these excellent tutorials and for your time in generating them.

Baykal Ertan


2. Wednesday, November 9, 2022 at 8:57:01 PM

You are most welcome! I would love to hear about what you liked or did not like, or what topics you would be interested in learning more about?

Andrew Simard


3. Wednesday, November 16, 2022 at 12:24:35 PM

Andrew, thank you for the always useful content.
Could you share your thoughts about how to create reports and report viewers from a database while using TMS WEB Core and related technologies?

Wilfred Oluoch


4. Thursday, November 17, 2022 at 5:28:34 AM

Thanks for the feedback!

My initial approach would probably be to do all the report generation on the XData side, so you can use a regular old Delphi VCL app and all the report generation tools and database access tools available there (I am fond of ReportBuilder from Digital Metaphors, for example). Then on the TMS WEB Core client side, you would just be requesting reports and passing parameters in, getting PDF files back via the REST API provided by XData. I think there are examples of this sort of thing in the TMS Support Center and elsewhere.

If you wanted to do absolutely everything in TMS WEB Core on the client side, things are a little more complicated depending on the database and complexity of the reports that you want to generate. You can still create PDFs and do all kinds of fancy things with reporting, just that you do not have quite as many tools to help you.

One of the ideas for a future blog series is a demo of the first approach, so if that is of interest maybe it could be bumped up the list a little bit.

Andrew Simard


5. Thursday, November 17, 2022 at 8:27:23 AM

fwiw, here is a blog article about using TMS Flexcel for generating reports in a TMS WEB Core web client app
https://www.tmssoftware.com/site/blog.asp?post=463

Bruno Fierens


6. Friday, November 18, 2022 at 12:11:32 AM

Yes.. a demo of the first approach (report generation on the XData side and use with ReportBuilder would be great !

Kamran


7. Friday, November 18, 2022 at 12:35:46 AM

Yes, and this post on creating PDFs from scratch in a TMS WEB Core client app is also a great way to get started.
https://www.tmssoftware.com/site/blog.asp?post=1013

Andrew Simard


8. Saturday, November 19, 2022 at 12:58:42 PM

Hi Andrew
I would just like to say .. and I am sure a lot of people would agree.
I found your posts highly informative, well thought out, unassuming of any prior knowledge and on point.
I am always looking forward to your next post.. amongst the other excellent posts.
Keep them coming.

* Thank you *

Kamran


9. Saturday, November 26, 2022 at 1:42:55 AM

You are most welcome! Many more posts will be arriving momentarily.

Andrew Simard


10. Wednesday, May 10, 2023 at 6:17:38 PM

Very helpful, Andrew. Thanks for the pointer to these posts!

Peterson Terry


11. Wednesday, May 10, 2023 at 6:24:25 PM

Happy to help! While the TMS Support Center is an excellent resource for finding solutions to problems, the blog posts are intended to be a little more proactive - hopefully helping to avoid the problems we otherwise might encounter.

Andrew Simard


12. Tuesday, October 17, 2023 at 11:14:07 AM

Hi Andrew,
Maybe there is an example using "Calling Delphi From JavaScript" in form dynamically ?
Thank You

Mokhammad Aris




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post