Blog
All Blog Posts | Next Post | Previous Post
TMS Scripter Now Speaks JavaScript
Today
Sooner or later, a successful Delphi application meets a customer who needs it to work a little differently. A discount rule that depends on the region. An extra validation before an invoice is saved. A report column computed with a formula of their own. A menu command that exports data to a partner's format.
You can build each of these into the application, and ship a new version to every customer for a rule only one of them needs. Or you can make that part of the application scriptable, and let the rule be written - by your support team, a consultant, or the customer's own IT staff - without recompiling anything.
Scripting has one practical question, though: in what language will those people write? Most of them have never written Object Pascal. Almost all of them have written some JavaScript.
A quick introduction to TMS Scripter
TMS Scripter is a scripting engine for Delphi applications. It runs scripts inside your application at runtime and gives them access to the objects, classes and functions that you choose to expose. It also includes an IDE that you can embed in your application, with a code editor, a form designer and a debugger, so your users can write and test their scripts where they use them.
Until now, scripts could be written in Pascal or Basic syntax. TMS Scripter 9.0 adds JavaScript.
What JavaScript support means
JavaScript scripts use the syntax people already know: function, var, let and const, if/else, for, while and do..while, switch, try/catch/finally, throw, ++, +=, the ?: operator, array literals and // comments.
They run on the same engine as the Pascal and Basic scripts. That has a few practical consequences:
- Everything you expose to scripts is available in JavaScript. Classes registered by RTTI, objects added with
AddObject, libraries, functions, events - the same registration serves the three languages. - Nothing new to deploy. There is no external JavaScript runtime, DLL or package to ship with your application.
- The languages work together. A JavaScript script can use a unit written in Pascal or Basic, and the other way around.
- The tooling is the same. The Scripter IDE supports JavaScript units (
.jscfiles), with syntax highlighting, code completion, forms declared in script, and debugging with breakpoints and watches.
Hello, JavaScript
This is a complete console application that runs a JavaScript script:
program HelloScripter;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.Variants,
atScripter;
var
Scripter: TatScripter;
ScriptResult: Variant;
begin
Scripter := TatScripter.Create(nil);
try
// The language of the script: slJavaScript, slPascal or slBasic
Scripter.CurrentScript := Scripter.AddScript(slJavaScript);
// In a console application, ShowMessage writes to the console
Scripter.SourceCode.Text :=
'var name = "World";' + sLineBreak +
'ShowMessage("Hello, " + name + "!");' + sLineBreak +
'return 6 * 7;';
// Compiles and runs the script, and returns the value given by its return statement
ScriptResult := Scripter.ExecuteAsVariant;
WriteLn('The script returned ', VarToStr(ScriptResult));
finally
Scripter.Free;
end;
end.
AddScript creates a script in the language you choose. If most of your scripts are in JavaScript, set DefaultLanguage to slJavaScript instead.
A business rule in JavaScript
Back to the discount rule. The rule lives in a script file that can change without a new build of the application:
// pricing.jsc
function discount(total, customerType) {
if (customerType == "VIP")
return 15;
if (total >= 100)
return 10;
return 0;
}
The application compiles the script once, and calls the function whenever it prices an order:
uses
atScripter;
type
TPricingRules = class
private
FScripter: TatScripter;
public
constructor Create(const RulesFile: string);
destructor Destroy; override;
function Discount(Total: Double; const CustomerType: string): Double;
end;
constructor TPricingRules.Create(const RulesFile: string);
begin
inherited Create;
FScripter := TatScripter.Create(nil);
FScripter.CurrentScript := FScripter.AddScript(slJavaScript);
FScripter.SourceCode.LoadFromFile(RulesFile);
// Compiled once; its functions can then be called as often as needed
FScripter.Compile;
end;
destructor TPricingRules.Destroy;
begin
FScripter.Free;
inherited;
end;
function TPricingRules.Discount(Total: Double; const CustomerType: string): Double;
begin
Result := FScripter.ExecuteSubroutineAsVariant('discount', [Total, CustomerType]);
end;
When the customer wants 5% off for orders from 50, someone edits pricing.jsc. The Delphi code doesn't change.
Your Delphi objects, in JavaScript
Rules become much more useful when they work with the objects of your application. Here is an ordinary Delphi class - nothing in it knows about Scripter:
type
TOrder = class
// private fields and getters omitted
public
function AddItem(const AProduct: string; AQuantity: Integer; AUnitPrice: Double): TOrderItem;
function Total: Double;
property Number: Integer read FNumber;
property Customer: TCustomer read FCustomer;
property Items[Index: Integer]: TOrderItem read GetItem;
property ItemCount: Integer read GetItemCount;
end;
One call to DefineClassByRTTI makes its public members available to scripts, and AddObject gives the script an instance to work with:
uses
System.TypInfo, atScript, atScripter;
procedure RunOrderScript(Scripter: TatScripter; Order: TOrder);
begin
// True also registers the classes used by TOrder: TCustomer and TOrderItem
Scripter.DefineClassByRTTI(TOrder, '', [mvPublic, mvPublished], True);
Scripter.AddObject('Order', Order);
Scripter.Execute;
end;
The script uses the order with its Delphi names - properties, methods, indexed properties, and the objects they return:
Print("Order " + IntToStr(Order.Number) + " for " + Order.Customer.Name);
Order.AddItem("Croissant", 3, 2.5);
var juice = Order.AddItem("Orange juice", 1, 3);
juice.Quantity = 2;
for (var i = 0; i < Order.ItemCount; i++) {
var item = Order.Items[i];
Print(IntToStr(item.Quantity) + " x " + item.Product + ": " +
FormatFloat("0.00", item.Total()));
}
Print("Total: " + FormatFloat("0.00", Order.Total()));
(Print is not a Scripter routine: it is a function that the demo application exposes to its scripts, to write to its output pane. Your application can expose its own functions the same way.)
Events work the same way. A script function can handle an event of any object, by assigning its name to the event property:
function SaveClick(Sender) {
ShowMessage("Saving " + edName.Text);
}
btSave.OnClick = "SaveClick";
Pascal, Basic and JavaScript, together
If you already have Pascal or Basic scripts, you don't have to rewrite them to start using JavaScript. Each script has its own language, and scripts use each other by unit name, whatever language each one is written in:
procedure AddUnit(Scripter: TatScripter; Language: TScriptLanguage;
const AUnitName, FileName: string);
var
ScriptUnit: TatScript;
begin
ScriptUnit := Scripter.AddScript(Language);
ScriptUnit.UnitName := AUnitName;
ScriptUnit.SourceCode.LoadFromFile(FileName);
end;
procedure AddUnits(Scripter: TatScripter);
begin
AddUnit(Scripter, slPascal, 'TaxRules', 'TaxRules.psc');
AddUnit(Scripter, slBasic, 'Formats', 'Formats.bsc');
AddUnit(Scripter, slJavaScript, 'Discounts', 'Discounts.jsc');
end;
A JavaScript script then imports the three units and calls their functions as if they were its own:
import TaxRules, Formats, Discounts;
var Net = ApplyDiscount(100, 10); // Discounts.jsc
var Total = AddTax(Net); // TaxRules.psc
Print(FormatPrice(Total)); // Formats.bsc
A team can keep its tested Pascal routines and write new scripts in JavaScript, or let each user write in the language they prefer.
Forms, the IDE and debugging
JavaScript scripts can declare forms, too. A //$FORM directive links the script to a form file. The components of the form are available by name, the global variables of the script become properties of the form, and its functions become methods:
//$FORM TCustomerForm, CustomerForm.dfm
import Controls;
var CustomerId;
function Init(Id) {
CustomerId = Id;
Caption = "Customer " + IntToStr(Id);
edName.OnChange = "NameChange";
}
function NameChange(Sender) {
Caption = "Customer " + IntToStr(CustomerId) + ": " + edName.Text;
}
Another script creates and shows instances of the form like any other class:
import Classes, Controls, Forms, CustomerForm;
var Form = new TCustomerForm(Application);
Form.Init(1);
Form.edName.Text = "Alfreds Futterkiste";
if (Form.ShowModal() == mrOk)
ShowMessage("Saved " + Form.edName.Text);
Form.Free();
The Scripter IDE handles JavaScript units like Pascal and Basic ones: you design the form, write the code with highlighting and code completion, and run or debug the project. This is a complete game written in JavaScript, running from the IDE:
If you build your own script editor instead, the new TScrJavaScriptMemoStyler component gives the TScrMemo editor JavaScript syntax highlighting:
ScrMemo1.SyntaxStyles := ScrJavaScriptMemoStyler1;
JavaScript syntax, Delphi objects
It is worth being clear about what this is: the syntax of JavaScript for scripting your Delphi application, not a browser or Node.js runtime. Scripts work with the objects your application provides, not with npm packages or the DOM. That keeps scripts small and focused on your domain, and keeps the behavior consistent with Pascal and Basic scripts. A few differences follow from it:
- Names are not case-sensitive, as in Delphi:
Order.Total,order.totalandORDER.TOTALare the same. - Objects are Delphi objects.
new TStringList()calls the constructor,Free()destroys the object. Object literals, classes, prototypes and closures are not supported. - There is no JavaScript standard library (
Math,JSON,Date, string methods). Scripts use the built-in routines -IntToStr,FormatFloat,Copy,Nowand many others - and the libraries your application registers. catchhas no parameter: the error is available inLastExceptionMessage.
The full list is in Differences from standard JavaScript. The JavaScript language reference is written for script authors, so you can hand it to the people who write the scripts of your application.
Where it fits
- Business rules and formulas - prices, discounts, taxes, commissions, validations - that change more often than your release cycle.
- Plugins and extensions: let customers add menu commands, handle events and automate tasks in your application.
- Per-customer customization of a product that ships the same executable to everybody.
- Automation and macros for power users who know JavaScript from spreadsheets, browsers or web development.
- Gradual adoption: add JavaScript next to your existing Pascal and Basic scripts, without migrating them.
Conclusion
TMS Scripter 9.0 lets the people who customize your application write in the language they most likely already know:
- JavaScript syntax, on the same engine as Pascal and Basic, with nothing new to deploy.
- Full access to your Delphi objects, registered once for all three languages.
- Mixed languages: JavaScript, Pascal and Basic scripts that use each other.
- IDE support: JavaScript units, forms, code completion and debugging.
To learn more, read the JavaScript language reference and the chapter Working with Scripter, see what's new in TMS Scripter 9.0, and visit the TMS Scripter product page. The examples in this post are based on the new demos of TMS Scripter, where every script is available in JavaScript, Pascal and Basic - so you can also compare the three languages side by side.
Wagner Landgraf
This blog post has not received any comments yet. Add a comment.
All Blog Posts | Next Post | Previous Post