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.
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.
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.
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.
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.
I have TWebEdit mapped to <input>s, creating a new form removes any text in the FIRST created form.
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;
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. 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.
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.