Render fields in group and pages in email template

In a more advanced or complex multi-step form, fields are organized into different groups. However, in the default email template rendering, all fields are displayed in a flat structure.

Has anyone implemented a way to group fields in emails so that they reflect the same structure as the form itself?

Hi @bjarnef I think the IFormServicemight be able to help you out you should be able to retrieve the Form model then from there I believe you can retrieve field metadata for the fields and/or you should also be able to retrieve in sets? Then it’s just a mapping exercise to match up the fields fromFormsHtmlModel to the correct area

The Umbraco.Forms.Core.Models.FormsHtmlModel dosen’t know about pages and fieldsets

So something like…

@using Umbraco.Forms.Core.Services
@inject IFormService _formService


var form = _formService.Get(Model.FormId);
var fieldPages = form!.Pages;
@foreach (var page in form.Pages)
{
	// Access Page: page.Caption

	foreach (var fieldset in page.FieldSets)
	{
		// Access Fieldset: fieldset.Caption

		foreach (var container in fieldset.Containers)
		{
			foreach (var field in container.Fields)
			{
				// Match the structural field to the submitted data model
				var submittedField = Model.Fields.FirstOrDefault(x => x.Id == field.Id);

				if (submittedField != null && !ignoreFields.Contains(submittedField.FieldType))
				{
					
				}
			}
		}
	}
}

should replace the existing loop in the Example Template…

@foreach (var field in Model.Fields.Where(x => ignoreFields.Contains(x.FieldType) == false)) {..}

Yes, I ended up using IFormService as well and a helper method.
It works okay if workflow executes right after submission, but there’s a risk form definition may change after a submission and e.g. before a workflow executes on approval or rejection.

It would be useful it forms include scheme/meta data about this (avoiding the additional lookup) and I think record could use this to structure fields in boxes, especially with a complex/advanced form.

public interface IFormsEmailModelFactory
{
    FormEmailViewModel Create(FormsHtmlModel model);
}

public class FormsEmailModelFactory(IFormService formService) : IFormsEmailModelFactory
{
    private readonly IFormService _formService = formService;

    public FormEmailViewModel Create(FormsHtmlModel model)
        => FormsEmailModelMapper.ToEmailViewModel(model, _formService);
}
builder.Services.AddScoped<IFormsEmailModelFactory, FormsEmailModelFactory>();
public static class FormsEmailModelMapper
{
    public static FormEmailViewModel ToEmailViewModel(
        FormsHtmlModel model,
        IFormService formService)
    {
        var form = formService.GetFromCache(model.FormName)
            ?? throw new InvalidOperationException($"Form '{model.FormName}' was not found.");

        var valuesByFieldId = model.Fields
            .DistinctBy(x => x.Id)
            .ToDictionary(
                f => f.Id,
                f =>
                {
                    var values = f.GetValues();

                    return values.Length switch
                    {
                        0 => string.Empty,
                        1 => values[0]?.ToString() ?? string.Empty,
                        _ => string.Join(", ", values.Select(v => v?.ToString()))
                    };
                });

        var vm = new FormEmailViewModel
        {
            FormName = model.FormName,
            ValuesByFieldId = valuesByFieldId
        };

        foreach (var page in form.Pages)
        {
            if (page is null)
                continue;

            var pageVm = new FormEmailPage
            {
                Caption = page.Caption ?? string.Empty
            };

            foreach (var fieldset in page.FieldSets)
            {
                if (fieldset is null)
                    continue;

                var fieldsetVm = new FormEmailFieldset
                {
                    Caption = fieldset.Caption ?? string.Empty
                };

                foreach (var container in fieldset.Containers)
                {
                    if (container is null)
                        continue;

                    foreach (var field in container.Fields)
                    {
                        if (field is null)
                            continue;

                        valuesByFieldId.TryGetValue(field.Id, out var value);

                        fieldsetVm.Fields.Add(new FormEmailField
                        {
                            FieldTypeId = field.FieldTypeId,
                            FieldId = field.Id,
                            Alias = field.Alias,
                            Caption = field.Caption,
                            Value = value
                        });
                    }
                }

                pageVm.Fieldsets.Add(fieldsetVm);
            }

            vm.Pages.Add(pageVm);
        }

        return vm;
    }
}

public class FormEmailViewModel
{
    public string FormName { get; set; } = string.Empty;

    public List<FormEmailPage> Pages { get; set; } = [];

    public Dictionary<Guid, string> ValuesByFieldId { get; set; } = [];

    public string? GetValue(Guid fieldId)
        => ValuesByFieldId.TryGetValue(fieldId, out var v) ? v : null;
}

public class FormEmailPage
{
    public string Caption { get; set; } = string.Empty;
    public List<FormEmailFieldset> Fieldsets { get; set; } = [];
}

public class FormEmailFieldset
{
    public string Caption { get; set; } = string.Empty;
    public List<FormEmailField> Fields { get; set; } = [];
}

public class FormEmailField
{
    public Guid FieldTypeId { get; set; }
    public Guid FieldId { get; set; }
    public string Alias { get; set; } = string.Empty;
    public string Caption { get; set; } = string.Empty;
    public string? Value { get; set; }
}