Forms Module
Enables authors to define forms that can be added to pages. The form fields are configurable with validations. Form submissions can be stored in the database and/or processed by actions.
Configuration
appsettings Key: ConfinityForms
| Configuration Key | Description | Default |
|---|---|---|
| UrlParameterEncryptionKey | Secret used to encrypt the url parameters passed to form inputs. | null |
Actions
Actions can be added to the form configuration. They will be executed in the sequence they are configured and can change the submitted data. Developers can implement custom actions. Confinity has an action that sends a mail for every form submission.
Custom Actions
Developers can implement custom actions which can be added to forms by authors. To implement a custom action, follow these steps:
- Define a model with the settings for your action by inheriting from
FormSubmitEventListenerModel. The author needs to provide those settings when adding the action.
public sealed class HappinessApiModel : FormSubmitEventListenerModel
{
public string? RestEndpoint { get; set; }
}
- Implement your listener by inheriting from
IFormSubmitEventListener<T>whereTis your model.
public class HappinessApiFormSubmitEventListener : IFormSubmitEventListener<HappinessApiModel>
{
private readonly ILogger<HappinessApiFormSubmitEventListener> _logger;
private readonly HttpClient _httpClient;
public HappinessApiFormSubmitEventListener(
ILogger<HappinessApiFormSubmitEventListener> logger,
HttpClient httpClient)
{
_logger = logger;
_httpClient = httpClient;
}
public async Task<SubmitListenerResult> BeforeSubmitAsync(FormSubmit<HappinessApiModel> submit)
{
try
{
var request = submit.Request;
int happinessLevel = (int)request.FormData["happiness-level"];
var data = new { Ip = request.ClientIpAddress?.ToString() ?? "unknown", HappinessLevel = happinessLevel };
string json = JsonSerializer.Serialize(data);
var httpContent = new StringContent(json, System.Text.Encoding.UTF8, MediaTypeNames.Application.Json);
await _httpClient.PostAsync(submit.Config.RestEndpoint, httpContent);
}
catch (Exception e)
{
_logger.LogError(e, "Error while transferring happiness data");
return new SubmitListenerResult(false, "Error while transferring happiness data.");
}
return new SubmitListenerResult(true);
}
}
- Register your model and define an editor (if needed). Register your listener with with the services collection.
public class CustomActionExampleModule : IModuleConfiguration
{
public string ModuleKey => "my-module";
public void AddModule(IModuleContext module)
{
module.Configure.RegisterConfinityContent<HappinessApiModel>("Happiness API")
.Icon(Icon.CakeCandles)
.Editor(form => form.AddTab(tab =>
{
tab.AddInput(p => p.RestEndpoint);
}));
module.Services
.AddTransient<IFormSubmitEventListener<HappinessApiModel>, HappinessApiFormSubmitEventListener>();
}
public void UseModule(IApplicationBuilder app)
{
}
}
Form Field Types
Confinity offers a bunch of different form field types like Input, Checkbox, or Conditional Group out of the box.
Custom Form Field Types
Developers can implement custom form field types which can be added to forms by authors. To do so, follow these steps:
- Chose the right base type for you Model:
InputFieldModelBaseorFormFieldModelfor inputting dataViewComponentModelwith attribute[FormItem]for generic form elements that should be selectable directly on the formConfinityContentwhen your form element is a child element for a specific parent (e.g. an option for a select single)
- (optional) Implement
IFormItemCollectionModelif your element groups a bunch of form fields together - (optional) Implement
IFormItemConditionModelif your element is only shown based on a condition - (optional) Implement
IListOfOptionModelsif your element has multiple options which can be selected (this is needed to properly format the selected options in E-Mails)
public sealed class DateModel : InputFieldModelBase
{
public DateOnly Min { get; set; }
public DateOnly Max { get; set; }
}
- Implement a view component for your model.
public class DateInputViewComponent : ViewComponent
{
public IViewComponentResult Invoke(DateModel model)
{
return View(model);
}
}
@using Confinity.Forms.Tooltip
@using Confinity.Pages
@model DocsDemos.Modules.Forms.DateModel
@{
string fieldId = Model.Id.ToString();
static string ToJsString(DateOnly date)
{
return date.ToDateTime(TimeOnly.MinValue).ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT";
}
}
<div class="form-group">
@if (!string.IsNullOrWhiteSpace(Model.Label))
{
<label for="cfy-f-@fieldId">
@Model.Label
@if (Model.IsRequired())
{
<span class="required-marker">*</span>
}
@if (!string.IsNullOrWhiteSpace(Model.Tooltip))
{
@await Component.InvokeConfinityViewComponentAsync(new TooltipModel(Model.Tooltip))
}
</label>
}
<div class="input-group">
<input type="date" id="cfy-f-@fieldId" name="@Model.FieldName" class="form-control" aria-describedby="@(!string.IsNullOrWhiteSpace(Model.Description) ? $"help{fieldId}" : "")"
min="@ToJsString(Model.Min)" max="@ToJsString(Model.Max)">
</div>
<div class="invalid-feedback"></div>
@if (!string.IsNullOrWhiteSpace(Model.Description))
{
<small id="help@(fieldId)" class="form-text text-muted">@Model.Description</small>
}
</div>
- Register your model, view component and and define an editor (if needed).
public void AddModule(IModuleContext module)
{
module.Configure.RegisterConfinityContent<DateModel>("Date")
.ViewComponent<DateInputViewComponent>()
.Icon(Icon.Calendar)
.Editor(form => form.AddTab(tab =>
{
tab.AddDateOnly(p => p.Min!);
tab.AddDateOnly(p => p.Max);
}));
}
Custom validations
Developers can implement custom validations for their custom form fields which can be added to forms by authors. To do so, follow these steps:
- Implement the model, it must inherit
ValidationModel
public sealed class EqualsFieldValidationModel : ValidationModel
{
public string? FieldName { get; set; }
}
- The validator has to implement
IFormFieldValidator
public class EqualsFieldValidator : IFormFieldValidator
{
public bool CanValidate(ValidationModel model)
{
return model is EqualsFieldValidationModel;
}
public FieldValidatorResult IsValid(ValidationModel model, string? value,
IReadOnlyDictionary<string, string> formValuesPerField)
{
if (string.IsNullOrEmpty(value)
|| model is not EqualsFieldValidationModel equalsValidationModel
|| string.IsNullOrEmpty(equalsValidationModel.FieldName)
|| !formValuesPerField.TryGetValue(equalsValidationModel.FieldName, out string? otherValue))
return new FieldValidatorResult { IsValid = true };
var result = new FieldValidatorResult { IsValid = value.Equals(otherValue) };
if (!result.IsValid)
{
result.ErrorMessage = "Values do not match";
}
return result;
}
}
- Register your model, register your validator with the DIC and add a condition for your validator in the editor configuration of the custom form field.
public void AddModule(IModuleContext module)
{
//Register the validation model
module.Configure.RegisterConfinityContent<EqualsFieldValidationModel>("Equals field")
.Icon(Icon.Equals)
.Editor(form => form.AddTab(tab =>
{
tab.AddInput(p => p.FieldName);
}));
//Register the validator
module.Services.AddTransient<IFormFieldValidator, EqualsFieldValidator>();
module.Configure.RegisterConfinityContent<DateModel>("Date")
.ViewComponent<DateInputViewComponent>()
.Icon(Icon.Calendar)
.Editor(form => form.AddTab(tab =>
{
tab.AddDateOnly(p => p.Min!);
tab.AddDateOnly(p => p.Max);
//Add the validation
tab.AddConfinityContent(p => p.Validations)
.Where(t => t == typeof(EqualsFieldValidationModel));
}));
}
File upload
The file upload brings it's own UI. Limited styling is possible by overwriting css. Here is a list of selectors to overwrite certain styles.
- Drop area:
.confinity-form-file-upload-component [data-cfy-drop-area] - Upload label:
.confinity-form-file-upload-component .upload-label - Info label:
.confinity-form-file-upload-component .info-label - File extension for upload:
.confinity-form-file-upload-component [data-cfy-file-extension] - action remove file:
.confinity-form-file-upload-component [data-cfy-file-remove] - File is uploading:
.confinity-form-file-upload-component [data-cfy-file-uploading] - File size:
.confinity-form-file-upload-component [data-cfy-file-size] - File name:
.confinity-form-file-upload-component [data-cfy-file-name]
Error messages must be translated on the UI. The attribute data-cfy-has-error contains one of the following error keys:
wrong sessionId: the provided session id is no longer valid / does not exist. see TempFile for detailsmissing fileId: the file id was not provided. see TempFile for detailsmissing content: the file does not have content (size is zero).too big: the file ist bigger than what is configured as allowed size.extension not allowed: the file extension is not on the allowed file list.content not allowed: the content is not allowed based on the configuration.chunk out of order: the chunks did arrive out of order. This is ensured by the client so someone tampered with it.space capacity exceeded: the configured space capacity is exceeded. see TempFile for detailssession capacity exceeded: the configured session capacity is exceeded. see TempFile for detailsfile capacity exceeded: the configured file capacity is exceeded.unable to process: there was an internal processing error or the virus scanner detected something.unknown error: multiple reasons -> check console for more details.
Customize buttons
Buttons can be customized by overwriting the file Shared/Components/ConfinityFormButton/Default.cshtml.
If all the buttons should look the same, you can simply return a button:
@model Confinity.Forms.FormButtonModel
<button type="@Model.ButtonType()" @Model.DataAttributes() class="btn btn-primary">@Model.Label</button>
Warning
make sure to set the type and data attributed used by Confinity to link the buttons together.
To customize the buttons based on it's usage, you can switch over the usage property:
@using Confinity.Forms
@model Confinity.Forms.FormButtonModel
@switch (Model.Usage)
{
case FormButtonUsage.WizardBack:
<button type="@Model.ButtonType()" @Model.DataAttributes() class="btn btn-link d-none">@Model.Label</button>
break;
case FormButtonUsage.WizardForward:
<button type="@Model.ButtonType()" @Model.DataAttributes() class="btn btn-primary ms-auto">@Model.Label</button>
break;
case FormButtonUsage.Submit:
default:
<button type="@Model.ButtonType()" @Model.DataAttributes() class="btn btn-primary">@Model.Label</button>
break;
}
Customisation for javascript frameworks
When using javascript frameworks it can get hard to serve the dom for the Confinity forms module to work. Therefore we pass javascript events around so you can react on them accordingly. You have to add the attribute data-cfy-forms-event on the elements where you want to receive the events. the recommended way is to define them as close to the native html input elements as possible. The events bubble up the dom so that you can catch the events at any point.
Validation Events:
The attribute data-cfy-forms-event is expected to be as close to the input for the given validation as possible.
event type: cfy-forms-validation
payload: {isValid: boolean, isTouched: boolean, message?: string (the error message)}
In order for the validation events to work, you have to ensure, that isTouched is set. To do this you have to emit an event on the input yourself.
event type: cfy-forms-input event detail: {bubbles: true, detail: {type: 'change'}} the expected values for detail.type is 'change' (for changes) or 'blur' (when the user leaves the input)
example: myInputElement.dispatchEvent(new CustomEvent('cfy-forms-input', {bubbles: true, detail: {type: 'change'}}))
Wizard Events
Add the attribute data-cfy-forms-event to all the elements which should receive this event.
event type: cfy-forms-wizard
payload: {active: string (the id of the active step)}
Submit Event
Add the attribute data-cfy-forms-event to all the elements which should receive this event.
event type: cfy-forms-submit
payload: none