Smart Pascal: Cool tricks for better code
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!
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.