[Pas2js] Pas2JS-Widgets with LFM streaming

Sven Barth pascaldragon at googlemail.com
Sat Jul 27 11:23:13 CEST 2019


Am 27.07.2019 um 09:33 schrieb Michael Van Canneyt:
>
>
> On Fri, 26 Jul 2019, Sven Barth via Pas2js wrote:
>
>> Hello together!
>>
>> I've experimented a bit with Pas2JS and the Pas2JS-Widgets by 
>> heliosroots ( 
>> https://github.com/heliosroots/Pas2JS_Widget/tree/master ) and 
>> implemented loading of unmodified LFM files. It's a bit cumbersome 
>> for now, cause it needs changes in the project's HTML source as well 
>> as the main project file for each form added, but as a proof of 
>> concept it works. I've attached the patch that needs to be applied to 
>> the Pas2JS-Widget master.
>
> Impressive :-)

Thanks, though without your streaming implementation it would not have 
been possible that easily. ;)

>
>>
>> To use it you proceed as follows:
>> - make sure you have installed Pas2JSDsgn (from Pas2JS) and 
>> Pas2JS_Designer_Package (from Pas2JS-Widgets)
>> - make sure you at least once opened Pas2JS_RTL (from Pas2JS) and 
>> Pas2JS_Widget (from Pas2JS-Widgets)
>> - create a new "Web Browser Application"
>> - add Pas2JS_RTL and Pas2JS_Widget as requirements
>> - add new forms using New -> Module -> Web Form (Pas2JS)
>> - remove the overridden Loaded method from any forms
>> - replace the main program's code with this (of course adjust the 
>> form/unit names to your needs):
>>
>> === code begin ===
>>
>> program tp2j;
>>
>> {$mode objfpc}
>>
>> uses
>>   Forms, FormLoader, Interfaces, Unit1;
>>
>> procedure DoInit(aData: TObject);
>> begin
>>   Application.Initialize;
>>   Application.CreateForm(TWForm1, WForm1);
>>   Application.Run;
>> end;
>>
>> begin
>>   LoadForms([TWForm1], @DoInit, Nil);
>> end.
>>
>> === code end ===
>
> I think you can avoid this change by checking the 'Run rtl when all 
> page resources are fully loaded' options when creating your project.
>
> This will change the script tag to
>   window.addEventListener("load", rtl.run);
> so the program only runs when all forms are actually loaded, avoiding the
> need to wait for the load.

Sadly this does not work. I don't even see the browser fetching the 
file... :'(

>
> The changes to the html could be implemented without too much effort 
> in the IDE package.
>
> Another approach would be to simply embed the .lfm in the html page in a
> script tag.

I personally would prefer to keep them separated to make versioning easier.

However what might be interesting (even for further functionality) might 
be to run the HTML page through a template processor (*cough* fptemplate 
*cough*) and replace some tag with the contents of all forms. Or 
something like that...

>
>>
>> - add a script tag in front of the project's script tag in the HTML 
>> file:   <script src="unit1.lfm" type="application/x-lazarus-form" 
>> id="TWForm1.lfm"></script>
>> - play around with the form (add components (from the Pas2JS 
>> component tab only!), add events, etc.)
>> - before compiling make sure that the project *does not* contain a 
>> requirement of Pas2JS_Designer_Package (the IDE currently adds this 
>> when adding a component)
>> - make sure that the *.LFM files are at the same location as the *.JS 
>> file
>> - load your application in the browser -> you should now see your form
>>
>> A screenshot showing the resulting browser and the IDE is attached. :)
>
> Nice job :-)

Yes. Though I'd really like to know how TMS dealt with the sizes. 
Currently the problem is when I do this on a 92 DPI Lazarus (my Win7 at 
work) I get buttons and labels that are too small for their contents and 
on my HighDPI Win10 the button and label looks huge compared to the 
content... (obviously the widgets need to deal with the different 
sizes/take into account the design DPI, but the principal problem of 
WYSIWYG remains)

>
>>
>> This can obviously be further improved:
>> - adding support also for frames and data modules
>> - caching the converted object stream (so that it doesn't need to be 
>> converted for each creation of a form)
>> - add some way of asynchronous loading?
>
> There is the dynload unit which can help in this.

I don't see a DynLoad unit. You mean Rtl.ScriptLoader and/or 
Rtl.UnitLoader? It's the former my FormLoader unit is based on. But 
loading the content itself is not the problem (I've done that part 
already ;) ). My (conceptual) problem is that the code is usually 
synchronous. E.g. take the default main program:

=== code begin ===

begin
   Application.Initialize;
   Application.CreateForm(TWForm1, WForm1);
   Application.Run;
end.

=== code end ===

Or the way to show another form:

=== code begin ===

SomeVar := TWForm2.Create(Application);
SomeVar.Show;

=== code end ===

In both cases it makes no sense to continue with the code after 
CreateForm or Create as the resource must be there to show something 
useful. Currently the loading inside TCustomForm looks like this:

=== code begin ===

procedure TCustomForm.ProcessResource;
begin
   if not InitResourceComponent(Self, TWForm) then
     raise EResNotFound.CreateFmt(
       rsFormResourceSNotFoundForResourcelessFormsCreateNew, [ClassName]);
end;

constructor TCustomForm.Create(AOwner: TComponent);
begin
   //GlobalNameSpace.BeginWrite;
   try
     CreateNew(AOwner, 1); // this calls BeginFormUpdate, which is ended 
in AfterConstruction
     if (ClassType <> TWForm) and not (csDesigning in ComponentState) then
     begin
       //Include(FFormState, fsCreating);
       try
         ProcessResource;
       finally
         //Exclude(FFormState, fsCreating);
       end;
     end;
   finally
     //GlobalNameSpace.EndWrite;
   end;
end;

=== code end ===

And InitResourceComponent is this:

=== code begin ===

function InitResourceComponent(Instance: TComponent; RootAncestor: TClass
   ): Boolean;
begin
   Result := InitLazResourceComponent(Instance, RootAncestor);
end;

function InitLazResourceComponent(Instance: TComponent; RootAncestor: TClass
   ): Boolean;

   function InitComponent(ClassType: TClass): Boolean;
   var
     ResName: String;
     Stream: TStream;
     BinStream: TMemoryStream;
     Reader: TReader;
     script: TJSElement;
   begin
     //DebugLn(['[InitComponent] ClassType=',ClassType.Classname,' 
Instance=',DbgsName(Instance),' RootAncestor=',DbgsName(RootAncestor),' 
ClassType.ClassParent=',DbgsName(ClassType.ClassParent)]);
     Result := False;
     if (ClassType = TComponent) or (ClassType = RootAncestor) then
       Exit;
     if Assigned(ClassType.ClassParent) then
       Result := InitComponent(ClassType.ClassParent);

     Stream := nil;
     ResName := ClassType.ClassName;

     (*************)
     script := Document.getElementById(ResName + '.lfm');
     if Assigned(script) and (script.textContent <> '') then
       Stream := TStringStream.Create(script.textContent);

     if Stream = nil then
       Exit;

     try
       //DebugLn('Form Stream "',ClassType.ClassName,'"');
       try
       BinStream := TMemoryStream.Create;
       try
         ObjectTextToBinary(Stream, BinStream);

         BinStream.Position := 0;

         Reader := TReader.Create(BinStream);
         try
           Reader.ReadRootComponent(Instance);
         finally
           Reader.Free;
         end;
       finally
         BinStream.Free;
       end;
       except
         on E: Exception do begin
Writeln(Format(rsFormStreamingError,[ClassType.ClassName,E.Message]));
           raise;//exit;
         end;
       end;
     finally
       Stream.Free;
     end;
     Result := True;
   end;


begin
   if Instance.ComponentState * [csLoading, csInline] <> []
   then begin
     // global loading not needed
     Result := InitComponent(Instance.ClassType);
   end
   else try
     //BeginGlobalLoading;
     Result := InitComponent(Instance.ClassType);
     //NotifyGlobalLoading;
   finally
     //EndGlobalLoading;
   end;
end;

=== code end ===

In the block marked with (*************) I'd essentially need to load 
the form's resource dynamically from the server (the loading itself 
isn't the problem) and then wait until all has been received. My 
knowledge in JS isn't as good yet as to know the correct approach for 
that, my gut tells me that something akin to "async/await" would be 
necessary here.

>
> Nice ! slowly we go ahead. And more surprises are still coming... :-)
Definitely looking forward to that. :)

Regards,
Sven


More information about the Pas2js mailing list