Вы находитесь на странице: 1из 240

Using Elevate Web Builder 2 Web and Mobile Apps

2 nd Edition

By Erick Engelke erickengelke@gmail.com Kitchener, Canada Copyright © 2016 Erick Engelke

All rights reserved. No parts of this book may be reproduced, stored in a retrieval system or transmitted in any form or by any means without prior written permission from the author, except in brief quotations embedded in critical articles or reviews.

Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information in this book is sold without warranty, either express or implied. Neither the author, the publisher or its affiliates, dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book.

The author has endeavored to provide trademark information about all the companies and products mentioned in this book by the use of capitals. However, the author and publisher cannot guaranty the accuracy of this information.

Published August 2016 ISBN-13: ISBN-10:

Using Elevate Web Builder

Web and Mobile Apps

2 nd Edition

Erick Engelke This book is dedicated to my wife, Rosemary.

I am very grateful for the keen proofreading and eternal patience of Tim Young of Elevate. In 1995 there was a new product called Delphi which revolutionized Windows development for many programmers. It introduced the Object Pascal language and the Visual Component Library (VCL) which greatly simplified writing Windows applications, and database applications in particular.

The idea was called RAD for Rapid Application Development and it benefitted small shops and individual programmers because they could quickly produce applications which competed with big firms, and it helped big firms because they could do more… faster.

In the years since, Delphi has been reworked to support native Linux, Android, IOS and Macintosh applications. Its open source cousin: Free Pascal with Lazarus have supported these and other platforms too. Their motto is ‘write once, compile anywhere’.

Several groups have translated Delphi’s basic architecture to Web applications which generate JavaScript as the run time language. The two I have used are Smart Mobile Studio SMS and Elevate Web Builder or EWB. They are not identical to Delphi, but still so similar one feels eerily at home

This book focuses on EWB. I think it marks an epoch in Web development that ushers in an age of reasonable tools for fast and reliable production of Web pages.

If you have a Delphi background, you will be instantly familiar with much of EWB. But even newcomers will find it inviting. By basing the product on the ideas of Delphi, the Elevate developers have inherited a lot of well-tuned principles. Pascal is a wonderful language. It has the power of C and thus has been used to write whole operating systems, but unlike C and its derivatives (C++, JavaScript, PHP), it has very strong type checking. I believe the loose typechecking of these other languages leads to many bugs and run-time errors because the execution does not match the programmer’s intent.

With Pascal, the compiler knows exactly how you have defined a variable or constant or object, and it uses it only in that manner. This not only makes your execution more predictable, it means most programming errors are found at compile time and not when your users go to execute the code. It also means the compiler can use efficient strategies, and that leads to fast execution.

There is an excellent reference to the general Object Pascal language written by Marco Cantu. It makes a great read and a reference text, and it is applicable to EWB, Free Pascal, Delphi and others.

EWB is a product of Elevate Software in the state of New York, USA. They started out in 1998 with database technologies which they sell world-wide. In 2011 recently they branched into web development with EWB.

I found they have excellent customer service with product support being provided by the actual developers as well as many satisfied customers. They test their products rigorously using automated tests, so you are not likely to discover a

bug. EWB is written in Delphi and bears a strong resemblance to the Delphi Integrated Development Environment. There is also a command line version of the compiler for those who chose not to use the IDE.

The EWB IDE includes:

• Code editor

• Object Inspector for the component library

• Project Manager to keep track of source code

• Compiler

• Optional code obfuscator / compressor

• Web server and database server

• Online help

It currently lacks a debugger, however by default the code generated is attractive JavaScript which can be debugged by your favorite web browser.

When you are ready to ship your completed application, you can chose to compress the source code. This converts it to minimalist JavaScript functions, variable names and spacing to compress the size of the output files, and effectively obfuscates it from easy reading or modification.

For a typical project, compressed and obfuscated HTMLand JS files typically hover around 1 MB total, which is the size of a typical Jpeg file and can easily be loaded by phones and other network devices on cellular, Wifi and other Internet options.

I will focus on the most popular applications, which are Form based applications. You can create other pages without using the libraries, but I feel that is best done as an advanced project, if at all. The libraries benefit you with the wisdom and experience of several years of development and refinement. And the cost (in size and performance) is low. The reader is assumed to know some programming language like Pascal, C, PHP, C++, JavaScript or similar.

This is not a definitive introduction. There are many texts which spend hundreds or thousands of pages doing justice to that topic. Instead, this is intended to get a typical programmer up-to-speed in EWB’s implementation of Object Pascal.

Pascal is a case-insensitive language. You can use any combination of capitalization at any time, so: myObject is the same as MyObject. Just try to follow some convention or your code will look ugly.

EWB uses Unicode for all elements. It is capable of creating applications for any Unicode language. However, EWB cannot support some Eastern languages in the Code Editor.

For comments, EWB supports C++like // single line comments and { } for multiline

comments.

There are a few variable types:

• Integer – 52 bit signed integer

• Boolean – True or False

• Double – 64 bit floating point, maximum precision of 16 digits

• Char (Character)

• String

• DateTime – milliseconds since epoch midnight Jan 1, 1970 UTC

• Array of …

• Object

• Variant – raw native JavaScript type, not typed, can be cast to other types Strings are immutable, you cannot change characters by saying s[3] = ‘3’ for a variable s : string.

Delphi allows dynamic and static range arrays. EWB only supports dynamic arrays. They are always zero based, and the size (length) of the array must be set in code, just like dynamic arrays in Delphi.

In the declaration you specify a type but not a range. Var myTimes : array of DateTime; To set the length of an array, use SetLength. SetLength( myTimes, 10 ); To get the length of an array, use GetLength

ShowMessage( GetLength( myTimes )); Once you have defined the length of the array, feel free to reference the 0 based elements. myTimes[3] := now; Later we will see how you can implement a form of associate arrays, but they are implemented with TStrings and not arrays:

Eg option[‘name’] := ‘Harry’;

The standard operators are:

And, Not, Or , Xor - Boolean and arithmetic * / div - + mod shl shr - arithmetic

a mod b is like a % b in C++.

Shl an shr are bitwise shift left and right.

Div is integer divide, whereas / is floating point divide.

There is also a + string operator which concatenates strings.

Rather than specify the order of operations, I will give you advice that works with EVERY language, so you will never make a mistake again:

Use parentheses to tell the compiler the order you wish! Object Pascal is an object-oriented language (OOP) gains its benefits from Objects.

If you do not come from an OOP backgroup, objects are like C structures but on steroids. Not only do they contain data, they do so much more. They contain methods, which are functions and procedures that implement functionality for the object.

Objects offer:

Encapsulation: The object is self-aware. It can contain data that only the objects methods can access, and it can present that data in special ways. For example, it can act like an array, but when you access an element, one of the methods is called to perform an action such as update a cell in an on-screen table. So the upstream program is simplified because it just appears to access a read-only, read/write or write-only attribute, but in reality it is calling code.

Inheritance Classes descend from other classes and inherit their strengths, while adding or replacing functionality. This creates a class hierarchy with no limit on its depth.

Before we get much farther, let’s give some examples:

Type TMainForm = class( TForm ) // visual form, knows how to display itself TUser = class( TPersistent ) // will be user instances, can read/write itself from storage TFileName = class( TObject ) // we will define things like name, date, subdir… Every visual and non-visual component in the library is based on objects: buttons, menus, text boxes, textual captions, timers, etc.

EWB only supports single inheritance, meaning classes have a single parent, which can also be a descendants of still other classes. This differs from some languages which permit classes to inherit from one or more direct classes.

Much has been written about the argument of whether single inheritance is better or worse. I’ll simple leave it as a fact that many great programs have been written with single- inheritance languages and that it is simpler to understand and implement.

If no inheritance is specified, the class will inherit from TObject. Classes can have four

types of members:

• Private class members – accessible only to the class itself

• Protected class members

• Public class members

• Published class members

For starting developers, you will use three class types:

The protected section of a form is not used for anything in the IDE - the IDE manipulates a special section that is internally called the designer public section. You'll notice that the IDE puts things in the section at the top with no label - that's the designer public section. It has the same scope behavior as public.

Private will be for variables and methods you create which only your class can use. By default, put all variables and methods into private.

Public is the designation for all variables and methods you wish to expose to other classes and procedures. Move variables and methods to Public when you realize they need to be accessed externally.

Object methods can be simple functions and procedures, they can also be declared as virtual, abstract and override. Virtual methods can be overridden in descendant classes.

Abstract methods indicate the virtual method is NOT implemented in the base class, but must be implemented by descendant classes. Your application will not compile until you implement the functionality – which in turn reduces the problem of not implementing all the requirements.

Inherited methods can augment the functionality in a base class, and can even call the base class function. Overloaded methods have multiple declarations. Such as add( x, y : integer) :

integer; add( x, y : double ):double; overload; Note that EWB, unlike several other Object Pascal variants, does not require you to use the overload keyword.

Constructors and Destructors are special methods that handle the process of creating and destroying instances. In Delphi the constructors and destructors allocate and free memory – which is not necessary in our JavaScript environment. However, they are also useful for initializing variables and cleaning up afterwards.

Typically, you will not create files like C’s Header files which are separate from the source code (.H files in the C example). Instead, a declaration section at the top of the Pascal file does a similar thing.

Files look like:

unit Main; interface // types, variables, objects and methods used // and implemented in this file

implementation // the rest of the file is private to this file end.

Pascal is a single-pass language meaning the compiler only reads through the source once, whereas C is typically a two to four pass language. As a result, Pascal compiles much faster, but it must be organized more carefully.

If you define, say, TClassDog, and it descends from TClassMammal, then you must define TClassMammal before you define TClassDOG. Functions and procedures may be passed variables, but these variables only have scope inside the method called.

These methods can also reference variables and methods specified in the current class. Pascal has the love-it-or-hate-it WITH keyword, where you can specify: WITH InstanceDog do ….

so you do not have to specify the variable name on each line. There are many arguments for and against this keyword. I’ll leave it to you, but remind you that WITH overrides the normal scope of any local or class variables/properties/methods.

Most modern programming languages let you cast from one variable type to another.

Source Type Valid Target Types Integer Integer Boolean Double DateTime Enumeration Double Double String String Char Char Char

String DateTime DateTime Integer Boolean Boolean Integer Enumeration Enumeration Integer Class Instance Any same class or ancestor class type

We will use an example later where we iterate through visual components on the screen and only react to TButtons by using is to query the class type. is does not simply look at the current class type, it works for all inherited types too.

var

i

: integer;

c

: TComponent;

but : TBbutton;

begin

for i := 0 to Component.Count -1 do begin c = Components[i]; if ( c is TButton ) then begin

but := TButton( c );

but.Caption := ‘a button’; end;

end;

Good programming deals with exceptions you never expected, and those that you did anticipate too. In Object Pascal, the keywords are try, except, on exception, and raise.

For example, if the user tries to enter negative money, you should raise an exception.

If (

a < 0 ) then

Raise Exception.Create(‘do not use negative money’); The component library and the run-time raise exceptions whenever necessary. Use

try

try a := StrToInt( edit1.Text );

except

except

to handle exceptions.

ShowMessage(‘Could not convert ‘+edit1.Text+‘ to a number’);

end;

Or you can access the exception class variable to print out the general exception

information.

try

a := StrToInt( edit1.Text );

except

on E: Exception do ShowMessage(‘Error:’+E.message); end;

In the exception handler you can re raise the exception for the next stacked exception handler. Object Pascal also implements a finalize section to implement code that always executes whether an exception is raised or not.

try

finally .// this code is always called end;

Visual applications have a default handler if you do not handle the exception. It normally prints a generic message with the exception text. You can define a TApplication.OnError event handler which can do other things, such as give your phone support number.

EWB is not an island to itself. You may also wish to call external JavaScript libraries or access variables in the Document Object Model (DOM), etc. Remember that JavaScript is case sensitive, so you must be careful with casing of references. However, only the developer of external interfaces must be concerned with case sensitivity of JavaScript, so application code which references it from EWB Pascal is free to use any capitalization you choose. EWB will emit the JavaScript using the case of the external interface definition and not the case of the references to it.

The most common example of external interfaces is to access the Window’s URL. Here we see how to switch to Google’s home page. uses webdom; s := ‘http://google.com’; window.location.replace( s ); Like Delphi, EWB uses a message queue to sequence operations. Async is used to schedule future code execution. We have a whole chapter on this later.

One of the most used object classes is TStrings and its relative TStringList which both encapsulate lists of strings. After you create() a TStringList, you can add strings with member functions add(), delete(). You can access individual lines as though it were an array of strings. And you can deal with the entire list as though it were a single string using the .Text attribute; You can sort() and find() strings.

If you assign strings with a NameValueSeparator (defaults to =), you can use it as though it were an associative array.

Eg.

var sl : tstringlist; name : string; age : integer;

begin sl := tstringlist.Create; sl.add(‘Name=Erick’); sl.add(‘Password=Friday’); sl.Values[ ‘Age’] := ‘49’; name := sl.Values[‘Name’] age := IntToStr( sl.Values[‘Age’]);

end;

age := IntToStr( sl.Values[‘Age’]); end; The visual form designer is comprised of the form, the

The visual form designer is comprised of the form, the tabbed units editor, and the components one can select.

The object inspector show properties and events related to a selected component. You can select something visual, like a button, something nonvisual, or even the form itself to adjust its properties and events.

A messages window at the bottom shows messages such as the compiled size, or errors if a

compiler error is being reported to you.

If you select Project | Options | Application, you can set the title where appears on the

browser’s header. This step is necessary to have a professional looking page.

We’ll start with a simple application and look at how it works. Start EWB and press the New Project button, or File | New… | Project. Select Visual Project with Form Support. When asked, pick TForm as the ancestor Form.

So we have a blank form. As a matter of practice, save the files right away and run the form by pressing F9 or Run | Run. It will wish to save the form and the project, put them in a new subdirectory to keep things organized.

EWB will then show the empty form. Not very interesting yet, just a blank form. Press the close X button above the blank page, this will return to edit mode.

From the Visual Objects collection on the upper right hand of the screen, pick the T icon and then tap on the empty form to place it anywhere on the form. This is a text label, it displays text. Being the first TLabel on your form, it will be called Label1. TEdit is the class name, Label1 is the instance name.

Then move your mouse over the other icons until you find TButton, which is a button Place

it on the form too: Button1.

Double click on the button you just placed. This will open the code editor on the button’s OnClick event handler, the method called when one clicks on a button.

Add the following code:

procedure TForm1.Button1Click(Sender: TObject); begin Label1.Caption := 'hello'; end; Note, you can toggle back and forth between the code editor and the visual form designer by pressing F12.

This is a program. Press F9 to Run it. Both the label and button are visible, but they say:

Caption, which is the default text for the Caption property. But when you press the button, it changes the Label1’s Caption property to ‘hello’, and through encapsulation, that updates the screen automatically.

You’ve written your first program. Now let’s make it more interactive. Add a TEdit, which will be Edit1. It accepts user input.

procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

i := Edit1.Text;

Label1.Caption := i + 1;

end;

This will not compile when you press F9, because i is an Integer type and Edit1.Text (which is the user input) is a string and not all strings can be turned into numbers. And it would also complain because integers cannot be converted to strings in the next line

To fix this, change the code to:

procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

i := StrToInt(Edit1.Text);

Label1.Caption := IntToStr(i+1);

Now it compiles and runs. Enter a number and we will show the following integer. But you could still enter a text string like George instead of a number, and you will get an error message: Invalid Integer Value and a line number like 619.

Let’s see the actual code. Open the output subdirectory from your source subdirectory and you will find both an HTMLand a JS (JavaScript) file. The error refers to the StrToInt() function which is implemented not in Pascal but in JavaScript.

function strtoint($0)

{

if ($0 != "" && $0[0] == "0" && ($0[1] == "x" || $0[1] == "X")) var $1 = parseInt($0,16); else var $1 = parseInt($0,10); if (!isNaN($1)) return $1;

else

throw "Invalid integer value";

};

Yes, your code is converted to JavaScript and executed by the JavaScript engine in your web browser.

A nice way to deal with this is using Try/Except exception blocks. procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

try

i := StrToInt(Edit1.Text);

Label1.Caption := IntToStr(i+1);

except

Label1.Caption := 'That was not a number';

When an error arises, the Try/Exception block jumps to the Exception code and executes it. If there are no exceptions, the Exception block never executes. The complete source code to our form is:

unit Unit1;

interface

uses WebCore, WebUI, WebForms, WebCtrls, WebLabels, WebBtns,

WebEdits;

type

TForm1 = class(TForm) Label1: TLabel; Button1: TButton; Edit1: TEdit; procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

try

i := StrToInt(Edit1.Text);

Label1.Caption := IntToStr(i+1);

except Label1.Caption := 'That was not a number'; end.

Let’s clean it up a bit. Going back to the Visual Form Designer, look to the left of the screen and you will find the Object Inspector. Click once on the Label1 and the Object Inspector will show its default values. Caption is set to Caption, change this to enter a number. This changes the startup value of the Caption.

Click once on the button1 and it its Caption to Go. Now your application looks better. You can drag the visual controls around the screen to make the position nicely.

There is only one other piece of code to write, and it is mostly hidden and usually automatically written, it’s the project Application source. Go to Project | View Source and you will find something like:

project Project1; contains Unit1; uses WebForms, WebCtrls;

begin Application.Title := ''; Application.LoadProgress := False; Application.CreateForm(TForm1); Application.Run;

End

We will explore this code later, but for now the important thing is that it creates TForm1/Form1 (our form had a name), and then it runs the application, which is all the open forms, of which there was simply Form1.

We will change the application again to show how live it is. Go to the System tab on the Visual Form Designer and place a TTimer on the form. The TTimer is a non-visual component meaning it doesn’t display anything. Double click on it and enter the following code it its Timer1Timer event handler.

procedure TForm1.Timer1Timer(Sender: TObject); begin Label1.Caption := DateTimeToStr( now ); end; This timer event handler is called once every second and it updates the display text to say the current time. Press F9 to run.

Using the Object Inspector you can change the Interval parameter from 1000 ms to 10000 for every 10 seconds. The beauty of EWB is that you write clean simple functions instead of messy JavaScript which is error prone.

Our last change shows a useful tool for displaying answers and can help tremendously in debugging: using ShowMessage() which pops up a message on the users screen

In our previous example, the time would overwrite the user’s results, and this is not ideal. So change the code for the Button1 to:

procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

try

i := StrToInt(Edit1.Text);

ShowMessage( IntToStr(i+1 ), ‘debug’);

except

ShowMessage( 'That was not a number',’debug’); end;

end;

Run your app again. Now input generates a Modal message box that blocks all input until the user presses Ok. Unlike in Windows or even alert() in JavaScript, this box does not halt program flow, the program keeps executing but input is blocked. This behavior is due to the Message Queue which we will examine later. There are some things to note. EWB applications are SPA or Single Page Applications, like Gmail, Facebook, etc. This means you load a page once and do everything without loading subsequent HTML/JavaScript programs from the network. This makes the experience fast and responsive to users. It feels like a Windows program. And now web apps are as easy to write as Delphi Windows programs.

Your application can access the network, and we will do plenty of that in time, but typically it will do AJAX (Asynchronous JavaScript And XML) calls to a server and update itself as needed in the background. Only the simplest apps have a single form, real apps usually have multiple forms that either appear simultaneously or in some sequence.

With EWB you can have as many open forms as you please (and your JavaScript run-times allow), easily in the hundreds if not more.

Each form type is written in a separate source file, however, you may create several instances of the same form type, in which case all instances would be based on the same file.

Simply take your single-form app as we had in the previous chapter and add more forms by pressing File | New | Form and then selecting the base class for your Form. Make this one of type TDialog

Press F9 to run, and you will see there are two forms created, one will be over the top of the other. You can grab the dialog box by its top and drag it off of your old Form1.

Our problem is that Form2 is being create and displayed along with Form1 by the Application’s source. So fix it by editing Project | View Source

project Project1; contains Unit1, Unit2; uses WebForms, WebCtrls;

begin

Application.Title := ''; Application.LoadProgress := False;

Application.CreateForm(TForm1);

// comment out Application.CreateForm(TForm2); Application.Run; end.

Run the program and we only see the one form. Place a TLabel on Form2. It’s time to get into the good habit of always changing your components to useful names, so go to the object inspector and change its name property to: lbUserAnswer which stands for Label User Answer.

Then change the Form1’s button click handler to procedure TForm1.Button1Click(Sender: TObject); var

i : integer;

begin

if not Assigned( Form2 ) then Form2 := TForm2.Create( Application );

try

i := StrToInt(Edit1.Text);

Form2.lbUserAnser.Caption := IntToStr( i + 1 ); Form2.Show;

except

ShowMessage( 'That was not a number'); end;

end;

and after the Implementation keyword add: uses unit2;

which indicates the module references things in the Interface section of the Unit2 file which has our second form. Press F9 and your program will display its output in the new output

window.

The Form2 variable is defined as type TForm2 in Unit2, which we included in the uses statement.

We use if not Assigned( Form2) because by default the object/TForm1 is not assigned any memory – well, it was assigned, but we commented that out in the Project Source, so it must be created manually.

Form2 := TForm2.Create(Application) creates the object and sets all the parameters to their defaults, including all the parameters of all visual and nonvisual components of the form. Form2.Show shows the form, because merely creating and modifying a form isn’t enough, you have to show it too.

The main points of this exercise are that:

- EWB generates a single web page (SWP) no matter how many forms are visible and how many buttons one presses

- Forms are each designed in a separate unit

- Forms are auto-created in the Application source, you must comment them out if you do not want them created and displayed at start time.

- You must instantiate each form variable with

TFormN.Create(Application).

- The only global variables we know about (so far) are Application, Form1 and Form2.

The rest are objects within these object classes. The next few chapters introduce you to classes available in the Visual Form Designer. The online help is a complete and updated reference; my goal here is to describe the controls and list their most noteworthy parameters.

Among the classes you can drop in a form, the most common are from the Standard group. They are almost all text-based, though some have optional icons.

The label is usually text, numbers, and other things which are being presented. These are cornerstones of your visual form. You can update the Caption at any time to update the display.

Like most controls, it is possible to edit the many object attributes. Some commonly changed ones are:

• AutoSize – typically leave as true, otherwise you must adjust the size if you reassign the Caption

• Caption – the text to display

• Font parameters, which include Color, GenericFamily and Size

• Background, which includes Fill… Color to highlight a text field

• Format … Wrap to turn on line wrapping

• Tag – which can hold an arbitrary number, useful for holding any data number you want

• OnClick() event handler

handler.

• OnSize() is called when the size changes for any reason. This is a special label which has

a balloon holding the text and an optional icon property which specify one of a few standard icons. You may choose to make it invisible by set Visible property to False at design time, then set Visible := True to make it appear. Setting the Animations property you can make it pop up nicely.

yes labels can receive events, and OnDblClick() double click

Yet another label extension. This one displays an alert at the top of the window. AllowClose determines whether an X close button appears to close the error message or not. Orientation determines whether a left sided or right sided X is used to close the error message. It is common to create the TAlertLabel at design time but only make its Visible property true when necessary, such as when the network request fails or a password is incorrect.

The TButton is another workhorse.

• Its OnClick() handler is called when the user presses the button

• Its OnMouseEnter() / OnMouseExit() can be used to detect a passing

mouse for helping the user with a balloon text or some other notice.

• Icon can specify an icon

The TIconButton specifies a button with an icon (glyph) and no text or border.

• Like most controls, you can specify a different cursor for when the mouse pointer is over the control

• Sometimes you may want to fire events as long as a button is pressed, like in a video game. Set the RepeatClick to True and

RepeatClickInterval to a value expressed in milliseconds, Use TDialogButton with a TDialogForm to set the modal result of a form.

• If you set ModalCancel to True, this button will be called when the user presses Esc.

• If you set ModalEnter to True, this button will be called when the user pressed

Enter/Return.

• You must set ModalResult to the value which will be returned from the modal dialog box.

TCheckBox encapsulates the CheckBox functionality.

• Set the Caption to the text string you wish to display beside the checkbox

• SelectionState can have three states that can be read or written o ssSelected – an X appears

o

ssUnselected – no X appears

o

and ssIndeterminate – a dash appears to indicate we don’t know

To act on a change to the state there are several options

o

onClick – whenever it is clicked

o

onChange – only when the value changes

o

onMouseDown/onMouseUp

• setting the SelectionState in software will also invoke events, this can be confusing because you might have assumed only user activity would fire the events.

• There is a ReadOnly property which means the TCheckBox can be used for output if you wish, such as upon completion of a task. TRadioButton work together in a group like the push buttons on a car stereo. o

SelectionState is similar to TCheckBox, except ssInderterminate does not show anything

o When using with a database, ValueSelected is the value returned when the particular TRadioButton is selected.

o All TRadioButtons on the same page are in the default group. o Use with TGroupPanel to specify multiple groups of TCheckBoxes

TEdit is the Edit box.

• Specify ReadOnly = True if you wish it to be output only, no writing

• Value contains the entry of the user, or the preset value

• MaxLength is the maximum number of characters allowed in a field.

Use this!

• InputType can be set to various values. It does not restrict the input but on many devices it determines what type of virtual keyboard is displayed.

o

tiNumber

o

tiEmail

o

tiNone

o

tiURL

It is possible to modify keystrokes entered by intercepting the

OnKeyPress or OnKeyDown. For example, you could make a sound if a user entered a letter in a number field.

• It is common to hook the OnKeyUp() event handler to process

keystrokes after the Edit has placed them into the Value buffer.

This control is used just like TEdit, but it displays dots instead of characters. MultiLineEdit is a self-explanatory name.

• The text entry is stored as a TStrings property called Lines. You can specify Lines[0],

Lines[1], etc. up to Lines.Count -1

• You can also read or set the whole string with Lines.Text

• MaxLength is the number of characters allowed

• ReadOnly is useful for transforming this to a text output box

• ScrollBars can be defined as sbHorizontal, sbVertical, sbNone or sbBoth. Scroll bars are only displayed if necessary.

The TCalendar shows a complete calendar.

• The default View is cvMonth, you can also set it to cvCentury, cvDecade, and cvYear

• Read or write the date by editing the Text field

• Dates are stored in US American format: month/day/year, you may have to convert to your locality’s format

• See also:TDateEditComboBox

Use the TButtonComboBox to select among several button values.

• The button values are stored in a TStrings called Items with the current value specified by ItemIndex, which can be 0 to Items.Count -1, or -1 if nothing is selected

• Text contains the current value, which is either empty string or one of Items

• Often handle OnCLick() or OnChange() events TListBox is a box containing a list of strings.

• The strings are stored in Items, indexed by ItemIndex, or -1 if nothing is selected

• OnClick and OnChange are useful events to handle

The TDateEditComboBox is an Edit box which features a drop down calendar if the user clicks on it.

• The date is stored in American format month/date/year

• Read or write the Text property

• Your program or the user can specify a time following the date and it will be viewable in the Text property, but the built-in calendar only specifies a new date

• The application can read or write SelectedDate which is the DateTime selected by the

user but returns DateTime(0) if the date is invalid OR the user specified a time on that date. SelectedDate is in the local time zone

This is an Edit/ComboBox which is useful for situations where a user needs to be able to enter in a value or select it from a separate dialog, such as a lookup dialog that has a data- bound grid in it.

• Set AutoComplete = True to have the browser suggest values

Use the TFileComboBox to specify a filename for uploading or downloading.

• Many browsers call the operating system’s File handling dialogs

• Text is the filename with path.

The TGrid is a wonderful component that creates a spreadsheet-like display.

• RowSelect – means select one whole row at a time

• ShowLines gives a classy lined look

• ColumnHeaders = True shows a top column shaded as a topic heading

• MultiSelect allows multiple rows to be selected at once (it requires

RowSelect := True)

• Selected items are in a Selected array

• TGridCells are arranged in TGridColumns and TGridRows

• We will show several examples later

The graphics controls are… well

graphical.

TImage encapsulates an image such as a Jpeg or Png format file.

• Specify the image filename in URLproperty

• Visible can be toggled to view or make invisible the thame

Displays a little icon from the standard collection This version of the icon adds the StartAnimating and StopAnimating methods. TPaint specifies a surface on which you can draw as needed. We will have an example later.

The TSlideShow presents a nice slide show of your images

• takes a collection of Jpeg or Png files in a ImageURLs TStrings.

• Set Loop := True to recycle the slides

• DisplayTime and FadeTime determine how long images are displayed

and fade expressed in milliseconds There is presently only one indicator. The TProgressBar is a graphical indicator of how far progress has been made toward some goal.

• MinValue <= Position <= MaxValue

• The default values are MinValue = 0, MaxValue = 100

The multimedia controls play audio or videos. There are presently no recording controls because JavaScript alone does not support it. PhoneGap, however, does support audio and video recording if you are doing mobile apps.

• DefaultRatePlayback 1 for 100%, less than 1 is slower, greater than 1 is faster

• AutoPlay – whether to play immediately

• Loop – whether to continuous replay the audio

• Muted – is audio muted

• SourceURL– url for the audio

• PosterImageURL– image to display for the clip when not playing

• SourceURL– URLfor the clip

• DefaultRatePlayback – 1 = 100%, < 1 is slower, >1 is faster

• Loop – continuous replay

• Muted – is audio muted

Containers hold other visual controls. Sometimes they have visual boarders, but not always.

A THeaderPanel is the header that goes at the top of a form or a panel. Typically you

would place a TLabel into the THeader which would say the name of the virtual window.

It is automatically included in a TPanel.

This is a basic panel with no scrollbars, no header.

The TPanel component is a panel control with a border and a caption bar with minimize/restore and close buttons.

• Set CaptionBar.Caption to the name of the Window

The TPage is a tabbed control To achieve a tabbed control, place a TPagePanel on the form, then press the + button in the upper right hand corner for each new TPage within the control.

• Set the Tab.Caption property to the name of the panel

The TScrollPanel is used to add scroll bars to a control if they are needed, and ScrollBars

is set to sbHorizontal, sbVeritcal, or sbBoth.

THTMLForm exposes an HTML‘form’ within the displayed page.

• Set URLto the page to view

• Method can be fmPost, fmGet, fmDelete fmHead or fmPut.

• A later example will show how to use the THMLForm to upload files.

TSizer is used to allow the user to adjust the size of controls. See the chapter on layouts.

A TSizeGrip modifies controls so a user can resize them, simply drop the TSizeGrip onto a

control and you can drag the lower right hand corner larger or smaller, up to constraints in the control’s constraints, or the size of the bounding control. There is only one menu class, TMenu.

• Use OnClick event which is fired when someone presses a menu option

• Index is the currently selected item.

• Use the + symbol to add menu options.

• Set Visible to show or hide the menu

TToolBar is a generic class which can hold multiple TToolBarButtons.

The TDataSetToolBar shows a perfect example of a TToolBar in use, it has buttons for common actions like adding, deleting, modifying records and more. The TLink control has a clickable caption which leads a browser to a different page.

• Set URLto the intended URL

Use TBrowser to have an iframe with your content. We will have several examples.

The TScript component allows you to include a JavaScript file in your application dynamically. You must still specify an external interface from the EWB compiler. We use this extensively in our JavaScript chapter.

Implement a Google map into your application. Specify Locations on the map. Set Options such as MapType for roads or other options. In our Cordova/PhoneGap example we desmonstrate how to use the TMap.

TTimer is the interval timer, the onTimer event is called periodically

• Interval is the time between event firings in milliseconds

• Enabled means the timer will fire when the interval happens

• Timing is not guaranteed, the event may fire sometime after the

specified number of milliseconds if the system is busy doing other things. We all know computers have various screen sizes and resolutions, but add mobile devices to the mix, and you may have a tiny display or the massive display of a graphics workstation for you page.

You can design your web form using the WYSIWYG (what you see is what you get) visual form designer.

WYSIWYG (what you see is what you get) visual form designer. The alignment panel contains tools

The alignment panel contains tools to assist you with beautifying your WYSIWYG form.

The WYSIWYG feature is great, no doubt, but do you design a separate page for mobile, and then which one in particular?

EWB supports dynamic layout and sizing of controls known in the industry as Responsive Web Design. It seems a little bit intimidating at first, but it is powerful and relatively simple once you get started. After only a few projects, you will get the hang of the layouts and many of us use it in every new project.

Every visual control has:

• Constraints.Max.Wdith, Max.Height, Min.Width and Min.Height these specify the

maximum size the object can grow to fill, and the minimum size to which it can shrink. The IDE and the run-time will both respect these constraints.

LayoutOrder – the order in which objects are added among layout groups.

Layout parameters:

o

Consumption

o

Overflow

o

Position

o

Reset (Boolean)

o

Stretch

Margins (left, right, top, bottom)

Padding (left, right, top, bottom)

Objects are laid out in order specified by LayoutOrder. First 0, then 1, etc. and they are placed into the remaining rectangle of space in the Form or container.

Position tells from where to start drawing the object. Often you will start at the TopLeft and proceed either to the right or down. Sometimes buttons start at the bottom right and grow leftward or up.

Consumption specifies if the direction the object grows (left, right, etc.) and eats into the remaining rectangle. Overflow says what to do when you run out of space in the current direction, like go Right, go Down, etc. Reset says to reset to the beginning since the last Reset (or the start).

Stretch tells whether to optionally grow in a direction, or not at all. If an object stretches, its minimum and maximum constraints are observed. If it doesn’t stretch, they are ignored because it stays the designed size.

Margin says how close an object should be to other objects, and padding is the space inside the object reserved for spacing.

An example is needed to see how these factors all play together. This example is found on the EWB web site under Responsive Design.

We’ve highlighted the direction, changes in direction, etc. LayoutOrder 0 grows (consumption) loBottom, so downward.

Thus LayoutOrder 1 is below it. And it grows to the right, but has a RESET=True, so the rectangle goes to the last reset position which is at the top of the remaining rectangle.

LayoutOrder 2 is a tricky one: it stretches to the right (consumption), but grows toward the bottom, so the remaining rectangle is below it.

LayoutOrder 3 naturally falls below LayoutOrder 3 because we grew to the bottom. Now we grow to the right. Where LayoutOrder 4 is placed depends on the remaining space. If there is room, we continue growing to the right. But if you resize the rectangle smaller, #4 has to go below #3, because #3 specified overflow would go below.

It is useful to stretch and squeeze the browser window. Also, many browsers support a responsive design mode which simulates the size of common devices like cell phones, tablets, etc.

Using responsive design, your pages can be attractive and functional on all devices. But you will have to test and apply the rules as we have done here to make sure it grows and shrinks the way you want.

When doing responsive design, you often want to have the MultiLineEdit box grow as needed. This can easily be accomplished in code by setting the OnKeyUp handler as follows;

procedure TForm1.MultiLineEdit1KeyUp(Sender: TObject; Key:

Integer; ShiftKey, CtrlKey, AltKey: Boolean);

var

c : integer;

begin

c := MultiLineEdit1.Lines.Count;

MultiLineEdit1.Height := (c+1) * (3*MultiLineEdit1.Font.Size) div 2; end;

Don’t let the math scare you, it’s just c (the number of lines of text) incremented by one so there is always a bit of extra room, multiplied by a bit more than the font height. Sometimes you want the form or panel to be resizable on the desktop.

Start by setting your form’s layout to TopLeft placement and stretch BottomRight. Then place a TScrollPanel on your form , set its layout to TopLeft placement with strech BottomRight, and finally set the Scroll property to ssBoth.

If you only want part of the form to scroll, use a TBasicPanel to delimit the area that will not scroll, using Layout parameters to occupy that space. Then use the above steps with TScrollPanel on the remaining space to create a scrollable area.

You can impose Contstraints that limit the minimum and maximum size of your scollable area. EWB supports {$DEFINE xxx} and {$IFDEF xxx}/{$ENDIF} much like Delphi.

You can use this to mark of a section of code that you want to temporarily disable. Often you will have a problem with a function, so {$ifdef DEBUG} it out, and {$ifdef DEBUG} out the definition in the TYPE section too.

Another great use is to comment out commens with embedded { and } characters, like JSON or sample C/PHP/JavaScript code. You cannot normally comment out these things because every end brace } frustratingly turns off comments.

I use {$ifdef xxx} between different versions of my code. Modern browsers allow you to store some data locally in the local and session storage. The space is not enormous, but it is big enough to store useful strings.

Local storage is conveniently accessed through the LocalStorage TPersistent class with functions like Set, Items (which gets data), and Clear which all operate on Key names. Local storage persists for a long time, possibly years.

Session storage is access through the SessionStorageƒ TPersistent class with the same functions as LocalStorage. Session storage generally is deleted when you exit or restart your browser (not just closing the window or tab).

Be aware that these data, although not visible to other Web applications, are not considered secure. The user or local applications can read the data, so do not store anything sensitive like Social Security Numbers, passwords, etc.

Session storage is particularly useful for Web site that have pages which load and change frequently but EWB usually does not usually involve a lot of different web pages. However, you may wish to store which windows are open, so if the user hits reload then you can do something intelligent to recover.

Here is the source code: unit storage1; interface uses WebCore, WebComps, WebUI, WebForms, WebCtrls, WebEdits,

Here is the source code:

unit storage1; interface uses WebCore, WebComps, WebUI, WebForms, WebCtrls, WebEdits, WebLabels, WebBtns; type

TForm1 = class(TForm) edSession: TEdit; edLocal: TEdit; Label1: TLabel; Label2: TLabel; btSave: TButton; btRetrieve: TButton; Label3: TLabel; Label4: TLabel; btClear: TButton; procedure btSaveClick(Sender: TObject); procedure btRetrieveClick(Sender: TObject); procedure btClearClick(Sender: TObject);

private

{ Private declarations } public

{ Public declarations } end;

var Form1: TForm1; implementation

procedure TForm1.btSaveClick(Sender: TObject); begin SessionStorage.Set('text', edSession.Text); LocalStorage.Set('test2', edLocal.Text); end;

procedure TForm1.btRetrieveClick(Sender: TObject); begin edSession.Text := SessionStorage.items['text']; edLocal.Text := LocalStorage.items['test2']; end;

procedure TForm1.btClearClick(Sender: TObject); begin SessionStorage.Clear('text');

LocalStorage.Clear('test2');

btRetrieveClick( Sender ); // update onscreen copies end;

end.

As we alluded to earlier, you can compile to JavaScript and debug that. And we will discuss techniques built into EWB and also some not yet included in EWB, such as adding breakpoints, using JavaScript variable watches, and more advanced DEWBUnit Unit Testing is introduced.

The first step is to ensure you have not enabled compression/encryption on your project. This way the generated HTML/JavaScript is human readable.

When trying to figure out raised exceptions, a method I found helpful for debugging exceptions is to place a try / except block around code that has problems. Then declare a local variable var ProgressNum : integer. In various places around your function, assign ProgressNum to various different values (0,1,2,3…). And in your exception handler have

Except on Exception: E do ShowMessage(‘ERROR: ‘+E.message + IntToStr( ProgressNum ));

End;

This code fragment will pinpoint the last good spot in your program before the exception. Note that EWB exceptions are based on JavaScript exceptions, which differ from Delphi Pascal exceptions. For example any number div 0 in JavaScript does not generate a divide

by zero error, instead the returned result is infinity. If you depend on a divide by zero exception, you will have to detect a zero and raise the exception yourself. Another trick is to use ShowMessage() to show variable contents and how far you have progressed. This has a problem under EWB in that the message boxes are all displayed concurrently, so you will have a bunch of message boxes present.

It is possible to send debugging messages to the JavaScript debugging console. We demonstrate this functionality later (see the index for Console.Log)

Native JavaScript is typically riddled with bugs due to the fact the language is interpreted rather than compiled and not strongly typed. For these reasons, almost every web browser supports debugging features for JavaScript programmers. We’ll learn how to use them. First, you must download my debugcode.js JavaScript file and add it to your project (Project | Options | External Files | … ) Then add the following few lines to your interface section.

External TConsole emit console = class Public procedure log( s :string); End;

external function eval( s : string ):object; external console : TConsole;

Start by adding a breakpoint to any function where you are having problems. Just add the line:

eval(‘debugger;’);

On most browsers, F12 enables the debug screen. If it doesn’t on yours, either find it on the menu or switch to an easier-to-use debugger like Chrome or Firefox. F12 and debug output/breakpoints do not work on the built-in browser of the EWB IDE.

Click on the “console” button on your browser’s developer pane, it should show no error messages except the messages you outputted with debugoutput(). If you see errors here there is something that should be fixed. You can output text to the console window with console.log(‘stuf’), and this is a handy way to debug – output everything that happens until something stops.

When debugger command executes, it will bring up the debugger.

Step over the debugger statement and return to the calling function, which is typically what you will want to debug. You can usually see the contents of any variables and classes by clicking on their names inside the source code, and many browsers let you set a Watch variable, just like Delphi’s debugger.

Many EWB programs have been written without the use of JavaScript debugging, but once you see these tricks, you can speed up development by improving your efficiency debugging your code.

Another way to set breakpoints is to edit the JavaScript output, search for some word in your source code (constants, variable and class names are good candidates), and insert the command debugger; where the trouble is, then run the application.

Unit Testing is the systematic creation of tests to validate software, it’s an important step in making professional quality software.

The idea is simple, put your logic into subroutines or non-GUI class methods, then create a sequence of tests and run them against your code and test the actual outputs against expected outputs. For example, look at the following two subroutines using DEWBUnit,

procedure TestProcedure( assert : TEWBU_test ); var one : integer; begin one := 1; assert.equal( one, 1 , 'one is 1'); end;

QEWBUnit’s assert.equal() method checks that the first two parameters are equal. If they are, all is well, otherwise it logs the error that one has some other value.

Consider a less fortunate situation:

procedure TForm1.TestMethod( assert : TEWBU_test); var one : integer; animal : string; begin

assert.timelimit(4);

assert.expect( 3 ); one := 1; animal := 'cow';

Button1.Caption := 'ha'; assert.equal( one, 1 , 'one = 1'); assert.equal( animal,'dog', 'animal = dog');

assert.okay('happiness found'); assert.fail('homework'); raise exception.Create('divide by zero');

end;

QEWBUnit is told the time limit for this routine and all its results is 4ms (assert.timelimit) and that the expected number of assertions is 3 (assert.expect).

Assert.equal passes for one = 1, but fails for animal = dog. Assert.ok logs ‘happiness found’, and also logs the failure ‘homework’.

Finally, QEWBUnit logs the exception created and not handled by your code. All you need to add this is copy QEWBUnit.* to your project and add the few lines:

procedure TForm1.Button1Click(Sender: TObject); begin hide; qewbunitform := TQEWBUnitPage.Create( Nil ); qewbunitform.Test( 'test of a procedure', TestProcedure ); qewbunitform.Test( 'test of a class method', TestMethod ); qewbunitform.Test( 'test of a procedure AGAIN', TestProcedure); qewbunitform.Start;

end;

The code creates an instance of the Tester, then queues tests on a procedure, then on the TestMethod method, and again on the initial procedure.

The Start function runs the tests in the queue. In the results below, I have checked the “Show only failures” checkbox, and the results show only test2 failed, and which failures it had.

Among the results are the number of milliseconds of each stage. This value is not

Among the results are the number of milliseconds of each stage. This value is not absolutely correct as the QEWBUnit code does consume some time doing its chores. In general, you will find pure JavaScript typically take 50ms or less, whereas GUI operations are often over 100ms to complete. Network operations can take 10ms to seconds or even minutes.

Note the Assert parameter is different for each function called. And your function may return before the whole operation is completed due to the asynchronous model – such as if you have networking or GUI calls which happen asynchronously.

The results are green for success, red for failure, and orange for an incorrect number of results when you set a count with Assert.expect().

In a production application you would define important functions and typical results, then write the function to implement the results. Eg. procedure TForm1.TestMethod( assert : TEWBU_test); var

t : DateTime; yesterday : DateTime; lastweek : DateTime; lastyear : DateTime;

begin assert.timelimit(100); // no way should it take 100 ms t := now; assert.equal( fn(t), ‘right now’ , 'test for right now'); yesterday := DateTime( Integer( t ) – 1000*24*60*60 ); assert.equal( fn(yesterday),’yesterday’, 'test yesterday'); lastweek := DateTime( Integer(t ) – 1000*8*24*60*60 )); assert.equal( fn(lastweek),’last week’, ‘test last week’); lastyear := DateTime( Integer(t ) – 1000*370*24*60*60 )); assert.equal( fn(lastyear),’last year’, ‘test last year’); end;

Now that you have write the function fn(x:DateTime). Typically you would leave the tests available to test every component before every new product release; this will ensure small changes do not break working code.

There are DUnit testers for a number of platforms. I looked at using a pure JavaScript version, but instead got frustrated and decided to write DEWBUnit in pure EWB code to integrate better this environment. There are many reasons you might want to use Painting on the form, some include:

- Bar/pie/line graphs

- Schematics

- Simple maps

- Modifications to images (which we do in the FileSave example)

When you place a TPaint on your form, you create a surface called a canvas on which you can draw using:

- Text with various fonts

- Lines (curved and straight)

- Boxes

- Circles/elipses/arcs

There are a constants to keep in mind:

- Font

- Color

- Line width

- Position

For example, to draw a box, set the canvas FillColor to a color of your choosing and call FillRect( x, y, width, height), and it and all other things will be drawn in that color until you specify a new color.

Call FillText( caption, x, y) to place text on the canvas using the current color. Several line oriented functions follow a path which you can later fill or leave unfilled.

For example, when we are going to draw a pie segment, we start a path at the center, move to and create an arc,, then ClosePath back to the centre, and finally we set a color and tell it to fill.

BeginPath; // start afresh // move to center MoveTo(CHART_COORD_X,CHART_COORD_Y);

// create an arc Arc(CHART_COORD_X,CHART_COORD_Y ,CHART_RADIUS,StartAngle,EndAngle); // finish the pie wedge ClosePath;

// get the color and fill it FillColor := clRed; Fill;

The following example shows how to create pie and bar graphs from the same data.

Place a TPaint surface on your form, make it quite large or the following code will clip meaning it goes beyond the size of the TPaint and is therefore not displayed.

Add two TButtons, Button1 and Button2 in the upper left hand corner, these will each draw a graph. unit pie; interface uses WebCore, WebUI, WebForms, WebCtrls, WebPaint, WebBtns; type

TForm1 = class(TForm) Paint1: TPaint; Button1: TButton; Button2: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject);

private

{ Private declarations } procedure ClearChart; public

{ Public declarations } procedure DrawPie;

procedure DrawBars;

end; var Form1: TForm1; implementation

const CHART_COORD_X = 200; CHART_COORD_Y = 200;

CHART_RADIUS = 150; CHART_STEP = 50;

CHART_LABEL_X = 700; CHART_LABEL_Y = 100; CHART_LABEL_HEIGHT = 30;

// Routine to clear the canvas, ie. draw a white box over it procedure TForm1.ClearChart; begin

with Paint1.Canvas do begin FillColor:=clWhite; FillRect(0,0, Width, Height ); end;

end;

procedure TForm1.DrawPie; var I: Integer; Data: array of Integer=[12,23,34,45]; Colors: array of TColor= [clElevateLightRed,clElevateLightBlue,clElevateLightOrang e,clElevateLightGreen]; Labels: array of String=['North','South','East','West']; LabelX: Integer= 400; LabelY: Integer= 100; Total: Integer=0; Angles: array of Double; StartAngle: Double; EndAngle: Double; Begin

ClearChart; // Total will be the sum total of all numbers in pie for I:=0 to Length(Data)-1 do Inc(Total,Data[I]); // create a list of angles SetLength(Angles,Length(Data)); for I:=0 to Length(Data)-1 do

Angles[I]:=((Data[I]/Total)*PI*2);

StartAngle:=(-PI/2);

with Paint1.Canvas do begin StrokeStyle:=dsColor; Font.Name:='Segoe UI';

Font.Size:=14;

Font.Color:=clElevateLightBlack; TextBaseLine:=blTop; StrokeColor:=clTransparent;

LineWidth:=1;

// cycle through the numbers drawing the pie for I:=0 to Length(Data)-1 do begin EndAngle:=(StartAngle+Angles[I]); with Paint1.Canvas do begin BeginPath; // start afresh

// move to center MoveTo(CHART_COORD_X,CHART_COORD_Y); // create an arc Arc(CHART_COORD_X,CHART_COORD_Y ,CHART_RADIUS,StartAngle,EndAngle); // finish the pie wedge ClosePath;

// get the color and fill it FillColor:=Colors[I]; Fill;

// draw the little box

FillRect(LabelX,(LabelY+(CHART_LABEL_HEIGHT*I)),20,20);

// draw our black text FillColor:=clElevateLightBlack; FillText(Labels[I],(LabelX+CHART_LABEL_HEIGHT),

(LabelY+(CHART_LABEL_HEIGHT*I))+2); end;

StartAngle:=EndAngle;

end;

Paint1.Canvas.BeginPath; // reset subpath for canvas end;

procedure TForm1.DrawBars;

var

I : Integer;

x, y: double;

Data: array of Integer=[12,23,34,45]; Colors: array of

TColor=

[clElevateLightRed,clElevateLightBlue,clElevateLightOrang e,clElevateLightGreen]; Labels: array of String=['North','South','East','West']; LabelX: Integer= 400; LabelY: Integer= 100; MaxHeight: Integer=0;

begin

ClearChart;

// get the height for scaling for I:=0 to Length(Data)-1 do MaxHeight := max( MaxHeight, Data[I] );

with Paint1.Canvas do

begin

StrokeStyle:=dsColor;

Font.Name:='Segoe UI';

Font.Size:=14;

Font.Color:=clElevateLightBlack;

TextBaseLine:=blTop;

StrokeColor:=clTransparent;

LineWidth:=1;

end;

for I:=0 to Length(Data)-1 do

begin

y := ( Data[I] * CHART_RADIUS ) / MaxHeight;

FillRect(LabelX,(LabelY+(CHART_LABEL_HEIGHT*I)),20,20);

FillColor:=clElevateLightBlack;

FillText(Labels[I],(LabelX+CHART_LABEL_HEIGHT),

(LabelY+(CHART_LABEL_HEIGHT*I))+2);

end;

end;

Paint1.Canvas.BeginPath; // reset subpath for canvas procedure TForm1.Button1Click(Sender: TObject); begin

DrawPie; end; procedure TForm1.Button2Click(Sender: TObject); begin DrawBars; end; end.

Unit: WebUI Alpha CompositeOperation FillColor FillGradient Global Alpha (Transparancy) of canvas, default 1.0

How are source pixels combined with destination pixels during drawing operation, default coSourceOver

Fill color when FillStyle set to dsColor Gradient to use when FillStyle set to dsGradient

FillPattern

FillStyle

LineCapStyle

LineJoinStyle LineWidth MiterLimit

ShadowBlur ShadowColor ShadowOffsetX ShadowOffsetY StrokeColor StrokeGradient StrokePattern StrokeStyle

TextAlign

TextBaseLine

Pattern to use when FillStyle set to dsPattern One of: dsColor, dsGradient, dsPattern

How are lines terminated when drawn, default csButt, other options are csRound and csSquare How are lines joined when they intersect, default jsMitre, others jsLevel, jsRound Line width for drawing (stroking) on the canvas Upper limit when LineJoineStyle is set to jsMitre, default 10

How much blur, default is 0, which is crisp What color is the shadow if ShadowBlur is not zero Horizontal offset of shadow, default 0 Veritcal offset of shadow, default 0 Line color when StrokeStyle is set to dsColor Line gradient when StrokeStyle set to dsGradient Line pattern when StrokeStyle set to dsPattern

Line drawing stype: One of dsColor, dsGradient, dsPattern How is text aligned when drawing, default: taLeftJustify, other values taRightJustify, taCenter

Vertical alignment of text, default: blAphabetic, other values blBottom, blHanging, blIdographic, blMiddle, blTop Arc ArcTo

BeginPath Add an arc along the current subpath. Add a straight line and an arc to the current subpath Discard any existing subpath and begin new subpath with no defined starting point. BezierCurveTo Add a cubic Bezier curve to the path. ClearRect Draw a transparent clBlack rectangle, does not affect current path Clip ClosePath

Compute the intersection of the inside of the current path with the current clipping region and use the smaller region as the new clipping region.

ConvertToDataURL

Close the path by adding a line back to the first point, then start a new Path

Convert the contents of the Canvas to a URL, specify either Image/PNG, or image/Jpeg

DrawImage Fill

Draw an image onto the canvas

fill the current path of the canvas with the color, gradient, or pattern specified by the FillStyle, FillColor, FillGradient, and FillPattern

FillRect

FillText

IsPointInPath

LineTo

MeasureText

MoveTo

QuadraticCurveTo

Rect

Rotate

Save

Fill the specified rectangle with the color, gradient, or pattern specified by the FillStyle, FillColor, FillGradient, and FillPattern properties.

Draw the specified text at the specified point with the font specified by the Font property, and the color, gradient, or pattern specified by the FillStyle, FillColor, FillGradient, and FillPattern properties.

Does the point fall within or on the edge of the current path? Add a line segment to the current path starting at the current point. Measure the length of the specified text if drawn with the current font settings

Move the current point to the specified location and begin a new Subpath

Add a quadratic Bezier curve to the current path, and the new position will be the ending point.

Add a rectangle to the current path of the canvas. The added rectangle has its own subpath not connected to any other subpaths in the current path. After this method is called, the current point on the canvas is set to the point specified by the first two parameters

Change the transformation matrix such that all subsequent drawing is rotated by the specified number of radians.

Push the current properties of the canvas to a stack for drawing some time later. Use Restore to recover the settings.

Scale SetTransform Stroke StrokeRect StrokeText Transform Translate Change the transformation matrix to scale the future X and Y drawing by these factors. Directly set the transformation matrix.

Stroke (draw) the current path of the canvas with the color, gradient, or pattern specified by the StrokeStyle, StrokeColor, StrokeGradient, or StrokePattern properties. The LineCapStyle, LineJoinStyle, MiterLimit and LineWidth properties control how lines of the current path are drawn.

Stroke (draw) the specified rectangle with the color, gradient, or pattern specified by the StrokeStyle, StrokeColor, StrokeGradient, or StrokePattern properties. The LineCapStyle, LineJoinStyle, MiterLimit and LineWidth properties control how the lines of the rectangle are drawn.

Draw the outline of the specified text at the specified point with the font specified by the Font property, and the color, gradient, or pattern specified by the StrokeStyle, StrokeColor, StrokeGradient, or StrokePattern properties. The LineCapStyle, LineJoinStyle, MiterLimit and LineWidth properties control how the lines that make up the outline of the font characters are drawn. See online notes about condensed text if the specified text does not fit within the specified width.

Directly set the transformation matrix.

Move the following commands to the horizontal and vertical offsets specified, ie. slide the image in a given direction. I recommend ‘The JavaScript Bible’ which has details on using these thinly disguised JavaScript functions. Sometimes you must know a little bit about JavaScript or HTML5 to do what you want. Such is the case with resizing images or manipulating them.

Here we have a little program with a ButtonComboBox whose five items are set to

Here we have a little program with a ButtonComboBox whose five items are set to 100 percent, 50 percent, 33 percent, 25 percent and 200 percent. Its onChange property is set to

ButtonComboBox1Change;

We add a TImage Image1 used to load an image. We set its URLproperty to any picture of our choosing. Its OnLoad property is set to Image1Load which resizes the image to its fully loaded size and makes it invisitble.

And we place a TPaint Paint1 onto the form. unit resize1; interface uses WebCore, WebDom, WebUI, WebForms, WebCtrls, WebEdits, WebBrwsr, WebPaint; type

TForm1 = class(TForm) ButtonComboBox1: TButtonComboBox; Image1: TImage; Paint1: TPaint; procedure ButtonComboBox1Change(Sender: TObject); procedure Image1Load(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1; external function eval( s: string) : object; implementation

procedure TForm1.ButtonComboBox1Change(Sender: TObject); var percent : integer;

i

: integer;

s

: string ;

newwidth, newheight : double;

context : THTMLCanvasRenderingContext2D; paint, img : THTMLElement;

begin

s := ButtonComboBox1.Text; i := Pos(' ', s ); if i > 0 then begin

s := Copy( s, 1, i - 1); percent := StrToInt( s ); end else

percent := 100; newwidth := (Image1.ActualWidth * percent) / 100; newheight := ( Image1.ActualHeight * percent ) / 100; Paint1.Width := Round(newheight); Paint1.Height := Round(newheight); Image1.clientID := 'image'; Paint1.clientID := 'paint'; img := THTMLElement(window.document.getELementById('image')); img := THTMLElement(img.childNodes[0]);

paint := THTMLElement( window.document.getElementById('paint')); context := THTMLCanvasElement( Paint ).getContext('2d');

context.drawImage(img, 1,1, newwidth, newheight); end;

procedure TForm1.Image1Load(Sender: TObject); begin Image1.Width := Image1.ActualWidth; Image1.Height := Image1.ActualHeight; Image1.Visible := False;

end;

end.

EWB does not use HTMLIDs, so we set the clientID that in turn sets the HTMLID to something recognizable, in this case ‘image’ and ‘paint’. The variable Paint is set to the HTMLelement JavaScript finds with the ID paint.

The variable is set to the HTMLelement JavaScript with the ID image, but EWB uses the container of the Paint1 object which is a DIV, so we have to get its one and only child, which is the true paint. From there, it’s a simple matter of getting a HTML5 2d drawing context, and then drawing the image on using whatever size we want. Providing attractive displays is not enough, we often need the ability to transfer files between the web client and server, and the other direction too.

The ability to upload files is one of the important functions of web pages.

The power of EWB can lead you to forget that it is limited by the JavaScript restrictions, one of which is that file access is severally crippled to prevent web sites from stealing files from your hard drive.

It is possible to upload files to a web server using techniques similar to other web page editors.

First, drop a Containers | THTMLForm onto your form. This will be the HTMLform that is uploaded to the web server. Only controls inside this form are uploaded.

Place a TFileComboBox into the THTMLForm, and also a Edit and a TButtton. If these are not inside the THTMLForm the whole thing will fail.

Place a Web | TBrowser elsewhere on the form. It will hold the output from the web server. Set HTMLForm1’s Output to Browser1 which you just placed on the form.

Set the HTMLForm1.URLto http://localhost/formsubmit which is EWB’s sample web page that just echoes back what you send it. Set the Button1’s OnClick method to:

procedure TForm1.Button1Click(Sender: TObject); begin

HTMLForm1.Submit;

end;

Now when you run your web page, you can select a file to upload, enter a text string in Edit1, and press Button1 to upload the file. The WebBrowser1 will display the output from the web page.

By default, HTMLForm1 will POST the data as a MultiPartForm, which is appropriate for binary data. You can change this in the Encoding parameter to feTextPlain or feURLEncoded, and in Method to any valid HTTP method.

To download a data file you act as though you are going to replace the current web page with a new one. This causes the browser to do an HTTP/HTTPS GET of the specified URL.

uses webdom; … url := ‘report.php’; s := url + ‘?report=transaction.csv’ window.location.replace( s );

Then the HTTP/HTTPS server script indicates through MIME (Multimedia Internet Mail Extensions) that the file is not HTMLbut rather some other format, and that its filename is whatever, and that the file should be downloaded and not displayed. If you do not indicate that the file should be saved with a filename, it will instead open up in the appropriate application (eg. PDF viewer, Excel, Word, etc.).

report.php :

header('Status: 200'); header('Content-type: text/csv'); $u = "answers.csv"; header("Content-disposition: attachment; filename=$u;");

print “a, 1, 3, 5\n”;

If you wish, you can download the file in a Web | TBrowser, which will load the new file into an HTMLIFRAME simply by setting the TBrowser’s URLto the appropriate web address. This has the benefit that you can use POST to download the file, and POST arguments are hidden from web server logs unlike GET arguments. EWB uses a totally event driven model where nothing ‘blocks’.

There is no sleep() function, no waiting for network events like a return to our HTTP/HTTPS request, no waiting for user input. Instead we use a TTimer to schedule a delay, a network request invokes HTTP/HTTPS request handler when the request fails or success, keystrokes and other input generate event methods for affected components.

This is a change from Delphi, where ShowMessage() or MessageBox() would block until the user accepted the results… we cannot block, we can only complete event handlers. (The exceptions to this non-blocking are described under window.alert() and similar functions of the JavaScript chapter.)

There is also no concept of a TThread or multithreading in normal EWB (though we present a multithreading solution in a later chapter). But there are workarounds; we can schedule several short programming segments to occur in sequence. This is accomplished with the async keyword.

For example, consider this code procedure TForm1.Button1Click(Sender: TObject); var i, j, k : integer;

begin

for i := 1 to 10000 do begin Edit1.Text := IntToStr( i ); for j := 1 to 1000 do

k := j;

end;

end;

It is a for loop that would update Edit1’s Text to the numbers 0 through 10,000. And since there is an internal for j := loop, it would delay between each update. The for j loop is not intended to provide a specific delay, it’s just an example of something keeping the cpu busy. Try it. It doesn’t work the way you thought. The only things that happen are that there is a five second delay while it calculates, and then the number 10000 is displayed.

EWB executes all code, then schedules an update to the display when all changes are made.

In this example, if you want to show intermediate results, the trick is to use async to schedule future code to execute. Async saves the state of all parameters necessary to complete the call sometime in the future.

procedure TForm1.UpdateOneEdit1( v : integer ); var

j , k : integer;

begin

Edit1.Text := IntToStr( v ); for j := 1 to 1000 do

k

if ( v < 1000 ) then async UpdateOneEdit1( v + 1 ); end; procedure TForm1.Button2Click(Sender: TObject); var i, j, k : integer;

:=

j

;

begin

async UpdateOneEdit1( 0 );

end;

The UpdateOneEdit() chains to a future UpdateOneEdit() by calling the async function. This code will update the counter for every number and is slowed down by the counter. Note, the counter speed will vary by CPU, some will be faster or slower than others. The ShowProgress and HideProgress can be used to display a busy message during slow execution. ShowProgress presents a modern animation that informs users about the delay and disables the user input until you call HideProgress.

procedure TForm1.UpdateOneEdit1( v : integer ); var

j , k : integer;

begin

Edit1.Text := IntToStr( v );

for j := 1 to 1000 do

k

if ( v < 1000 ) then async UpdateOneEdit1( v + 1 ) else HideProgress;

end;

:=

j

;

procedure TForm1.Button2Click(Sender: TObject); begin ShowProgress( 'updating'); async UpdateOneEdit1( 0 );

end;

In normal operations like generating a web request, you will call ShowProgress() and then the request function without using async. The request will start the HTTP/HTTPS connection then return to the event handler.

When the HTTP/HTTPS result is returned, your request handler is called, so you process the data and then call HideProgress. Tag is a property on every component that has no declared use, it’s usable by you for any purpose you want.

For example, when you write a calendar app, you can use Tag to hold the datetime or an event number of the calendar event you are displaying in a TLabel. Then when the user clicks on the TLabel, you know exactly when it will happen.

Another popular use is to set a TEdit’s Tag := 0 in the OnShow event, and set it to 1 in an OnChange event. Then you would know if the user changed any details, and which details since the edit was available.

You can set the Tag to an ordinal in an array of objects or a TObjs or TStringList, so the Tag will give an index to which object or string it references.

You are limited only by your imagination and the 52 bits of the signed Integer type. Every environment seems to have a different format for storing datetimes. In EWB we use the 52 bit integer to hold the number of milliseconds since the Epoch of January 1, 1970 UTC (universal time coordinates).

Very likely UTC is not your current timezone, so you often want to present local time to the

user, and read local time from them, but often servers store UTC time if they are expected

to serve a world-wide market.

There are many functions to convert between DateTime and other formats, usually integers

or strings:

YearOf( x : DateTime , optionalUTC :boolean ) returns the year as an integer. If you do not specify the optionalUTC parameter, it defaults to false, meaning local time.

MonthOf… returns the month DayOf … returns the day of the month

DateToStr( x : DateTime , optionalUTC : boolean ) DateTimeToStr( x : DateTime, optionalUTC : Boolean ); TimeToStr( x : DateTime, optionalUTC : Boolean ); StrToDateTime( x : DateTime , optionalUTC :Boolean);

The DateTime and String conversion funcitons depend on the TFormatSettings ShortDateFormat

M The month number with no leading zero

MM The month number with a leading zero if

the month number is less than 10 d The day number with no leading zero

dd The day number with a leading zero if the day number is less than 10

yy The last two digits of the year number with a leading zero

yyyy The full four digits of the year number

and the ShortTimeFormat is defined with

h The hour number (12-hour clock) with no leading zero hh The hour number (12-hour clock) with a leading zero if the hour number is less than 10 H The hour number (24-hour clock) with no leading zero

m The minute number with no leading zero

mm The minute number with a leading zero if the minute number is less than 10

s The second number with no leading zero

ss The second number with a leading zero if the second number is less than 10

tt The AM/PM designation for a 12-hour clock literal

DateTimeToISOStr() returns a standard ISO string which is computer parseable and yet human readable. It is based on UTC time.

The ability to do animations can give your application a nice look. You can animate any

control, including containers as well as individual TButtons, etc.

The animations will occur whenever there is a change to the size or visibility of a control.

You do not have to schedule a TTimer to do the animation, animations are built-in.

The following minimalist program shows how to animate a TButton and TListBox. Every

time you press the TButton, the TButon and TListBox will grow 50% by slowly expanding.

unit main; interface uses WebCore, WebUI, WebForms, WebCtrls, WebLabels, WebLists, WebBtns; type

TForm1 = class(TForm) ListBox1: TListBox; Button1: TButton; procedure Button1Click(Sender: TObject);

private

{ Private declarations } public

{ Public declarations } end;

var Form1: TForm1; implementation procedure EaseIn( Ctrl : TControl ); var h : integer;

begin

ctrl.BeginUpdate; h := Ctrl.Height; with Ctrl.Animations.Height do begin

Duration := 1000; Style := asQuadEaseIn; end; Ctrl.Height := h +(h div 2); ctrl.EndUpdate; end;

procedure TForm1.Button1Click(Sender : TObject); begin EaseIn( ListBox1 ); EaseIn( Button1 ); end; end.

Nomally you would click on the Object Inspector and set the Animations as constants rather than entering them in code.

The basic animations are:

- Height

- Left

- Opacity

- Top

- Visible

- Width

Each attribute has

- Duration in milliseconds, best set to 500 or so

- Style

Style can be one of: asBackEaseIn asBackEaseInOut asBackEaseOut

asBackEaseOutIn Easing equation function for a back easing in: accelerating from zero velocity. Easing equation function for a back easing in/out: acceleration until halfway, then deceleration. Easing equation function for a back easing out: decelerating from zero velocity. Easing equation function for a back easing out/in: deceleration until halfway, then acceleration. asBounceEaseIn

asBounceEaseInOut

asBounceEaseOut

asBounceEaseOutIn

asCircEaseIn

asCircEaseInOut

asCircEaseOut

asCircEaseOutIn

asCubicEaseIn

asCubicEaseInOut

asCubicEaseOut

asCubicEaseOutIn

asElasticEaseIn Easing equation function for a bounce (exponentially decaying parabolic bounce) easing in: accelerating from zero velocity. Easing equation function for a bounce (exponentially decaying parabolic bounce) easing in/out: acceleration until halfway, then deceleration. Easing equation function for a bounce (exponentially decaying parabolic bounce) easing out: decelerating from zero velocity. Easing equation function for a bounce (exponentially decaying parabolic bounce) easing out/in: deceleration until halfway, then acceleration. Easing equation function for a circular easing in: accelerating from zero velocity. Easing equation function for a circular easing in/out: acceleration until halfway, then deceleration. Easing equation function for an exponential easing out/in: deceleration until halfway, then acceleration. Easing equation function for a circular easing in/out: acceleration until halfway, then deceleration. Easing equation function for a cubic easing in: accelerating from zero velocity. Easing equation function for a cubic easing in/out: acceleration until halfway, then deceleration. Easing equation function for a cubic easing out: decelerating from zero velocity. Easing equation function for a cubic easing out/in: deceleration until halfway, then acceleration. Easing equation function for an elastic (exponentially decaying sine wave) easing in: accelerating from zero velocity. asElasticEaseInOut

asElasticEaseOut

asElasticEaseOutIn

asExpoEaseIn

asExpoEaseInOut

asExpoEaseOut

asExpoEaseOutIn

asLinear

asNone

asQuadEaseIn

asQuadEaseInOut

asQuadEaseOut

asQuadEaseOutIn

asQuartEaseIn

asQuartEaseInOut Easing equation function for an elastic (exponentially decaying sine wave) easing in/out: acceleration until halfway, then deceleration. Easing equation function for an elastic (exponentially decaying sine wave) easing out: decelerating from zero velocity. Easing equation function for an elastic (exponentially decaying sine wave) easing out/in: deceleration until halfway, then acceleration. Easing equation function for an exponential easing in: accelerating from zero velocity. Easing equation function for an exponential easing in/out: acceleration until halfway, then deceleration. Easing equation function for an exponential easing out: decelerating from zero velocity. Easing equation function for an exponential easing out/in: deceleration until halfway, then acceleration. Easing equation function for a simple linear tweening, with no easing. No animation style.

Easing equation function for a quadratic easing in: accelerating from zero velocity. Easing equation function for a quadratic easing in/out: acceleration until halfway, then deceleration. Easing equation function for a quadratic easing out: decelerating from zero velocity. Easing equation function for a quadratic easing out/in: deceleration until halfway, then acceleration. Easing equation function for a quartic easing in: accelerating from zero velocity. Easing equation function for a quartic easing in/out: acceleration until halfway, then deceleration. asQuartEaseOut

asQuartEaseOutIn

asQuintEaseIn

asQuintEaseInOut

asQuintEaseOut

asQuintEaseOutIn

asSineEaseIn

asSineEaseInOut

asSineEaseOut

asSineEaseOutIn Easing equation function for a quartic easing out: decelerating from zero velocity. Easing equation function for a quartic easing out/in: deceleration until halfway, then acceleration. Easing equation function for a quintic easing in: accelerating from zero velocity. Easing equation function for a quintic easing in/out: acceleration until halfway, then deceleration. Easing equation function for a quintic easing out: decelerating from zero velocity. Easing equation function for a quintic easing in/out: acceleration until halfway, then deceleration. Easing equation function for a sinusoidal easing in: accelerating from zero velocity. Easing equation function for a sinusoidal easing in/out: acceleration until halfway, then deceleration. Easing equation function for a sinusoidal easing out: decelerating from zero velocity. Easing equation function for a sinusoidal easing in/out: deceleration until halfway, then acceleration.

There are a lot of choices. Most developers follow some theme rather than a random attack of the GUI on the poor user. Use them tastefully. As we will see later, TGrid is very useful in database applications. But sometimes you will use TGrid on its own, just as a convenient way to display and maybe update data.

One noteworthy application is in generating an online receipt for a transaction. The grid looks good, and it can have a variable number of rows and columns. For our TGrid example, place a TButton on the form and a TGrid. The rest we can do in code. procedure TForm1.Button1Click(Sender: TObject); begin

with Grid1 do begin if ColumnCount < 2 then begin NewColumn; NewColumn; AppendRow; AppendRow;

end; Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] := 'Name'; Rows.Row[0].Value[1] := 'Erick';

Rows.Row[1].Value[0] := 'Org'; Rows.Row[1].Value[1] := 'Engineering';

end; end;

We first check the column count, otherwise we’d keep appending columns every time you press the button.

We set column[1] as editable so a user can change its values. Not so for column[0], it is readonly. A popular question that appears on the message board asks how to make color versions of TButtons or TGrids.

The long answer is that you can create class decendant controls. That is truly the best possible result, as these controls can then be added to the component palette and employed in all your subsequent projects.

The source code to the component library is included with your EWB license, so you can refer to many example components there.

But doing that takes a lot of coding and clicking and perfecting. Suppose you want a quick- and-dirty solution which solves your particular problem without all the effort.

The first thing to do is pick the parent control on which you wish to base your new control. Consider TButton, we’ll add a color to it so there is a new property called color of type TColor.

unit tcolorbutton1; interface uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebBtns, WebLabels; type

TForm1 = class(TForm) Button1: TButton; Label1: TLabel; procedure Form1Show(Sender: TObject); procedure Button1Click(Sender: TObject);

private { Private declarations } butRed, butGreen, butBlue : TColorButton; function MakeColorButton( caption : string;

color : TColor ; myleft : integer ):TColorButton; public { Public declarations } end;

TColorButton = class(TButton) protected fFillColor : TColor; procedure InitializeProperties; override; procedure UpdateInterfaceState; override; function getFillColor :

TColor; procedure setFillColor(const col : TColor );

published property FillColor : TColor read getFillColor write setFillColor; end; var Form1: TForm1; implementation

procedure TColorButton.UpdateInterfaceState; begin inherited UpdateInterfaceState; Element.Background.Fill.Color := fFillColor; end;

procedure TColorButton.InitializeProperties; begin inherited InitializeProperties; fFillColor := clRed; // default is some vibrant color end;

function TColorButton.getFillColor : TColor; begin result := fFillColor; end;

procedure TColorButton.setFillColor( const col : TColor ); begin fFillColor := col; Element.Background.Fill.Color := col;

end; // user application code

function TForm1.MakeColorButton( caption : string ;color :

TColor ; myleft : integer ):TColorButton; begin

result := TColorButton.Create( self ); result.Parent := self; result.Left := myleft; result.Top := Button1.Top; result.FillColor := color; result.Caption := caption; result.OnClick := Button1Click;

end; procedure TForm1.Form1Show(Sender: TObject); begin

butRed := MakeColorButton( 'red', clRed, Button1.Width + 10

+

Button1.Left ); butGreen := MakeColorButton( 'green', clGreen, 2*(Button1.Width + 10 ) + Button1.Left );

butBlue := MakeColorButton( 'blue', clBlue, 3*(Button1.Width

+

10) + Button1.Left ); end;

procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage( 'button says ' + TButton(Sender).Caption); end; end.

Looking at TForm, we declare three buttons (butRed, butGreen and butBlue) and a MakeColorButton() function which creates the buttons parallel to a normal TButton we dropped on the form.

private { Private declarations } butRed, butGreen, butBlue : TColorButton; function MakeColorButton( caption : string;

color : TColor ; myleft : integer ):TColorButton;

MakeColorButton just makes a regular button, but adds a FillColor which is new.

function TForm1.MakeColorButton( caption : string ; color :

TColor ; myleft : integer ):TColorButton; begin result := TColorButton.Create( self ); result.Parent := self;

result.Left := myleft; result.Top := Button1.Top; result.FillColor := color; result.Caption := caption; result.OnClick := Button1Click; end;

The form’s OnShow handler creates the three buttons. procedure TForm1.Form1Show(Sender: TObject); begin butRed := MakeColorButton( 'red',

clRed, Button1.Width + 10 + Button1.Left ); butGreen := MakeColorButton( 'green', clGreen, 2*(Button1.Width + 10 ) + Button1.Left ); butBlue := MakeColorButton( 'blue', clBlue, 3*(Button1.Width + 10) + Button1.Left ); end;

So now we just need a class that does everything TButton does, but adds a color background. We declare it as a descendant of TButton class, and we add an fFillColor value and two helper function getFillColor and setFillColor which get and set the published property FillColor.

TColorButton = class( TButton ) protected fFillColor : TColor; procedure InitializeProperties; override; procedure UpdateInterfaceState; override; function getFillColor :

TColor; procedure setFillColor(const col : TColor );

published property FillColor : TColor read getFillColor write setFillColor; end;

getFillColor and setFillColor just access the fFillColor data. function TColorButton.getFillColor : TColor; begin result := fFillColor; end;

procedure TColorButton.setFillColor( const col : TColor ); begin

fFillColor := col; Element.Background.Fill.Color := col; end;

We also define two protected override functions. The system calls these overridden functions instead of the default functions of TButton of the same name.

procedure InitializeProperties; override; procedure UpdateInterfaceState; override; InitializeProperties simply calls the inherited (TButton’s) InitializeProperties, then sets a default color in case the user forgets.

procedure TColorButton.InitializeProperties; begin inherited InitializeProperties; fFillColor := clRed; // default is some vibrant color end;

Finally, here’s the magic function:

procedure TColorButton.UpdateInterfaceState; begin inherited UpdateInterfaceState; Element.Background.Fill.Color := fFillColor; end;

It calls the usual UpdateInterface of the TButton, but then reasserts the Background Fill Color as whatever we’ve defined. That’s it. Of course, you have to know that UpdateInterfaceState is called to get the component to update itself. But now you know. Creating color, bold, italics, underline and strikeout options on grids is quite easy. The result looks like this

The trick is simply to create an OnCellUpdate event handler which adjusts the font characteristics.

The trick is simply to create an OnCellUpdate event handler which adjusts the font characteristics. First, add a TGrid to your form, then use the OnShow event to add data to your Grid. unit colorgrid1; interface uses WebCore, WebUI, WebForms, WebCtrls, WebGrids; type

TForm1 = class(TForm) Grid1: TGrid; procedure Form1Show(Sender: TObject);

private

{ Private declarations }

procedure OnCellUpdate( Col : TGridColumn ;

ACell: TGridCell ); public

{ Public declarations }

end; var Form1: TForm1; implementation procedure TForm1.OnCellUpdate( Col : TGridColumn ;

ACell: TGridCell ); var textcell : TGridTextCell; newcol : TColor; fo : TFont;

begin textcell := TGridTextCell( ACell ); fo := ACell.Font; case textcell.index of

0 : begin

fo.Color := clRed; fo.style.italic := True; fo.style.underline := True;

end;

1 : fo.Color := clBlue;

end; end; procedure TForm1.Form1Show(Sender: TObject); begin with Grid1 do begin

if ColumnCount < 2 then begin NewColumn; NewColumn; AppendRow; AppendRow;

end; Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] := 'Name'; Rows.Row[0].Value[1] := 'Erick'; Rows.Row[1].Value[0] := 'Org'; Rows.Row[1].Value[1] := 'Engineering';

Columns[1].OnCellUpdate := OnCellUpdate; end; end;

First, add a TGrid to your form, then use the OnShow event to add data to your Grid. procedure TForm1.Form1Show(Sender: TObject); begin with Grid1 do begin

if ColumnCount < 2 then begin NewColumn;

NewColumn;

AppendRow;

AppendRow;

end; Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] := 'Name'; Rows.Row[0].Value[1] := 'Erick'; Rows.Row[1].Value[0] := 'Org'; Rows.Row[1].Value[1] := 'Engineering';

Columns[1].OnCellUpdate := OnCellUpdate; end; end;

Notice that we hard code the values above. In practice you can either set them to variables or use the database to query the value. The trick to getting color is simply to create an OnCellUpdate event handler which adjusts the font characteristics.

procedure TForm1.OnCellUpdate( Col : TGridColumn ;

ACell: TGridCell ); var textcell : TGridTextCell; newcol : TColor; fo : TFont;

begin textcell := TGridTextCell( ACell ); fo := ACell.Font; case textcell.index of

0 : begin

fo.Color := clRed; fo.style.italic := True; fo.style.underline := True;

end;

1 : fo.Color := clBlue; end; end;

TextCell.index contains the row number of the value, so this example bases color and font information on the particular row. And remember, in the OnShow event we only applied the OnCellUpdate handler to Column #1. In practice you could apply it to many columns.

Finally, you may want to base your decision of particular colors on other factors. For example, if the data is N/A, or < 0, or whatever, you may want to flag data problems with red.

if textcell.Data = 'N/A' then fo.Color := clRed;

Persistence is the topic of storing data and recovering it later. Serialization is the ability to take an object, flatten it out to some intermediate format for sending over a network or storing in a database. Deserialization is the inverse, taking the intermediate format and converting it back into a usable object (though possibly in a different environment and/or programming language).

In a nutshell, the problem is this:

1. We have an object holding some data, say a customer record, or an accounting record, or

an inventory item sitting on the server database.

2. We want to get the data from that object into a usable format on the Web client written

with EWB.’

3. To get to that client, the data must traverse the Internet in some intermediate format as a

series of bytes.

EWB knows how to convert data from structures into two common formats: JSON (JavaScript Object Notation). We will see just how easy it is with the first example below.

unit readerwriter1a; interface uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns; type

TClient = class ( TPersistent ) private fClientID : integer; fFirstName : string; fSurname : string; fAddress : string; fCity : string; fStateProvince : string; fCountry : string; fPostalZip : string;

published property ClientID : integer read fClientID write fClientID;

property FirstName : string read fFirstName write fFirstName;

property Surname : string read fSurname write fSurname; property Address : string read fAddress write fAddress; property City : string read fCity write fCity; property StateProvince : string read fStateProvince write fStateProvince; property Country : string read fCountry write fCountry; property PostalZip : string read fPostalZip write fPostalZip; end;

TForm1 = class(TForm) MultiLineEdit1: TMultiLineEdit; Button1: TButton; procedure Form1Show(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Form1Create(Sender:

TObject);

private

{ Private declarations } client : TClient; Reader : TReader; Writer : TWriter;

public

{ Public declarations } end;

var Form1: TForm1;

implementation procedure TForm1.Form1Create(Sender: TObject); begin

client := TClient.Create; Reader := TReader.Create; Writer := TWriter.Create;

end;

procedure TForm1.Form1Show(Sender: TObject); begin Writer.Initialize; client.Save( Writer ); MultiLineEdit1.Lines.Text := Writer.Output;

end;

procedure TForm1.Button1Click(Sender: TObject); begin // save contents Reader.Initialize( MultiLineEdit1.Lines.Text ); client.Load( Reader );

// now write them back out again Form1Show( Sender ); end; end.

The data structure TClient is a bit special in that we have defined published properties such as FirstName, with read and write properties that save the data into private data properties. For convention sake, we name the private properties f…

When we start our application, the onCreate method creates an empty TClient variable and a TReader and TWriter. We will use later

procedure TForm1.Form1Create(Sender: TObject); begin client := TClient.Create; Reader := TReader.Create; Writer := TWriter.Create; end;

In the OnShow event hander we display the JSON of the TClient structure in a few short lines:

procedure TForm1.Form1Show(Sender: TObject); begin Writer.Initialize; client.Save( Writer ); MultiLineEdit1.Lines.Text := Writer.Output;

Then the user is free to edit the JSON and press Button1 which reloads the TClient from the MultiLineEdit1 text box.

procedure TForm1.Button1Click(Sender: TObject); begin // save contents Reader.Initialize( MultiLineEdit1.Lines.Text ); client.Load( Reader );

// now write them back out again Form1Show( Sender ); end;

By calling Form1Show(), the procedure rereads the JSON notation and updates it on the

display. The output of this simple program looks like this:

display. The output of this simple program looks like this:
You can see, edit and update the JSON and thus the TClient. Now add an

You can see, edit and update the JSON and thus the TClient. Now add an fBirthday to the private part of TClient, and Birthday to the published part.

Private

fBirthday : DateTime Published

property Birthday : DateTime read fBirthday write fBirthday;

and change OnCreate to initialize the birthday field to today:

procedure TForm1.Form1Create(Sender: TObject); begin client := TClient.Create; client.Birthday := now; Reader := TReader.Create; Writer := TWriter.Create; End;

When you rerun the program it displays the date as the number of milliseconds since January 1, 1970 UTC. Eg:

"birthday": 1469207693068 If you prefer, you can change the default output to ISO date/time by changing

procedure TForm1.Form1Create(Sender: TObject); begin client := TClient.Create; client.Birthday := now; Reader := TReader.Create(dtfISO8601); Writer := TWriter.Create(dtfISO8601);

And the output changes to:

"birthday": "2016-07-22T17:24:37.585Z" or something similar which is more human readable. But it may still be confusing because the time is UTC zoned.

To the novice programmer it seems annoyingly redundant to specifiy private and published properties. But there are several good reasons.

Published properties are used by TReader and TWriter. Having a private property such as Phone number could exist without being copied to and fro in the JSON.

Another benefit is that you can have handlers which correct or interpret the data. For example, many systems like to specify gender of someone. Depending on your location and

current views of gender identity, it may allow Male, Female and Unspecified, or maybe other designations. But people often just enter M or F or U. So what if the system automatically fixed the data to the long accepted formats.

TClient = class ( TPersistent ) Private

fSex : string; procedure LocalWriteSex( s : string );

Published

property Sex : string read fSex write LocalWriteSex; end;

procedure TClient.LocalWriteSex( s : string ); begin case LowerCase(s) of 'm', 'male' : fSex := 'male'; 'f', 'femail' : fSex := 'female'; else fSex := 'unspecified'; end; end;

And update the OnCreate form to set the Gender.

procedure TForm1.Form1Create(Sender: TObject); begin client := TClient.Create; client.Birthday := now; client.Sex := 'u'; Reader := TReader.Create(dtfISO8601); Writer := TWriter.Create(dtfISO8601); end;

Sometimes you know you will have arrays of items and will want to often code them as JSON arrays – eg. party guests from your TClients. The easiest way to accomplish this uses TCollections. But to do so, you must have TClient inherit from TColllectionItem (which is a descendant of TPersistent) instead of TPersistent.

If you took the above examples and substituted TCollectionItem for TPersistent, everything works identically. Consider the case where we have people and their cousins, who are also people.

We will build a program which dumps a person and their cousins to persistent storage…

actually just to a MultiLineEdit box. And we will have a button which rereads that MultiLineEdit box and reloads it into our structure. You can tell it works if you change the spacing or the entries, and they revert back to EWB’s spacing after you hit the button.

unit persistarray1; interface uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns; type

TPerson = class (TCollectionItem ) private fname :string; fcousins : TPeople;

published property name : string read fname write fname; property cousins : TPeople read fcousins write fcousins;

end; TPeople = class( TCollection ) private

fPerson : TPerson; published property person : TPerson read fPerson write fperson; end;

TForm1 = class(TForm) MultiLineEdit1: TMultiLineEdit; Button1: TButton; procedure Form1Create(Sender: TObject); procedure Form1Show(Sender: TObject); procedure Button1Click(Sender:

TObject);

private

{ Private declarations } me : TPerson; reader : TReader; writer : TWriter;

public

{ Public declarations } end;

var

Form1: TForm1; implementation procedure TForm1.Form1Create(Sender: TObject); var cousin : TPerson;

begin me := TPerson.Create; me.name := 'Erick'; me.cousins := TPeople.Create( TPerson );

cousin := TPerson( me.cousins.Add ); cousin.name := 'Paul';

cousin := TPerson( me.cousins.Add ); cousin.name := 'Marsha'; procedure TForm1.Form1Show(Sender: TObject); begin

writer.Initialize; me.Save( writer ); MultiLineEdit1.Lines.Text := writer.Output; end; procedure TForm1.Button1Click(Sender: TObject); begin me.Free; // totally destroy previous copy to start fresh me := Nil;

me := TPerson.Create; reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load( reader ); Form1Show( Sender );

end;

end.

In our OnCreate routine we initialize me to be Erick and set two of my cousins. We also create a TReader and TWriter for use later. procedure TForm1.Form1Create(Sender: TObject); var cousin : TPerson;

begin me := TPerson.Create; me.name := 'Erick'; me.cousins := TPeople.Create( TPerson );

cousin := TPerson( me.cousins.Add ); cousin.name := 'Paul'; cousin := TPerson( me.cousins.Add ); cousin.name := 'Marsha';

In our OnShow event handler we dump out the data, just like before. procedure TForm1.Form1Show(Sender: TObject); begin writer.Initialize; me.Save( writer ); MultiLineEdit1.Lines.Text := writer.Output; end; It displays the results:

{

"name": "Erick", "cousins": { } }

As you can see, cousins is empty. It turns out we need to extend TPeople to know how to write out the cousins property. We add to TPeople:

protected procedure SaveProperties( AWriter : TWriter ); override and the code

procedure TPeople.SaveProperties( AWriter : TWriter ); var person : TPerson; i : integer; begin // handle things we don't do ourselves inherited SaveProperties( AWriter );

// now save the people list AWriter.PropertyName('items'); AWriter.BeginArray( Count > 0 ); for i := 0 to Count - 1 do begin

if i > 0 then AWriter.Separator; person := TPerson( items[i] ); person.Save( AWriter );

end; AWriter.EndArray( Count > 0 ); end;

So we simply write out the property name (which we call items), start an array, cycle through the array and write each member, then end the array.

In our situation, TPeople has only one field/array we want to persist, the items field. But if there were other useful fields, we could use the inherited function to persist them. So for future expansion we included the inherited function at the start.

If we rerun the program it now lists:

{

"name": "Erick", "cousins": { "items": [ {

"name": "Paul" }, { "name": "Marsha" } ] } } This is exactly what we want.

The process of reading in the data is a little bit more complicated. When you look at the above JSON, cousins is a member of TPerson returning a TPeople, and to TPeople we are adding an array called items. So we need to extend TPerson and TPeople with some more overrides.

First we add the cousins property to TPerson. We use SameText() to do a caseinsensitive comparision of the PropertyName. We see they want to load cousins, so we need to create a cousins : TPeople structure. We skip over the used PropertyName and PropertySeparator, and load the object which is our cousins structure.

And if the PropertyName is not one of the ones we are specifically looking for, we are a good citizen and pass it along to the default handler.

function TPerson.LoadProperty( AReader : TReader ):boolean; var

PropertyName : string; begin result := False;

PropertyName := AReader.GetPropertyName;

if SameText( PropertyName , 'cousins' ) then begin // we need to create a home for the cousins first cousins := TPeople.Create( TPerson ); result := True; // we are handling it AReader.SkipPropertyName; AReader.SkipPropertySeparator;

cousins.LoadObject( AReader ); // ArrayElements( AReader ); end else result := inherited LoadProperty(AReader ); end;

We need to do a similar section on TPeople to read the items list.

function TPeople.LoadProperty( AReader : TReader ):boolean; var

PropertyName : string; begin result := False;

PropertyName := AReader.GetPropertyName;

if SameText( PropertyName , 'items' ) then begin // we need to create a home for the cousins first result := True; // we are handling it AReader.SkipPropertyName; AReader.SkipPropertySeparator;

LoadArray( AReader ); end else result := inherited LoadProperty(AReader );

Here the difference is we use LoadArray() because items is an array. Finally, we need to add the code to load the ArrayElement (TPerson) for each of the items.

function TPeople.LoadArrayElement( AReader : TReader ):boolean; var

tempperson : TPerson; propertyname : string; begin

tempperson := TPerson( Add ); tempperson.LoadObject( AReader); Result := True;

end;

The complete code is below. I’ve used {$ifdef} / {$endif} to comment out the code to show the initial version and the complete version so you can see what needed to be added.

unit persistarray1; interface uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns; type {$DEFINE DEMO}

TPerson = class (TCollectionItem ) private fname :string;

fcousins : TPeople;

published property name : string read fname write fname; property cousins : TPeople read fcousins write fcousins;

{$IFDEF DEMO} protected function LoadProperty( AReader : TReader ):boolean;

override; {$ENDIF} TPeople = class( TCollection ) private

fPerson : TPerson; published property person : TPerson read fPerson write fperson; {$IFDEF DEMO} protected procedure SaveProperties( AWriter : TWriter ); override; function LoadProperty( AReader : TReader ):boolean; override; function LoadArrayElement( AReader : TReader ):boolean; override; {$ENDIF} end;

TForm1 = class(TForm) MultiLineEdit1: TMultiLineEdit; Button1: TButton; procedure Form1Create(Sender: TObject); procedure Form1Show(Sender: TObject); procedure Button1Click(Sender:

TObject);

private

{ Private declarations } me : TPerson; reader : TReader; writer : TWriter;

public

{ Public declarations } end;

var Form1: TForm1; implementation

{$IFDEF DEMO} procedure TPeople.SaveProperties( AWriter : TWriter ); var

person : TPerson; i : integer; begin

// handle things we don't do ourselves inherited SaveProperties( AWriter ); // now save the people list AWriter.PropertyName('items'); AWriter.BeginArray( Count > 0 ); for i := 0 to Count - 1 do begin

if i > 0 then AWriter.Separator; person := TPerson( items[i] ); person.Save( AWriter );

end; AWriter.EndArray( Count > 0 ); end;

function TPeople.LoadArrayElement( AReader : TReader ):boolean; var tempperson : TPerson; propertyname : string; begin tempperson := TPerson( Add ); tempperson.LoadObject( AReader); Result := True; end;

function TPerson.LoadProperty( AReader : TReader ):boolean; var

PropertyName : string; begin result := False;

PropertyName := AReader.GetPropertyName;

if SameText( PropertyName , 'cousins' ) then begin // we need to create a home for the cousins first cousins :=

TPeople.Create( TPerson ); result := True; // we are handling it AReader.SkipPropertyName; AReader.SkipPropertySeparator;

cousins.LoadObject( AReader ); // ArrayElements( AReader ); end else result := inherited LoadProperty(AReader ); end;

function TPeople.LoadProperty( AReader : TReader ):boolean; var

PropertyName : string; begin result := False;

PropertyName := AReader.GetPropertyName;

if SameText( PropertyName , 'items' ) then begin // we need to create a home for the cousins first result := True; // we are handling it AReader.SkipPropertyName; AReader.SkipPropertySeparator;

LoadArray( AReader ); end else result := inherited LoadProperty(AReader ); end; {$ENDIF}

procedure TForm1.Form1Create(Sender: TObject); var cousin : TPerson;

begin me := TPerson.Create; me.name := 'Erick'; me.cousins := TPeople.Create( TPerson );

cousin := TPerson( me.cousins.Add ); cousin.name := 'Paul'; cousin := TPerson( me.cousins.Add ); cousin.name := 'Marsha'; reader := TReader.Create; writer := TWriter.Create; end; procedure TForm1.Form1Show(Sender: TObject); begin writer.Initialize; me.Save( writer );

MultiLineEdit1.Lines.Text := writer.Output; end; procedure TForm1.Button1Click(Sender: TObject); begin

me.Free; // totally destroy previous copy to start fresh me := Nil;

me := TPerson.Create; reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load( reader ); Form1Show( Sender );

end;

end.

If you are planning to use persistence to deal with MySQL, you will want to exchange data in MySQL’s time format, and to most people’s delight, the local timezone is used rather than UTC.

I’ve used {$IFDEF DEMO} to mark the small bit of code you need to add to a class or to its ancestor class.

Much of the code for for this example is stolen from the previous examples. Again, we display the serialized version, and when you press the button, it deserializes it back to the object.

unit persistime1; interface uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns; type {$DEFINE DEMO}

TPerson = class (TCollectionItem ) private fname :string; fbirthday : DateTime;

published property name : string read fname write fname; property birthday : DateTime

read fbirthday write fbirthday; {$IFDEF DEMO} protected procedure SaveProperty(AWriter: TWriter; const AName:

String); override;

function LoadProperty( AReader : TReader ):boolean; override; {$ENDIF} end;

TForm1 = class(TForm) MultiLineEdit1: TMultiLineEdit; Button1: TButton; procedure Form1Create(Sender: TObject); procedure Form1Show(Sender: TObject); procedure Button1Click(Sender:

TObject);

private

{ Private declarations } me : TPerson; reader : TReader; writer : TWriter;

public

{ Public declarations } end;

var Form1: TForm1; implementation

{$IFDEF DEMO} procedure TPerson.SaveProperty(AWriter:

TWriter; const AName: String); var

TempType: Integer; TempInstance: TObject; dt : DateTime; s : string; TempShortTimeFormat : string; TempShortDateFormat : string;

begin TempType:=PropertyType(AName); if (TempType <> TYPE_INFO_UNKNOWN) then

begin if TempType = TYPE_INFO_DATETIME then begin AWriter.PropertyName( AName );

dt := DateTime(GetProperty(AName)); TempShortTimeFormat := FormatSettings.ShortTimeFormat; // save it TempShortDateFormat := FormatSettings.ShortDateFormat; // save it FormatSettings.ShortTimeFormat := 'H:mm';

FormatSettings.ShortDateFormat := 'yyyy/M/d';

s := DateTimeToStr(dt);

FormatSettings.ShortTimeFormat :=

TempShortTimeFormat;

FormatSettings.ShortDateFormat :=

TempShortDateFormat;

AWriter.StringValue(s);

end

else inherited SaveProperty( AWriter, AName ); end;

end;

function TPerson.LoadProperty( AReader : TReader ):boolean;

var

PropertyName : string; PropType : Integer;

s : string;

dt : DateTime; TempShortTimeFormat : string;

TempShortDateFormat : string;

begin

result := False; PropertyName := AReader.GetPropertyName;

PropType:= PropertyType(PropertyName); if PropType = TYPE_INFO_DATETIME then begin AReader.SkipPropertyName; AReader.SkipPropertySeparator;

s := Areader.ReadString;

TempShortTimeFormat := FormatSettings.ShortTimeFormat; // save it TempShortDateFormat := FormatSettings.ShortDateFormat; // save it FormatSettings.ShortTimeFormat := 'H:mm'; FormatSettings.ShortDateFormat := 'yyyy/M/d'; dt := StrToDateTime(s); FormatSettings.ShortTimeFormat := TempShortTimeFormat; FormatSettings.ShortDateFormat := TempShortDateFormat;

SetProperty(PropertyName,dt ); end else result := inherited LoadProperty(AReader ); end; {$ENDIF} procedure TForm1.Form1Create(Sender: TObject); var cousin : TPerson;

begin me := TPerson.Create; me.name := 'Erick'; me.birthday := now;

reader := TReader.Create; writer := TWriter.Create; end; procedure TForm1.Form1Show(Sender: TObject); begin writer.Initialize; me.Save( writer ); MultiLineEdit1.Lines.Text := writer.Output; end;

procedure TForm1.Button1Click(Sender: TObject); begin me.Free; // totally destroy previous copy to start fresh me := Nil;

me := TPerson.Create; reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load( reader ); Form1Show( Sender );

end;

end.

When we save a class, the class calls SaveProperty for each property passing a name, so we use an overload method on SaveProperty. We call PropertyType() to get the property type of the property, and if it is a DateTime we handle it ourselves. Otherwise we just call the inherited function to handle all other types. We need to change the default date and time formatting properties, so we temporarily save their formats and return them again when we are done.

procedure TPerson.SaveProperty(AWriter: TWriter; const AName: String); var

TempType: Integer;

TempInstance: TObject; dt : DateTime;

s : string;

TempShortTimeFormat : string; TempShortDateFormat : string;

begin

TempType:=PropertyType(AName); if (TempType <> TYPE_INFO_UNKNOWN) then

begin

if TempType = TYPE_INFO_DATETIME then begin AWriter.PropertyName( AName ); dt := DateTime(GetProperty(AName)); TempShortTimeFormat := FormatSettings.ShortTimeFormat; // save it TempShortDateFormat := FormatSettings.ShortDateFormat; // save it FormatSettings.ShortTimeFormat := 'H:mm'; FormatSettings.ShortDateFormat := 'yyyy/M/d'; s :=

DateTimeToStr(dt); FormatSettings.ShortTimeFormat :=

TempShortTimeFormat;

FormatSettings.ShortDateFormat :=

TempShortDateFormat;

AWriter.StringValue(s);

end

else inherited SaveProperty( AWriter, AName ); end;

end;

The code to deserialize is also straightforward. We just check if the type is DateTime, swap around the format strings, read in the value, and restore the format string.

function TPerson.LoadProperty( AReader : TReader ):boolean;

var

PropertyName : string; PropType : Integer;

s : string;

dt : DateTime; TempShortTimeFormat : string;

TempShortDateFormat : string;

begin

result := False;

PropertyName := AReader.GetPropertyName;

PropType:= PropertyType(PropertyName); if PropType = TYPE_INFO_DATETIME then begin AReader.SkipPropertyName; AReader.SkipPropertySeparator;

s := Areader.ReadString; TempShortTimeFormat := FormatSettings.ShortTimeFormat; // save it

TempShortDateFormat := FormatSettings.ShortDateFormat; // save it FormatSettings.ShortTimeFormat := 'H:mm'; FormatSettings.ShortDateFormat := 'yyyy/M/d'; dt := StrToDateTime(s); FormatSettings.ShortTimeFormat := TempShortTimeFormat; FormatSettings.ShortDateFormat := TempShortDateFormat;

SetProperty(PropertyName,dt ); end else result := inherited LoadProperty(AReader ); end;

Creating database web applications is where EWB truly shines. This makes sense because Elevate Software was traditionally a database technology company.

Since version 2.05, EWB supports multiple TDatabase components, how many, it’s actually unlimited. Database is created automatically at application startup time for both visual and non-visual projects. It keeps track of all datasets used by the application and contains properties, methods and events for starting and commiting data transactions as well as rolling them back.

TDataSet manages data between the application and the database for both visual and non- visual projects. You typically have one TDataSet for each SQLtable in use.

TDataSets can be dropped on the visual form at design time or created at runtime. The important Columns property contains the column definitions for the dataset.

Database.LoadColumns loads the columns for the dataset directly from the database, transparently marshalling the values through a JSON (JavaScript Object NotatioN) string across a network connection for the user. TDataSet.LoadColumns similarly loads the columns using a JSON format, but it is up to your application to provide that JSON string. This provides a handy way of locally inserting values without having the baggage of a whole database setup.

Rows of data are loaded from the server using Database.LoadRows, or TDataset.LoadRows following the same server versus local Once the TDataSet is loaded, you can navigate using the First, Prior, Next and Last methods. The TDataSet can also call Insert, Update and Delete to deal with various row operations.

Insert takes a parameter: Append:Boolean. If true, the TDataSet is extended at the end, otherwise the new row is inserted at the current position. Remember to Save after your insert.

Update prepares the current TDataSet row for an update. Call Save when done. Delete deletes the current TDataSet row. TDataset.Find is used to find a specific or nearest match. It can specify a case insensitivity and a Locale insensitivity. The nearest match requires an active sort on the database (Sorted = True). If there is an active sort on the dataset, CaseInsensitive must match the SortCaseInsensitive property. TDataSet.Sort invokes the sorting if and only if Sorted is True and sort columns have been specified for the dataset. SortCaseInsensitive and SortLocaleInsensitive control how the sort is performed.

You only need call Sort after the sort directions are initially assigned for the columns. Subsequent to the sort, any row operations will automatically maintain the correct order of the active sort.

Our first database app will be a static local database on the Web client itself and is simple enough to show the principles without getting into the challenges of network communications and server configuration.

Create a new project. Drop a Database | TDataSet onto the form, it’s a nonvisual component, so it can be placed anywhere. Name it People. It will be our list of people.

Double-click on People’s Columns property in the Object Inspector, this opens the Columns editor. In the Columns Editor, press the + button to add a new column, it will be called PeopleDataColumn1 by default. In the Object Inspector, change the column name to PeopleID and the DataType to dtString. Repeat to add a column named PeopleName with DataType dtString and Length 50. Place two edit boxes on the form, name one edID and the edName. Set their DataSet properties to People (to match our DataSet), and their DataColumns to PeopleID and PeopleName respectively – do this by clicking the down arrow on the TEdit’s DataSet and selecting people, and down arrow on DataColumn and selecting the appropriate column.

Add a Toolbars | TDataSetToolBar to the form and set its DataSet to People. Now click on the main form and add the following OnShow handler and add procedure TForm1.Form1OnShow(Sender: TObject); begin Database.AutoTransactions := False; People.Open; People.LoadRows( '{ "rows": [

{ "PeopleID": "1",

"PeopleName": "Hon" },

{ "PeopleID": "2",

"PeopleName": "Daniel" }]}', False); end;

Press F9 and you will have a navigable database application, albeit a minimalist one. You can insert, delete, modify and search the records. Since there is no backend, all the data changes are lost.

You can add additional records programmatically with the following code in a TButton OnClick handler:

with People do begin Open; Insert; Columns['PeopleID'].AsString := '1'; Columns['PeopleName'].AsString:='Erick'; Save; Insert; Columns['PeopleID'].AsString := '2'; Columns['PeopleName'].AsString:='Rosie'; Save; end;

Let’s add a Master-Detail relationship. Add a TGrid, set its DataSet property to people, and press F9. The TGrid automatically uses the correct column names and displays when you run it.

Clicking on a name in the master table brings it into the current record field for the TEdits.

Change a field in its TEdit, hit update, and the master table reflects the change. The TDataSet replaces large amounts of code you would think you might have to write.

If you typed in the previous example, you may have run into problems entering the JSON code.

JSON is a popular standard for data exchange, not unlike XML. But it differs from XMLin that it is quite compact and fast for machines to parse. It is also relatively human-readable. But entering it manually is a pain, you should never have to do that.

EWB supports database transactions, where you make a bunch of changes on the client dataset and then can either commit the to the server or rollback the changes.

First set:

Database.AutoTransactions := False; The code is basically:

Database.StartTransaction; … do some operations …. if happy then

Database.Commit Else Database.RollBack;

You can test if you have outstanding changes to commit/rollback by testing Database.NumPendingRequests > 0. If the result of NumPendingRequests is non-zero when you think it should be zero, you may have to call Database.RetryPendingRequests.

Database.InTransaction is true if a transaction of ours is currently active. Database.TranactionLevel returns a non-negative integer to determine the depth of the transaction.

You can test the TDataSet.State for

-

dsInsert : inserting

-

dsUpdate: updating

-

dsBrowse: browsing

-

otherwise, not active

-

The transactions are not Database Locks, they do not prevent others from making changes. They are only pertinent to the client dataset – ie. the dataset in memory.

There is an excellent example in the EWB samples, see the transactions folder. Automatic transactions are a feature you can use if you set Database.AutoTransactions := True;

When you use AutoTransactions, the simple act of inserting or editing a row in the database locks the row in the client dataset until you commit with a Save operation. No transaction code needs to be written – assuming your database supports EWB’s locking. Many databases do not, so then you should turn off Automatic Transactions.

That remains true even if you have a more complicated setup, like a master/detail form. When you start inserting or editing the master row, EWB starts a transaction that does not commit until you save the master row. And during that time, any changes in the detail view will be executed in a nested transaction.

The single call to Save on the master row will commit all nested changes at once.

I recommend re-reading/refreshing the data rows periodically as other users may have

changed them, and also EWB can get out of sync after making changes it doesn’t understand. For example, when you create a new SQLrecord with SQLCREATE/Database Insert, the key field (eg. Index number) does not get assigned until the database enters the transaction, but it never updates the client. So you shoud refresh the client after every operation, and otherwise after long periods of inactivity. For this chapter we will only consider PHP databases using SQL. The reader is assumed to know some PHP and SQL. We will construct a skeletal PHP program less than 100 lines long that simulates a database server and logs the EWB-intended operations from the JSON based RESTful database operations.

This database server code is in lieu of an official PHP database from Elevate which was planned for release 2.05. My implementation is basic in many ways. The functionality it lacks: is a long list

• localization

• datetime support

• sorting

• full UTF8 character support (non-alphanumeric characters may change)

• BLOBs

• SQLstring length limiting

• Type validation

• Error checking

• Access controls

• Etc.

REST is a truly open standard of how to represent large collections of data objects over a network connection. Using JSON (or XMLon some other systems) over HTTP/HTTPS, REST allows one to specify the CRUD (create, read, update, delete) operations of a database. But it is truly stateless, you can reboot your server or replace it with a new server, and the REST operations will still make sense.

EWB uses two REST HTTP verbs: GET which gets one or more entries (akin to SQL SELECT), and PUT which batch uploads a transaction of one or more operations (such as SQL’s UPDATE, CREATE, DELETE).

The rest of this section is optional, you can read it to learn, or you can skip to the example.

All operations specify a Database name and a DataSet name to the server in the $_SERVER variable and a request=operationname. Also, if you specify a Userid and Password, they are transferred in the X-EWBUser and X-EWBPassword parameters of $_SERVER.

The GET/SELECT operation specifies one or more optional parameters, like dept=Engineering in addition to the aforementioned parameters. So a query to SELECT the list of people in Engineering would look like the following GET HTTPS://our.com/prog?method=rows&database=company&dataset=staff&d ept=Engineering And our server will return a JSON array containing the data. Other INSERT, UPDATE, DELETE operations are POSTed as HTTPS://our.com/prog? method=commit&database=companyf

Then it will contain a JSON array of the list of operations to be completed in the transaction with a dataset. Each operation will have an opcode: 1 for INSERT, 2 for UPDATE, and 3 for DELETE, as well as a list of before and after parameters specifying the object in question before the operation and the values after the operation.

I think this represents the bare minimum client you would want to consider for an EWB database application. The form has an empty TGrid named Grid1, a TDataSet named DataSet1 and buttons names btInsert, btUpdate and btDelete.

unit sqlclient1; interface uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebData, WebGrids, WebBtns; type TForm1 =

unit sqlclient1; interface uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebData, WebGrids, WebBtns; type

TForm1 = class(TForm) DataSet1: TDataSet; Grid1: TGrid; btInsert: TButton; btUpdate: TButton; btDelete: TButton; procedure Form1Show(Sender: TObject); procedure btInsertClick(Sender: TObject); procedure btUpdateClick(Sender: TObject); procedure btDeleteClick(Sender: TObject);

private { Private declarations } procedure Select_Query;

procedure CommitError(Sender: TObject; const ErrorMsg:

String); procedure AfterCommit(Sender: TObject);

public { Public declarations } end;

var Form1: TForm1; implementation

procedure TForm1.Form1Show(Sender: TObject); var col : TDataColumn; colgrid : TGridColumn; begin Database.OnCommitError := CommitError; Database.AfterCommit := AfterCommit;

Database.AutoTransactions := False; Database.DatabaseName := 'staff'; // important URL to our database PHP application Database.BaseURL := 'simple.php';

// you will normally fill these in from user input Database.UserName := 'joe'; Database.Password := 'hill'; // could replace with JWT token

// define the dataset, you could also do this visually col := Dataset1.Columns.Add; col.Name := 'StaffID'; col.DataType := dtInteger;

col := Dataset1.Columns.Add; col.Name := 'Name'; col.DataType := dtString;

// define the grid, could also be done visually

Grid1.Dataset := Dataset1;

Grid1.NewColumn;

colgrid := Grid1.Columns[0]; colgrid.Header.Caption := 'StaffID'; colgrid.DataColumn := 'StaffID';

Grid1.NewColumn;

colgrid := Grid1.Columns[1]; colgrid.Header.Caption := 'Name'; colgrid.DataColumn := 'Name';

// execute our first query Select_Query(); end;

procedure TForm1.CommitError(Sender: TObject; const ErrorMsg: String); begin

HideProgress; ShowMessage( ErrorMsg , 'SQL Error'); if window.confirm('Do you want to retry the commit ?') then

begin ShowProgress('Retrying commit Database.RetryPendingRequests;

');

end; end; procedure TForm1.AfterCommit(Sender: TObject); begin HideProgress; end;

procedure TForm1.Select_Query; begin

Dataset1.Params.Add('dept=Engineering');

Database.LoadRows( DataSet1 ); end;

procedure TForm1.btInsertClick(Sender: TObject); begin

Dataset1.Insert;

Dataset1.Columns['Name'].AsString := 'Steve Smith'; Dataset1.Save; end;

procedure TForm1.btUpdateClick(Sender: TObject); begin

Dataset1.First;

Dataset1.Update;

Dataset1.Columns['Name'].AsString := Dataset1.Columns['Name'].AsString + ' Esquire';

Dataset1.Save;

end;

procedure TForm1.btDeleteClick(Sender: TObject); begin

Dataset1.Last;

Dataset1.Delete;

end;

end.

The most important setup details appear in the page’s onShow handler. The AutoTransactions is new for 2.05 and eliminates the need to start and end a transaction to do simple operations. BaseURLis the URLto our database server PHP application.

Database.AutoTransactions := True; Database.DatabaseName := 'staff'; // important URL to our database PHP application Database.BaseURL := 'simple.php';

// you will normally fill these in from user input Database.UserName := 'joe'; Database.Password := 'hill'; // could replace with JWT token

We also set the user name and the user password. These will be passed to our server. In a modern OAuth2 environment, you would replace the password with a JSON Web Token (JWT), but that’s another story.

Next in the function we add zero or more criteria to narrow the search/SELECT, by adding “dept=Engineering” we are saying only Engineering users will be considered. Then we load the DataSet.

Dataset1.Params.Add('dept=Engineering');

Database.LoadRows(DataSet1);

The rest of the operations are straightforward.

This code is very incomplete, it is meant to show you how to access the parameters to produce SQLcommands. To keep it simple, there little error checking, no data validation or access controls in this sample, so it would be a nightmare to place on the big bad Internet in its present form. This is an educational tool only.

In about 90 lines of boilerplating, and a few lines PHP to SQLconversion code you will

replace (functions starting with sql

examples, including the transaction example.

),

this code can function with most EWB database

It does not handle BLOBs (binary large objects) for things like pictures, but it can return a URLand you can use that to reference a picture for download. simple.php <?php

function sql_select($user, $pass, $database, $dataset, $inputargs)

{

$args = 'WHERE '; $i = 0; foreach ( $inputargs as $a=>$b) {

if ( $i++ > 0 ) $args .= " && "; $args .= " ( $a = '$b' )";

}

savelog( "SQL: SELECT * FROM $dataset $args"); return( '{ "rows" : [{"StaffID":1 , "Name":"Erick Jones"}, {"StaffID":2 , "Name":"Rosie Jones"}]}'); }

function sql_insert($user, $pass, $database, $dataset, $outputargs)

{

$i = 0; foreach ( $outputargs as $a=>$b) { if ( $i++ > 0) {

$sqla .= ','; $sqlb .= ',';

}

$sqla .= $a;

$sqlb .= "'$b'";

}

savelog( "SQL: INSERT INTO $dataset ( $sqla ) VALUES ( $sqlb )"); return '';

}

function sql_update($user, $pass, $database, $dataset, $inputargs, $outputargs)

{

$inargs = 'WHERE '; $i = 0; foreach ( $inputargs as $a=>$b) {

if ( $i++ > 0 ) $inargs .= " AND "; $inargs .= " ( $a = '$b' )";

}

$outargs = 'SET '; $i = 0; foreach ( $outputargs as $a=>$b) {

if ( $i++ > 0 ) $outargs .= " && "; $outargs .= " $a = '$b' ";

}

savelog("SQL: UPDATE $data $outargs $inargs"); return '';

}

function sql_delete($user, $pass, $database, $dataset, $inputargs)

{

$inargs = 'WHERE '; $i = 0; foreach ( $inputargs as $a=>$b) {

if ( $i++ > 0 ) $inargs .= " AND "; $inargs .= " ( $a = '$b' )";

}

savelog("SQL: DELETE FROM $dataset $inargs"); return '';

}

function sql_userpassword( $user, $pass )

{

return 1;

}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

=-= =-=-=-=-=-= define("EWB_OPERATION_UNKNOWN", 0); define("EWB_OPERATION_INSERT", 1); define("EWB_OPERATION_UPDATE", 2); define("EWB_OPERATION_DELETE", 3);

header('Cache-Control: no-cache'); //header('Content-Type:vapplication/json; charset=utf-8'); function safetext( $src )

{

return( htmlentities( $src, ENT_QUOTES ));

}

function savelog( $msg )

if ( $f = fopen("log","a+")) { fwrite( $f, "$msg\n" ); fclose( $f );

}

}

function getpostdata()

{

$postdata = file_get_contents("php://input"); return ($postdata);

}

function rest_error($msg) {

header("EWB_HTTP_ERROR", "HTTP/1.0 500 $msg"); exit(0);

}

if( ! isset($_REQUEST['method'])) exit; $method = $_REQUEST['method'];

$user = (isset($_SERVER['X-EWBUser']))? safetext( $_SERVER['X-EWBUser']): ''; $pass = (isset($_SERVER['X-EWBPassword']))? safetext($_SERVER['X-EWBPassword']) : '';

if ( ! sql_userpassword( $user, $pass )) rest_error( “bad userid or password”); switch($method)

{

case 'rows':

$options = array(); if( ! isset($_REQUEST['dataset']))

rest_error('missing dataset'); $dataset = safetext( $_REQUEST['dataset'] ); if( ! isset($_REQUEST['database']))

rest_error('missing database'); $database = safetext( $_REQUEST['database'] ); foreach($_REQUEST as $directive=>$value) if(($directive != 'database') && ($directive != 'dataset') && ($directive != 'method')) $options[safetext($directive)] = safetext($value); } $output = sql_select($user, $pass, $database, $dataset,

$options); break; case 'commit':

$data = getpostdata(); $json = json_decode($data, TRUE); $json2 = $json['operations'];

foreach($json2 as $operation) { if( ! isset($operation['operation'])) rest_error('missing operation'); $op = $operation['operation']; if( ! isset($_REQUEST['database'])) rest_error('missing database'); $database = safetext( $_REQUEST['database'] ); if( ! isset($operation['dataset'])) rest_error('missing dataset'); $dataset = safetext( $operation['dataset'] );

$inputargs = array(); foreach ( $operation['beforerow'] as $a=>$b) $inputargs[safetext($a)] = safetext($b);

$outputargs = array(); foreach ($operation['afterrow'] as $a => $b ) $outputargs[safetext($a)] = safetext($b);

switch($op) { case EWB_OPERATION_INSERT:

$output = sql_insert($user, $pass, $database, $dataset, $outputargs); break; case EWB_OPERATION_UPDATE:

$output = sql_update($user, $pass, $database, $dataset, $inputargs, $outputargs); break; case EWB_OPERATION_DELETE:

$output = sql_delete($user, $pass, $database, $dataset, $inputargs); break; default: rest_error("invalid db op $op"); }

}

break;

default: rest_error("Invalid method $method"); } print "$output\n";

?>

You can see we handle the two types of requests described ealier, rows for SELECT and commit for all INSERT, DELETE and UPDATE commands. This simple program produces an output log file called log in the local subdirectory (assuming it has write permissions).

For this example output, I ran the web page which called rows to get a list of Engineering people, and then I called UPDATE and DELETE by starting a commit, editing, deleting, and closing commit.

SQL: SELECT * FROM DataSet1 WHERE ( dept = 'Engineering' ) SQL: INSERT INTO DataSet1 ( StaffID,Name ) VALUES ( '','Steve Smith' )

SQL: UPDATE SET Name = 'Erick Jones Esquire' WHERE ( StaffID

= '1' ) AND ( Name = 'Erick Jones' )

SQL: DELETE FROM DataSet1 WHERE ( StaffID = '' ) AND ( Name

= 'Steve Smith' )

You may wonder why we pass the userid and password (or its cousin the JWT token) to the SQLfunction. As you build more complex systems, undoubtedly you will start using other REST services to answer your queries, and you will need to either identify the user or pass the OAuth2 JWT token. Until then, it doesn’t hurt to have an unused parameter present. I would recommend that you reopen the dataset after closing any commit if you use commits, or after you make any change if you use autocomits, because the master table can get out of sync when a person makes nonsensical edits. For example, it allows you to set, change or clear the person id number, which no database ever should allow, and as we saw in the output above, unassigned values like StaffID of INSERTed records may be missing, making the subsequent DELETE fail.

I built the JSON by hand in the request= rows response. Sometimes the PHP internal JSON building library consumes too much memory and the process terminates, so building JSON by hand avoids that problem.

For a production database you would replace the sql

would clean up the arguments, test their sanity, and then test user permissions before performing any database operation.

functions with your own. They

What is surprising to many developers from other technologies, when you write these

functions you are enabling them to be used by whomever and however that person may choose. There is no guarantee the database request came from any specific line of your code, it could be another developer, or it could be a hacker. Code wisely my friend.

SQLtable JOINs allow you to SELECT and UPDATE on multiple tables at once. Suppose you were showing a table of userids joined with addresses. You could SELECT * FROM userids NATURALJOIN addresses WHERE dept = ‘Engineering’ Likewise, you can update fields on the JOIN

UPDATE userids NATURALJOIN addresses SET street=’king’ WHERE userid = ‘tomk’ You only need to differentiate tables during INSERT and DELETE operations. Normally you will add the three A’s, Access control, Authorization and Auditing.

Only the simplest databases do not care for who is connected – say perhaps a phone book app. But if one can enter or change data, usually some form of access control and authorization is required to keep the bad people on the Internet from destroying your data.

Even read-only data is precious. If your customer list were exposed, your competitors could steal your customers. As mentioned previously, you will sometimes want to download a BLOB, such as a PDF document, or a jpeg image.

While this code does not handle BLOBs, it can reference a URL. That might seem awkward, wouldn’t the URLhave to check the user’s permission to view the file? Not really, you can specify a difficult-to-guess and impossible-toenumerate filename and that will secure your files.

YouTube does exactly that with its videos. It generates a random URLwith a packed base64 encoding for each filename at store time. If another file exists with that name, it tries other names until it finds a free one. But the namespace is so enormous, multiple hits are rare.

Also, the odds of a person requesting the any correct filename are miniscule and can be made infitessimal if we also check for excessively many failed attempts from a given IP address and impose an exponential backoffs after n failures. The code to accomplish this is relatively straightforward, but be sure you are using a good random number generator, or else your filenames will be predictable. ObjectPascal over JavaScript can only do so much on the client. Eventually you will want to communicate with a server for:

- Database transactions

- Messaging/Email

- Any number of things not doable by the client directly

- The EWB environment includes built-in functionality to marshal data into a standard

format (JSON) and use protocols (HTTP/HTTPS) to connect to servers and ask them to do things.

HTTP/HTTPS have verbs such as:

- GET

- PUT

- POST

- DELETE

- HEAD

These commands are enough to Create/Read/Update/Delete (CRUD) objects on the web in

a RESTful manner.

They can also perform RPC (remote procedure calls) where the Delphi code in the client ends up calling a PHP function in the server, passing parameters to and fro.

The RPCs on the server can talk to other services, such as Google’s or Microsoft’s network services, and perform all manner of tasks such as geolocation, scheduling, messaging, etc.

Some of our examples follow the Elevate strategy directly, others are mashups of our own. We will be sure to explain each. Remember CORS (Cross Origin Resource Sharing) imposes limitations on your web page:

it means your web page only communicates with the server that is hosting the page. If you

intend other servers to be used, you must enable Cross-Origin Resource Sharing on both the development site, and the production site. This is not specifically a EWB topic, it applies to all web client technologies.

If data is going across a network, you really want it encrypted. The following functions can help. CheckHTTPS determines if either the connection is HTTPS, or if the connection is to localhost (ie. the local machine) which means unencrypted is okay. function CheckHTTPS: boolean; var

hostname : string; protocol : string; begin

protocol := LowerCase(window.location.protocol); hostname := LowerCase(window.location.hostname);

result := ( hostname = 'localhost')or( protocol = 'https:');

end;

MakeHTTPS makes the current page HTTPS unless it is on the localhost, in which case it can stay unenecrypted.

procedure MakeHTTPS;

var

s : string;

hostname, port, search : string; pathname , name : string;

begin

if not CheckHTTPS then begin hostname := window.location.hostname; port := window.location.port; search := window.location.search; pathname := window.location.pathname;

s := 'https:' + hostname;

if ( port <> '' ) then s := s + ':'+ port; if (pathname <>

'') then s := s + pathname; if ( search <> '' ) then s := s

+ search;

window.location.replace( s ); end;

end;

This first example does not follow Elevate’s standard exchange of data, but it is easy to read and will set the stage for future examples.

We will define a protocol where our Object Pascal client send data parameters to a PHP application’s function, and that function returns data variables to the client. This is an RPC or Remote Procedure Call.

For our first simple example, we will get a message-of-the-day for a given user, so we need to pass the userid, a password to verify it is truly our trusted user, and a function name for the request.

Eg.

userid = ‘erick’ password = ‘happy’ method = ‘advice’ : remote function call

And we will get returned result = 0 : meaning failure result = 1 : meaning success

and if result =1, we will be returned some advice in the variable wisdom. Eg. wisdom = ‘the secret to life is EWB’

For our example, use a TEdit to hold a userid, a TPasswordEdit to enter a password, a TButton to invoke the transaction, and a TLabel to hold the result. Here are the two main EWB functions: We call GetWisdom and it has a callback called GetWisdomResults that updates the TLabel.

procedure TForm1.Button1Click(Sender: TObject); begin GetWisdom( Edit1.Text, PasswordEdit1.Text); Label1.Caption := ‘…busy’; end;

procedure

TForm1.GetWisdomResults(results:Boolean;wisdom:string);

begin

if results then

Label1.Caption := wisdom else Label1.Caption := 'results not found'; end;

The support functions in EWB are:

procedure TForm1.GetWisdom( userid, passwordtext : string ); begin if not assigned( WebRequest ) then WebRequest := TServerRequest.Create( self );

with WebRequest do begin OnComplete := GetWisdomRequestComplete; RequestContent.Clear; RequestHeaders.Clear; URL:='loginmodule.php'; Method:=rmPost; RequestHeaders.Values['Content-Type']:='text/plain'; RequestContent.Values['userid']:= userid; RequestContent.Values['password']:= passwordtext;

RequestContent.Values['method'] := 'GetWisdom'; Execute; end;

end;

This code allocates a TServerRequest if we don’t already have a previous one, sets the request handler and marshals in the data and the method name. When the server replies, the following code is invoked:

procedure TForm1.GetWisdomRequestComplete(Request:

TServerRequest);

var

rpcresults : TStringList; begin if (Request.StatusCode <> HTTP_OK) then begin

ShowMessage('Error: '+Request.ResponseContent.Text); GetWisdomResults( False, ''); end else begin rpcResults := TStringList.Create; rpcResults.Text := Request.ResponseContent.Text;

if rpcResults.Values['result'] = '1' then

GetWisdomResults( True, rpcResults.Values['wisdom']) else GetWisdomResults( False, ''); rpcResults.Free; end; end;

Now all that’s needed is the PHP server. $data = parse_data(); if( ! isset($data['method'])) print "result=0\nerror=No method specified\n";

else

{

$method = $data['method']; switch($method)

{

case 'GetWisdom': GetWisdom($data);

default: print "result=0\nerror=method $method not found\n"; exit();

}

This code parses the POST data, checks for a method, and if we understand the method, invokes it with the data. Here is the parse_data() function:

function parse_data() { global $HTTP_RAW_POST_DATA;

$z = array(); $s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s); foreach($xx as $a=>$b) {

$yy = explode('=', trim($b)); if(count($yy) > 1)

$z[$yy[0]] = $yy[1]; } return ($z);

}

The RPC function we call is:

function GetWisdom($data) { if(CheckPassword($data)) {

$res = 'Secret to life is EWB'; print "result=1\n"; print "secret=$res\n";

}

else

{

print "result=0\n"; }

Nice and simple. Note, it calls CheckPassword which would normally be an LDAP lookup, or an OAuth2 call, or a database lookup, or a RADIUS call. We’ll just hard code it for now:

function CheckUserid($data) { if(isset($data['userid']) && isset($data['password'])) { $userid = $data['userid']; $password = $data['password']; if(($userid == 'erick') && ($password == 'happy')) return

(1);

}

return (0);

}

Most of this code was boiler-plating, except the call to GetWisdom().

I’m not recommending you follow this path, but it shows the steps involved if you choose to roll your own protocol or support an existing system. And once you have one function, like our GetWisdom() in PHP, adding more is trivial.

Here is our full example EWB source:

unit simpleweb; interface uses WebHttp, WebCore, WebUI, WebForms, WebCtrls, WebBtns, WebEdits, WebLabels, WebBrwsr; type

TForm1 = class(TForm) Button1: TButton; Edit1: TEdit; PasswordEdit1: TPasswordEdit; Label1: TLabel; Browser1: TBrowser; procedure Button1Click(Sender: TObject);

private

{ Private declarations }

WebRequest : TServerRequest; procedure GetWisdom( userid, passwordtext : string ); procedure GetWisdomResults( results : Boolean ; wisdom :

string ); procedure GetWisdomRequestComplete(Request:

TServerRequest);

public

{ Public declarations } end;

var Form1: TForm1;

implementation

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

=-==-=-=-=-=-=-=-=-=-=

procedure TForm1.GetWisdom( userid, passwordtext : string ); begin if not assigned( WebRequest ) then WebRequest := TServerRequest.Create( self );

with WebRequest do begin OnComplete := GetWisdomRequestComplete; RequestContent.Clear; RequestHeaders.Clear; URL:='loginmodule.php'; Method:=rmPost; RequestHeaders.Values['Content-Type']:='text/plain'; RequestContent.Values['userid']:= userid; RequestContent.Values['password']:= passwordtext; RequestContent.Values['method'] := 'GetWisdom'; Execute; end;

end;

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

=-==-=-=-=-=-=-=-=-=-=

procedure TForm1.GetWisdomRequestComplete(Request:

TServerRequest); var rpcresults : TStringList;

begin if (Request.StatusCode <> HTTP_OK) then begin ShowMessage('Error: '+Request.ResponseContent.Text); GetWisdomResults( False, '');

end else begin Browser1.DocumentText := Request.ResponseContent.Text; rpcResults := TStringList.Create; rpcResults.Text := Request.ResponseContent.Text; if rpcResults.Values['result'] = '1' then

GetWisdomResults( True, rpcResults.Values['wisdom']) else GetWisdomResults( False, ''); rpcResults.Free;

end;

end;

procedure TForm1.Button1Click(Sender: TObject); begin GetWisdom( Edit1.Text, PasswordEdit1.Text); end;

procedure TForm1.GetWisdomResults( results : Boolean ; wisdom : string ); begin

if results then

Label1.Caption := wisdom else Label1.Caption := 'results not found'; end; end.

And here is the complete PHP <?php function parse_data() { global $HTTP_RAW_POST_DATA;

$z = array(); $s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s); foreach($xx as $a=>$b) {

$yy = explode('=', trim($b)); if(count($yy) > 1)

$z[$yy[0]] = $yy[1]; } return ($z);

}

function CheckUserid($data) { if(isset($data['userid']) && isset($data['password'])) { $userid = $data['userid']; $password = $data['password'];

if(($userid == 'erick') && ($password == 'happy')) return

(1);

}

return (0);

}

function GetWisdom($data) { if(CheckPassword($data)) {

$res = 'Secret to life is EWB'; print "result=1\n"; print "secret=$res\n";

}

else

{

print "result=0\n"; }

}

$data = parse_data(); if( ! isset($data['method'])) print "result=0\nerror=No method specified\n";

else {

$method = $data['method']; switch($method)

{

case 'GetWisdom': GetWisdom($data); break; default: print "result=0\nerror=method $method not found\n"; exit();

}

}

?>

There are a few shortcomings to this simple protocol:

- It’s not a standard way to exchange data. A standards based way would use XML, JSON

or similar.

- You cannot specify multi line strings. You would have to break a string apart into separate lines and say:

Address1 = 200 Stoke Dr. Address2 = Waterloo, On. Address3 = Canada Address0 = 3

Setting Address0 to 3 indicates there are three Address lines. It’s not hard, but it’s not great. Unlike the standards, it’s not self-specifying. You need to make up and know what all the parameters are. EWB likes results in JSON format, and that solves these problems. Only minor changes are needed to the client and server. First, the server:

We will use the PHP json_encode() function which takes a PHP object as its parameter. So first we will define the results type:

class result {

public $wisdom = "";

}

We will define a global $error set to null. If an error is found, functions just need to set $error to a value and the PHP code will return an HTTP error with the error code returned as the sole text.

$data = parse_data(); $result = new result(); if( ! isset($data['method'])) {

$error = "No method specified"; } else

{

$method = $data['method']; switch($method)

{

case 'GetWisdom': GetWisdom($data); break; default:

$error = "method $method not found"; }

}

// if error is set, then we had an error, otherwise success if ( $error !== null ) { header("HTTP/1.1 400"); print $error; } else { print json_encode( $result );

}

Here is our updated GetWisdom():

function GetWisdom($data ) { global $error, $result; if(CheckPassword($data)) {

$res = 'Secret to life is EWB'; $result->wisdom= $res;

}

else

{

$error = 'bad userid or password';

}

}

So the whole PHP file is:

<?php

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= class result { public $wisdom = "";

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // boilerplate function parse_data()

{

global $HTTP_RAW_POST_DATA;

$z = array(); $s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s); foreach($xx as $a=>$b) {

$yy = explode('=', trim($b)); if(count($yy) > 1)

$z[$yy[0]] = $yy[1]; } return ($z);

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // simple check userid/password function CheckPassword($data )

{

global $result; if(isset($data['userid']) && isset($data['password'])) {

$userid = $data['userid']; $password = $data['password']; if(($userid == 'erick') && ($password == 'happy'))

return (1);

}

return (0);

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // updated GetWisdom

function GetWisdom($data ) { global $error, $result; if(CheckPassword($data)) {

$res = 'Secret to life is EWB'; $result->wisdom= $res;

}

else

{

$error = 'bad userid or password'; }

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // main code $data = parse_data(); $result = new result(); if( ! isset($data['method'])) {

$error = "No method specified"; } else

{

$method = $data['method']; switch($method)

{

case 'GetWisdom': GetWisdom($data); break; default:

$error = "method $method not found"; }

}

// if error is set, then we had an error, otherwise success if ( $error !== null ) { header("HTTP/1.1 400"); print $error;

} else {

print json_encode( $result );

}

?>

The major changes to the client program is that we use a TWisdom to hold the result set:

type TWisdom = class ( TPersistent ) private fwisdom : string; published property wisdom : String read fwisdom write fwisdom; end; The data receiver is greatly simplified:

procedure TForm1.GetWisdomRequestComplete(Request:

TServerRequest);

begin

if (Request.StatusCode <> HTTP_OK) then begin ShowMessage('Error: '+Request.ResponseContent.Text); GetWisdomResults( False, Request.ResponseContent.Text);

end

else

begin

if not Assigned( reader ) then reader := TReader.Create; if not Assigned( wisdom ) then wisdom := TWisdom.Create; reader.Initialize( Request.ResponseContent.Text ); wisdom.Load( reader ); GetWisdomResults( True ,''); end; end;

And the upcall method is simplified too:

procedure TForm1.GetWisdomResults( results : Boolean ; errmsg : string ); begin if results then Label1.Caption := wisdom.wisdom else Label1.Caption := 'results not found:' + errmsg; end;

So the whole client looks like this:

unit simpleweb3; interface uses WebHttp, WebCore, WebUI, WebForms, WebCtrls, WebBtns, WebEdits, WebLabels, WebBrwsr; type TWisdom = class ( TPersistent ) private fwisdom : string; published property wisdom : String read fwisdom write fwisdom; end;

TForm1 = class(TForm) Button1: TButton; Edit1: TEdit; PasswordEdit1: TPasswordEdit; Label1: TLabel; Browser1: TBrowser; procedure Button1Click(Sender: TObject);

private

{ Private declarations }

WebRequest : TServerRequest; Wisdom : TWisdom; reader : TReader; procedure GetWisdom( userid, passwordtext : string ); procedure GetWisdomResults( results : Boolean ; errmsg:

string ); procedure GetWisdomRequestComplete(Request:

TServerRequest);

public

{ Public declarations } end;

var Form1: TForm1; implementation

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

=-=-

procedure TForm1.GetWisdom( userid, passwordtext : string ); begin if not assigned( WebRequest ) then WebRequest := TServerRequest.Create( self );

with WebRequest do begin OnComplete := GetWisdomRequestComplete; RequestContent.Clear; RequestHeaders.Clear; URL:='jsonserver.php'; Method:=rmPost; RequestHeaders.Values['Content-Type']:='text/plain'; RequestContent.Values['userid']:= userid; RequestContent.Values['password']:= passwordtext; RequestContent.Values['method'] := 'GetWisdom'; Execute; end;

end;

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

=-= procedure TForm1.GetWisdomRequestComplete(Request:

TServerRequest);

begin

if (Request.StatusCode <> HTTP_OK) then begin ShowMessage('Error: '+Request.ResponseContent.Text); GetWisdomResults( False, Request.ResponseContent.Text);

end

else

begin

if not Assigned( reader ) then reader := TReader.Create; if not Assigned( wisdom ) then wisdom := TWisdom.Create; reader.Initialize( Request.ResponseContent.Text ); wisdom.Load( reader ); GetWisdomResults( True ,''); end; end;

procedure TForm1.Button1Click(Sender: TObject); begin GetWisdom( Edit1.Text, PasswordEdit1.Text); end; procedure TForm1.GetWisdomResults( results : Boolean ; errmsg : string ); begin if results then

Label1.Caption := wisdom.wisdom else Label1.Caption := 'results not found:' + errmsg; end; end.

In this chapter we will deal with interfacing with JavaScript by loading JavaScript modules, calling JavaScript functions, interfacing with the Document Object Model, and learning how to translate code fragments into Object Pascal.

There are a number of reasons why you would want to call JavaScript routines.

In some cases, you will call JavaScript for the above reasons, other times you will choose to implement the functionality in EWB Object Pascal. Personally, I prefer Object Pascal for the reasons I outlined in the first chapter, even though the resulting code that executes is in fact JavaScript.

For the rest of this chapter and any units you write which involve JavaScript, include WebDom in your uses line. It contains the essentials needed to interface with JavaScript and you will get random errors if you forget it.

Define your types in the interface section. Declare all types before you declare variables.

Things written in JavaScript are of class object according to EWB terminology, whereas most EWB classes are of class TObject. JavaScript itself is a classless language, it’s an object language.

EWB prepends a header on class variables of type TObject. Consider

Type TDog = class( TObject ) public petname : string;

end; var mydog : TDog; begin mydog = TDog.Create; mydog.petname := ‘harry’; end;

EWB will convert this to the following JavaScript:

mydog = test_tdog.$p.create.call(new test_tdog()); mydog.petname = "harry";

which gives a similar but slightly different result than usual JavaScript: mydog := new Object(); mydog.petname := ‘harry’;

In many cases the default class(TObject) will work, but there are some libraries which will get upset because the mydog object is different due to the creation technique.

If you get an error and need to simulate the above JavaScript code, you have to do things slightly differently. First, declare the class of no ancestor type, or of type object, either way it will be a descendant of object. You will also have to declare it external.

Type External TGlobalDog = class Public

Property petname : string read wirte; End; Var myglobaldog : TGlobalDog;

begin myglobaldog := TGlobalDog( eval( ‘new Object();’)); myglobaldog.petname := ‘harry’; end;

This will produce the JavaScript code:

myglobaldog := eval(‘ new Object()’); myglobaldog.petname := ‘harry’;

EWB has two types we will use frequently in this chapter: variant and object. Object refers to classes. Variant refers to any type: it can be an integer, string, object, etc.

When you need to access a JavaScript function, declare it external as we did above with eval(). External function eval ( s : string ) : object; If you do not care about the result of a JavaScript function, you can declare it as a procedure rather than function. Variant is the type of JavaScript variables if you are not concerned about the type, and cast it to the type you want.

var External myArg : variant; S : string;

Begin S := string( myArg ); End;

If the external object is a class with methods and properties, declare them as such. Declare the class of no particular type, or declare it of type object, which is the same thing. For example, consider a JavaScript class we call library, declared to the EWB class TLibrary.

Type External Tlibrary emit library = class

Public Property name : string read; Property obj : integer read

write; Function init( arg1: integer;

arg2 : variant ):integer; End;

Then declare the global variable of that type Var external library : TLibrary; And you can use it in code:

Begin Result := Library.init( 1, 3 ); End;

Note, EWB does not reference the global variable until your code does. So if you load the library later, you must wait until it’s fully loaded before accessing the variables. The easiest way to do this are to use TScript to load the library, then either call the library from its onload handler, or set a flag during onload that indicates the library is available for use. We’ll see an example of this flagging later in the OAuth 2.0 example.

Some of these preliminaries are difficult now, but they should become clearer with the examples shown next. The DOM is used to access parts of your web page to read, change, or create new ones. You can also use the DOM to add JavaScript or CSS files if you wish. Google is your friend to learn about the DOM.

To access the DOM, include the WebDom EWB module to your source file. It exposes the external (meaning JavaScript) variable window of class type TWindow.

var external window: TWindow; And TWindow is defined by the EWB uses files as:

external TWindow emit Window = class public { Properties } property closed: Boolean read; property defaultStatus: String read write; property document: TDocument read; property event: TEvent read; // IE-only property frames: TWindowList read; property google:

TGoogle read; property history: THistory read; property innerHeight: Integer read;

// Supported by IE9 or higher

property innerWidth: Integer read; // Supported by IE9 or higher property localStorage: TStorage read; property location:

TLocation read; property name: String read write; property navigator: TNavigator read; property opener: TWindow read; property orientation: Integer read; // Mobile platforms only property outerHeight: Integer read; // Not supported by IE property outerWidth: Integer read; // Not supported by IE property pageXOffset: Integer read; // Not supported by IE property pageYOffset: Integer read; // Not supported by IE property parent: TWindow read; property screen: TScreen read; property screenLeft: Integer read; // IE-only property screenTop: Integer read; // IE-only property screenX:

Integer read; // Not supported by IE property screenY:

Integer read; // Not supported by IE property sessionStorage: TStorage read; property status: String read write; property top: TWindow read; property window: TWindow read; { Events } property onblur: TEventHandler read write; property onerror: TErrorEventHandler read write; property onfocus: TEventHandler read write; property ongooglemapsload:

TGoogleMapsLoadHandler read write; property onload: TEventHandler read write; property onresize: TEventHandler read write; property onunload: TEventHandler read write; { Methods } procedure addEventListener( const type: String; listener: TEventHandler; useCapture: Boolean); procedure alert(const message: String); procedure blur; procedure cancelAnimationFrame(animationId: Integer); procedure clearInterval(intervalId: Integer); procedure clearTimeout(timeoutId: Integer); procedure close;

function confirm(const question: String): Boolean; procedure detachEvent(const type: String; handler: TEventHandler); procedure focus; function getComputedStyle( elt: TDOMElement; const pseudoElt: String):

TCSS2Properties;

procedure moveBy(dx, dy: Integer); procedure moveTo(x, y: Integer); function open( const url: String; const name: String=''; const features: String=''; replace: Boolean=False): TWindow; procedure print; function prompt( const message: String; default: String): String; procedure removeEventListener( const type: String; listener: TEventHandler; useCapture: Boolean); function requestAnimationFrame( callback: TAnimationHandler): Integer; procedure resizeBy(dw, dh: Integer); procedure resizeTo(w, h: Integer); procedure scrollBy(dx, dy: Integer); procedure scrollTo(x, y: Integer); function setInterval( code: TIntervalHandler; intervalId: Integer):Integer; function setTimeout( code: TIntervalHandler; intervalId: Integer):Integer; end;

I will not normally show the whole classes, but this one is quite useful and I wanted to pique your interest. The emit word means to use the following JavaScript class but refer to it with an EWB name, usually starting with T for convention sake.

Some of the methods and values are specific to certain web browsers, eg. Orientation, OuterHeight, etc. What can you do with it? Almost anything! You can call window.print and the page will ask the user if the current window should print. If you want to pop up a message and temporarily stop your code, call window.alert(‘some message’). It creates a modal dialog.

If you wanted to ask a question of a user, you could call window.confirm(‘are you there’) and check the Boolean result. If the user

replies with Ok, the result is true. Cancel corresponds to false.

You can see that window.screen returns a TScreen, which is defined as:

external TScreen emit Screen = class public

{ Properties }

property availHeight: Integer read; property availWidth:

Integer read; property colorDepth: Integer read; property height: Integer read; property width: Integer read;

end;

So you could use that to see the color depth, the screen height, etc. Look at THTMLElement:

external THTMLElement emit HTMLElement = class(TDOMElement) public

{ Base Properties }

….

This is because an THTMLElement is a special type of TDOMElement. Other DOM elements include CSS and JavaScript which are definitely not HTMLand the THTMLElement properties and methods do not apply.

If you are given a TDOMElement for an HTMLelement, you can cast it to THTMLElement and then call the THTMLElement methods or access its properties.

For example, suppose you were given an HTML<div id=’qunit’></div> tag pair and wanted to insert a message between them:

The JavaScript way would be to call the DOM;

function message( var msg ) {

var html = window.document.getElementById(‘qunit’); html.innerHTML = msg;

}

The EWB procedure to do the same would be:

procedure message( msg : string ); var html : THTMLElement; begin html := THTMLElement( window.document.getElementById('qunit') ); html.innerHTML :=

msg;

end;

Note the extra step of casting window.document.getElementByID() to type THTMLElement because it just returns a TDOMElement. This allows the compiler to type check your assignment.

Suppose you wanted to create that ‘qunit’ DIV on the fly, you could use window.document.body.appendChild and window.document.createElement.

Procedure createqunit; Var ele : TDomElement; html : THTMLDocument; begin ele := window.document.createElement('div'); ele.setAttribute('id','qunit'); html := THTMLDocument(window.document ); html.body.appendChild( ele ); end;

If you wanted to load an external CSS file, you could do that by extending the HEAD DOM element.

Procedure loadqunitcss; Var css : TDomElement; html : THTMLDocument; url : string; begin url := 'qunit.css'; css := window.document.createElement('link'); css.setAttribute('rel', 'stylesheet'); css.setAttribute('type', 'text/css'); css.setAttribute('href', url); html := THTMLDocument(window.document ); html.body.appendChild( css ); end;

While you could do something similar to our loadqunitcss to load a JavaScript file, I prefer to use the EWB strategy and either have a GUI created TScript, or call TScript when you need it with:

Procedure Form1.loadqunitscript; begin script := TScript.Create( self ); script.onerror := script1error; script.onload := script1load; script.url :='qunit.js'; end;

And then define script1error which displays an error, and script1load which can then call the loaded script.

In JavaScript redirecting to a different page is accomplished with

window.location.href = ‘http://google.com’; EWB Object Pascal is almost identical:

window.location.href := ‘http://google.com’;

It is common to include libraries for specific functionality because including a library is a

lot easier than recoding it in EWB ObjectPascal.

In general, you will usually call either global procedures/functions, or deal with JavaScript

classes. We will work with the Clippy.JS library that provides simulated Microsoft Office Clippy

characters. See the documentation and demonstrations at: https://www.smore.com/clippy- js

A typical use would load clippy with the EWB external libraries code, or a TScript

strategy as discussed in the previous chapter. You will also need to have it load a JQuery library using the same technique.

Then the JavaScript documents tell you to call something like: