Skip to content

Custom control development

Under the TMS RADical Web umbrella, TMS WEB Core is the foundation framework. As one of the goals of TMS RADical Web is to bring RAD to web development for Delphi developers, it is only logical that using components to develop web applications is an essential part. While TMS WEB Core already comes with a wide range of components out of the box, having an extensible component framework is a key feature. In this article, we will have a look at building custom components for TMS WEB Core. TMS WEB Core components can be categorized in roughly 4 types:

  • Non-visual components
  • Visual controls wrapping a HTML element or hierarchy of HTML elements
  • Visual controls using the FNC abstraction layer (that cross-framework, cross-platform and web-enabled)
  • Visual controls wrapping jQuery controls

Non-visual components

The good news here is that non-visual components for TMS WEB Core are identical to nonvisual components for VCL or FMX applications. In TMS WEB Core, the base classes TComponent & TPersistent are available and new non-visual components can be inherited from these base classes and properties, events, methods can be added. The non-visual component can be added to web forms just like VCL non-visual components can be added to VCL forms. Note though that the standard VCL non-visual components included in Delphi can't be used asis on web forms. After all, all this code needs to run directly in the browser. But already out of the box, TMS WEB Core offers many equivalents to standard VCL non-visual components like the TWebTimer for example being equivalent for TTimer or a TWebDataSource as equivalent for TDataSource. There is one key requirement to make your custom non-visual component available on the Delphi component palette when a web project is opened and that is to decorate the component with an attribute TMSWebPlatform (defined in WebLib.Controls.pas):

  [ComponentPlatforms(TMSWebPlatform)]
  TMyWebComponent = class(TComponent)
  private
  // your private variables & methods here
  protected
  // your protected methods here
  public
  // your public methods here
  published
  // your published properties and events here
  end;

Visual controls wrapping HTML elements

The architecture for this type of controls is based on writing a Pascal class that wraps the HTML element or element hierarchy. The Pascal wrapper class will do the following:

  • create the HTML element(s) in the DOM or bind to existing HTML elements in the HTML file associated with the web form
  • bind the HTML element JavaScript events to Pascal class methods
  • reflect Pascal class properties on HTML element(s) attributes

To create such component, it can inherit from TCustomControl that already includes much of the required logic. Key virtual methods and essential properties defined in TCustomControl are:

  TCustomControl = class(TComponent)
  protected
    function CreateElement: TJSElement; virtual;
    function ContainerElement: TJSElement; virtual;
    procedure BindElement; virtual;
    procedure UpdateElementSize; virtual;
    procedure UpdateElementVisual; virtual;
    procedure UpdateElementData; virtual;
    procedure CreateInitialize; virtual;
  published
    property ElementID;
    property ElementClassName;
  end;

Override the CreateElement function to create the HTML element or HTML element hierarchy needed for the control. This function returns a reference to the parent or container HTML element for the control. If only a single HTML element will be needed in the custom control, this is as simple as:

function TMyCustomControl.CreateElement: TJSElement;
begin
  Result := document.createElement('DIV');
end;

The parent or container element returned by the CreateElement function can be retrieved from other places in the control code via the function ContainerElement.

The CreateElement function will be called automatically from the base class when the control ElementID is empty at the time the control parent is set. When ElementID is not empty, the container element is retrieved from the DOM based on ElementID value, i.e. the control class will use the HTML element returned by document.getElementById(ElementID). By default, JavaScript event binding is done on the container element. The base class already binds the JavaScript onwheel, onclick, onmousedown, onmouseup, onmousemove, onmouseleave, onmouseenter, onkeydown, onkeyup, onkeypress, onfocus and onblur events. The base class already maps these JavaScript events on virtual class methods with a signature compatible with VCL, that are easy to override. These are for example the available virtual key and mouse event related methods:

  TCustomControl = class(TComponent)
  protected
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); virtual;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); virtual;
    procedure MouseMove(Shift: TShiftState; X,Y: Integer); virtual;
    procedure DoMouseEnter; virtual;
    procedure DoMouseLeave; virtual;
    procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); virtual;
    procedure DblClick; virtual;
    procedure KeyDown(var Key: Word; Shift: TShiftState); virtual;
    procedure KeyPress(var Key: Char); virtual;
    procedure KeyUp(var Key: Word; Shift: TShiftState); virtual;
  end;

So, from our custom control, all we need to do is override these virtual methods, so it is very similar to writing VCL custom controls.

Three more important virtual methods that will typically be overridden are:

  • procedure UpdateElementSize; virtual;
  • procedure UpdateElementVisual; virtual;
  • procedure UpdateElementData; virtual;

The UpdateElementSize procedure is supposed to do the necessary HTML element attribute changes when the position and/or size of the control changes. The base class TCustomControl will already handle this for the container element Top,Left,Width & Height. (when the control is absolute positioned that is).

The UpdateElementVisual method is the place where typically Pascal class properties that affect the visual appearance of controls, are mapped onto HTML element(s) attributes. The UpdateElementData method is the place where properties that affect data connected to controls is updated in the HTML element. To illustrate this, let's assume our custom control mapping on a HTML DIV element has a color property to set background color of the DIV and a text property for the text in the HTML DIV element. The corresponding code for this is:

uses
  Classes, WEBLib.Controls, Web;
TMyCustomControl = class(TCustomControl)
private
  FColor: TColor;
  FText: string;
  procedure SetColor(const AValue: TColor);
  procedure SetText(const AValue: string);
protected
  function CreateElement: TJSElement; override;
  procedure UpdateElementVisual; override;
  procedure UpdateElementData; override;
published
  property Color: TColor read FColor write SetColor;
  property Text: string read FText write SetText;
end;
function TMyCustomControl.CreateElement: TJSElement;
begin
  Result := document.createElement('DIV');
end;
procedure TMyCustomControl.SetColor(const AValue: TColor);
begin
  if (AValue <> FColor) then
  begin
    FColor := AValue;
    UpdateElementVisual;
  end;
end;
procedure TMyCustomControl.SetText(const AValue: string);
begin
  if (AValue <> FText) then
  begin
    FText := AValue;
    UpdateElementData;
  end;
end;
procedure TMyCustomControl.UpdateElementVisual;
var
  el: TJSHTMLElement;
begin
  inherited;
  el := TJSHTMLElement(ContainerElement);
  el.style.setProperty('background-color', ColorToHTML(Color));
end;
procedure TMyCustomControl.UpdateElementData;
var
  el: TJSHTMLElement;
begin
  inherited;
  el := TJSHTMLElement(ContainerElement);
  el.innerHTML := Text;
end;

Finally, to finish this first basic custom control example, let's add a click handler. As the base class already binds the container element 'onclick', this is as simple as:

TMyCustomControl = class(TCustomControl)
published
  property OnClick;
end;

For the sake of completeness, let's discuss also how to map control methods on HTML element JavaScript events. This is done with the HTML element addEventListener() method.

Example:

TMyCustomControl = class(TCustomControl)
protected 
  function HandleDoXXXX(Event: TEventListenerEvent): Boolean; virtual;
  procedure BindEvents; override;
end;

procedure TMyCustomControl.BindEvents;
begin     
  inherited;
  Container.addEventListener('xxxx',@HandleDoXXXX);
end;

function TMyCustomControl.HandleDoXXXX(Event: TEventListenerEvent): Boolean;
begin
  // code to be executed when Javascript event XXXX is executed
  Result := true;
end;

Assume the HTML event has a JavaScript event named XXXX, the control class method HandleDoXXXX will be called when this JavaScript event is triggered.

Visual controls using the FNC abstraction layer

A second approach to create custom controls for TMS WEB Core is by inheriting from the base class TTMSFNCCustomControl defined in the TMS FNC Core. The good news is that by doing so, the control code will work for VCL applications, FMX applications, LCL applications and of course also web applications. Technically, for a web application, an FNC web control will internally create a HTML CANVAS element. It maps all needed JavaScript events on this CANVAS to the control virtual methods and it offers an FNC TTMSFNCGraphics Pascal wrapper class to perform the painting of these controls. To get started, the FNC units we will use are:

  WEBLib.TMSFNCCustomControl : defines the base class TTMSFNCCustomControl
  WEBLib.TMSFNCGraphics : defines the class TTMSFNCGraphics for painting
  WEBLib.TMSFNCTypes : defines various types used with custom controls    
  WEBLib.TMSFNCGraphicsTypes : defines various types for handling painting

The class interface can be defined as:

  TMyFNCCustomControl = class(TTMSFNCCustomControl)
  private
    FColor: TColor;
    FText: string;
    FDown: boolean;
    procedure SetColor(const AValue: TColor);
    procedure SetText(const AValue: string);   
  protected
    procedure HandleMouseDown(Button: TTMSFNCMouseButton; Shift: TShiftState; X, Y: Single); override;
    procedure HandleMouseUp(Button: TTMSFNCMouseButton; Shift: TShiftState; X, Y: Single); override;
    procedure HandleKeyPress(var Key: Char; Shift: TShiftState); override;
    procedure Draw(AGraphics: TTMSFNCGraphics; ARect: TRectF); override;
  published
    property Color: TColor read FColor write SetColor;
    property Text: string read FText write SetText;
  end;

The implementation for the property setters is:

procedure TMyFNCCustomControl.SetColor(const AValue: TColor);
begin
  if (AValue <> FColor) then
  begin
    FColor := AValue;
    Invalidate;
  end;
end;
procedure TMyFNCCustomControl.SetText(const AValue: string);
begin
  if (AValue <> FText) then
  begin
    FText := AValue;
    Invalidate;
  end;
end;

To have the control draw itself, all we need to do is override the FNC control Draw() virtual method.

procedure TMyFNCCustomControl.Draw(AGraphics: TTMSFNCGraphics; ARect: TRectF);
begin
  inherited;
  if FDown then
    AGraphics.Fill.Color := gcGray
  else
    AGraphics.Fill.Color := Color;
  AGraphics.DrawRectangle(ARect);
  AGraphics.DrawText(10,10,FText);
end;

Let's implement for this basic sample a key event handler that will add the character pressed to the control text and the mouse down that will show the control in a different color.

procedure TMyFNCCustomControl.HandleKeyPress(var Key: Char; Shift: TShiftState); 
begin
  Text := Text + Key;
end;
procedure TMyFNCCustomControl.HandleMouseDown(Button: TTMSFNCMouseButton; Shift: TShiftState; X, Y: Single); 
begin
  FDown := true;
  Invalidate;
end;
procedure TMyFNCCustomControl.HandleMouseUp(Button: TTMSFNCMouseButton; Shift: TShiftState; X, Y: Single); 
begin
  FDown := false;
  Invalidate;
end;

Visual controls wrapping jQuery controls

Creating a Pascal wrapping class for a jQuery UI control has in fact several similarities with creating a wrapping class for HTML elements as jQuery UI controls are exactly that, a hierarchy of HTML elements. What is a bit different is that typically the jQuery control is created by calling a JavaScript function that creates it. The jQuery object then typically exposes its own events and our control needs to bind to these events. To facilitate this, the TMS WEB Core framework offers a base class TjQueryCustomControl. This class adds the virtual method InitjQuery() that is called when the jQuery control needs to be created and the function GetJQID function that returns the unique jQuery ID for our control. The jQuery control will by default be hosted in a HTML DIV element. What we learned from wrapping HTML elements, is that the virtual methods UpdateElementVisual() / UpdateElementData() are what is called when property changes need to be reflected in the control jQuery settings or data. To create a Pascal wrapper class for a jQuery control, the minimal approach is as such:

  TmyJQueryControl = class(TjQueryCustomControl)
  protected
    procedure InitJQuery; override; 
  end; 
procedure TmyJQueryControl.InitJQuery;
begin
  // create the jQuery control here
end;

To show the basic concepts, we demonstrate this with a minimal wrapper for the a nice jQuery progress bar offered here: https://kimmobrunfeldt.github.io/progressbar.js/

Following the docs of this library, to create the jQuery progressbar, we need the following JavaScript code for a half circle progressbar:

   var bar = new ProgressBar.SemiCircle(div, {options});

To update the position of the progressbar, we can use bar.animate(position); // with position a value between 0 and 1.

So, our full control code becomes:

  TjQueryProgressBar = class(TjQueryCustomControl)
  private
    FPosition: double;
    FBar: TJSElement;
    procedure SetPosition(const Value: double);
  protected
    procedure InitJQuery; override;
    procedure UpdateElementVisual; override;
  published
    property Position: double read FPosition write SetPosition;
  end;
{ TjQueryProgressBar }
procedure TjQueryProgressBar.InitJQuery;
var
  eh: TJSElement;
begin
  eh := ElementHandle;
  asm
    this.FBar = new ProgressBar.SemiCircle(eh, {
    strokeWidth: 6,
    easing: 'easeInOut',
    duration: 1400,
    color: '#FFEA82',
    trailColor: '#eee',
    trailWidth: 1,
    svgStyle: null
    });
  end;
end;

 procedure TjQueryProgressBar.SetPosition (const Value: double);
 begin
   if (FPosition <> Value) then
 begin
     FPosition := Value;
     UpdateElementVisual;
   end;
 end;
 procedure TjQueryProgressBar.UpdateElementVisual;
 begin
   inherited;
   if IsUpdating then
     Exit;
   if not Assigned(FBar) then
     Exit;
   asm
     this.FBar.animate(this.FPosition);
   end;
end;

Note here the asm/end blocks in the code. As for reasons of simplicity, we have not taken the effort to create a Pascal wrapper class for the ProgressBar jQuery object, we need to directly access this jQuery object with JavaScript. It is in the asm/end block in our Pascal code that we can directly write this JavaScript code. This code does not get compiled but is just directly generated inline as-is. As you can see, we map a private variable FBar to the created jQuery object created in the InitJQuery call and from the UpdateElementVisual override, this FBar object is accessed to call its animate() function to update the value. Also noteworthy is that from the asm/end block, we access the instance asthis".

After creating an instance of this control, we can simply add the following code to the button click handler:

   WebjQueryProgressBar1.Position := 0.5;

And the result becomes: