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:
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:
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:
And the result becomes: