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

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

Browser Caching.

When working on TMS WEB Core projects, it is usually very quick to switch between editing code and then running it in the browser. And the browser developer tools are very useful when trying to troubleshoot what might be happening in your app. However, the browser tends to cache whatever it can, whenever it can, and sometimes that means that your in-development app gets cached as well. So when you make a change and then look at it in the browser, it shows you the old version - your change is not visible.

One way to help is to do a hard refresh with Ctrl+Shift+R, Ctrl+F5 or hold down the shift key while clicking the refresh button. If you're using Chrome, when the developer tools are open, you can also long-click on the refresh button and then get the option to "Empty Cache and Hard Reload".

Also, under the Network tab in the developer tools window, there is the option to "disable cache". This can help ensure that resources are re-downloaded if you're troubleshooting any kind of network issues. There is also an option there to throttle the speed, so you can slow down the connection if you want to test a download/upload progress indicator or anything like that.

2 Likes

Community Participation

These support forums are a great place to get help with issues related to specific TMS products, and often a little beyond those. Many customers also actively participate, helping one another in this community of developers. But we've got other tools to help support and encourage even more community participation.

GitHub has a number of repositories containing complete TMS WEB Core projects. Many of the TMS Software Blog posts have projects that are subsequently uploaded to GitHub, for example, and these often grow beyond what the blog posts cover, taking on a life of their own. I'd also encourage anyone with projects that can be shared to do so on GitHub and to also add "topics" to their repositories, like tms-web-core or tms-xdata, to make it easier for everyone to find them. GitHub also has its own issue and discussion pages that can be associated with each repository to help further community participation.

Another potential resource is 𝕏 (formerly known as Twitter). Historically, Delphi developers are a bit on the shy side it would seem, but there are literally hundreds of thousands of other developers using this platform on a daily basis, to great effect. A new WEB Core And More Community has been set up to help with that. A great place to announce any new GitHub repositories, for example, or to have conversations beyond what might be suitable for the forums here. I've also got an account on 𝕏 that I use to share TMS WEB Core and related content, @WebCoreAndMore.

Got any other community participation ideas? Other websites? Please share!

Memory Management

A rather important change when moving from, say, VCL projects to TMS WEB Core projects (or JavaScript projects generally) is how memory is managed. In our VCL projects, we've been spending a long time getting accustomed to allocating objects and then using something like "free" to return those resources to the operating system, hopefully avoiding things like memory leaks, carrying around objects that we no longer need, and that sort of thing—super-important in that environment.

In JavaScript, though, there's no equivalent concept of "free". Instead, JavaScript uses a garbage collection mechanism. This periodically looks at what resources have been allocated and frees up those that are no longer needed. As developers, we typically don't have any access to this garbage collection mechanism at all. It is of course implemented differently in each browser and can change dramatically even between browser versions.

This means that we don't have to worry so much about that memory usage. We just have to be careful to define the objects we need in the scope that we need them, and let the rest take care of itself. True, this can be unsettling and might appear to be more than a bit reckless. I'm with you there! But that's just how it works, and it tends to work pretty well almost all of the time.

Of course, it helps that a typical browser instance tends to allocate a substantial amount of memory just to run by itself, often far more than any kind of object memory requirements most apps might require on their own. For example, having a quick peek at Firefox (about:memory) shows several GB allocated for the browser itself and several hundred MB allocated to each tab, typically. So fretting about freeing up that TStringList is likely not something you should be overly concerned with.