Module 6
Expansion of form with repeating group
In this module you will expand the application you have built in the previous modules to support more of the functionality that the municipality of Sogndal wishes to implement.
Topics covered in this module
- Repeating groups
- Validation
- Data processing
Tasks
Requirements from the municipality
To tailor the best possible offer to new residents, you would like to have a list of their previous residences.
On the data page, you want to allow users to enter their previous residences. Previous residences should include the following fields:
- Street Address
- Postal Code
- City
It should be possible to enter up to 10 former residences.
Tasks
- Add a group component to the page to collect personal information.
- Add an address component below the group component.
- For both components, add appropriate headings and link them to relevant fields in the data model.
Note that the “Maximum number of repetitions” must be adjusted locally.
Useful documentation
Knowledge check
Requirements from the municipality
If a newcomer enters the postal code 1337
as one of their previous residences, they must confirm their excellence by adding a symbol in the address field before they can proceed.
Therefore, we want an error message to appear on the relevant field with the following text:
Vi er beæret over å motta en '1337' innbygger til Sogndal kommune!
Du må imidlertid bekrefte din uovertruffenhet ved å legge til en 🌟 i adressefeltet for å gå videre.
Oppgaver
- Add a validation to the
Postnr
field for previous residential addresses.
Useful documentation
Knowledge check
Requirements from the municipality
There is an address in Sogndal which is often misspelled by newcomers which leads to case workers having to spend a lot of time manually correcting it. Therefore, we want the app to automatically fix this mistake when the misspelled address is detected.
If the user enters Sesame Street 1
in the field Innflytter.Adresse.Gateadresse
, it should automatically be corrected to Sesamsgate 1
.
For all other addresses the field should remain the same.
Tasks
- Create a file for data processing.
- Add processing of the address field as described above.
Remember to implement the solution in Program.cs
as before.
Useful documentation
Knowledge check
ProcessDataWrite
is executed when the user writes data, meaning when the user fills in a field or updates an existing value. ProcessDataRead
is executed when the user reads data from the database, for example, when navigating to a previous instance of the application and retrieving previously filled data.Summary
In this module, you have learned about repeating groups and how to configure them as part of the user interface. You have also explored how to set up custom validations in the backend for cases that cannot be defined as part of data model restrictions. Finally, you have seen how to set up data processing to enable data manipulation at runtime.
Solution Proposal
We have added a component for a repeating group in Altinn Studio Designer with an address component as a “child.”
The group component is linked to the data model field Innflytter.TidligereBosteder
,
and the address component is linked to the fields Innflytter.TidligereBosteder.Gateadresse
,
Innflytter.TidligereBosteder.Postnr
, and Innflytter.TidligereBosteder.Poststed
.
The number of allowed repeating groups is determined by maxOccurs
for the field in the data model.
We also need to set maxCount
to 10
on the group component to prevent the user from (visually) creating more groups than allowed.
For now, this must be done locally in the page’s layout file (see below).
We have also added a heading to clarify the distinction between previous and current addresses.
App/ui/layouts/innflytterPersonalia.json
{
"$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4//schemas/json/layout/layout.schema.v1.json",
"data": {
"layout": [
{
"id": "tidligere-bosteder-overskrift",
"type": "Header",
"size": "M",
"textResourceBindings": {
"title": "innflytterPersonalia.tidligere-bosteder-overskrift.title"
}
},
{
"id": "Group-tidligere-bosteder",
"type": "RepeatingGroup",
"maxCount": 10,
"dataModelBindings": {
"group": "Innflytter.TidligereBosteder"
},
"textResourceBindings": {
"add_button": "innflytterPersonalia.Address-adresse"
},
"children": [
"Address-tidligere-bosted"
]
},
{
"id": "Address-tidligere-bosted",
"type": "Address",
"dataModelBindings": {
"address": "Innflytter.TidligereBosteder.Gateadresse",
"zipCode": "Innflytter.TidligereBosteder.Postnr",
"postPlace": "Innflytter.TidligereBosteder.Poststed"
},
"simplified": true,
"required": true,
"textResourceBindings": {
"title": "innflytterPersonalia.Address-tidligere-bosted.title"
}
}
]
}
}
The following text resources have been added:
App/config/texts/resources.nb.json
{
"$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/text-resources/text-resources.schema.v1.json",
"language": "nb",
"resources": [
{
"id": "innflytterPersonalia.Address-adresse",
"value": "adresse"
},
{
"id": "innflytterPersonalia.Address-tidligere-bosted.title",
"value": "Tidligere bosted"
},
{
"id": "innflytterPersonalia.tidligere-bosteder-overskrift.title",
"value": "Tidligere bosteder"
}
]
}
- Add validation logic in the
ValidateData
method inInstanceValidation.cs
:
App/logic/Validation/InstanceValidation.cs
...
public async Task ValidateData(object data, ModelStateDictionary validationResults)
{
if (data.GetType() == typeof(Skjema))
{
Skjema skjema = (Skjema)data;
string elitePostalCode = "1337";
string eliteSymbol = "🌟";
if (skjema?.Innflytter.TidligereBosteder != null)
{
List<Adresse> tidligereBosteder = skjema.Innflytter.TidligereBosteder;
int i = 0;
foreach (Adresse adresse in tidligereBosteder)
{
if (adresse.Postnr == elitePostalCode && !adresse.Gateadresse.Contains(eliteSymbol))
{
validationResults.AddModelError("Innflytter.TidligereBosteder[" + i + "].Postnr", "Innflytter.TidligereBosteder.validation_message");
}
i++;
}
}
}
await Task.CompletedTask;
}
...
- Add a text resource for the error message:
App/config/texts/resources.nb.json
{
"$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/text-resources/text-resources.schema.v1.json",
"language": "nb",
"resources": [
...,
{
"id": "Innflytter.TidligereBosteder.validation_message",
"value": "Vi er beæret over å motta en '1337' innbygger til Sogndal kommune! Du må imidlertid bekrefte din uovertruffenhet ved å legge til en 🌟 i adressefeltet for å gå videre."
}
]
}
Extra Challenge
This solution only changes the address for previous residences. Update the code so that the validation also includes the current address.
- Create a class that implements
IDataProcessor
as described in data processing and add data processing logic:
App/logic/DataProcessing/DataProcessor.cs
...
namespace Altinn.App.AppLogic.DataProcessing;
public class DataProcessor : IDataProcessor {
public async Task<bool> ProcessDataRead(Instance instance, Guid? dataId, object data)
{
return await Task.FromResult(false);
}
public async Task<bool> ProcessDataWrite(Instance instance, Guid? dataId, object data)
{
bool edited = false;
if (data.GetType() == typeof(Skjema)) {
Skjema skjema = (Skjema)data;
if (skjema?.Innflytter.TidligereBosteder != null) {
List<Adresse> tidligereBosteder = skjema.Innflytter.TidligereBosteder;
int i = 0;
foreach (Adresse adresse in tidligereBosteder) {
if (adresse.Gateadresse == "Sesame Street 1") {
adresse.Gateadresse = "Sesamgate 1";
edited = true;
}
i++;
}
}
}
return await Task.FromResult(edited);
}
}
- Register the implementation in
Program.cs
App/Program.cs
...
{
// Register your apps custom service implementations here.
...
services.AddTransient<IInstanceValidator, InstanceValidator>();
services.AddTransient<IDataProcessor, DataProcessor>();
}
...
Extra Challenge
This solution only changes the address for previous residences and only for Sesame Street 1
.
Update the code so that:
- The processing also includes the current address.
- The change is applied to all street numbers.