Home > Delphi, JavaScript, Object Pascal, OP4JS, Smart Mobile Studio > Smart Pascal: Cool tricks for better code

Smart Pascal: Cool tricks for better code

February 2, 2017 Leave a comment Go to comments

What is the most costly operation for a HTML5 application? While there are many to pick from the most time-consuming tasks are without a doubt sizing elements and creating elements.

Both of these operations are time-consuming (in cpu terms) because they have a direct impact on the entire layout. In other words, when you create a visible element (which happens when a Smart visual control is created), the entire DOM is affected. The same naturally happens when you adjust the size of an element, because the browser will go through all it’s nodes and re-build its calculated stylesheet (which is a hidden stylesheet that the browser amalgams together).

Since this is the case it makes sense to try to avoid sizing controls as much as possible. You can’t completely avoid it of course – but the less change to width and height the better.

Use the percentages

The Smart Pascal RTL follows the traditional, Delphi and LCL esquire component model. This means that the position and width or height of a control is measured in pixels. But HTML, as you no doubt are aware of, can also work in percentages. This is a lot faster since the browser completely takes care of sizing.

A neat trick you can use is to alter the size of a child control using percentages – and this way avoid any manual calls to Resize(). It all depends on the situation of course, but if (for example) you have a child control that should always be the total width of its parent – you can in fact directly set the width to 100%.

Since the width and height properties are managed by the RTL you want to avoid altering those. They expect values to be in pixels, so changing these values can result in unexpected side-effects. But you can modify the minimum and maximum size styles without affecting the RTL.

w3_setStyle(FContent.Handle, 'min-width', '100%');
w3_setStyle(FContent.Handle, 'min-height', '100%');

The above code takes a child control (“FContent” in this example) and forces it to cover 100% of the parent’s surface. This will work as long as the actual width property does not exceed 100%. If your container is 200 pixels wide, the above code will work fine unless you manually change width to be 201 or more.

I actually use this quite often, especially when I create any form of lists, listboxes or menu systems. Normally the child items is expected to cover the whole width of the parent (a row in a grid for example), with variable height. In that case I can just adjust the height and leave the width to the browser.

Pre calculate content

Like i mentioned above the most time-consuming tasks are size-changes and creation of elements. Of the two, creation of controls is by far the most time consuming for the document object model.

While the RTL gives you controls that are more or less compatible with Delphi or LCL, they have the downside of being quite heavy. There is a lot of code involved which gives the components great depth – but that depth comes at a cost.

What the browser does really fast however, is to create elements in bulk. Just stop and think about it for a while. If creating elements is so time-consuming – then why does pages appear almost instantly? Well, if you examine the webkit HTML renderer you will discover that parsing and creating elements en-mass is highly optimized. This is sadly not the case for the createElement() function that Smart uses to create components at runtime.

What this means is that its faster to create 1000 child elements as raw HTML and inject the text into the DOM – than it is to create 1000 controls. And not just fast: extremely fast!

But then we face a problem, namely that our RTL does something very useful for us: it keeps track of handles for each element and exposes the functionality as object pascal. If we just dump in a ton of HTML then how are we going to locate, use and manipulate our child elements?

A thin wrapper

This is where I tend to use a thin wrapper. This is a class designed to introduce as little code as possible – and only expose the underlying functionality of the document object model. The DOM is actually quite rich in functions even though they can be intimidating at first.

Here is a thin wrapper I use quite a lot.

unit SmartCL.ThinWrapper;

interface

uses
  System.Types, System.Colors,
  SmartCL.System, SmartCL.Graphics, SmartCL.Components,
  SmartCL.Fonts, SmartCL.Borders;

type

  TElementArray = class external
  protected
    function GetItems(const Index : integer) : TControlHandle; external array;
  public
    property length: integer;
    property items[const Index : integer] : TControlHandle read GetItems; default;
  end;

  TWrappedElement = class
  private
    FHandle:  TControlHandle;
  protected
    function  ValueToInt(const Value: variant): integer;
    function  IntToPixels(const Value: integer): string;
    function  GetColor: TColor;
    procedure SetColor(const NewColor: TColor);
  public

    class function GetElementById(const Parent: TControlHandle;
              const ChildId: string): TControlHandle; overload;

    class function GetElementsByType(const Parent: TControlHandle;
              TagName: string;
              var Items: array of TControlHandle): boolean; overload;

  public
    property  Handle: TControlHandle read FHandle;

    property  Id: string
              read  ( FHandle.id )
              write ( Fhandle.id := Value );

    property  Title: string
              read  ( FHandle.title )
              write ( FHandle.title := Value );

    // Offset
    property  OffsetLeft: integer
              read  (ValueToInt(FHandle.offsetLeft))
              write (FHandle.offsetLeft := IntToPixels(Value));

    property  OffsetTop: integer
              read  (ValueToInt(FHandle.offsetTop))
              write (FHandle.offsetTop := IntToPixels(Value));

    property  OffsetWidth: integer
              read  (ValueToInt(FHandle.offsetWidth))
              write (FHandle.offsetWidth := IntToPixels(Value));

    property  OffsetHeight: integer
              read  (ValueToInt(FHandle.offsetHeight))
              write (FHandle.offsetHeight := IntToPixels(Value));

    // Scroll
    property  ScrollLeft: integer
              read  (ValueToInt(FHandle.scrollLeft))
              write (FHandle.scrollLeft := IntToPixels(Value));

    property  ScrollTop: integer
              read  (ValueToInt(FHandle.scrollTop))
              write (FHandle.scrollTop := IntToPixels(Value));

    property  ScrollWidth: integer
              read  (ValueToInt(FHandle.scrollWidth))
              write (FHandle.scrollWidth := IntToPixels(Value));

    property  ScrollHeight: integer
              read  (ValueToInt(FHandle.scrollHeight))
              write (FHandle.scrollHeight := IntToPixels(Value));

    // Client
    property  clientLeft: integer
              read  (ValueToInt(FHandle.clientLeft))
              write (FHandle.clientLeft := IntToPixels(Value));

    property  clientTop: integer
              read  (ValueToInt(FHandle.clientTop))
              write (FHandle.clientTop := IntToPixels(Value));

    property  clientWidth: integer
              read  (ValueToInt(FHandle.clientWidth))
              write (FHandle.clientWidth := IntToPixels(Value));

    property  clientHeight: integer
              read  (ValueToInt(FHandle.clientHeight))
              write (FHandle.clientHeight := IntToPixels(Value));

    // Node
    property  NodeName: string read (FHandle.nodeName);
    property  NodeType: string read (FHandle.nodeType);
    property  NodeValue: variant
              read  (FHandle.nodeValue)
              write (FHandle.nodeValue := Value);

    property  Children: TElementArray read ( TElementArray(FHandle.children) );

    property  InnerHTML: string
              read  ( Handle.innerHTML )
              write ( Handle.innerHTML := Value);

    property  InnerText: string
              read  ( Handle.innerText )
              write ( Handle.innerText := Value);

    property  Color: TColor read GetColor write SetColor;

    function  Wrap(const Handle: TControlHandle): TWrappedElement;

    function  GetElementById(const Id: string): TControlHandle; overload;

    function  GetElementsByType(const TagName: string;
              var Items: array of TControlHandle): boolean; overload;

    procedure Click;

    constructor Create(TagId: string); overload; virtual;
    constructor Create(Parent: TControlHandle; TagId: string); overload; virtual;
    constructor Create(TagHandle: TControlHandle); overload; virtual;
  end;

implementation

//#############################################################################
// TWrappedElement
//#############################################################################

constructor TWrappedElement.Create(TagId: String);
begin
  inherited Create;
  FHandle := BrowserAPI.Body.GetChildById(TagId);
end;

constructor TWrappedElement.Create(TagHandle: TControlHandle);
begin
  inherited Create;
  FHandle := TagHandle;
end;

constructor TWrappedElement.Create(Parent: TControlHandle; TagId: string);
begin
  inherited Create;
  FHandle := Parent.GetChildById(TagId);
end;

procedure TWrappedElement.Click;
begin
  FHandle.click();
end;

function TWrappedElement.Wrap(const Handle: TControlHandle): TWrappedElement;
begin
  result := TWrappedElement.Create( Handle );
end;

function TWrappedElement.IntToPixels(const Value: integer): string;
begin
  result := Value.ToString() + 'px';
end;

function TWrappedElement.ValueToInt(const Value: variant): integer;
begin
  asm
    if (@Value)
    {
      if (typeof(@Value) === "number") {
        @result = @Value
      } else {
        if (typeof(@Value) === "string")
        {
          @Value = parseInt(@Value);
          if (!isNaN(@Value))
            @result = @Value;
        }
      }
    } else {
      @result = 0;
    }
  end;
end;

function TWrappedElement.GetColor: TColor;
begin
  if (FHandle) then
  result := StrToColor( w3_getStyleAsStr(FHandle, 'backgroundColor') ) else
  result := clNone;
end;

procedure TWrappedElement.SetColor(const NewColor: TColor);
begin
  if (FHandle) then
  begin
    if NewColor <> clNone then
    FHandle.style.backgroundColor := ColorToWebStr(NewColor) else
    FHandle.style.backgroundColor := 'transparent';
  end;
end;

class function TWrappedElement.GetElementsByType(const Parent: TControlHandle;
         TagName: string;
         var Items: array of TControlHandle): boolean;
begin
  if (parent) then
  begin
    asm
      @items = (@Parent).getElementsByTagName(@TagName);
    end;
  end else
  Items.Clear();
  result := Items.Count > 0;
end;

function TWrappedElement.GetElementsByType(const TagName: string;
         var Items: array of TControlHandle): boolean;
begin
  if (FHandle) then
  begin
    asm
      @items = (@FHandle).getElementsByTagName(@TagName);
    end;
  end else
  Items.Clear();
  result := Items.Count > 0;
end;

class function TWrappedElement.GetElementById(const Parent: TControlHandle;
         const ChildId: string): TControlHandle;
begin
  if (Parent) then
  begin
    result := Parent.getElementById(ChildId);
    if not (result) then
      result := Parent.getElementById( ChildId.ToLower().Trim() );
  end else
  result := unassigned;
end;

function TWrappedElement.GetElementById(const Id: string): TControlHandle;
begin
  if Id.Length > 0 then
  begin
    result := FHandle.getElementById(Id);
    if not (result) then
      result := FHandle.getElementById( Id.ToLower().Trim() );
  end else
  result := unassigned;
end;

end.

If you are pondering how on earth is that going to help, here is how it works.

All Smart controls are simply JavaScript code designed to manage a real, underlying HTML element. The default element type TW3CustomControl creates is DIV. Controls like TW3TextBox overrides the function that creates this element and replace that with an input element instead. And other controls do the same.

So just because something is not visible to a fully blown TW3CustomControl, doesn’t mean it’s not there. And by using the wrapper we can easily hook rouge or non classified html elements without creating them.

Let’s for example say you inject a bit of HTML into a panel, like this:

procedure TForm1.MakeHTML;
var
  x: integer;
  LHtml: string;
begin
  // make X number of div items
  for x:=1 to 100 do
  begin
    LHtml += Format('
<div id="obj%d">Item #%d</div>
', [x,x]);
  end;

  // inject into our panel
  W3Panel1.InnerHTML := LHtml;
end;

To access one of these DIV elements (which we now have 100 of), we can use our thin wrapper to make it programatically easier:

procedure TForm1.MakeHTML;
var
  x: integer;
  LHtml: string;
  LItem: TWrappedElement;
begin
  // make X number of div items
  for x:=1 to 100 do
  begin
    LHtml += Format('
<div id="obj%d">Item #%d</div>
', [x,x]);
  end;

  // inject into our panel
  W3Panel1.InnerHTML := LHtml;

  // Create wrapper for item #12
  // We pass the handle to the parent (which is the form)
  // and the name. The class will find the element
  LItem := TWrappedElement.Create(W3Panel1.Handle, 'obj12');
end;

Voila! LItem can now be used just like any other Smart control. But remember that this is a thin wrapper, meaning that you are actually digging into the document object model directly.

I should mention a few words about the browser’s calculated stylesheet. One of the things you are going to notice is that width and height is not always going to be correct. This is not due to our code, but because the browser always regards your values as proposals, not absolutes.

So even if you set a control to say, 400px in width – depending on the situation the browser may find it more suitable to set 389px instead. And if you make the mistake of reading it back from the stylesheet – it will report 400px. But this is because the stylesheet is regarded as a proposal.

What you need to do is to write to the stylesheet, but read from the calculated styles. This is why the Smart RTL calls the w3_getStyleAsInt() function when reading these values.

Just so you know in case you think the wrapper is reporting wrong values. It’s not. The browser just works differently because of CSS. Cascading means that styles will merge together, so a button can have 50 gradients assigned to it – and they will all be amalgamated into one and represented in the calculated stylesheet as a single whole.

Other tricks

I could go on for days but I think these two will be handy for now. I would urge you to examine and learn how to use the GetElementByType() and GetElementById() so you get familiar with navigating the DOM like that. GetElementByType() is really cool – it allows you to extract a list of items based on type.

So to get all the DIV elements you can simply do:

var LDivs := LItem.GetElementByType(W3Panel1.Handle, 'div');

I should also mention that a thin-wrapper is only as good as you make it. The code above was never made to do the same as TW3CustomControl can. There is no cosy class wrapping of fonts, constraints, borders or reading of style properties. To enjoy these high-level RTL functions you will have to stick to the RTL.

But, for cases where you want to pre-generate relatively simple elements, like rows in a listbox or some form of menu items – a thin wrapper can mean the difference between useless and brilliant.

Oh, you might be interested in a “Styles” wrapper. I have only fleshed out a handfull of properties, but it does make low-level access a lot easier. If you finish it PM me. The documentation can be found here: http://www.w3schools.com/jsref/dom_obj_style.asp

  TWrappedStyle = class external
  public
    // stretch|center|flex-start|flex-end|space-between|space-around|initial|inherit
    property  alignContent: string;

    // stretch|center|flex-start|flex-end|baseline|initial|inherit
    property  alignItems: string;

    // auto|stretch|center|flex-start|flex-end|baseline|initial|inherit
    property  alignSelf: string;

    // http://www.w3schools.com/jsref/prop_style_animation.asp
    property  animation: string;

    // time|initial|inherit
    property  animationDelay: string;

    // normal|reverse|alternate|alternate-reverse|initial|inherit
    property  animationDirection: string;

    // time|initial|inherit
    property  animationDuration: string;

    // none|forwards|backwards|both|initial|inherit
    property  animationFillMode: string;

    // number|infinite|initial|inherit
    property  animationIterationCount: string;

    // none|keyframename|initial|inherit
    property  animationName: string;

    // linear|ease|ease-in|ease-out|cubic-bezier(n, n, n, n)|initial|inherit
    property  animationTimingFunction: string;

    // running|paused|initial|inherit
    property  animationPlayState: string;

    // color image repeat attachment position size origin clip|initial|inherit
    property background: string;

    // scroll|fixed|local|initial|inherit
    property backgroundAttachment: string;

    // color|transparent|initial|inherit
    property backgroundColor: string;

    // url('URL')|none|initial|inherit
    property backgroundImage: string;

    // http://www.w3schools.com/jsref/prop_style_backgroundposition.asp
    property backgroundPosition: string;

    // repeat|repeat-x|repeat-y|no-repeat|initial|inherit
    property backgroundRepeat: string;

    // border-box|padding-box|content-box|initial|inherit
    property backgroundClip: string;

    // padding-box|border-box|content-box|initial|inherit
    property backgroundOrigin: string;
  end;

Well, more and more tricks will surface, so stay tuned guys!

Advertisements
  1. February 2, 2017 at 6:19 pm

    Jon,

    I have created the “Smart Bank Simulating ATM machine”, with withdrawal / deposit / transfer / bill payment / extract / balance options enabled. Test PIN=1234

    See the Live Project at: rawgit.com/smartpascal/smartms/master/games/ProjSmartBank/www/index.html

    This ATM project, for instance, the file size (main.js + sms.js) is around 64K!

    I’m not using neither the SmartCL framework nor visual designer 🙂
    SmartMS is power enough. I’ll prefer to hand written pure CSS/HTML5 code.

    a) for DOM manipulation in SMS:
    I’ve created a small wrapper to have my own custom DOM – it is a small DOM library that utilizes most edge and high-performance methods for DOM manipulation. You don’t need to learn something new, its usage is very simple because it has the same syntax as well known jQuery library with support of the most popular and widely used methods and jQuery-like chaining. This small lib (sms.js is less than 20k!).

    b) It would be nice if:
    b.1. “a lite visual designer” where we could drag n’ drop a visual control / widget on the canvas, and just insert it raw HTML/CSS;

    b.2. for the lite visual designer, with HTML5 code completion. Using this approach, a lightweight route have to be implemented to navigate through the forms;

    b.3. we could “re-scale” the visual widgets on the form automatically (without the layout manager). e.g. in the ATM machine, If you resize the browser, all controls are resizable – re-scaled; I’m using the the CSS3 scale attribute.

  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: