How to design a layout and use it multiple times in the same form?

I have created a TWebForm, MainForm.

I have created another TWebForm, SubForm.

I want MainForm to host multiple instances of SubForm.
I do not know how many I need until runtime, thus I want to add them via code and cannot just chuck multiple frames in with the Form designer.
I need to populate fields based in SubForm at runtime.

SubForm does not need to be a TWebForm.

In essence, I need to allow the user to add or remove rows (SubForm), and for the user to be able to input data into each row.

Is this possible without resorting to JavaScript?
No example from the demo nor anyone (that I could find) on the internet has tried to do this.

FormHosting demo with the difference that you create new TWebPanel instances that should host the new forms you need dynamically at runtime in code?

You can have a look through this thread -> Form management to speedup app

While he wasn't making copies of the same form, he was loading lots of forms and dealing with various issues that are likely to be of interest to you.

I have managed to add one form. Whenever I add another in the same way, only one of the forms get any sub components.

procedure TMainForm.Foo;
var
  r1, r2: TCustomForm;
  d1, d2: TWebHTMLDiv;
begin
  d1 := TWebHTMLDiv.Create(Self);
  d1.Parent := divToHostContent;
  d1.ElementID := 'd1';
  r1 := TCustomForm.CreateNew(d1.ElementID, nil);

  d2 := TWebHTMLDiv.Create(Self);
  d2.Parent := divToHostContent;
  d2.ElementID := 'd2';
  r2 := TCustomForm.CreateNew(d1.ElementID, nil);
end;

As you can see, the second div, d2, is flat.

<div id="divToHostContent" zindex="0" class="godsflex" tabindex="-1" role="" style="top: 27px; left: 0px; width: 916px; height: 325px; position: absolute; box-sizing: border-box;">
    <div id="d1" zindex="0" tabindex="-1" role="" style="color: rgb(0, 0, 0); outline: none; top: 0px; left: 0px; width: 640px; height: 480px; position: absolute; box-sizing: border-box; user-select: none; font-family: Arial; font-style: normal; font-size: 8pt;">
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <div class="input-row">
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id0" name="id0">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id1" name="id1">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id2" name="id2">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id3" name="id3">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id4" name="id4">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id5" name="id5">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id6" name="id6">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id7" name="id7">
            </div>
            <div class="input-group">
                <label>Foo bar</label>
                <input type="text" id="id8" name="id8">
            </div>
            <button class="input-group delete-row">Delete row</button>
        </div>
    </div>
    <div id="d2" zindex="0" tabindex="-1" role="" style="color: rgb(0, 0, 0); outline: none; top: 0px; left: 0px; width: 100px; height: 25px; position: absolute; box-sizing: border-box; user-select: none; font-family: Arial; font-style: normal; font-size: 8pt;"></div>
</div>

The same goes if I create a TWebPanel first, but in this case p2 gets generated correctly and not p1.

procedure TMainForm.Foo;
var
  r1, r2: TCustomForm;
  d1, d2: TWebHTMLDiv;
begin
  p1 := TWebPanel.Create(self);
  p1.Parent := divToHostContent;
  p1.ElementID := 'p1';
  r1 := TCustomForm.CreateNew(p1.ElementID, nil);

  p2 := TWebPanel.Create(self);
  p2.Parent := divToHostContent;
  p2.ElementID := 'p2';
  r2 := TCustomForm.CreateNew(p2.ElementID, nil);
end;

Is that a typo on the second to last line?

 r2 := TCustomForm.CreateNew(d1.ElementID, nil);

Might need to be...

 r2 := TCustomForm.CreateNew(d2.ElementID, nil);

Not sure if that's your problem, but keep in mind that HTML id's need to be unique, and strange things happen when they aren't.

That was a typo! But the result is the same (only d2 is now correct instead of d1).

Hm. If you comment out the second block, the first element is created, and if you comment out the first block, the second element is created? Meaning that it is replacing the previously loaded form whenever you load a new form? So if you swapped the blocks, #1 would be generated instead of #2? Are you actually loading a new form using TYourForm instead of TCustomForm?

Also, I find adding the callback function to be helpful in cases where creating the form takes a bit of time and can help troubleshoot whether it is being called at all.

I'm also wondering if there's a timing thing. I don't normally create forms in such quick succession after changing the page, so maybe some of the changes need to use await or something to get the ordering right? Not sure about that though.

I added a procedure to run after .createNew, and it only logs once.

procedure aftercreate(aForm:Tobject);
  begin
      console.log('I am done!');
      console.log(aForm);
  end;
var

I was also thinking that it might be a timing issue and that I need to use await or TAwait.ExecP<T>(...) but I cannot figure out how to do it.

Definitely something amiss. Seems that when CreateNew() is doing its thing, it adds stuff to the DOM and then relabels it after, so while it is there, and another CreateNew() comes along, it gets messed up. I tried with the async variation of CreateNew but that didn't seem to be better, and it creates something different, not in the same place as the normal one somehow. Odd. Inserting some delays seemed to get the job done, but reducing those to the point where they aren't noticed might be something to try. This is less than ideal of course, depending on how quickly these are created and how much control you have over it.

unit Unit1;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, Vcl.Controls, WEBLib.WebCtrls,
  Vcl.StdCtrls, WEBLib.StdCtrls;

type
  TForm1 = class(TWebForm)
    btnAddForm: TWebButton;
    divHost: TWebHTMLDiv;
    procedure WebFormCreate(Sender: TObject);
    [async] procedure btnAddFormClick(Sender: TObject);
    [async] function AddForm2(aMessage: String):TObject;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses Unit2;

{$R *.dfm}

procedure TForm1.WebFormCreate(Sender: TObject);
begin
   asm window.sleep = async function(msecs) {return new Promise((resolve) => setTimeout(resolve, msecs)); } end;
end;

procedure TForm1.btnAddFormClick(Sender: TObject);
var
  f1, f2, f3: TForm2;
begin
  f1 := (await(AddForm2('f1')) as TForm2);
  f2 := (await(AddForm2('f2')) as TForm2);
  f3 := (await(AddForm2('f3')) as TForm2);

  // long enough to add multiple sets before the text is updated
  asm await sleep(3000); end;

  f1.WebLabel1.Caption := f1.WebLabel1.Caption + ' ok';
  f2.WebLabel1.Caption := f2.WebLabel1.Caption + ' great';
  f3.WebLabel1.Caption := f3.WebLabel1.Caption + ' lovely';
end;

function TForm1.AddForm2(aMessage: String):TObject;
var
  f: TForm2;
  d: TWebHTMLDiv;

  procedure AfterCreate(aForm: TObject);
  begin
    (aForm as TForm2).WebLabel1.Caption := IntToStr(btnAddForm.tag)+' '+aMessage+FormatDateTime(' hh:nn:ss.zzz ',now);
  end;

begin
  btnAddForm.Tag := btnAddForm.Tag + 1;
  d := TWebHTMLDiv.Create('d'+IntToStr(btnAddForm.Tag));
  d.Parent := divHost;
  f := TForm2.CreateNew(d.ElementID, @AfterCreate);
  Result := f;
  asm await sleep(500); end;
end;

end.

MultiForms.zip (9.0 KB)

If you remove the delays, or shorten them to 10ms or less, things tend to get a little squirrelly. Maybe the TMS folks can suggest a more bullet-proof approach here.

A few ms, or even seconds, could be viable. Thanks for same code, I will look at it tomorrow.

If you just add a new form with a button click, the delay between one button click and another is plenty to not have any problems. When programmatically creating forms, this problem arises. Delays are a workaround at best. If it happens that CreateNew() is somehow called multiple times simultaneously by other means, then the same conflict can arise. Or at least so it seems.

Andrew, your code works. I can design a TWebForm, create and add it just I wanted. But as it stands now, there are a few cavets.

  1. I have TWebEdit mapped to <input>s, creating a new form removes any text in the FIRST created form.
  2. All forms behave as if they are the same.
    Example: This will log the same first value for each form
// Some form hosting TMyForm
type
  TOtherForm = class(TWebForm)
  ...
  Forms: TList<TMyForm>;
  end;

// Save each created form
procedure TOtherForm.btnAddMyFormClick(Sender: TObject);
var
  f: TMyForm;
begin
  f := (await(AddForm) as TMyForm);
  Forms.Add(f);
end;

// This will log the same first value for each form
procedure TOtherForm.btnAddMyFormClick(Sender: TObject);
var
  f: TMyForm;
begin
  for f in Forms do
begin
  console.log(f.edLabel.Text); // edLabel is mapped to an input
end;
  1. Below is a minifed example. id is given a different value for each created object. When btnTest is clicked, it will print the value of id for all created forms.
type
  TMyForm= class(TWebForm)
    edLabel: TWebEdit;
    btnTest: TWebButton;
    procedure btnTestClick(Sender: TObject);
  public
    id: string;
  end;

procedure TMyForm.btnTestClick(Sender: TObject);
begin
  console.log(id);
end;

All in all, thanks for helping me this solve this. :slight_smile: I will still have to leverge some JS, but I can live with that for now.

The ideal outcome would be if I could design a HTML file, connect it to a pas/dfm file via control bindings, create multiple instances like here and be able to access them individually in delphi code to get their input fields' values.

Glad to hear you're making progress!

The issue you're having with default values and so on is perhaps related to how the elements on the page are created. Just be careful about maintaining unique ids throughout the process as best you can. I haven't had a situation where I create multiple instances of the same form. But I do create instances of forms with a lot of elements on them, so I'm careful to include a unique unit name in the id values or I run into the same problem. Getting into the habit of having good naming conventions is even more important when an object on one form can be rendered with the same id as an object on another form. Not something we normally have to think about with Delphi usually providing cover for this kind of thing.

As far as JavaScript, over time I've slowly incorporated more and more JavaScript code, particularly for UI work but for lots of other things, JSON handling is another big one, to the point where I think the scale has tipped to where there is more JavaScript code than Delphi code in my projects now. It is super fun! You can get all the structure that Delphi provides, and then break all the rules with JavaScript. And the reverse, all the insanity of the wild west of JavaScript can be significantly tamped down by wrapping it in Delphi functions and forms. Pretty ideal environment I think.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.