Tips for new TMS Web Core developers and in particular for seasoned Delphi developers

Use Third-Party Javascript Libraries

One of the most powerful aspects of TMS Web Core is its ability to easily support popular Javascript libraries with very little effort, greatly expanding the capabilities of your app in any number of ways. TMS includes direct support for a small handful of libraries through the Project Manager's "Manage Javascript Libraries" function. Popular libraries like Bootstrap, Ace Editor, and now even Font Awesome can be added to your project with just a couple of mouse clicks. Some of these even have wrappers that make it easy to add functionality to your app using familiar IDE components.

For the many thousands of other JS libraries that are not directly supported or do not have a ready-made wrapper, they can still be used without much effort at all simply by referencing them in your project's HTML file and adding the JS code to your project as you would any other file. This is in part what the "Manage Javascript Libraries" is doing behind the scenes.

Many JS libraries are available through "Content Delivery Networks" or CDNs. These can be used to add libraries to your project without having to include the files in your own distribution - they are loaded by the client at runtime, reducing the size of your app that has to be served up by your own hosting service and potentially increasing performance by having the libraries loaded from a server that is closer to where the client app is running. For example, an earlier posting in this thread describes how to add the Font Awesome library to your project using the JSDelivr CDN.

Just keep in mind, when using a CDN, that the library is managed by someone else which introduces some trade-offs. If you link to the "latest" version of the library, it may be updated with breaking changes that may adversely impact your app. If you instead link to a static version, you might not get the benefit of security updates that may also adversely impact your app, just as if you hosted a copy of the library yourself.

How you use the actual JS library is of course dependent on what the JS library does. Some libraries are discussed in detail elsewhere in this Support Center forum including examples and tips on getting the most of the library within the TMS Web Core environment.

Are you using a library that isn't already included in TMS Web Core? Or do you have questions about how to integrate a library you're interested in? Would be interesting to see what libaries are used most by TMS Web Core users. At the moment, I've got more than a dozen in my project. The usual suspects are there like Font Awesome, Bootstrap and JQuery. But I've also been using popular libraries like Tabulator, CodeMirror, FlatPickr, SunEditor, Toastr, File-Save, PrintJS and others.

4 Likes

ElementID - Tread carefully!

Most TMS Web Core components have an ElementID property availablie. This maps directly to the HTML element's ID property when the page is generated. It is important to first note that an HTML ID should be unique in a page (specifically the DOM) and that it is case-sensitive. If this property is left blank, TMS Web Core will generate a unquie ElementID when needed, but it isn't a fixed value. It may very well change in a subsequent build. So if you need to use it, be sure to set it yourself.

One use-case for this property is to connect a TMS Web Core component to an element within an HTML template. The docs explain this pretty well, and the IDE has tools for managing these linkages.

Another use-case is to use the values in your custom CSS. So if you have a component that you want to apply custom CSS styling to that you can't otherwise do directly in the IDE, you can assign something like myElementID to the ElementID property of the component and then in your CSS you can use the

#myElementID { }

selector to add whatever custom CSS you might need. Naturally this should be used somewhat sparingly as you wouldn't want to have to use that specific of a CSS selector often as it kind of defeats the purpose of CSS. But it can certainly come in handy to fix up little tweaks here and there.

Yet another use-case is when using third-party JS components. These often need to be linked to an HTML ID. So if that happens to be one of your components, this is a way to make that happen. Sometimes, like with Tabulator, not filling in this ElementID can introduce unexpected behaviour, so a good thing to keep an eye on if you're having troubles.

There can be problems though, particularly if you use the same ElementID on multiple components and they somehow make it onto the same page at the same time. So for example, let's say you use the same ElementID naming convention across different forms - maybe just the name of the component. If two forms get loaded up at the same time with components that have the same ElementID, conflicts will arise that may not be that easy to track down. In Delphi, if you have two controls on different forms with the same name, it isn't really a problem because they have the form name to use as a way to descriminate between them, but that's not the case here.

So if you're loading up multiple forms into the same page at the same time, this is a good thing to keep an eye out for. And if you happen to be loading the same form multiple times, consider either not assigning the ElementID or assigning it to something unique when the form is created. Here's an example of the kinds of problems that can crop up unexpectedly when these ElementID naming collisions occur. Usually there's a console log message about it, but sometimes it isn't so obvious.

And finally, if you happen to use FNC components, the ElementID property (and others) are conspicuously absent. To work around that, you can try something like adding a TWebHTMLDiv component to your form and then add the FNC component to that and be on your merry way.

3 Likes

Excellent advice. Be particularly careful when calling popups - they make look like their own form, but they are part of the webpage being displayed when they are called. Learnt the hard way.

1 Like

If you use javascript (asm) in a function remember that result isn't defined, you have to use Result as JS is case sensitive. So

This works

class function THTMLHelper.elementHeight(const aElementId: string): integer;
begin
  asm
    Result = parseInt($("#" + aElementId).height());
  end;
end;

and this doesn't

class function THTMLHelper.elementHeight(const aElementId: string): integer;
begin
  asm
    result = parseInt($("#" + aElementId).height());
  end;
end;
5 Likes

In addition to that, in order to keep the Delphi IDE happy and not break the LSP, I always wrap my asm end blocks with {$IFNDEF WIN32} {$ENDIF}, so it looks like this

  {$IFNDEF WIN32} 
     ASM 
     ...
     END; 
  {$ENDIF}

Also, I think it is good practice to first convert all function parameters to local variable before accessing them in JavaScipt, like so

Function Foo(Bar1 : String; Bar2 : Integer) : String;
Var
 LBar1,LResult ; String;
 LBar2 : Integer;
Begin
 {$IFNDEF WIN32} 
   ASM 
    LResult = yourjsfunction(LBar1,LBar2);
   END; 
 {$ENDIF}
 Result := LResult
End;

Still, the compiler is unhappy and gives you unused variables warnings. To also get rid of those, use:

If LBar1='' then;
If LBar2=0 then;

So in total, you end up with:

Function Foo(Bar1 : String; Bar2 : Integer) : String;
Var
 LBar1,LResult ; String;
 LBar2 : Integer;
Begin
 If LBar1='' then;
 If LBar2=0 then;
 {$IFNDEF WIN32} 
   ASM 
    LResult = yourjsfunction(LBar1,LBar2);
   END; 
 {$ENDIF}
 Result := LResult
End;
2 Likes

Really catching ALL exceptions

In regular Delphi, catching a generic exception is done like this

Try
 ...
Except on E:Exception do
 Begin
 End
End

Well, in the JS environment, this does in fact not catch ALL errors. Errors created by the JS environment are NOT derived from class Exception and therefore On E:Exception does not catch these errors. As the Delphi ExceptObj is also not available, some JS helps again, like so:

Function GetMessageFromException(E : Exception) : String;
Var RMsg : String;
Begin
 RMsg := '';
 {$IFNDEF WIN32}
 ASM
  {if (!pas.SysUtils.Exception.isPrototypeOf(E)) {
   RMsg = E.message;
  } else {
   RMsg = E.fMessage;
  }}
 END;
 {$ENDIF}
 Result := RMsg;
End;

{---------------------------------------}

Procedure Foo;
Var e : Exception;
Begin
  Try
    ...
  Except
   {$IFNDEF WIN32} ASM e = $e; END; {$ENDIF}
   Console.error(GetMessageFromException(e));
  End
End;
6 Likes

Using [async] and await()

One key difference in developing web apps as compared to VCL apps is the asynchronous nature of web browsers. Maintaining a good user experience is paramount, so every effort is made to minimize any kind of blocking action. As a result, many function calls often return immediately with a JSPromise result, long before the actual work of the function is completed. This can be confusing or problematic as the flow of your code doesn't necessarily follow the order of the statements being executed.

These kinds of functions typically have callback functions that get called when the work of the funciton is actually completed. This is needed to make apps work in a predictable way but really makes a bit of a mess of things when it comes to trying to understand program flow, particularly when there are callback functions everywhere.

Wrapping these kinds of function calls with await() makes it possible for the program to simply wait at that point in the code for the work of the function to complete before continuing on to the next statement. It does it in a non-blocking way so that the user experience isn't negatively impacted. This often removes the need for callback functions and code readability goes up dramatically.

If you have a function or a procedure that includes an await() call in it, it must be declared initially with an [asyc] attribute. Failing to do so will generate an error at compile time, so not at all hard to fix if you forget.

[async] procedure WebButton1Click(Sender: TObject);

Also best to not try anything fancy here. Keep [async] on the same line as the declaration :slight_smile:

3 Likes

Bootstrap Tooltips

While TMS Web Core supports hints as they work in the VCL, sometimes a little more control over the look of them is desirable. If you're using Bootstrap in your project, you can use their tooltips as well. As this would likely happen often, a procedure to do this might look like the following.

procedure SetBootstrapTooltip(btn: TWebButton);
var
  ElemID: String;
  ToolText: String;
begin
  ElemID := btn.ElementID;
  ToolText := btn.Hint;
  asm
    var btn = document.getElementById(ElemID)
    var tooltip = new bootstrap.Tooltip(btn, {
      title: ToolText,
      placement: 'right',
      customClass: 'MyCustomToolTipClass',
      delay: { "show": 1000, "hide": 250 }
    });
  end;
end;

Setting the (optional) custom class allows you to add your own CSS to the tooltip div that is generated. And you can change the placement to 'right', 'left', 'top', 'bottom' or add further tweaks using the CSS margins. Many other options can be found in the Bootstrap 5 Tooltip documentation.

Oh, and if you want to fiddle with the styling while using the browser debugging tool, see the Browser Debugging Tip near the beginning of this thread to see how to have the tooltip stay visible after you've moved the cursor away from it.

1 Like

Excellent tip!
So good that this approach deserves to be automatic when a Bootstrap type project is chosen. We'll add this as built-in feature for such projects.

3 Likes

Learning JSON

As a Delphi developer, there often isn't any need to use JSON in your apps. It may come up if you connect to REST data sources or pass data back and forth between some other external system. In TMS WEB Core apps, this is much more likely to come up, likely because you're doing exactly these kinds of things. Fortunately, there are plenty of resources available.

Here in the Support Center, there are many topics that cover aspects of JSON. Here are a few.

On the TMS Blog there are also posts that focus specifically on JSON.

And in addition to his wonderful TMS WEB Core Book, which includes a section on JSON, Dr. Holger Flick also recently made a video on the topic, How it works with Holger: How to use JSON in Delphi [Beginner] - YouTube.

I'm sure there are numerous other resources available, so as usual if you have any questions at all, just create a post here in the Support Center.

1 Like

Using FNC Components

While there are a lot of great components already included with TMS WEB Core, there are also a lot of FNC Components that work great as well. But when adding an FNC Component to a TWebForm, you might find yourself reaching for the familiar ElementID or ElementClassName properties, only to find them conspicuously absent.

If you find yourself in a situation where you need those properties (or any of the related properties), the trick is to first add something like a standard TWebHTMLDiv component to your TWebForm and then add your FNC Component to that, essentially using the TWebHTMLDiv as a container for your FNC Component. This allows you to set whatever you like in the ElementID and ElementClassName properties of the container, allowing you to position or size it as you like. Then, you can set the FNC Component to fit within this container.

2 Likes

Making the body section of TWebTableControl Scrollable

Typically if you want to make a TWebTableControl or a TWebDBTableControl scrollable then you would place it inside a TWebScrollBox. The problem here is that when you scroll you lose the headings.

You can get around this by adding the following to your CSS

#YourTableId tbody {
  display: block;
  width: 720px;
  overflow: auto;
  height: 300px;
}

#YourTableId table {
  width: 700px;
  table-layout: fixed;
  border-collapse: collapse;
}

table#YourTableId {
  max-width: 700px;
}

#YourTableId th {
  width: 116.5px;
}

#YourTableId td {
  width: 116.5px!important;
}

This will make the section scrollable and leave the column headings visible. Obviously change the sizes to match you table.

3 Likes

Search the TMS Blog for TMS WEB Core Examples

While the TMS WEB Core Developer's Guide is great, and the Support Center has the answers to many questions, another resource that I often overlooked initially is the TMS Blog. While there are many posts that relate to new product announcements and other things, it is also a great resource for example code and even complete TMS WEB Core projects that you can download and explore right away.

I've personally contributed dozens of posts (with more coming once or twice each week), mostly related to using various JavaScript libraries directly within TMS WEB Core projects. But also posts about using TMS WEB Core with Visual Studio Code, Raspberry Pi, and more. And that's just a drop in the bucket compared to all of the other posts made by TMS staff and others, covering numerous with detailed code examples and links to additional information. One way to get notified of all the activity on the TMS Blog is by signing up for the TMS Newsletter.

2 Likes

I am glad to have finally found this, it really helped. In later versions it seems the default exception is accessible as a string with less code and eliminate the local variable E.
This allows you to catch TypeError: this.WebLabel1 is null .

procedure TfrmMarshalDemo.WebFormCreate(Sender: TObject);
begin
  try
    // raise Exception.Create('I created this');
    LoadFeatureTree;
    FFrame.SetupLabels;
  Except
    on E: Exception do // handle exception types
      Console.log(E.Message);
    else // handle everything else
      Console.log(String(JS.JSExceptValue));
  End
end;

Reference is here:
https://wiki.freepascal.org/Pas2js_Transpiler#Translating_try..except

3 Likes

Await and async is well explained later in this thread, a function always returns a promise. It took me several reads of this, but eventually the behavior made sense.

1 Like

If you are referring to class variables use in an asm block, you can just put the "this." in front of the variable name and will work fine. Below the MediaPlayer is part of the form class, by putting the "this." in front it works.

{$IFDEF WEBLIB}
asm
  vw = this.MediaPlayer.FElement.videoWidth;
  vh = this.MediaPlayer.FElement.videoHeight;
  cw = this.MediaPlayer.FElement.clientWidth;
  ch = this.MediaPlayer.FElement.clientHeight;
end;
{$ENDIF}
1 Like

WARNING:

I've found that the compiler's <ctrl><shift>-C option to create headers from methods in the implementation section doesn't know about these kinds of decorations on things in the interface section. It will take something like the above and do the following:

[async]
procedure xyz( aaa : integer );
procedure WebButton1Click(Sender: TObject);
1 Like

Solution to CORS errors if you have access to the server containing the files:

If you have a hosting account running Apache, you can put this snippet into the .htaccess file in the folder where the files are to add the header to the response packet your client will need to access the files being blocked by CORS. (It may be needed in the home directory as well, but it should open up access for all subfolders, so be careful. Do some testing.)

<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "*"
</IfModule>
1 Like

Be cognizant of the lifetimes of variables being used to support methods with long execution times

I've been working with the Web Audio API and I made a series of calls like this:

sl := TStringlist.Create;
// fill sl with some data
await( LoadBuffers( sl ) );
await( PlayBuffers( sl ) );
sl.Free;

The call to sl.Free occured before the last buffer had finished playing, even though the await did its job correctly. As a result, when the buffer finished playing and the internal logic continued on to resolve the last track, it blew up because the list of buffers it was given was now empty. In fact, it was gone! But the reference to it still worked enough to say that the .Count value was zero, and it threw an error that was totally unexpected.

The moral of the story here is this:

if you are working with things that can take a lot of time to run (many seconds) and they depend on data in a buffer, then make sure that said buffer has a lifetime that goes beyond the actual clock-time needed to complete the entire operation.

The above block of code makes perfect sense in the VCL world, but may be subject to a time-warp of sorts in the WEB Core world. :slight_smile:

In this case, the await releases after the buffer starts playing, not after it completes.

1 Like

Another solution to bypass CORS for your own files

If you have files you want to share, you don't want to put them on your own server account (maybe b/c you don't have one), and you're running into CORS issues with a script trying to access them elsewhere, there's a way to use Dropbox that has been around for a while but isn't very well-known.

First, I'll say that Dropbox has an advantage over lots of file-sharing sites in that you can access it in a few different ways:

  1. Directly on their Website
  2. Their app running in your OS so it looks like a normal folder
  3. Via their API (TMS Cloud Pack has an interface for it)

Also, their daily bandwith limitaions are absurdly high.

Basic [ie, "free"] accounts and accounts on a trial of a Dropbox team: 20 GB of bandwidth and 100,000 downloads per day .
Plus, Family, Standard, and Professional accounts: 1 TB and unlimited downloads per day.
Dropbox team Advanced and Enterprise accounts: 4 TB and unlimited downloads per day.

You can get hold of a link to post online from their website or their app and it looks like this:

https://www.dropbox.com/s/x12nrdio8i9oe52/sample-3s.mp3?dl=0

The querystring arg dl=0 can be changed to dl=1 for use in a browser to initiate a download rather than opening it up in the browser. But if you do that via a script, you'll get a CORS error.

So here's the secret: change the link to look like this:

https://dl.dropboxusercontent.com/s/x12nrdio8i9oe52/sample-3s.mp3

That is, change the domain name and remove the ?dl=0 querystring argument from the end of the URL.

This version runs in my WEB Core app while the earlier one gives a CORS error.

According to the whois record, the domain name dropboxusercontent.com is owned by Dropbox themselves, so it's not going to be intercepted by some unknown 3rd-party (which is I think how it got started). Searching Google for it shows a lot of complaints that it doesn't work, but most of them were from several years ago. Not much lately.

1 Like