terça-feira, 16 de agosto de 2016

Smart Mobile Studio - SearchBar Component


SearchBar Component

Let's say I have a big list, 23,018 cities (DOM elements) on a page, the user interact with the page (searching for cities). When the user type a key to fetch data it causes page reflow. Look, the searchbar is using documentFragment to reduce redraws and repaints.
In the following experiment, I'm going to use this SearchBar component. We're filtering a big list 23,018 cities.

SearchBar Source


unit uSearchBar;

{ ╔═════════════════════════════╗
  ║  _______  _______  _______  ║
  ║ (  ____ \(       )(  ____ \ ║
  ║ | (    \/| () () || (    \/ ║
  ║ | (_____ | || || || (_____  ║
  ║ (_____  )| |(_)| |(_____  ) ║
  ║       ) || |   | |      ) | ║
  ║ /\____) || )   ( |/\____) | ║
  ║ \_______)|/     \|\_______) ║
  ║ component by: warleyalex    ║
  ╚═════════════════════════════╝ }


interface

uses
  SmartCL.System,
  SmartCL.Components,
  DataModule,
  FW7, ECMA.JSON, w3C.HTML5, W3C.Console, W3C.DOM;

type
  THandleArray = array of variant;
  TNotifyEvent  = procedure(Sender: TObject);

  TSearchBar = class(TW3CustomControl)
  private
    { Private declarations }
    FSelected:  Boolean;
    FStore: Variant;
    FLeft, FTop, FWidth, FHeight: variant = 0;

  protected
    { Protected declarations }
    procedure setSelected(Const Value:Boolean);
    procedure createSearchBar;
    procedure InitializeObject; override;
    { procedure ObjectReady; override; }
empty;
    procedure ListItems(arrayStore: variant);
  public
    { Public declarations }
  published
    { Published declarations }
     property store: Variant read FStore write FStore;
     property SearchItems: Boolean read FSelected write setSelected;
  end;

implementation

function document: variant; external "document" property;

{ ╔════════════════════════════════════════════╗
  ║ Insert here the Component Events Handlers  ║
  ╚════════════════════════════════════════════╝ }

procedure SearchBarOnFocus(e: variant);
begin
  var input := SMS(e.target);
  input.parents(".searchbar").addClass("searchbar-active");
end;

procedure SearchBarOnClick(e: variant);
begin
  var btn = SMS(e.target);
  btn.parents(".searchbar").removeClass("searchbar-active");
end;

procedure SearchBarOnBlur(e: variant);
begin
  var input = SMS(e.target);
  input.parents(".searchbar").removeClass("searchbar-active");
end;

{ ╔═══════════════════════════════════════════════════════════════════════╗
  ║ ### refs ###                                                          ║
  ║ You can set up references / bindEvents listeners to components        ║
  ║ This allows you to retrieve and manipulate components on the page     ║                            ║
  ╚═══════════════════════════════════════════════════════════════════════╝ }

var bindings : THandleArray =
[CLASS
  element  := '.searchbar input';
  event    := 'focus';
  handler  := @SearchBarOnFocus;
END,
CLASS
  element  := '.searchbar-cancel';
  event    := 'click';
  handler  := @SearchBarOnClick;
END,
CLASS
  element  := '.searchbar input';
  event    := 'blur';
  handler  := @SearchBarOnBlur;
END];

procedure bindEvents(bindings: THandleArray);
var i : integer;
begin
 for i := Low(bindings) to High(bindings) do
  begin
  SMS(bindings[i].element).on(bindings[i].event, bindings[i].handler);
 end;
end;

{ ╔════════════════╗
  ║ Constructor    ║
  ╚════════════════╝ }
/*Constructor TSearchBar.create(AOwner: TW3Component;left,top,width,height: variant=0);
begin
inherited Create(AOwner);
  FLeft   := left;
  FTop    := top;
  FWidth  := width;
  FHeight := height;
end;*/

procedure TSearchBar.ListItems(arrayStore: variant);
var
  i, len: integer;

begin
len := arrayStore.length;
if len = 0 then arrayStore := "";

var myList   := document.getElementById('list_'+Self.Handle.id);
var tempFrag := document.createDocumentFragment();

while (myList.firstChild <> nil) do begin
 myList.removeChild(myList.firstChild);
end;

for i := 0 to len - 1 do
begin
 var listItem := JHTMLLIElement(document.createElement('LI'));
 listItem.setAttribute("class", "item-content");
 tempFrag.appendChild(listItem);

 var div0 := JHTMLDivElement(document.createElement('DIV'));
 div0.setAttribute("class", "item-inner");
 listItem.appendChild(div0);

 var div_0 := JHTMLDivElement(document.createElement('DIV'));
 div_0.setAttribute("class", "item-title");
 div0.appendChild(div_0);
 div_0.appendChild( JText(document.createTextNode(arrayStore[i])) );
end;
myList.appendChild(tempFrag.cloneNode(true));

end;

procedure TSearchBar.setSelected(Const Value:Boolean);
begin
//DataModule1
  if value<>FSelected then
  begin
    FSelected:=Value;
    If Value then
    begin
      SMS('input#search'+Self.Handle.id).on("change keyup", procedure(e: variant)
      begin
        var SearchIn := ( SMS('input#search'+Self.Handle.id).val() );
        DataModule1.findByKey(SearchIn, Store, procedure(ds: variant)
        begin
          Self.ListItems(ds);
        end);
      end);

    end;
  end;
end;

{ ╔══════════════════════════════════════════════════╗
  ║ Insert here the innerComponent (generated code)  ║
  ╚══════════════════════════════════════════════════╝ }
procedure TSearchBar.createSearchBar;
begin
{ ╔═══════════════════════════════════════════════════════════════════════════╗
  ║ Since the document fragment is in memory and not part of the main DOM     ║
  ║ tree, appending children to it does not cause page reflow (computation    ║
  ║ of elements position and geometry). Consequently, using documentfragments ║
  ║ often results in better performance.                                      ║
  ╚═══════════════════════════════════════════════════════════════════════════╝ }
  var docFragment: variant = JDocumentFragment(document.createDocumentFragment()); // contains all gathered nodes
  

  var div1 := JHTMLDivElement(document.createElement('DIV'));
  div1.setAttribute("class", "bar");
  docFragment.appendChild(div1);

  var div_0 := JHTMLDivElement(document.createElement('DIV'));
  div_0.setAttribute("class", "searchbar ");
  div1.appendChild(div_0);

  var a := JHTMLAnchorElement(document.createElement('A'));
  a.setAttribute("class", "searchbar-cancel");
  div_0.appendChild(a);

  var text_0 := JText(document.createTextNode("Cancel"));
  a.appendChild(text_0);

  var div_1 := JHTMLDivElement(document.createElement('DIV'));
  div_1.setAttribute("class", "search-input");
  div_0.appendChild(div_1);


  var label := JHTMLLabelElement(document.createElement('LABEL'));
  label.setAttribute("class", "icon icon-search");
  label.setAttribute("for", "search");
  div_1.appendChild(label);

  var search := JHTMLInputElement(document.createElement('INPUT'));
  search.setAttribute("type", "search");
  search.setAttribute("id", "search"+Self.Handle.id);
  search.setAttribute("placeholder", "type some city...");
  div_1.appendChild(search);
  //-------------------------------------------
  var pageindex := JHTMLDivElement(document.createElement('DIV'));
  pageindex.setAttribute("class", "content");
  docFragment.appendChild(pageindex);

  var pageindex1 := JHTMLDivElement(document.createElement('DIV'));
  pageindex1.setAttribute("class", "list-block search-here searchbar-found");
  pageindex.appendChild(pageindex1);

  var list := JHTMLUListElement(document.createElement('UL'));
  list.setAttribute("id", "list_"+Self.Handle.id);
  pageindex1.appendChild(list);
  //-----------------------------
  Self.Handle.appendChild( docFragment );
end;

procedure TSearchBar.InitializeObject;
begin
  inherited;
{ ╔══════════════════════════════════════════════════╗
  ║ this is a good place to initialize the component ║
  ╚══════════════════════════════════════════════════╝ }
  createSearchBar;
  //WriteLn (Self.Handle.id);

{ ╔═════════════════════════════════════════════════════════════════════╗
  ║ make the control display itself correctly when its ready in the DOM ║
  ╚═════════════════════════════════════════════════════════════════════╝ }
  Handle.ReadyExecute(procedure()
  begin
    (* call createInnerComponent *)
    bindEvents(bindings);

{ ╔══════════════════════════════════════════════════╗
  ║ some basic style attributes after render         ║
  ╚══════════════════════════════════════════════════╝ }
    Self.Handle.style['left'] := null;
    Self.Handle.style['width'] := '100%';
    Self.Handle.style['height'] := '100%';
  end);

end;

/*procedure TSearchBar.FinalizeObject;
begin
  inherited;

end;*/

TDataModule { TODO }

domingo, 14 de agosto de 2016

Smart Mobile Studio - Variant

Variant data type

The Variant data type provides a flexible general purpose data type. It can hold basically anything. You can use variants when interacting with native javascript objects or external libraries.

We can also use variants in external classes. The External classes are the glue between Smart Pascal and the javascript world. It's important to say these external classes are case sensitive, in fact, the external code is only interfaced – this is not a real class, but an external class. This means that we only need the definition while the implementation is done by the external JavaScript code.

The Variants are treated as "unknown" by the smart pascal codegen. Variants are primarily used to transport native values and objects between methods dealing with javascript objects or browser objects. So Smart Mobile Studio compiler deals with variants as unknown values. Use it with care - there are potentials for run time errors and poor code clarity when using variants. For instance, the compiler does not check if a method of an object actually exists.

this

In this example, I'm going to define an external global variable. Note that, external variables must be declared in global scope. We're going to use this in global context. The this keyword behaves differently in JavaScript compared to other language. In Object Oriented languages, like smart pascal, the this keyword refers to the current instance of the class (aka Self).

In JavaScript the value of this is determined mostly by the invocation context of function and where it is called. Se, we can interface with native javascript objects with object pascal and create powerful components.

We can declare this variable using two ways:

{ using external variable }
var this external "this" : variant;
 { or using external function }
function this: variant; external 'this' property;

then we can use this external variable, such as:

procedure HandleCallback(a: variant);
begin
 console.log(a);
end;

procedure InvokeByVariant;
var mSrc: variant;
begin
mSrc := this.document;

(* invoke our method *)
HandleCallback(mSrc);
end;

Variant parameter

Let's suppose we have a method which looks like:

procedure HandleCallback(data: variant);
begin
  //
end;

In the sample code, it remains unclear the argument data. I'd call them a better name because that gives you more of a chance of understanding at a glance what your code means. You can customize it, so the method looks like this now:

type
 TJSONData = Variant;

procedure HandleCallback(data: TJSONData);
begin
//
end;

Literal object using the keyword Class

I'd like to create a JS literal object in smart pascal, like this:

var object1 = {
  "child" : {
    "Callback" : function () {
       console.log(this);
       return this;
    }
  }
};

As has been said: variants are treated as "unknown" by the smart pascal codegen. The smart compiler does no check if a method f.i. actually exists! Use it with care.






var object1 := CLASS
  that: variant = class external;

  child = class
    Callback := function(): variant
    begin
     console.log(this);
     Result := this;
    end;
  end;
END;

procedure CallMe(unknown: variant);
begin
  unknown.Name := "MyName";
  unknown.Age  := 102;
  unknown.Callback(unknown);
end;

CallMe(Object1.child);

CallMe(CLASS
  Callback := lambda
    console.log('this is a test');
  end;
END);

JS Emitted:

function CallMe(unknown) {
 unknown.Name = "MyName";
 unknown.Age = 102;
 unknown.Callback(unknown);
};

var object1 = {
 "child" : {
 "Callback" : function () {
    var Result = undefined;
    console.log(this);
    Result = this;
    return Result
 }
 }
 ,"that" : {
 }
};
CallMe(object1.child);
CallMe({
 "Callback" : function () {
 console.log("this is a test");
 }
});   

Array of Array of Integer

Now I'd like an object such as: {"values":[[0,100],[1000,200],[2000,300],[3000,400],[4000,500]] }

I've got weird result using this approach:

var preloaded = CLASS
  values : array [0..4] of array [0..1] of Integer = [ [0, 100],[1000, 200],[2000, 300],[3000, 400],[4000, 500] ];
END;

console.log(JSON.stringify(preloaded));
{"values":[[4000,500],[1000,200],[2000,300],[3000,400],[4000,500]]}

an easy workaround would be defining a local variable like this:

var valor : array [0..4] of array [0..1] of Integer = [ [0, 100],[1000, 200],[2000, 300],[3000, 400],[4000, 500] ];

var preloaded2 = CLASS
  values := valor;
END;

console.log(JSON.stringify(preloaded2));
{"values":[[0,100],[1000,200],[2000,300],[3000,400],[4000,500]]}

JSON Array Object

Now I would like to create a JSON object like this:

[{"id":1,"name":"Item#1"},{"id":2,"name":"Item#2"},{"id":3,"name":"Item#3"},{"id":4,"name":"Item#4"},{"id":5,"name":"Item#5"},{"id":6,"name":"Item#6"},{"id":7,"name":"Item#7"},{"id":8,"name":"Item#8"},{"id":9,"name":"Item#9"},{"id":10,"name":"Item#10"}]


var obj := CLASS
  that: variant = class external;

  child = class
    Callback := function(): variant
    begin
     Result := this;
    end;
  end;

  "JSONArray1": array of variant;

  "JSONArray2": array of variant;

  "JSONArray3": array of variant;

  "JSArray": array of variant;

  "name": string = "obj";

  "f"   : TFunc = function(): variant
          begin
            result := this.that;
          end;
END;



for var x:=1 to 10 do
begin
  obj.JSONArray1.add(CLASS
    name := "Item#" + IntToStr(x);
    id := x;
  END);
end;

Javascript Array

...and a simple javascript array list like:
["Item#1", "Item#2", "Item#3", "Item#4", "Item#5", "Item#6", "Item#7", "Item#8", "Item#9", "Item#10"]


for var x:=1 to 10 do
begin
  obj.JSArray.add("Item#" + IntToStr(x));
end;



Array of JSON Array

Playing more, with smart pascal, how about to create this weird array of array of objects?

[[{"age":1,"nome":"Nome#1"},{"cep":"35700-001","address":"Addr#1"},{"country":"Country 1","city":"City#1"}],[{"age":2,"nome":"Nome#2"},{"cep":"35700-002","address":"Addr#2"},{"country":"Country 2","city":"City#2"}],[{"age":3,"nome":"Nome#3"},{"cep":"35700-003","address":"Addr#3"},{"country":"Country 3","city":"City#3"}],[{"age":4,"nome":"Nome#4"},{"cep":"35700-004","address":"Addr#4"},{"country":"Country 4","city":"City#4"}],[{"age":5,"nome":"Nome#5"},{"cep":"35700-005","address":"Addr#5"},{"country":"Country 5","city":"City#5"}],[{"age":6,"nome":"Nome#6"},{"cep":"35700-006","address":"Addr#6"},{"country":"Country 6","city":"City#6"}],[{"age":7,"nome":"Nome#7"},{"cep":"35700-007","address":"Addr#7"},{"country":"Country 7","city":"City#7"}],[{"age":8,"nome":"Nome#8"},{"cep":"35700-008","address":"Addr#8"},{"country":"Country 8","city":"City#8"}],[{"age":9,"nome":"Nome#9"},{"cep":"35700-009","address":"Addr#9"},{"country":"Country 9","city":"City#9"}],[{"age":10,"nome":"Nome#10"},{"cep":"35700-0010","address":"Addr#10"},{"country":"Country 10","city":"City#10"}]]

In my experiments, 
for var x:=1 to 10 do
begin
  var objeto :=
    [CLASS
      nome   : string  = "Nome#" + IntToStr(x);
      age    : integer =  + x;
    END,
    CLASS
      address: string  = "Addr#" + IntToStr(x);
      cep    : variant = '35700-00'+ IntToStr(x);
    END,
    CLASS
      city   : string  = "City#" + IntToStr(x);
      country: string  = "Country " + IntToStr(x);
    END];
obj.jsonarray2.push(objeto);
end;

 The codegen generated weird pusha javascript instruction. I had to replace to push to get working.

var obj = null,
 x$32 = 0;
var objeto = [null,null,null];
/// anonymous TClassSymbol
/// anonymous TClassSymbol
TApplication.InitApp(Self);
obj = {
 "that" : {
 }
 ,"name" : "obj"
 ,"JSONArray3" : []
 ,"JSONArray2" : []
 ,"JSONArray1" : []
 ,"JSArray" : []
 ,"f" : function () {
 var Result = undefined;
 Result = this.that;
 return Result
 }
 ,"child" : {
 "Callback" : function () {
    var Result = undefined;
    Result = this;
    return Result
 }
 }
};
for(x$32=1;x$32<=10;x$32++) {
 /// anonymous TClassSymbol
 /// anonymous TClassSymbol
 /// anonymous TClassSymbol
 objeto = [{
 "age" : x$32
 ,"nome" : "Nome#"+x$32.toString()
 }, {
 "cep" : "35700-00"+x$32.toString()
 ,"address" : "Addr#"+x$32.toString()
 }, {
 "country" : "Country "+x$32.toString()
 ,"city" : "City#"+x$32.toString()
 }];
 obj.JSONArray2.pusha(objeto.slice(0));
}

to be continued...

It would be handy in SMS, in this instance, to have a kind of TDataModule - collection of data stores. How to use in-memory data stores in Smart. This TDataModule should have agnostic functions like findById, findByName, etc..
  
I ended up spending a decent amount of time trying different things to get this TW3DataSet  as in memory dataStore. To save some headaches, I decided to create my own data store, it’s ugly, unfinished, I find it more flexible to use it through the units. I don't know if the right stack, anyway, stay tuned.

segunda-feira, 8 de agosto de 2016

Smart Mobile Studio - Hybrid UI

I can say that is possible to write complex applications with Smart Mobile Studio, you just have to know its limitations. Limitations, you will also face with other tools like TypeScript. For instance, depending on what you plan to do, you can use only the SMS IDE, and use pure Object Pascal, of course, you have to do everything by hand, at least you have control over everything.

Sooner or later, you will stumble working on the user interface. This is often the biggest challenge facing any mobile development project. SMS offer a solution to the user interface - the SCL framework.

SCL approach

As you know, the SMS UI part is handled by the SCL (SmartCL Framework). It has a basic component library, including source code. Simply dragging and dropping visual components on the visual designer, and changing their appearance and behaviors as desired with the Object Inspector, like the old Delphi 7.

You even can create new components based upon existing components and add then to the component library. These components responds to events, typically user actions, and it invokes changes on the view.

Hand-written code

One thing that I tend to disagree is using the RAD approach to build, develop and manage the user interface. I sincerely do not like of this methodology, which I believe is difficult to work on real projects (with a lot of views).

I'll prefer hand-written code (write HTML/CSS code manually), I consider to be more readable, more maintainable and believe to offer more customization and abstractions. CSS is all about presentation, and it takes a good amount of tweaking to get things looking right across different browsers.
Using this approach, the visual designer is not required.

Hybrid Approach

SCL (SmartCL framework) has its strengths, removing completely SmartCL* components from your smart project can severely restrict the use of the some nice features.

The short of it is: You’re not going to be able to build an entire application in SMS Visual Designer (at least not yet anyway), but it is an nice tool for quickly creating UI and prototype apps.

What is a hybrid? The proposal is using hybrid designer-developer combo.  Part developer (views created using the visual designer / SCL), part designer (views polished by true designers).

Once you understand the limitations of the SmartCL*, what is and what isn't possible currently in your design. Some views, can be created by using the visual designer. Another part —  a talented person, a HTML5/CSS designer. I’m convinced that hand-coding is an essential to create beautiful views. Hand-coding also lets you create smaller files than a software package. It's faster to create finished, tidy web page templates by hand-coding than it is to use a WYSIWYG editor, for instance.

Using this methodology, it is possible to interfacing some mostly used frameworks like ionics, F7, Material Design UI with the SCL visual designer. Using this idea will give us plenty of benefits.  

In the following experiment, I'm going to use the hybrid methodology. The intro, blog, gallery, tabs, login, social, video and contact views should be created by using hand coding approach. Only two views (Form1 and Form2) were created using the SMS visual designer.

My 2 cents

I, myself, know how to code pascal to some extent. But I find myself coding less and less, ultimately. Since designing a more complex component task is time consuming. I still feel obliged to learn and pick up new coding skills every once in a while, which is nice – I always got overloaded styling a component.

I'm frustrating with the UI processes, instead of focusing on the smart strengths - on the logic, the smart compiler to write and debug business logic in its powerful language. Using the current approach we need to be a rockstart designer-developer to design beautiful designers.

quinta-feira, 4 de agosto de 2016

Working with small JSON data sets in memory using Smart Mobile Studio

TW3DataSet

TW3Dataset is simply a small in-memory datastore that allowed you to save small data. You can save data to a normal string or stream. It also can be used as an intermediate format, you can both push data to a server (small number of records) as well as retrieve data from a server. 
If you want to persist some JSON information in the browser. Depending on user interaction with the application, you want to store 5-6 different JSON object into memory, the TW3DataSet is an option. This is great to create prototyping applications.
Note: storing large data in memory has a couple of disadvantages:
  • non-scalable — when you decide to use more processes, each process will need to make same api request;
  • fragile — if your process crashes you will lose the data.
Also working with large amount of data can block process for longer time than you would like.Solution: I'll use external storage! It can be MongoDB or RDBMS; - update data in separate process, triggered with cron; - don't drop the whole database: there is a chance that someone will make a request right after that (if your storage doesn't support transactions, of course), update records. Working with large JSON datasets can be a pain, particularly when they are too large to fit into memory.  

Example JSON

The following is an example of the JSON for the Products table. The JSON returned by the Delphi web server have this format: 

[{
 "productid" : "9V-BATTERY-12PK",
 "description" : "12-pack of 9-volt batteries",
 "listprice" : 20,
 "shipping" : 2
 }, {
 "productid" : "9V-BATTERY-4PK",
 "description" : "4-pack of 9-volt batteries",
 "listprice" : 4.5,
 "shipping" : 1.5
 }
]

We can fill a combobox using TW3Dataset, like in this example:

Define/Create the dataset 

Take a closer look at the above JSON format returned by our server (column and row data).  Before we create a dataset, we have to define what the table looks like, we have to define the field-definition property.
Ensure to add the System.Dataset unit;
  ProductsDS := TDataset.Create;
  
  ProductsDS.FieldDefs.Add('productid',ftString);
  ProductsDS.fieldDefs.Add('description',ftString);
  ProductsDS.fieldDefs.Add('listprice',ftFloat);
  ProductsDS.fieldDefs.add('shipping',ftFloat);
  ProductsDS.CreateDataset;
 DataSet Columns: When we define manually a dataset and the SaveToString method is called from the application.
The following JSON format is returned by our application:
{
 "dhMagic" : 51966,
 "dhCount" : 0,
 "dhFieldDefs" : {
  "ddMagic" : 3401235116,
  "ddDefs" : [{
    "fdName" : "productid",
    "fdDatatype" : 4
   }, {
    "fdName" : "description",
    "fdDatatype" : 4
   }, {
    "fdName" : "listprice",
    "fdDatatype" : 3
   }, {
    "fdName" : "shipping",
    "fdDatatype" : 3
   }
  ]
 },
 "dhData" : []
}
Field Types: Observe the following details the various column types and how they should be specified:
Field Type Code Description
ftUnknown 0 Unknown type - not specified collumn type
ftBoolean 1 Boolean
ftInteger 2 Integer
ftFloat 3 Float
ftString 4 String
ftDateTime 5 DateTime/Float
ftAutoInc 6 generated field
ftGUID 7 generated field

Adding records

This task is more or less identical to how you would do it under Delphi.
You have both append and insert operations. Let's use Append method for this example:

  procedure fillProductsDS;
  begin
    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := '9V-BATTERY-12PK';
    ProductsDS.Fields.FieldByName('description').AsString := '12-pack of 9-volt batteries';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 20;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := '9V-BATTERY-4PK';
    ProductsDS.Fields.FieldByName('description').AsString := '4-pack of 9-volt batteries';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 4.5;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 1.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'CALCULATOR-BUSINESS';
    ProductsDS.Fields.FieldByName('description').AsString := 'Business calculator';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 10;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 1;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'CASH-REGISTER';
    ProductsDS.Fields.FieldByName('description').AsString := 'Cash register with thermal printer';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 170;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 10;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-16GB';
    ProductsDS.Fields.FieldByName('description').AsString := '16GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 15;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-32GB';
    ProductsDS.Fields.FieldByName('description').AsString := '32GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 25;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'FLASH-USB-8GB';
    ProductsDS.Fields.FieldByName('description').AsString := '8GB USB flash drive';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 10;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.5;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'LABEL-MAKER';
    ProductsDS.Fields.FieldByName('description').AsString := 'Label maker - plastic labels';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 35;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PEN-BP-12PK';
    ProductsDS.Fields.FieldByName('description').AsString := '12-pack of ballpoint pens';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 12;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 0.6;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PHONE-HEADSET';
    ProductsDS.Fields.FieldByName('description').AsString := 'Hands-free phone headset';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 15;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PHONE-SYSTEM-4HS';
    ProductsDS.Fields.FieldByName('description').AsString := '4-handset phone system with main base';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 120;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 4;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'PROJECTOR-HD';
    ProductsDS.Fields.FieldByName('description').AsString := '1080p HD Projector';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 850;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 56;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'SCANNER-SF';
    ProductsDS.Fields.FieldByName('description').AsString := 'Sheet-feed paper scanner';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 150;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 7;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'SHREDDER-SF-CC';
    ProductsDS.Fields.FieldByName('description').AsString := 'Sheet-feed, cross-cut shredder with bin';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 8;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 10;
    ProductsDS.Post;

    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := 'USB-CARD-READER';
    ProductsDS.Fields.FieldByName('description').AsString := 'USB magnetic strip card reader';
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := 25;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := 2;
    ProductsDS.Post;
  end;

  { fill Customer dataset }  
  fillProductsDS;

 

Dataset row JSON structure

DataSet Rows: Observe the following details on the column "dhData", when the SaveToString method was called. The field "dhData" was populated, of course this could be requested from the web server using HTTP GET request.

{
 "dhMagic" : 51966,
 "dhCount" : 15,
 "dhFieldDefs" : {
  "ddMagic" : 3401235116,
  "ddDefs" : [{
    "fdName" : "productid",
    "fdDatatype" : 4
   }, {
    "fdName" : "description",
    "fdDatatype" : 4
   }, {
    "fdName" : "listprice",
    "fdDatatype" : 3
   }, {
    "fdName" : "shipping",
    "fdDatatype" : 3
   }
  ]
 },
 "dhData" : [{
   "productid" : "9V-BATTERY-12PK",
   "description" : "12-pack of 9-volt batteries",
   "listprice" : 20,
   "shipping" : 2
  }, {
   "productid" : "9V-BATTERY-4PK",
   "description" : "4-pack of 9-volt batteries",
   "listprice" : 4.5,
   "shipping" : 1.5
  }, {
   "productid" : "CALCULATOR-BUSINESS",
   "description" : "Business calculator",
   "listprice" : 10,
   "shipping" : 1
  }, {
   "productid" : "CASH-REGISTER",
   "description" : "Cash register with thermal printer",
   "listprice" : 170,
   "shipping" : 10
  }, {
   "productid" : "FLASH-USB-16GB",
   "description" : "16GB USB flash drive",
   "listprice" : 15,
   "shipping" : 0.5
  }, {
   "productid" : "FLASH-USB-32GB",
   "description" : "32GB USB flash drive",
   "listprice" : 25,
   "shipping" : 0.5
  }, {
   "productid" : "FLASH-USB-8GB",
   "description" : "8GB USB flash drive",
   "listprice" : 10,
   "shipping" : 0.5
  }, {
   "productid" : "LABEL-MAKER",
   "description" : "Label maker - plastic labels",
   "listprice" : 35,
   "shipping" : 2
  }, {
   "productid" : "PEN-BP-12PK",
   "description" : "12-pack of ballpoint pens",
   "listprice" : 12,
   "shipping" : 0.6
  }, {
   "productid" : "PHONE-HEADSET",
   "description" : "Hands-free phone headset",
   "listprice" : 15,
   "shipping" : 2
  }, {
   "productid" : "PHONE-SYSTEM-4HS",
   "description" : "4-handset phone system with main base",
   "listprice" : 120,
   "shipping" : 4
  }, {
   "productid" : "PROJECTOR-HD",
   "description" : "1080p HD Projector",
   "listprice" : 850,
   "shipping" : 56
  }, {
   "productid" : "SCANNER-SF",
   "description" : "Sheet-feed paper scanner",
   "listprice" : 150,
   "shipping" : 7
  }, {
   "productid" : "SHREDDER-SF-CC",
   "description" : "Sheet-feed, cross-cut shredder with bin",
   "listprice" : 8,
   "shipping" : 10
  }, {
   "productid" : "USB-CARD-READER",
   "description" : "USB magnetic strip card reader",
   "listprice" : 25,
   "shipping" : 2
  }
 ]
}

 

Loading/Saving records

TW3Dataset allows you to save your data to a normal string or a stream.

function    SaveToString:String;
Procedure   LoadFromString(Const aText:String);
 
Procedure   SaveToStream(const Stream:TStream);virtual;
Procedure   LoadFromStream(const Stream:TStream);virtual;
So we can store a dataset locally with SaveToString/SaveToStream methods
and retrieve the data using LoadFromString/LoadFromStream methods.
Example:

ProductsDS := TDataset.Create;
ProductsDS.LoadFromString( jsonData );
ProductsDS.CreateDataset;
This will load locally a dataset. We can use it as intermediate bridge, we can insert, append, delete data locally, and push the data to a server. In this example, let's just list two Products fields:

ProductsDS.Active := true;

procedure ListProductsDS;
begin
ProductsDS.first;
while not ProductsDS.EOF do
begin
  var id  := ProductsDS.fields.fieldbyname('productid').asString;
  var price := ProductsDS.fields.fieldbyname('listprice').asString;
  writeln(id + ' ' + price);
  ProductsDS.Next;
end;
end;

{ List ProductsDS }
ListProductsDS;

/*
9V-BATTERY-12PK   20
9V-BATTERY-4PK   4.5
CALCULATOR-BUSINESS     10
CASH-REGISTER   170
FLASH-USB-16GB   15
FLASH-USB-32GB   25
FLASH-USB-8GB   10
LABEL-MAKER   35
PEN-BP-12PK   12
PHONE-HEADSET   15
PHONE-SYSTEM-4HS  120
PROJECTOR-HD   850
SCANNER-SF   150
SHREDDER-SF-CC   8
USB-CARD-READER  25
*/

 

Load remote data

Another nice feature is that you can define your dataset locally and load data remotely. Let's suppose our Delphi REST server is returning this:
{"rows" : [{
 "productid" : "9V-BATTERY-12PK",
 "description" : "12-pack of 9-volt batteries",
 "listprice" : 20,
 "shipping" : 2
}, {
 "productid" : "9V-BATTERY-4PK",
 "description" : "4-pack of 9-volt batteries",
 "listprice" : 4.5,
 "shipping" : 1.5
}, {
 "productid" : "CALCULATOR-BUSINESS",
 "description" : "Business calculator",
 "listprice" : 10,
 "shipping" : 1
}, {
 "productid" : "CASH-REGISTER",
 "description" : "Cash register with thermal printer",
 "listprice" : 170,
 "shipping" : 10
}, {
 "productid" : "FLASH-USB-16GB",
 "description" : "16GB USB flash drive",
 "listprice" : 15,
 "shipping" : 0.5
}, {
 "productid" : "FLASH-USB-32GB",
 "description" : "32GB USB flash drive",
 "listprice" : 25,
 "shipping" : 0.5
}, {
 "productid" : "FLASH-USB-8GB",
 "description" : "8GB USB flash drive",
 "listprice" : 10,
 "shipping" : 0.5
}, {
 "productid" : "LABEL-MAKER",
 "description" : "Label maker - plastic labels",
 "listprice" : 35,
 "shipping" : 2
}, {
 "productid" : "PEN-BP-12PK",
 "description" : "12-pack of ballpoint pens",
 "listprice" : 12,
 "shipping" : 0.6
}, {
 "productid" : "PHONE-HEADSET",
 "description" : "Hands-free phone headset",
 "listprice" : 15,
 "shipping" : 2
}, {
 "productid" : "PHONE-SYSTEM-4HS",
 "description" : "4-handset phone system with main base",
 "listprice" : 120,
 "shipping" : 4
}, {
 "productid" : "PROJECTOR-HD",
 "description" : "1080p HD Projector",
 "listprice" : 850,
 "shipping" : 56
}, {
 "productid" : "SCANNER-SF",
 "description" : "Sheet-feed paper scanner",
 "listprice" : 150,
 "shipping" : 7
}, {
 "productid" : "SHREDDER-SF-CC",
 "description" : "Sheet-feed, cross-cut shredder with bin",
 "listprice" : 8,
 "shipping" : 10
}, {
 "productid" : "USB-CARD-READER",
 "description" : "USB magnetic strip card reader",
 "listprice" : 25,
 "shipping" : 2
}
]}
The dataset rows must be loaded at run-time, and the data rows can come from the web server application in JSON format. When the rows are loaded, you can specify that the rows be appended to the existing rows in the dataset, or completely replace the current rows in the dataset.
The above example of the JSON for the Products dataset returned by a server.
We can fill the predefined dataset remotely, from a JSON string, for instance:
procedure fillProductsDS(strJSON: string);
begin
  var resultSet := JSON.Parse(strJSON).rows;
  for i in resultSet do
  begin
    ProductsDS.Append;
    ProductsDS.Fields.FieldByName('productid').AsString   := resultSet[i].productid;
    ProductsDS.Fields.FieldByName('description').AsString := resultSet[i].description;
    ProductsDS.Fields.FieldByName('listprice').AsFloat    := resultSet[i].listprice;
    ProductsDS.Fields.FieldByName('shipping').AsFloat     := resultSet[i].shipping;
    ProductsDS.Post;
  end;
end;

fillProductsDS(dataJSON);

Tags: TW3Dataset; in-memory datastore; JSON store.