Generate Cross Platform Dynamic Forms At Runtime From JSON In Delphi
(tested in 10.2.1 Tokyo, but, can work in another version later)
(tested in 10.2.1 Tokyo, but, can work in another version later)
[SHOWTOGROUPS=4,19,20]
[/SHOWTOGROUPS]
- The Hospitality Survey Client project is part of the Hospitality Survey App template for Delphi 10.2.1 Tokyo that Embarcadero has released through their GetIt platform. The Hospitality Survey App consists of four different projects.
- In this blog post I will cover the dynamic form generator that is built into the Hospitality Survey Client project.
- Also keep in mind that the client can be deployed to Android, iOS, macOS, and Windows with a single code base and a single responsive UI.
- Basically how it works is on the server there is a database table which contains the list of survey questions to be asked to patrons from each tenant (in this case restaurant). The /survey/ end point in RAD Server is called from TBackendEndpoint (which is basically a TRESTClient) in the Survey App Client.
- The RAD Server end point returns the contents of the Questions table as the FireDAC JSON format.
- You can customize the questions using the Hospitality Survey Editor.
- The Client saves out the FireDAC JSON to a surveys.json file which is then loaded into an TFDMemTable.
- The GenerateSurvey() function (see below) loops through the records in the TFDMemTable and creates a TFrame for each record.
- Each record corresponds to a question in the database and you can see the different fields in a record below: ID
- An ID for the question.name
- A short name for the question with no spaces.title
- The text of the question as it will appear in the survey.type
- The type of question controls which question template is loaded on the client.
- The existing types are: rating, yes/no, edit, options/options
- If the type of the question is set to options this field is used to populate the options. It's value is a JSON array of options.value
- The value is where the user submitted data is stored. It can be left blank but could be used to provide a default answer.category
- The category of the question. This field is provided for expandability.tenant_id
- The tenant ID of the question.
- If the tenant_id field is blank all tenants will get the question.
- If it is set to a tenant only that tenant will get the question.
- The Type column determines which TFrame is loaded for that record.
- The built in types are: rating, yesno, edit, options.
- You can add your own row types as well by modifying the GenerateSurvey() procedure.
- You can see the units below for each of the dynamic TFrames including a header frame and a complete button frame for submitting the form.uSurveyHeaderFrame.pas
- Contains the header for the top of the survey.uRatingBarFrame.pas
- Contains the star rating track bar survey question type.uYesNoFrame.pas
- Contains the Yes/No survey question type.uEditFrame.pas
- Contains the edit survey question type.uComboBoxFrame.pas
- Contains the combo box survey question type.uCompleteFrame.pas
- Contains the complete button for the survey.
- The GenerateSurvey() procedure itself is pretty simple.
- It loops through the TFDMemTable dataset and checks the type field to see which TFrame to load and populate for that record.
- The options field is a JSON array that is used to populate the values for the yesno type and options type.
- The ID field is used to mark the TFrame with the specific question it was created from (FrameItem.Tag := BindSourceDBForm.DataSet.FieldByName('ID').AsInteger
- So that the value field can be filled out with the answer from the user.
- Once the survey has been completed by the user then the entire contents of the TFDMemTable are saved out to the FireDAC JSON format and uploaded back to the server via a POST from a TBackendEndpoint component to the /survey/complete endpoint.
- In the case of the Hospitality Survey App all of the uploaded records are saved for each collected survey.
- This allows the survey questions to be created, removed, and changed without affecting any of the existing surveys that have already been collected.
Код:
procedure TMainForm.GenerateSurvey(Sender: TObject);
var
FrameItem: TFrame;
FieldType: String;
FieldCategory: Integer;
JSONArray: TJSONArray;
I: Integer;
begin
FrameItem := TSurveyHeaderFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
FrameItem.Name := 'FSurveyHeader';
FrameItem.Align := TAlignLayout.Top;
FrameItem.Position.Y := 0;
BindSourceDBForm.DataSet.First;
//
while not BindSourceDBForm.DataSet.Eof do
begin
FieldCategory := BindSourceDBForm.DataSet.FieldByName('category').AsInteger;
FieldType := BindSourceDBForm.DataSet.FieldByName('type').AsString;
//
if FieldType = 'edit' then
begin
FrameItem := TEditFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
TEditFrame(FrameItem).QuestionText.Text :=
BindSourceDBForm.DataSet.FieldByName('title').AsString;
end;
//
if FieldType = 'yesno' then
begin
FrameItem := TYesNoFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
TYesNoFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
JSONArray := TJSONObject.ParseJSONValue(BindSourceDBForm.DataSet.FieldByName('options').AsString) as TJSONArray;
//
for I := 0 to JSONArray.Count - 1 do
begin
case I of
0:
begin
TYesNoFrame(FrameItem).ValueSpeedButton1.Text := JSONArray.Items.Value;
TYesNoFrame(FrameItem).ValueSpeedButton1.GroupName := BindSourceDBForm.DataSet.FieldByName('name').AsString;
end;
1:
begin
TYesNoFrame(FrameItem).ValueSpeedButton2.Text := JSONArray.Items.Value;
TYesNoFrame(FrameItem).ValueSpeedButton2.GroupName := BindSourceDBForm.DataSet.FieldByName('name').AsString;
end;
end;
end;
//
JSONArray.Free;
end;
//
if FieldType = 'rating' then
begin
FrameItem := TRatingBarFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
TRatingBarFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
end;
//
if FieldType = 'options' then
begin
FrameItem := TOptionsFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
TOptionsFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
JSONArray := TJSONObject.ParseJSONValue(BindSourceDBForm.DataSet.FieldByName('options').AsString) as TJSONArray;
TOptionsFrame(FrameItem).ValueComboBox.Items.BeginUpdate;
for I := 0 to JSONArray.Count - 1 do
begin
TOptionsFrame(FrameItem).ValueComboBox.Items.Add(JSONArray.Items.Value);
end;
//
TOptionsFrame(FrameItem).ValueComboBox.Items.EndUpdate;
JSONArray.Free;
end;
//
FrameItem.Name := 'F' + BindSourceDBForm.DataSet.FieldByName('ID').AsString;
FrameItem.Align := TAlignLayout.Top;
FrameItem.Tag := BindSourceDBForm.DataSet.FieldByName('ID').AsInteger;
FrameItem.Position.Y := BindSourceDBForm.DataSet.FieldByName('ID').AsInteger * 100;
//
BindSourceDBForm.DataSet.Next;
//
Application.ProcessMessages;
end;
//
FrameItem := TCompleteFrame.Create(TFMXObject(Sender));
FrameItem.Parent := TFMXObject(Sender);
FrameItem.Name := 'FComplete';
FrameItem.Align := TAlignLayout.Top;
FrameItem.Position.Y := 1000000;
TCompleteFrame(FrameItem).CompleteButton.OnClick := CompleteClick;
end;
- The selected option in each TFrame gets sent back to the TFDMemTable via the UpdateValueByID() procedure as you can see below. In the below code the Self.Tag field corresponds to the ID field of the question.
Код:
procedure TOptionsFrame.ValueComboBoxChange(Sender: TObject);
begin
MainForm.UpdateValueByID(Self.Tag,ValueComboBox.Items[ValueComboBox.ItemIndex]);
end;
- The UpdateValueByID(), procedure uses the Locate(), procedure on the DataSet to find the correct record and then update the value field.
Код:
procedure TMainForm.UpdateValueByID(ID: Integer; const Value: string);
begin
if BindSourceDBForm.DataSet.Locate('ID', VarArrayOf([ID]), []) = True then
begin
BindSourceDBForm.DataSet.Edit;
BindSourceDBForm.DataSet.FieldByName('value').AsString := Value;
BindSourceDBForm.DataSet.Post;
end;
end;
Последнее редактирование: