Archive

Archive for October 20, 2017

Making your own DOM events in Smart Pascal

October 20, 2017 Leave a comment

Being able to listen to events is fairly standard stuff in Smart Mobile Studio and JavaScript in general. But what is not so common is to create your own event-types from scratch that fire on a target, and that users of JS can listen to and use.

The word Events in cut out magazine letters pinned to a cork not

Now before you get confused and think this is a newbie post, I am talking about DOM (document object model) level events here; these are quite different from the event model we have in object pascal. So what im talking about is being able to create events that external libraries can use for instance. Libraries written in plain JavaScript rather than Smart Pascal.

Interesting events

While you may think that events like that, which are akin to all the other DOM events, have little or no use – think again. First of all you can dispatch them on any element and event-emitter. So you can in fact register events on common elements like Document. You can then use custom events as a bridge between your Smart code and third party libraries for instance. So if you have written a kick-ass media system and wants to sell it to a customer who only knows JavaScript – then using native JS events can act as a bridge.

Right, let’s look at a little unit I wrote to simplify this:

unit userevents;

interface

uses
  System.Types,
  System.Types.Convert,
  System.JSON,
  SmartCL.System;

type

  IW3Prototype = interface
    procedure AddField(FieldName: string; const DataType: TRTLDatatype);
    function  FieldExists(FieldName: string): boolean;
    procedure SetEventName(EventName: string);
  end;

  TW3CustomEvent = class(TObject, IW3Prototype)
  private
    FName:      string;
    FData:      TJSONObject;
    FDefining:  boolean;
    procedure   SetEventName(EventName: string);
    procedure   AddField(FieldName: string; const DataType: TRTLDatatype);
    function    FieldExists(FieldName: string): boolean;
    function    GetReady: boolean;
  public
    property    Name: string read FName;
    property    Ready: boolean read GetReady;

    function    DefinePrototype(var IO: IW3Prototype): boolean;
    procedure   EndDefine(var IO: IW3Prototype);
    function    NewEventData: TJSONObject;

    procedure   Dispatch(const Handle: TControlHandle; const EventData: TJSONObject);

    constructor Create; virtual;
    destructor  Destroy; override;
  end;

implementation

//############################################################################
// TW3CustomEvent
//###########################################################################

constructor TW3CustomEvent.Create;
begin
  inherited Create;
  FData := TJSONObject.Create;
end;

destructor TW3CustomEvent.Destroy;
begin
  FData.free;
  inherited;
end;

function TW3CustomEvent.GetReady: boolean;
begin
  result := (FDefining = false) and (FName.Length > 0);
end;

procedure TW3CustomEvent.Dispatch(const Handle: TControlHandle; const EventData: TJSONObject);
var
  LEvent: THandle;
  LParamData: variant;
begin
  if GetReady() then
  begin
    if (Handle) then
    begin
      // Check for detail-fields, get javascript object if available
      if EventData <> nil then
      begin
        if EventData.Count > 0 then
          LParamData := EventData.Instance;
      end;

      if (LParamData) then
      begin
        // Create event object with detail-data
        var LName := FName.ToLower().Trim();
        asm
        @LEvent = new CustomEvent(@LName, { detail: @LParamData });
        end;
      end else
      begin
        // Create event without detail-data
        var LName := FName.ToLower().Trim();
        asm
        @LEvent = new Event(@LName);
        end;
      end;

      // Dispatch event-object
      Handle.dispatchEvent(LEvent);
    end;
  end;
end;

procedure TW3CustomEvent.SetEventName(EventName: string);
begin
  if FDefining then
  begin
    EventName := EventName.Trim().ToLower();
    if EventName.Length > 0 then
      FName := EventName
    else
      raise EW3Exception.Create
      ('Invalid or empty event-name error');
  end else
    raise EW3Exception.Create
    ('Event-name can only be written while defining error');
end;

function TW3CustomEvent.FieldExists(FieldName: string): boolean;
begin
  if FDefining then
    result := FData.Exists(FieldName)
  else
    raise EW3Exception.Create
    ('Fields can only be accessed while defining error');
end;

procedure TW3CustomEvent.AddField(FieldName: string; const DataType: TRTLDatatype);
begin
  if FDefining then
  begin
    if not FData.Exists(FieldName) then
      FData.AddOrSet(FieldName, TDataType.NameOfType(DataType))
    else
      raise EW3Exception.CreateFmt
      ('Field [%s] already exists in prototype error', [FieldName]);
  end else
  raise EW3Exception.Create
  ('Fields can only be accessed while defining error');
end;

function TW3CustomEvent.NewEventData: TJSONObject;
const
   MAX_INT_16 = 32767;
   MAX_INT_08 = 255;
begin
  result := TJSONObject.Create;
  result.FromJSON(FData.ToJSON());

  result.ForEach(
    function (Name: string; var Data: variant): TEnumState
    begin
      // clear data with datatype value to initialize
      case TDataType.TypeByName(TVariant.AsString(Data)) of
      itBoolean:  Data := false;
      itByte:
        begin
          Data := MAX_INT_08;
          Data := $00;
        end;
      itWord:
        begin
          Data := $FFFF;
          Data := $0000;
        end;
      itLong:
        begin
          Data := 00000000;
        end;
      itInt16:
        begin
          Data := MAX_INT_16;
          Data := 0000;
        end;
      itInt32:
        begin
          Data := MAX_INT;
          Data := 0;
        end;
      itFloat32:
        begin
          Data := 1.1;
          Data := 0.0;
        end;
      itFloat64:
        begin
          Data := 20.44;
          Data := 0.0;
        end;
      itChar,
      itString:   Data := '';
      else        Data := null;
      end;
      result := esContinue;
    end);
end;

function TW3CustomEvent.DefinePrototype(var IO: IW3Prototype): boolean;
begin
  result := not FDefining;
  if result then
  begin
    FDefining := true;
    IO := (Self as IW3Prototype);
  end;
end;

procedure TW3CustomEvent.EndDefine(var IO: IW3Prototype);
begin
  if FDefining then
    FDefining := false;
  IO := nil;
end;

end.

Patching the RTL

Sadly there was a bug in the RTL that prevented the TJSONObject.ForEach() to function properly. This has been fixed in the update we are preparing now, but there will still be a few days before that is released.

You can patch this manually right now with this little fix. Just go into the System.JSON.pas file and replace the TJSonObject.ForEach() method with this one:

function TJSONObject.ForEach(const Callback: TTJSONObjectEnumProc): TJSONObject;
var
  LData:  variant;
begin
  result := self;
  if assigned(CallBack) then
  begin
    var NameList := Keys();
    for var xName in NameList do
    begin
      Read(xName, LData);
      if CallBack(xName, LData) = esContinue then
        Write(xName, LData)
      else
        break;
    end;
  end;
end;

Creating events

Events come in two flavours: those with data and those without. This is why we have the DefinePrototype() and EndDefine() methods – namely to define what data fields the event should take. If you dont populate the prototype then the class will create an event without it.

Secondly, events dont need to be registered somewhere. You create it, dispatch it to a handle (or element) and if there is an event-listener attached there looking for that name – it will fire.

Ok let’s have a peek:

  // Create a custom, new, system-wide event
  var LEvent := TW3CustomEvent.Create;
  var IO: IW3Prototype = nil;
  if LEvent.DefinePrototype(IO) then
  begin
    try
      IO.SetEventName('bombastic');
      IO.AddField('name', TRTLDataType.itString);
      IO.AddField('id', TRTLDataType.itInt32);
    finally
      LEvent.EndDefine(IO);
    end;
  end;

  // Setup a normal event-listner
  Display.Handle.addEventListener('bombastic',
    procedure (ev: variant)
    begin
      var data := ev.detail;
      if (data) then
        showmessage(JSON.stringify(Data));
    end);

  // Populate some event-data
  var MyData := LEvent.NewEventData();
  MyData.Write('name','John Doe');
  MyData.Write('id', '{F6EB5680-5DC1-422E-8F72-5C60EAC0B46F}');

  // Now send the event to whomever is listening
  LEvent.Dispatch(Display.Handle, MyData);

In the above example I use the Application.Display control as the event-target. There is no special reason for this except that it’s always available. You would naturally create events like this inside your TW3CustomControl (or perhaps the Document element, under a namespace).

You will also notice that any data sent ends up in the “detail” field of the event object. We use a variant datatype since that maps directly to any JS object and also lets us access any property (and create properties for that mapper); so thats why the “ev” parameter in addEventListner() is a variant, not a fixed class.

Well, hope you enjoy the show and happy coding!

PS: Smart now uses an event-manager to deal with input events (mouse, touch), but the other events works like before. Have a look at SmartCL.Events.pas to see some time-saving event classes. So instead of having to use ASM sections and variants, you can use object pascal classes to map any event.