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 (.jsc files), 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 JavaScript script that uses units written in Pascal, Basic and JavaScript, in the TMS Scripter Playground demo

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:

The Snake game, written in JavaScript, running from the TMS Scripter 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.total and ORDER.TOTAL are 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, Now and many others - and the libraries your application registers.
  • catch has no parameter: the error is available in LastExceptionMessage.

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 new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post