Jump to content
Sign in to follow this  
lynkfs

architecture

Recommended Posts

image.png

For some this is the ultimate in application architecture, a set of completely separated layers with clearly defined tasks.

One of the reasons would be that this type of application architecture facilitates client-server applications where these layers can be distributed over one or more servers. Like 'client'-'applicationserver'-'databaseserver'.

In practice this is usually not so clear-cut. Business logic can be partly implemented on the client, partly on the server (and/or partly in the database as well). In case of server-side rendering most of the presentation tasks are transferred to the server, which leaves only razor-thin clients. And logical servers can be split over multiple physical servers too.

Nevertheless it is a nice way of separating areas of concern.

Having these separate layers, it must be possible to establish some kind of communication between them and this post is about some of the available mechanisms in Smart.

Business Layer

Let's say we have a completely separated business logic layer with a data-structure of sorts and all necessary functions to maintain this data. As an example see below the layer for a "ToDo" list type of application.

type
  JToDoPerson = class
    id          : integer;
    name        : string;
  end;

type
  JToDoTask = class
    id          : integer;
    name        : string;
    owner       : string;
    statusidx   : integer;
  end;

type
  JToDoList = class
    tasks       : Array of JToDoTask;
    persons     : Array of JToDoPerson;
    statuses    : array[0..2] of string := ['todo','in progress','done'];
    task        : JToDoTask;
    person      : JToDoPerson;
    constructor create; virtual;
    procedure addTask(TaskId: integer; TaskName, Source: string);
    procedure delTask(TaskId: integer; TaskName, Source: string);
    procedure updTask(TaskId: integer; owner: string; status: integer; Source: string);
    function  getTask(TaskId: integer) : JToDoTask;
    procedure addPerson(PersonName, Source: string);
    procedure delPerson(PersonName, Source: string);
  end;  

In this demo, only the 'addPerson' method (adding a team-member to the team), is used.

Procedure JToDoList.addPerson(PersonName, Source: string);
begin
  person := JToDoPerson.Create;
  person.name := PersonName;
  inc(guid);
  person.id := guid;
  persons.add(person);
end;

 

Communication between Presentation layer and Business Logic layer.

In Smart the presentation layer is the collection of Forms and their visual components. Assuming there is a form with a button somewhere which, when clicked, adds a new team-member, there are various ways in which communication between the business-logic and the presentation layer can happen.

1) simply call the appropriate method from the button onclick handler

    // 01 add person by method call
    todolist.addPerson('name01','W3Button1');


2) use a custom event ('addPerson' event)

    // 02 add person by sending a custom event;
    asm @event = new CustomEvent('addPerson', { detail: ['name02','W3Button1'] }); end;
    browserapi.document.dispatchEvent(event);

    and in the constructor of the todo-list set up an event listener :

    // event listener for custom event
    browserapi.document.addEventListener('addPerson', procedure(e: variant)
      begin
        todolist.AddPerson(e.detail[0],e.detail[1]);
      end, false);


3) use a generic event ('businessLogic' event) and check for event-type

    // 03 add person by sending a generic event;
    var msg : variant := TVariant.CreateObject;
    msg.type := 'addPerson';
    msg.payload01 := 'name03';
    msg.payload02 := 'W3Button1';
    var payload : variant := new JObject;
    payload.detail := JSON.stringify(msg);
    asm @event = new CustomEvent('businessLogic', @payload); end;
    browserapi.document.dispatchEvent(event);

    and in the constructor of the todo-list set up an event listener :

    // event listener for generic event
    browserapi.document.addEventListener('businessLogic', procedure(e: variant)
      begin
        var msg := JSON.parse(e.detail);
        If msg.type = 'addPerson' then begin
          todolist.addPerson(msg.payload01, msg.payload02);
        end;
      end, false);


4)  use an inter-process event ('postMessage' event)

    // 04 add person by sending postMessage;
    browserapi.window.postMessage(['addPerson','name04','W3Button1'],'*');

    and in the constructor of the todo-list set up an event listener :

    browserapi.window.onmessage := procedure(evt:variant)
    begin
      if evt.data[0] = 'addPerson' then begin
        todolist.AddPerson(evt.data[1],evt.data[2]);   //'name01','W3Button1'
      end;
    end;

There are subtle differences between event-methods 2-4, but essentially they perform very similar.
I personally prefer using postMessage (4) as it involves the least amount of coding.

All of these methods work for single-device type applications (all 3 layers on client) and on fat-client client/server type applications (data on server, rest on client).

5)  use web-sockets as communication method between server and client

    // 05 add person by sending a 'message' message using websocket;
    var msg : variant := TVariant.CreateObject;
    msg.type := 'addPerson';
    msg.payload01 := 'name05';
    msg.payload02 := 'W3Button1';
    ws.send(JSON.stringify(msg));

    and in the constructor of the todo-list set up an event listener :

    asm @ws = new WebSocket('ws://localhost:1234', 'echo-protocol'); end;
    ws.addEventListener('message', procedure(e: variant)
      begin
        var msg := JSON.parse(e.data);
        If msg.type = 'addPerson' then begin
          todolist.AddPerson(msg.payload01, msg.payload02);
        end;
      end, false);

This method works for both fat-client and thin-client client/server applications.

The server used here is a simple node echo server ( see featured demos ) or, alternatively, in plain js :

//Create server and listen
var http = require('http');
var server = http.createServer(function(request, response) {});

server.listen(1234, function() {
    console.log((new Date()) + ' Server is listening on port 1234');
});

//Create Web Socket Server
var WebSocketServer = require('websocket').server;
wsServer = new WebSocketServer({
    httpServer: server
});

var count = 0;
var clients = {};

//Listening for Connections
wsServer.on('request', function(r){
  // Code here to run on connection

  //Accept the connection
  var connection = r.accept('echo-protocol', r.origin);


  // Specific id for this client & increment count
  var id = count++;
  // Store the connection method so we can loop through & contact all clients
  clients[id] = connection;
  console.log((new Date()) + ' Connection accepted [' + id + ']');

  //Listen for incoming messages and broadcast
  // Create event listener
  connection.on('message', function(message) {

    // The string message that was sent to us
    var msgString = message.utf8Data;
    console.log('received ' + msgString);

    // Loop through all clients
    for(var i in clients){
        // Send a message to the client with the message
        clients[i].sendUTF(msgString);
        var j = i + 1;
        console.log('sent ' + msgString + ' to client ' + j);
    }
  });

  //Listen for client disconnecting
  connection.on('close', function(reasonCode, description) {
    delete clients[id];
    console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
  });

});

The funny thing is that this server propagates these events to all connected clients.
So adding a team-member on any client auto-updates the team array on all other clients, without any user interaction. Cool.


Communication between Presentation layer and data layer.

Data-binding is a mechanism which synchronises data-elements with UI-components.

The above event-mechanisms could conceivably also be used to implement a simple data-binding mechanism.
The addPerson method f.i., once invoked, can send a message to the presentation layer ('personAdded')

  person := JToDoPerson.Create;
  person.name := PersonName;
  inc(guid);
  person.id := guid;
  persons.add(person);

  asm @event = new CustomEvent('personAdded', { detail: PersonName }); end;
  browserapi.document.dispatchEvent(event);

  which in it's turn can sync UI-components (TMemo) with the new data :

  // a Form's 'InitializeObject' section
  browserapi.document.addEventListener('personAdded', procedure(e: variant)
    begin
      W3Memo1.Text := W3Memo1.Text + #10 + e.detail;
    end, false);


 

Project-source :

<SMART>
  <Project version="2" subversion="9">
    <Name>temp</Name>
    <Created>T00:00:00.000</Created>
    <Modified>2018-01-17T14:26:47.488</Modified>
    <Version>
      <Major>0</Major>
      <Minor>0</Minor>
      <Revision>0</Revision>
    </Version>
    <VendorSpecific>
      <Apple>
        <FormatDetection>1</FormatDetection>
        <StatusBarStyle>black</StatusBarStyle>
        <WebAppCapable>1</WebAppCapable>
      </Apple>
      <ChromeApp>
        <Kiosk>0</Kiosk>
        <KioskOnly>1</KioskOnly>
        <OfflineEnabled>1</OfflineEnabled>
      </ChromeApp>
      <Cordova>
        <WidgetID></WidgetID>
        <AllowIntent>http://*/*
https://*/*
tel:*
sms:*
mailto:*
geo:*</AllowIntent>
      </Cordova>
    </VendorSpecific>
    <Options>
      <Compiler>
        <Assertions>1</Assertions>
        <Optimize>1</Optimize>
        <HintsLevel>1</HintsLevel>
        <ProjectSearchPath></ProjectSearchPath>
      </Compiler>
      <Codegen>
        <Obfuscation>0</Obfuscation>
        <RangeChecking>0</RangeChecking>
        <InstanceChecking>0</InstanceChecking>
        <ConditionChecking>0</ConditionChecking>
        <LoopChecking>0</LoopChecking>
        <InlineMagics>1</InlineMagics>
        <IgnorePublishedInImplementation>0</IgnorePublishedInImplementation>
        <EmitSourceLocation>0</EmitSourceLocation>
        <EmitRTTI>0</EmitRTTI>
        <Devirtualize>1</Devirtualize>
        <MainBody>1</MainBody>
        <CodePacking>0</CodePacking>
        <SmartLinking>1</SmartLinking>
        <Verbosity>1</Verbosity>
      </Codegen>
      <ConditionalDefines>
        <HandleExceptions>1</HandleExceptions>
        <AutoRefresh>0</AutoRefresh>
        <LegacySupportForIE>0</LegacySupportForIE>
      </ConditionalDefines>
      <Linker>
        <SourceMap>0</SourceMap>
        <CompressCSS>0</CompressCSS>
        <GenerateAppCacheManifest>1</GenerateAppCacheManifest>
        <GenerateChromeAppManifest>0</GenerateChromeAppManifest>
        <GenerateFireFoxManifest>0</GenerateFireFoxManifest>
        <GenerateWebAppManifest>1</GenerateWebAppManifest>
        <GenerateWidgetPackageConfigXML>0</GenerateWidgetPackageConfigXML>
        <GenerateCordovaConfigXML>0</GenerateCordovaConfigXML>
        <ExternalCSS>1</ExternalCSS>
        <Theme>default.css</Theme>
        <CustomTheme>0</CustomTheme>
        <EmbedJavaScript>1</EmbedJavaScript>
      </Linker>
      <Output>
        <JavaScriptFileName>main.js</JavaScriptFileName>
        <HtmlFileName>index.html</HtmlFileName>
        <OutputFilePath>www\</OutputFilePath>
      </Output>
      <Import />
      <Execute>
        <ServeManifest>0</ServeManifest>
        <Server>1</Server>
        <CustomFile></CustomFile>
        <LoadCustomFile>0</LoadCustomFile>
        <PauseAfterExecution>0</PauseAfterExecution>
        <ExecuteType>0</ExecuteType>
      </Execute>
      <WebFonts>
        <usewebfonts>1</usewebfonts>
        <webfontitem>
          <fontname>Ubuntu</fontname>
          <fonturl>https://fonts.googleapis.com/css?family=Ubuntu</fonturl>
        </webfontitem>
        <webfontitem>
          <fontname>Ubuntu</fontname>
          <fonturl>https://fonts.googleapis.com/css?family=Ubuntu</fonturl>
        </webfontitem>
      </WebFonts>
    </Options>
    <Files>
      <File type="main">
        <Name>temp</Name>
        <Created>2018-01-16T18:32:05.128Z</Created>
        <Modified>2018-01-17T11:03:42.612</Modified>
        <Source>
          <![CDATA[uses Globals, ecma.json, SmartCL.System, Unit1;

{$IFDEF SMART_INTERNAL_HANDLE_EXCEPTIONS}
try
{$ENDIF}
  var Application := TApplication.Create;
  Application.RunApp;

/*
  //check if local storage exists. If yes then
  writeln('getting data');
  var todolistjson := JSON.parse(browserapi.window.localStorage.getItem("todolist"));
  writeln(todolistjson.persons.length);
  //and then fill up todolist from todolistjson;

  browserapi.document.addEventListener('savedata', procedure(e: variant)
    begin
      writeln('saving data');
      browserapi.window.localStorage.setItem("todolist", JSON.stringify(e.detail));  //todolist
    end, false);
*/

{$IFDEF SMART_INTERNAL_HANDLE_EXCEPTIONS}
except
  on e: Exception do
    ShowMessage(e.Message);
end;
{$ENDIF}

]]>
        </Source>
      </File>
      <File type="unit">
        <Name>Unit1</Name>
        <Created>2018-01-16T18:32:05.128Z</Created>
        <Modified>2018-01-16T20:46:22.946</Modified>
        <Source>
          <![CDATA[unit Unit1;

interface

uses
  Pseudo.CreateForms, // auto-generated unit that creates forms during startup
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Forms, 
  SmartCL.Application, Form1;

type
  TApplication  = class(TW3CustomApplication)
  end;

implementation

end.]]>
        </Source>
      </File>
      <File type="form">
        <Name>Form1</Name>
        <Created>2018-01-16T18:32:05.128Z</Created>
        <Modified>2018-01-17T14:26:47.486</Modified>
        <Source>
          <![CDATA[unit Form1;

interface

uses 
  System.Types,
  System.Types.Convert,
  System.Objects,
  System.Time,
  SmartCL.System,
  SmartCL.Time,
  SmartCL.Graphics,
  SmartCL.Components,
  SmartCL.FileUtils,
  SmartCL.Forms,
  SmartCL.Fonts,
  SmartCL.Theme,
  SmartCL.Borders,
  SmartCL.Application,
  busLogic, Ecma.json, SmartCL.Controls.Button, SmartCL.Controls.Memo,
  SmartCL.Controls.RadioGroup;

type
  TForm1 = class(TW3Form)
    procedure W3Button1Click(Sender: TObject);
  private
    {$I 'Form1:intf'}
  protected
    procedure InitializeForm; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

implementation

uses Globals;

{ TForm1 }

procedure TForm1.W3Button1Click(Sender: TObject);
begin

  If W3RadioGroup1.Items[0].Checked then begin
    // 01 add person by method call
    todolist.addPerson('name01','W3Button1');
  end;

  If W3RadioGroup1.Items[1].Checked then begin
    // 02 add person by sending custom event;
    asm @event = new CustomEvent('addPerson', { detail: ['name02','W3Button1'] }); end;
    browserapi.document.dispatchEvent(event);
  end;

  If W3RadioGroup1.Items[2].Checked then begin
    // 03 add person by sending generic event;
    var msg : variant := TVariant.CreateObject;
    msg.type := 'addPerson';
    msg.payload01 := 'name03';
    msg.payload02 := 'W3Button1';
    var payload : variant := new JObject;
    payload.detail := JSON.stringify(msg);
    asm @event = new CustomEvent('businessLogic', @payload); end;
    browserapi.document.dispatchEvent(event);
  end;

  If W3RadioGroup1.Items[3].Checked then begin
    // 04 add person by sending postMessage;
    browserapi.window.postMessage(['addPerson','name04','W3Button1'],'*');
  end;

  If W3RadioGroup1.Items[4].Checked then begin
    // 05 add person by sending sendMessage using websocket;
    var msg : variant := TVariant.CreateObject;
    msg.type := 'addPerson';
    msg.payload01 := 'name05';
    msg.payload02 := 'W3Button1';
    ws.send(JSON.stringify(msg));
  end;

end;

procedure TForm1.InitializeForm;
begin
  inherited;
  // this is a good place to initialize components
end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}

  W3RadioGroup1.Add('01 - method call');
  W3RadioGroup1.Add('02 - custom event');
  W3RadioGroup1.Add('03 - generic event');
  W3RadioGroup1.Add('04 - postMessage');
  W3RadioGroup1.Add('05 - websocket');

  W3Memo1.Text := 'List of persons';
  browserapi.document.addEventListener('personAdded', procedure(e: variant)
    begin
      writeln('personAdded');
      W3Memo1.Text := W3Memo1.Text + #10 + e.detail;
    end, false);
end;
 
procedure TForm1.Resize;
begin
  inherited;
end;
 
initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.]]>
        </Source>
        <Design>
          <![CDATA[<?xml version="1.0" encoding="utf-16"?>
<Form version="2" subversion="9">
  <Created>2018-01-16T18:32:05.128</Created>
  <Modified>2018-01-17T14:25:27.765</Modified>
  <object type="TW3Form">
    <Caption>W3Form</Caption>
    <Name>Form1</Name>
    <object type="TW3Button">
      <Caption>add person</Caption>
      <Width>134</Width>
      <Top>240</Top>
      <Left>104</Left>
      <Height>32</Height>
      <Name>W3Button1</Name>
      <OnClick>W3Button1Click</OnClick>
    </object>
    <object type="TW3Memo">
      <Text>W3Memo</Text>
      <Width>184</Width>
      <Top>288</Top>
      <Left>80</Left>
      <Height>192</Height>
      <Name>W3Memo1</Name>
    </object>
    <object type="TW3RadioGroup">
      <Width>285</Width>
      <Top>32</Top>
      <Left>32</Left>
      <Height>196</Height>
      <Name>W3RadioGroup1</Name>
    </object>
  </object>
</Form>]]>
        </Design>
        <AutoCreate>
          <IsAutoCreate>1</IsAutoCreate>
          <IsMainForm>1</IsMainForm>
          <Order>1</Order>
        </AutoCreate>
      </File>
      <File type="unit">
        <Name>busLogic</Name>
        <Created>2018-01-16T18:33:01.624Z</Created>
        <Modified>2018-01-17T11:29:14.034</Modified>
        <Source>
          <![CDATA[unit busLogic;

interface

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

type
  JToDoPerson = class
    id          : integer;
    name        : string;
  end;

type
  JToDoTask = class
    id          : integer;
    name        : string;
    owner       : string;
    statusidx   : integer;
  end;

type
  JToDoList = class
    tasks       : Array of JToDoTask;
    persons     : Array of JToDoPerson;
    statuses    : array[0..2] of string := ['todo','in progress','done'];
    task        : JToDoTask;
    person      : JToDoPerson;
    constructor create; virtual;
    procedure addTask(TaskId: integer; TaskName, Source: string);
    procedure delTask(TaskId: integer; TaskName, Source: string);
    procedure updTask(TaskId: integer; owner: string; status: integer; Source: string);
    function  getTask(TaskId: integer) : JToDoTask;
    procedure addPerson(PersonName, Source: string);
    procedure delPerson(PersonName, Source: string);
    function  LoadList: JToDoList;
    function  GetList: JToDoList;
  end;

implementation

uses Globals, Ecma.json;

Constructor JToDoList.Create;
begin
  inherited Create;

  // reacts to custom event
  browserapi.document.addEventListener('addPerson', procedure(e: variant)
    begin
      todolist.AddPerson(e.detail[0],e.detail[1]);
    end, false);

  // reacts to generic event
  browserapi.document.addEventListener('businessLogic', procedure(e: variant)
    begin
      var msg := JSON.parse(e.detail);
      If msg.type = 'addPerson' then begin
        todolist.addPerson(msg.payload01, msg.payload02);
      end;
    end, false);

  // reacts to postMessage
  browserapi.window.onmessage := procedure(evt:variant)
  begin
    if evt.data[0] = 'addPerson' then begin
      todolist.AddPerson(evt.data[1],evt.data[2]);   //'name01','W3Button1'
      writeln(todolist.persons[todolist.persons.count-1].id);
      writeln(todolist.persons[todolist.persons.count-1].name);

      asm @event = new CustomEvent('savedata', { detail: todolist }); end;
      browserapi.document.dispatchEvent(event);
    end;
  end;

  // reacts to sendMessage using websockets
  asm @ws = new WebSocket('ws://localhost:1234', 'echo-protocol'); end;
  ws.addEventListener('message', procedure(e: variant)
    begin
      var msg := JSON.parse(e.data);
      If msg.type = 'addPerson' then begin
        todolist.AddPerson(msg.payload01, msg.payload02);
      end;
    end, false);

end;

Procedure JToDoList.addTask(TaskId: integer; TaskName, Source: string);
begin
  task := JToDoTask.Create;
  task.id := TaskId;
  task.name := TaskName;
  task.owner := '';
  task.statusidx := 0;
  tasks.add(task);
end;

Procedure JToDoList.delTask(TaskId: integer; TaskName, Source: string);
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      tasks.Delete(i);
    end;
  end;
end;

Procedure JToDoList.updTask(TaskId: integer; owner: string; status: integer; Source: string);
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      tasks[i].owner := owner;
      tasks[i].statusidx := status;
    end;
  end;
end;

Function JToDoList.getTask(TaskId: integer) : JToDoTask;
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      result := tasks[i];
    end;
  end;
end;

Function JToDoList.LoadList: JToDoList;
begin
  //
end;

Function JToDoList.GetList: JToDoList;
begin
  //
end;

Procedure JToDoList.addPerson(PersonName, Source: string);
begin
  person := JToDoPerson.Create;
  person.name := PersonName;
  inc(guid);
  person.id := guid;
  persons.add(person);

  asm @event = new CustomEvent('personAdded', { detail: PersonName }); end;
  browserapi.document.dispatchEvent(event);
end;

Procedure JToDoList.delPerson(PersonName, Source: string);
begin
  for var i := 0 to persons.length -1 do begin
    if persons[i].name = PersonName then begin
      person := persons[i];
      for var j := 0 to tasks.length -1 do begin
        if tasks[j].owner = PersonName then begin
          If tasks[j].statusidx = 1 then begin     //if 'in progress'
            tasks[j].statusidx := 0;               //reset to 'todo'
            tasks[j].owner := '';                  //unassign task
          end;
        end;
      end;
    end;
  end;
end;


end.

]]>
        </Source>
      </File>
      <File type="unit">
        <Name>globals</Name>
        <Created>2018-01-16T18:41:47.853Z</Created>
        <Modified>2018-01-17T11:12:16.808</Modified>
        <Source>
          <![CDATA[unit globals;

interface

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

var
  todolist : JToDoList;
  event: variant;
  guid : integer;
  ws   : variant;

implementation

initialization
//
  ToDoList := JToDoList.Create;
  guid := 0;

end.
]]>
        </Source>
      </File>
    </Files>
    <Target>Browser</Target>
    <Generator>Visual Components Project</Generator>
  </Project>
</SMART>

 

 

 

Share this post


Link to post
Share on other sites

MVC architecture

I'm interested lately in what type of application architectures are out there.

This post is about the Model-View-Controller pattern.
MVC has been around since the late 70's, so it's not exactly new. However there are many frameworks currently in use which are structured on MVC principles.

M stands for Model and encapsulates all data handling
V stands for Views and encapsulates all UX experiences
C stands for Controller which controls the internal logistics.

The idea is to be able to separate these functions into separate layers, so that different team members can work on different layers without interference, and/or these layers can be run on separate hardware.

A typical retrieval process then works like this :
1) the user clicks a button
2) this click is relayed to the controller
3) the controller validates the request and 
3b) if ok it sends a request to the model, 
3c) otherwise back to the view
4) the model processes the request and returns a result
5) the controller sends this result to the appropriate view
6) the view processes this and displays the end-result to the user 

Quite a few steps.

To get an idea how this works in practice, see project source below.
The main units are
- JModel. This is the unit that handles all data of the demo app (a ToDo app)
- JController is the request-validator and traffic-control unit
- JViewer handles the Views (Forms) and its visual controls

The demo perform 2 processes :
a- the initial display of existing team-members
b- the process to add a team-member

ad a.
The constructor of View1 sends a message 'listTeamMembers-step01' to the void
The constructor picks it up and broadcasts a message 'listTeamMembers-step02'
The model picks it up, sets up an array with all existing teammembers and sends a 'listTeamMembers-step03' out
The constructor picks it up and sends out a 'listTeamMembers-step04' message
View1 picks it up and formats the data in a memo

ad b.
the process is similar to a) with some added processing :
- the controller validates the request (<> '') and sends back an error message if necessary 
- the model checks if the new team-member already exists. If so it modifies the message with an error 

What do I think of this.
It is quite a bit of effort to separate these areas, however for larger projects it might well be advantageous.
It certainly needs a design-document prior to start coding.
Not a bad idea anyway.

 

<SMART>
  <Project version="2" subversion="9">
    <Name>index</Name>
    <Created>T00:00:00.000</Created>
    <Modified>2018-01-22T15:41:09.239</Modified>
    <Version>
      <Major>0</Major>
      <Minor>0</Minor>
      <Revision>0</Revision>
    </Version>
    <VendorSpecific>
      <Apple>
        <FormatDetection>1</FormatDetection>
        <StatusBarStyle>default</StatusBarStyle>
        <WebAppCapable>1</WebAppCapable>
      </Apple>
      <ChromeApp>
        <Kiosk>0</Kiosk>
        <KioskOnly>1</KioskOnly>
        <OfflineEnabled>1</OfflineEnabled>
      </ChromeApp>
      <Cordova>
        <WidgetID>com.smartmobilestudio.app</WidgetID>
        <AllowIntent>http://*/*
https://*/*
tel:*
sms:*
mailto:*
geo:*</AllowIntent>
      </Cordova>
    </VendorSpecific>
    <Options>
      <Compiler>
        <Assertions>1</Assertions>
        <Optimize>1</Optimize>
        <HintsLevel>1</HintsLevel>
        <ProjectSearchPath></ProjectSearchPath>
      </Compiler>
      <Codegen>
        <Obfuscation>0</Obfuscation>
        <RangeChecking>0</RangeChecking>
        <InstanceChecking>0</InstanceChecking>
        <ConditionChecking>0</ConditionChecking>
        <LoopChecking>0</LoopChecking>
        <InlineMagics>1</InlineMagics>
        <IgnorePublishedInImplementation>0</IgnorePublishedInImplementation>
        <EmitSourceLocation>0</EmitSourceLocation>
        <EmitRTTI>0</EmitRTTI>
        <Devirtualize>1</Devirtualize>
        <MainBody>1</MainBody>
        <CodePacking>0</CodePacking>
        <SmartLinking>1</SmartLinking>
        <Verbosity>1</Verbosity>
      </Codegen>
      <ConditionalDefines>
        <HandleExceptions>1</HandleExceptions>
        <AutoRefresh>0</AutoRefresh>
        <LegacySupportForIE>0</LegacySupportForIE>
      </ConditionalDefines>
      <Linker>
        <SourceMap>0</SourceMap>
        <CompressCSS>0</CompressCSS>
        <GenerateAppCacheManifest>0</GenerateAppCacheManifest>
        <GenerateChromeAppManifest>0</GenerateChromeAppManifest>
        <GenerateFireFoxManifest>0</GenerateFireFoxManifest>
        <GenerateWebAppManifest>0</GenerateWebAppManifest>
        <GenerateWidgetPackageConfigXML>0</GenerateWidgetPackageConfigXML>
        <GenerateCordovaConfigXML>0</GenerateCordovaConfigXML>
        <ExternalCSS>0</ExternalCSS>
        <Theme>None</Theme>
        <CustomTheme>0</CustomTheme>
        <EmbedJavaScript>0</EmbedJavaScript>
      </Linker>
      <Output>
        <JavaScriptFileName>index.js</JavaScriptFileName>
        <HtmlFileName>index.html</HtmlFileName>
        <OutputFilePath>www\</OutputFilePath>
      </Output>
      <Import />
      <Execute>
        <ServeManifest>0</ServeManifest>
        <Server>1</Server>
        <CustomFile></CustomFile>
        <LoadCustomFile>0</LoadCustomFile>
        <PauseAfterExecution>0</PauseAfterExecution>
        <ExecuteType>0</ExecuteType>
      </Execute>
      <WebFonts>
        <usewebfonts>0</usewebfonts>
        <webfontitem>
          <fontname>Ubuntu</fontname>
          <fonturl>https://fonts.googleapis.com/css?family=Ubuntu</fonturl>
        </webfontitem>
      </WebFonts>
    </Options>
    <Files>
      <File type="main">
        <Name>index</Name>
        <Created>2017-02-11T20:52:54.654Z</Created>
        <Modified>2018-01-21T21:55:34.809</Modified>
        <Source>
          <![CDATA[uses Globals;

]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JController</Name>
        <Created>2017-08-30T19:39:37.789Z</Created>
        <Modified>2018-01-22T14:09:25.416</Modified>
        <Source>
          <![CDATA[unit JController;

interface

uses
  JElement, JModel, ECMA.JSON;

type
  JW3Controller = class
  public
    constructor Create;
  end;

implementation

uses Globals;

{ JW3Controller }

constructor JW3Controller.Create;
begin
  inherited Create;

  // show first screen
  Viewer.PresentView('View1');

//
// listTeamMembers process
//
  document.addEventListener('listTeamMembers-step01', procedure(evt: variant)    //from View1
  begin
    msg := JSON.parse(evt.detail);
    DespatchMsg('listTeamMembers-step02', msg);                                  //to Model
  end, false);
//
  document.addEventListener('listTeamMembers-step03', procedure(evt: variant)    //from Model
  begin
    msg := JSON.parse(evt.detail);
    DespatchMsg('listTeamMembers-step04', msg);                                  //to View1
  end, false);
//
// addTeamMember process
//
  document.addEventListener('addTeamMember-step01', procedure(evt: variant)      //from View1
  begin
    msg := JSON.parse(evt.detail);
    if msg.payload01 <> '' then begin
      DespatchMsg('addTeamMember-step02', msg);                                  //to Model
    end else begin
      msg.payload02 := 'member name cannot be empty';
      DespatchMsg('addTeamMember-step04', msg);                                  //to View1
    end;
  end, false);
//
  document.addEventListener('addTeamMember-step03', procedure(evt: variant)
  begin
    msg := JSON.parse(evt.detail);
    DespatchMsg('addTeamMember-step04', msg);                                    //to View1
  end, false);

end;

end.


]]>
        </Source>
      </File>
      <File type="form">
        <Name>View1</Name>
        <Created>2017-09-08T18:19:10.866Z</Created>
        <Modified>2018-01-22T13:52:24.475</Modified>
        <Source>
          <![CDATA[unit View1;

interface

uses JElement, JView, JToolBar, JImage, JInput, JButton, JPanel, JTextArea;

type
  TView1 = class(TW3View)
  private
    {$I 'View1:intf'}
  protected
    procedure InitializeView; override;
    procedure InitializeObject; override;
    procedure Resize; override;
    ToolBar : JW3ToolBar;
    Label   : JW3Panel;
    Memo    : JW3TextArea;
  end;


implementation

uses Globals, ECMA.JSON;

{ TView1 }

procedure TView1.InitializeView;
begin
  inherited;
  // this is a good place to initialize components

  //listen for custom event 'listTeamMembers-step04'
  document.addEventListener('listTeamMembers-step04', procedure(evt: variant)
  begin
    Memo.ListMembers;
  end, false);

  //listen for custom event 'addTeamMember-step04'
  document.addEventListener('addTeamMember-step04', procedure(evt: variant)
  begin
    msg := JSON.parse(evt.detail);
    Label.setinnerHtml(msg.payload01 + ' ' + msg.payload02);
    Memo.ListMembers;
  end, false);

end;

procedure TView1.InitializeObject;
begin
  inherited;
  {$I 'View1:impl'}

//Init Logo
  var Image0 := JW3Image.Create(self);
  Image0.SetBounds(0, 0, 194, 45);
  Image0.setAttribute('src','images/logo.png');

//Init ToolBar
  var ToolBar := JW3ToolBar.Create(self);
  ToolBar.SetBounds(0, 45, screenwidth, 40);
  ToolBar.setProperty('background-color', '#699BCE');
  ToolBar.AddMenu('View#1','View1','white');
  ToolBar.AddMenu('View#2','View2','white');
  ToolBar.SetActiveMenu('View1');

  var Input01 := JW3Input.Create(self);
  Input01.SetBounds(50, 200, 200, 30);
  Input01.SetAttribute('placeholder','member name');

  Var Button1 := JW3Button.Create(self);
  Button1.SetBounds(270, 200, 100, 36);
  Button1.SetInnerHtml('add member');
  Button1.OnClick := procedure(sender: TObject)
  begin
    msg.payload01 := Input01.handle.value;
    DespatchMsg('addTeamMember-step01', msg);
    Input01.handle.value := '';
  end;

  Label := JW3Panel.Create(self);
  Label.SetBounds(50, 250, 500, 30);

  Memo := JW3TextArea.Create(self);
  Memo.SetBounds(420, 200, 320, 150);

  self.OnReadyExecute := procedure(sender: TObject)
  begin
    DespatchMsg('listTeamMembers-step01', msg);
  end;

end;

procedure TView1.Resize;
begin
  inherited;
end;

end.]]>
        </Source>
        <Design>
          <![CDATA[<?xml version="1.0" encoding="utf-16"?>
<Form version="2" subversion="9">
  <Created>2017-09-08T18:19:10.866</Created>
  <Modified>2017-09-08T18:19:10.867</Modified>
  <object type="TW3Form">
    <Caption>W3Form</Caption>
    <Name>Form1</Name>
  </object>
</Form>]]>
        </Design>
        <AutoCreate>
          <IsAutoCreate>1</IsAutoCreate>
          <IsMainForm>0</IsMainForm>
          <Order>1</Order>
        </AutoCreate>
      </File>
      <File type="form">
        <Name>View2</Name>
        <Created>2017-09-09T22:28:49.492Z</Created>
        <Modified>2018-01-22T12:30:19.880</Modified>
        <Source>
          <![CDATA[unit View2;

interface

uses JElement, JView;

type
  TView2 = class(TW3View)
  private
    {$I 'View2:intf'}
  protected
    procedure InitializeView; override;
    procedure InitializeObject; override;
    procedure Resize; override;
  end;

implementation

{ TView2 }

procedure TView2.InitializeView;
begin
  inherited;
  // this is a good place to initialize components
end;

procedure TView2.InitializeObject;
begin
  inherited;
  {$I 'View2:impl'}

end;

procedure TView2.Resize;
begin
  inherited;
end;

end.]]>
        </Source>
        <Design>
          <![CDATA[<?xml version="1.0" encoding="utf-16"?>
<Form version="2" subversion="9">
  <Created>2017-09-09T22:28:49.492</Created>
  <Modified>2017-09-09T22:28:49.493</Modified>
  <object type="TW3Form">
    <Caption>W3Form</Caption>
    <Name>Form2</Name>
  </object>
</Form>]]>
        </Design>
        <AutoCreate>
          <IsAutoCreate>1</IsAutoCreate>
          <IsMainForm>0</IsMainForm>
          <Order>2</Order>
        </AutoCreate>
      </File>
      <File type="unit">
        <Name>JViewer</Name>
        <Created>2018-01-21T19:41:22.962Z</Created>
        <Modified>2018-01-21T20:52:25.629</Modified>
        <Source>
          <![CDATA[unit JViewer;

interface

uses
  JElement, JView, View1, View2;

type
  JW3Viewer = class(TElement)
  public
    ViewNames: Array of String;
    ViewsClasses: Array of TViewClass;       //TViewClass = class of JW3View;
    ViewsInstances: Array of TW3View;
    constructor Create(parent: TElement); virtual;
    procedure CreateView(ViewName: String; aClassType: TViewClass);
    procedure PresentView(ViewName: String);
  end;

implementation

{ JW3Viewer }

constructor JW3Viewer.Create(parent: TElement);
begin
  inherited Create('div', parent);

  setProperty('width','100%');
  setProperty('height','100%');
  setProperty('background-color','white');

  //create Views
  CreateView('View1',TView1);
  CreateView('View2',TView2);

end;

procedure JW3Viewer.CreateView(ViewName: String; aClassType: TViewClass);
begin
//
  ViewNames.Add(ViewName);
  ViewsClasses.Add(aClassType);
  ViewsInstances.Add(nil);
end;

procedure JW3Viewer.PresentView(ViewName: String);
begin
//
  For var i := 0 to ViewNames.Count -1 do begin
    If ViewsInstances[i] <> nil then
      ViewsInstances[i].SetProperty('display','none');
    If ViewNames[i] = ViewName then begin
      If ViewsInstances[i] = nil       //View has never been displayed yet
        then ViewsInstances[i] := ViewsClasses[i].Create(self)
        else ViewsInstances[i].SetProperty('display','inline-block');

        (ViewsInstances[i] as ViewsClasses[i]).InitializeView;    //ClearView;
        (ViewsInstances[i] as ViewsClasses[i]).InitializeObject;  //ShowView;
    end;
  end;
end;

end.


]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JModel</Name>
        <Created>2018-01-21T20:10:14.232Z</Created>
        <Modified>2018-01-22T13:37:55.058</Modified>
        <Source>
          <![CDATA[unit JModel;

interface

type
  JToDoPerson = class
    id          : integer;
    name        : string;
  end;

type
  JToDoTask = class
    id          : integer;
    name        : string;
    owner       : string;
    statusidx   : integer;
  end;

type
  JToDoList = class
    tasks       : Array of JToDoTask;
    persons     : Array of JToDoPerson;
    statuses    : array[0..2] of string := ['todo','in progress','done'];
    task        : JToDoTask;
    person      : JToDoPerson;
    constructor create; virtual;
    procedure addTask(TaskId: integer; TaskName: string);
    procedure delTask(TaskId: integer; TaskName: string);
    procedure updTask(TaskId: integer; owner: string; status: integer);
    function  getTask(TaskId: integer) : JToDoTask;
    procedure addPerson(PersonName: string);
    procedure delPerson(PersonName: string);
    function  getPerson(PersonName: string) : JToDoPerson;
    function  getPersons : variant;
  end;


implementation

uses Globals, ECMA.JSON;

Constructor JToDoList.Create;
begin
  inherited Create;

  document.addEventListener('listTeamMembers-step02', procedure(evt: variant)
  begin
    msg := JSON.parse(evt.detail);
    msg.payload03 := getPersons;
    DespatchMsg('listTeamMembers-step03', msg);
  end, false);
//
  document.addEventListener('addTeamMember-step02', procedure(evt: variant)
  begin
    msg := JSON.parse(evt.detail);
    person := Model.getPerson(msg.payload01);   // check if already exists
    If person = nil then begin                  // if not then add to model
      Model.AddPerson(msg.payload01);
      msg.payload02 := 'added to database';     // add confirmation message (payload02)
      msg.payload03 := getPersons;              // add a list of all members (payload03)
      DespatchMsg('addTeamMember-step03', msg);
    end else begin
      msg.payload02 := 'already exists';        // otherwise add error message (payload02)
      DespatchMsg('addTeamMember-step03', msg);
    end;
  end, false);

  AddPerson('Steve McQueen');
  AddPerson('Dorothy Stallart');
  AddPerson('Nathalie Drupsteen');
  AddPerson('Pjotr Molibskanski');

end;

Procedure JToDoList.addTask(TaskId: integer; TaskName: string);
begin
  task := JToDoTask.Create;
  task.id := TaskId;
  task.name := TaskName;
  task.owner := '';
  task.statusidx := 0;
  tasks.add(task);
end;

Procedure JToDoList.delTask(TaskId: integer; TaskName: string);
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      tasks.Delete(i);
    end;
  end;
end;

Procedure JToDoList.updTask(TaskId: integer; owner: string; status: integer);
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      tasks[i].owner := owner;
      tasks[i].statusidx := status;
    end;
  end;
end;

Function JToDoList.getTask(TaskId: integer) : JToDoTask;
begin
  for var i := 0 to tasks.length -1 do begin
    if tasks[i].id = TaskId then begin
      result := tasks[i];
    end;
  end;
end;

Procedure JToDoList.addPerson(PersonName: string);
begin
  person := JToDoPerson.Create;
  person.name := PersonName;
  inc(guid);
  person.id := guid;
  persons.add(person);
end;

Procedure JToDoList.delPerson(PersonName: string);
begin
  for var i := 0 to persons.length -1 do begin
    if persons[i].name = PersonName then begin
      person := persons[i];
      for var j := 0 to tasks.length -1 do begin
        if tasks[j].owner = PersonName then begin
          If tasks[j].statusidx = 1 then begin     //if 'in progress'
            tasks[j].statusidx := 0;               //reset to 'todo'
            tasks[j].owner := '';                  //unassign task
          end;
        end;
      end;
    end;
  end;
end;

Function JToDoList.getPerson(PersonName: string) : JToDoPerson;
begin
  Result := nil;
  for var i := 0 to persons.length -1 do begin
    if persons[i].name = PersonName then begin
      result := persons[i];
    end;
  end;
end;

Function JToDoList.getPersons : variant;
begin
  list.clear;
  for var i := 0 to persons.length -1 do begin
    list[i] := inttostr(persons[i].id) + ': ' + persons[i].name;
  end;
  Result := list;
end;

end.

]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JElement</Name>
        <Created>2018-01-21T20:11:19.797Z</Created>
        <Modified>2018-01-21T21:42:44.361</Modified>
        <Source>
          <![CDATA[unit JElement;

interface

uses W3C.HTML5, W3C.DOM, W3C.CSS;

type
  TMouseClickEvent   = procedure(sender:TObject);
  TResizeEvent       = procedure(sender:TObject);
  TReadyExecuteEvent = procedure(sender:TObject);

type
  TW3CustomControl = class             //the visual designer expects a TW3CustomControl
  published                            //including references to event-handling
    property OnClick: TMouseClickEvent;
    property ReSize:  TResizeEvent;
  public
    name: string;
end;

type
//TElement = class                     //therefore TElement derives from the
  TElement = class(TW3CustomControl)   //dummy TW3CustomControl above.
  private
    procedure SetLeft(aLeft: integer);
    function  GetLeft: Integer;
    procedure SetTop(aTop: integer);
    function  GetTop: Integer;
    procedure SetWidth(aWidth: integer);
    function  GetWidth: Integer;
    procedure SetHeight(aHeight: integer);
    function  GetHeight: Integer;

    FOnClick:  TMouseClickEvent;
    FOnResize: TResizeEvent;
    FOnReadyExecute: TReadyExecuteEvent;
    procedure _setMouseClick(const aValue: TMouseClickEvent);
    procedure _setOnResize(const aValue: TResizeEvent);
    procedure _setOnReadyExecute(const aValue: TReadyExecuteEvent);
  public
    FElement: JHTMLElement;
    Handle: Variant;
    constructor Create(element: String; parent: TElement);
    destructor Destroy; override;

    Procedure SetProperty(S1: String; S2: String);
    Procedure SetAttribute(S1: String; S2: String);

    Procedure SetBounds(aleft, atop, awidth, aheight: integer);
    Procedure SetinnerHTML(S1: String);
    Function  GetinnerHTML : String;

    property  Left: Integer read getLeft write setLeft;
    property  Top: Integer read getTop write setTop;
    property  Width: Integer read getWidth write setWidth;
    property  Height: Integer read getHeight write setHeight;

    property  OnClick: TMouseClickEvent read FOnClick write _setMouseClick;
    procedure CBClick(eventObj: JEvent); virtual;

    property  OnReSize: TResizeEvent read FOnResize write _setOnResize;
    procedure CBResize(eventObj: JEvent); virtual;

    property  OnReadyExecute: TReadyExecuteEvent read FOnReadyExecute write _setOnReadyExecute;
    procedure CBReadyExecute(eventObj: JEvent); virtual;

    procedure Observe;
    procedure Clear;
    //procedure ReadyExecute(const OnReady: Procedure);       //not needed when using observe.

    procedure touch2Mouse(e: variant);

    tag, name: string;
end;

type
  TMutationObserver = class
  protected
    Constructor Create;virtual;
    procedure   CBMutationChange(mutationRecordsList:variant);virtual;
  public
    FHandle:    Variant;
end;

implementation

uses Globals;

{ TElement }

constructor TElement.Create(element: String; parent: TElement);
begin
  // cache element
  FElement := JHTMLElement(document.createElement(element));
  FElement.className := element;
  //FElement.id := TW3Identifiers.GenerateUniqueObjectId();

  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  FElementStyle.setProperty('visibility','visible');
  FElementStyle.setProperty('display','inline-block');
  FElementStyle.setProperty('position','absolute');
  FElementStyle.setProperty('overflow','auto');

  If parent = nil
    then Handle := document.body.appendChild(FElement)
    else Handle := parent.FElement.appendChild(FElement);

  SetBounds(0,0,0,0);

  FElement.addEventListener("click", @CBClick, false);
  window.addEventListener("resize", @CBResize, false);
  FElement.addEventListener("readyexecute", @CBReadyExecute, false);

  Observe;

end;

Procedure TElement.SetProperty(s1: String; S2: String);
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  FElementStyle.setProperty(S1, S2);
end;

Procedure TElement.SetAttribute(S1: String; S2: String);
begin
  FElement.setAttribute(S1, S2);
end;

Procedure TElement.SetBounds(aleft, atop, awidth, aheight: integer);
begin
  left   := aleft;
  top    := atop;
  width  := awidth;
  height := aheight;
end;

Procedure TElement.SetinnerHTML(S1: String);
begin
  FElement.innerHTML := S1;
end;

Function TElement.GetinnerHTML : String;
begin
  Result := FElement.innerHTML;
end;

procedure TElement._setMouseClick(const aValue: TMouseClickEvent);
begin
  FOnClick := aValue;
end;

procedure TElement.CBClick(eventObj: JEvent);
begin
  eventObj.stopPropagation;
  if Assigned(FOnClick) then
    FOnClick(Self);
end;

procedure TElement._setOnResize(const aValue: TResizeEvent);
begin
  FOnResize := aValue;
end;

procedure TElement.CBResize(eventObj: JEvent);
begin
  if Assigned(FOnResize) then
    FOnResize(Self);
end;

procedure TElement._setOnReadyExecute(const aValue: TReadyExecuteEvent);
begin
  FOnReadyExecute := aValue;
end;

procedure TElement.CBReadyExecute(eventObj: JEvent);
begin
//  eventObj.stopPropagation;
  if Assigned(FOnReadyExecute) then
    FOnReadyExecute(Self);
end;

procedure TElement.SetLeft(aLeft: Integer);
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  FElementStyle.setProperty('left',inttostr(aLeft)+'px');
end;

function  TElement.GetLeft: Integer;
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;

  var S : string := FElementStyle.getPropertyValue('left');
  if StrEndsWith(S,'px') then SetLength(S, S.Length-2);
//  alternatively : if Pos('px',S) > 0 then SetLength(S, S.Length-2);
  Result := StrToInt(S);
end;

procedure TElement.SetTop(aTop: Integer);
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  FElementStyle.setProperty('top',inttostr(aTop)+'px');
end;

function  TElement.GetTop: Integer;
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;

  var S : string := FElementStyle.getPropertyValue('top');
  if StrEndsWith(S,'px') then SetLength(S, S.Length-2);
  Result := StrToInt(S);
end;

procedure TElement.SetWidth(aWidth: Integer);
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  if aWidth = screenwidth
    then FElementStyle.setProperty('width','calc(100%)')
    else FElementStyle.setProperty('width',inttostr(aWidth)+'px');
end;

function  TElement.GetWidth: Integer;
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;

  var S : string := FElementStyle.getPropertyValue('width');
  if StrEndsWith(S,'px') then SetLength(S, S.Length-2);
  Result := StrToInt(S);
end;

procedure TElement.SetHeight(aHeight: Integer);
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;
  FElementStyle.setProperty('height',inttostr(aHeight)+'px');
end;

function  TElement.GetHeight: Integer;
begin
  var FElementStyle := JElementCSSInlineStyle(FElement).style;

  var S : string := FElementStyle.getPropertyValue('height');
  if StrEndsWith(S,'px') then SetLength(S, S.Length-2);
  Result := StrToInt(S);
end;

procedure TElement.Clear;
begin
  While assigned(FElement.firstChild) do
    FElement.removeChild(FElement.firstChild);
end;

destructor TElement.Destroy;
begin
  Felement.parentNode.removeChild(Felement);
end;

procedure TElement.Observe;
begin
  var MyObserver := TMutationObserver.Create;
  var v: variant := new JObject;
  v.attributes := true;
  v.attributeOldValue := true;
//  v.childList := true;

  MyObserver.FHandle.observe(handle, v);
end;

/*
//original readyexecute copied from rtl. Works but not needed
//usage :
//  Handle.ReadyExecute( procedure ()
//  begin
//    DoSomething;
//  end);
//
procedure TElement.ReadyExecute(const OnReady: Procedure);
var
  LExists:  Boolean;
Begin
  LExists := document.body.contains(self.handle);
  if LExists then
    OnReady()
  else
    window.setTimeout(procedure ()
    begin
      ReadyExecute(OnReady);
    end, 100);
end;
*/

procedure TElement.touch2Mouse(e: variant);
begin
//mapping touch events to mouse events. See JSplitter for example
//https://www.codicode.com/art/easy_way_to_add_touch_support_to_your_website.aspx

  var theTouch := e.changedTouches[0];
  var mouseEv : variant;

  case e.type of
    "touchstart": mouseEv := "mousedown";
    "touchend":   mouseEv := "mouseup";
    "touchmove":  mouseEv := "mousemove";
    else exit;
  end;

  var mouseEvent := document.createEvent("MouseEvent");
  mouseEvent.initMouseEvent(mouseEv, true, true, window, 1, theTouch.screenX, theTouch.screenY, theTouch.clientX, theTouch.clientY, false, false, false, false, 0, null);
  theTouch.target.dispatchEvent(mouseEvent);

  e.preventDefault();
end;


//#############################################################################
// TMutationObserver
//#############################################################################

Constructor TMutationObserver.Create;
var
  mRef: procedure (data:Variant);
  mhandle:  variant;
begin
  inherited Create;

  mRef:=@CBMutationChange;
  asm
    @mHandle = new MutationObserver(function (_a_d) {@mRef(_a_d);});
  end;
  Fhandle:=mHandle;
end;

procedure TMutationObserver.CBMutationChange(mutationRecordsList:variant);
var
  LEvent: Variant;
begin
  FHandle.disconnect();
  asm @LEvent = new Event('readyexecute'); end;
  mutationRecordsList[length(mutationRecordsList)-1].target.dispatchEvent(LEvent);
end;


initialization
//
  ScreenWidth := Window.innerWidth;

end.

]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JView</Name>
        <Created>2018-01-21T20:12:18.036Z</Created>
        <Modified>2018-01-21T20:13:28.608</Modified>
        <Source>
          <![CDATA[unit JView;

interface

uses
  JElement;

type
  TW3View = class(TElement)
  public
    procedure InitializeView; virtual;
    procedure InitializeObject; virtual;
    procedure ReSize; virtual;
    constructor Create(parent: TElement); virtual;
    Caption: String;
  end;

  TViewClass = class of TW3View;

implementation

uses Globals;

{ TW3View }

constructor TW3View.Create(parent: TElement);
begin
  inherited Create('div', parent);
  SetProperty('border','1px double #2196f3');
  Left := 5; Top := 5;
  setProperty('width','calc(100% - 12px)');
  setProperty('height','calc(100% - 12px)');
  setProperty('background-color','white');

  /* This forces the browsers that support it to
     use the GPU rather than CPU for movement */
  self.setProperty('will-change','transView');
  self.setProperty('-webkit-transView','translateZ(0px)');
  self.setProperty(   '-moz-transView','translateZ(0px)');
  self.setProperty(    '-ms-transView','translateZ(0px)');
  self.setProperty(     '-o-transView','translateZ(0px)');
  self.setProperty(        'transView','translateZ(0px)');

  OnResize := procedure(sender:TObject)
  begin
    screenwidth := window.innerWidth;
    ReSize;
  end;

end;

Procedure TW3View.InitializeView;
begin
  //clear View
  self.Clear;
end;

Procedure TW3View.InitializeObject;
begin
//
end;

Procedure TW3View.ReSize;
begin
//
end;

end.


]]>
        </Source>
      </File>
      <File type="unit">
        <Name>Globals</Name>
        <Created>2018-01-21T20:12:52.875Z</Created>
        <Modified>2018-01-22T12:15:18.624</Modified>
        <Source>
          <![CDATA[unit Globals;

interface

uses JController, JViewer, JModel, W3C.DOM, ECMA.JSON;

procedure DespatchMsg (msgtype: string; message: variant);

//framework globals
var Controller  : JW3Controller;
var Viewer      : JW3Viewer;
var Model       : JToDoList;
var ScreenWidth : Integer := 0;
var guid        : Integer := 0;
var event       : variant;                        //custom event instance
var msg         : variant := new JObject;         //custom event content
var list        : variant := new JObject;         //generic array
var document external 'document': variant;
var window   external 'window'  : variant;
var console  external 'console' : variant;

implementation

Procedure DespatchMsg (msgtype: string; message: variant);
begin
    var payload : variant := new JObject;
    payload.detail := JSON.stringify(msg);
    asm @event = new CustomEvent((@msgtype), (@payload)); end;
    document.dispatchEvent(event);
end;

initialization
//
  Viewer     := JW3Viewer.Create(nil);
  Controller := JW3Controller.Create;
  Model      := JToDoList.Create;
  asm @list = []; end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JPanel</Name>
        <Created>2018-01-21T20:14:01.488Z</Created>
        <Modified>2018-01-21T20:14:13.460</Modified>
        <Source>
          <![CDATA[unit JPanel;

interface

uses
  JElement;

type
  JW3Panel = class(TElement)
  public
    constructor Create(parent: TElement); virtual;
  end;

implementation

{ JW3Panel }

constructor JW3Panel.Create(parent: TElement);
begin
  inherited Create('div', parent);
end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JImage</Name>
        <Created>2018-01-21T20:14:43.273Z</Created>
        <Modified>2018-01-21T20:15:24.813</Modified>
        <Source>
          <![CDATA[unit JImage;

interface

uses
  JElement;

type
  JW3Image = partial class(TElement)
  public
    constructor Create(parent: TElement); virtual;
  end;

implementation

{ JW3Image }

constructor JW3Image.Create(parent: TElement);
begin
  inherited Create('img', parent);
end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JToolBar</Name>
        <Created>2018-01-21T20:15:05.540Z</Created>
        <Modified>2018-01-21T23:41:17.959</Modified>
        <Source>
          <![CDATA[unit JToolBar;


interface

uses
  JElement, JPanel;

type
  JW3ToolBar = class(TElement)
  private
    ToolBarItems: Array of JW3Panel;
  public
    constructor Create(parent: TElement); virtual;
    Procedure AddMenu(menuText, GotoView, color: string);
    Procedure SetActiveMenu(Viewname: string);
  end;

implementation

uses Globals;

{ JW3MenuBar }

constructor JW3ToolBar.Create(parent: TElement);
begin
  inherited Create('div', parent);
end;

procedure JW3ToolBar.AddMenu(menuText, GotoView, color: string);
begin
//
  var Panel0 := JW3Panel.Create(self);
  Panel0.SetBounds(20 + ((ToolBarItems.Count) * 100), 14, 90, 26);
  Panel0.SetinnerHtml(menuText);
  Panel0.setProperty('color', color);
  Panel0.setProperty('cursor','pointer');
  Panel0.SetProperty('font-size', '0.9em');

  ToolBarItems.Add(Panel0);
  Panel0.Tag := GotoView;
//  Panel0.OnClick := procedure(Sender:TObject)
//    begin
//      if Viewer.ViewNames.IndexOf((Sender as JW3Panel).tag) > -1        //if View
//        then Viewer.PresentView((Sender as JW3Panel).tag)               //then gotoView
//        else window.postMessage([self.handle.id,'click',GoToView],'*'); //else send message
//    end;
end;

procedure JW3ToolBar.SetActiveMenu(ViewName: String);
begin
//
  For var i := 0 to ToolBarItems.Count -1 do begin
    ToolBarItems[i].setProperty('font-weight', 'normal');
    ToolBarItems[i].setProperty('text-decoration', 'none');
    If ToolBarItems[i].Tag = ViewName then
    begin
      ToolBarItems[i].setProperty('font-weight', 'bold');
      ToolBarItems[i].setProperty('text-decoration', 'underline');
    end;
  end;
end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JButton</Name>
        <Created>2018-01-21T20:54:00.490Z</Created>
        <Modified>2018-01-21T20:54:13.033</Modified>
        <Source>
          <![CDATA[unit JButton;

interface

uses
  JElement;

type
  JW3Button = class(TElement)
  public
    constructor Create(parent: TElement); virtual;
  end;

implementation

uses Globals;

{ JW3Button }

constructor JW3Button.Create(parent: TElement);
begin
  inherited Create('button', parent);
  SetProperty('color','white');
  SetProperty('border-radius', '4px');
  SetProperty('background', '#699BCE');
  SetProperty('cursor','pointer');
  SetProperty('box-shadow',#'0 -1px 1px 0 rgba(0, 0, 0, 0.25) inset,
                             0 1px 1px 0 rgba(0, 0, 0, 0.10) inset;)');

end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JInput</Name>
        <Created>2018-01-21T20:54:52.523Z</Created>
        <Modified>2018-01-21T21:36:51.762</Modified>
        <Source>
          <![CDATA[unit JInput;

interface

uses
  JElement;

type
  JW3Input = class(TElement)
  public
    constructor Create(parent: TElement); virtual;
  end;

implementation

{ JW3Input }

constructor JW3Input.Create(parent: TElement);
begin
  inherited Create('input', parent);
  self.SetAttribute('type','text');
end;

end.
]]>
        </Source>
      </File>
      <File type="unit">
        <Name>JTextArea</Name>
        <Created>2018-01-22T12:56:18.740Z</Created>
        <Modified>2018-01-22T13:48:29.139</Modified>
        <Source>
          <![CDATA[unit JTextArea;

interface

uses
  JElement;

type
  JW3TextArea = class(TElement)
  public
    constructor Create(parent: TElement); virtual;
    procedure ListMembers;
  end;

implementation

uses Globals;

{ JW3TextArea }

constructor JW3TextArea.Create(parent: TElement);
begin
  inherited Create('textarea', parent);
end;

procedure JW3TextArea.ListMembers;
begin
  If msg.payload03 <> nil then   // list all team members
  begin
    self.handle.value := 'list of members' + #10 + '===============' + #10;
    for var y := 0 to msg.payload03.length -1 do begin
      self.handle.value += msg.payload03[y] + #10;
    end;
  end;
end;

end.
]]>
        </Source>
      </File>
    </Files>
    <Target>Browser</Target>
    <Generator>Visual JForms Template</Generator>
  </Project>
</SMART>

 

Share this post


Link to post
Share on other sites

Some experiments in data binding based on messaging (see previous posts in this thread), rather than using watch/observe (which according to mozilla are both in the process of being deprecated anyway)

1) binding visual controls

Adding a method or set of properties to one of the parents of TW3CustomControl by way of partial classes is feasible. The demo app(see below) has added a 'AddBindingFrom(Control)' method which takes care of the listening/despatch mechanism, and adds this method to all visual controls.

usage :

  Memo2.addBindingFrom(Memo1);

the contents of Memo2 will now always reflect the contents of Memo1 

 

Memo1.addBindingFrom(Memo2)

Adding this call makes the binding bi-directional, and the combined effect is that all changes to one Memo will be copied to the other and vice versa.

 

2) binding visual controls to data.

A similar method has been added which binds data objects to visual controls : AddBindingFromData(Object)

 

See demo here. The demo consists of a form with 2 Memo's, an EditBox and a Panel.

Binding is set from

  • the EditBox to the Panel (when something is typed in the editbox, it is reflected in the panel text)
  • The EditBox to Memo2¬†(when something is typed in the editbox, it is reflected in Memo2)
  • The EditBox to Memo1¬†(when something is typed in the editbox, it is reflected in Memo1)
  • The Memo2¬†to Memo1¬†(when something is typed in¬†Memo1, it is reflected in Memo2)
  • a data object to the Panel (a new¬†employee Joseph¬†is added to the data object on form initialisation and the text in the panel reflects that).
  Panel1.AddBindingFrom(EditBox1);
  Memo2.AddBindingFrom(EditBox1);
  Memo1.AddBindingFrom(EditBox1);
  Memo2.addBindingFrom(Memo1);
  Panel1.addBindingFromData('teammember');

  Model.addPerson('new employee Joseph');  //triggers change event to Panel

Seems to work well. Binding mechanism has been included in the native framework.

 

 

Share this post


Link to post
Share on other sites

Back to the MVC (model-view-controller) pattern.

Looking at the Controller bit, it really is a simple traffic light system. It takes a large number of inputs/messages, transforms them a bit if necessary and creates a large number of outputs/messages.

A perfect candidate to replace the whole thing with a neural network.

Let's call it the MVNN architectural pattern.

Application development / programming is then inching more towards an exercise in providing training examples rather than hard-coding if's and else's

and since there is a neural network component in the native framework .... 

stay tuned

 

Share this post


Link to post
Share on other sites

I must admit I'm getting a bit more comfortable with the MVC application architecture. The pro's are that applications are more structured, it does confine coding to distinctly separate areas and it forces a degree of analysis beforehand. The con's that there is generally more effort involved and that there are limitations in the choice of communication 'protocols' between the MVC components.

There is a framework in the making which

  • codifies application tasks into a generic structure
  • uses this structure to generate¬†the messaging scaffolding
  • allows binding patterns as in¬†V-C-M-C-V (view to controller to model to controller to view), V-M-V (direct bi-directional binding from visual elements to data), V-V (binding separate visual components on the same view/form), V-C-M-V and any other combination imaginable

 

image.png

 

The demo here doesn't amount to much, just a form where new members can be added to a fictitious team. However demo was produced using this framework, features multiple application tasks, includes data-validation outside the view, uses various mvc-patterns, has a back-end datastore -- and comes in at a mere 14kB compiled :)

 

Share this post


Link to post
Share on other sites

For interested parties, find below paired down version of the latest (and for me final) MVC demo / framework. I've made it as lean as possible.
Demo here  and project code here

Based on a suggestion (IELite), the M, V and C components each live in their own project. They are bound together by combining the three compiled js files in the main html file :

<body>  
    <script type="text/javascript" src="view.js"></script>
    <script type="text/javascript" src="controller.js"></script>
    <script type="text/javascript" src="model.js"></script>
</body>

and setting the relative output path in the linker options to a shared parent directory ('..\')

 


 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×