We finally went down the path of using hidden fields and displaying them dynamically via JavaScript.
The controller creates a list with a size of one hundred DTO objects. The template renders every DTO object, assigns its field property with the index, and adds a CSS class, called d-none
, that makes them invisible. In the th:field
attribute the index of the array is specified, so Spring can correctly map the form data to the DTOs.
<tr class="d-none form-group">
<th><i class="fa fa-fw fa-minus-circle" th:id="|remove-${iterator.index}|"></i></th>
<td> </td>
<td>
<input type="text" class="form-control col-lg-8" placeholder="Attributname"
th:field="*{attributes[__${iterator.index}__].name}"
th:id="|attributeName-${iterator.index}|"/>
</td>
</tr>
In the JavaScript part the d-none
CSS class is dynamically added and removed when the user navigates among the objects. We use the content of the attribute id
to get the index of the currently visible attribute.
As requested, I attached the source class for the DTO class with the 100 attributes created for it:
public class IEForm {
private static final int ATTRIBUTE_MAX_COUNT = 100;
@NotNull
@NotEmpty
@Valid
private List<IEAttributeForm> attributes;
public void removeCommaFromAttributeNames() {
for (IEAttributeForm processed : this.attributes) {
processed.setName(
processed.getName().substring(
0,
processed.getName().indexOf(',')
)
);
}
}
public List<IEAttributeForm> getFilledOutAttributes() {
List<IEAttributeForm> filledOutAttributes = new ArrayList<>();
int i = 0;
while (
(i < ATTRIBUTE_MAX_COUNT)
&& (this.attributes.get(i).getName() != null)
&& (this.attributes.get(i).getName().length() > 0)
&& (!Objects.equals(this.attributes.get(i).getPhysicalName(), IEAttributeForm.DEFAULT_NAME))
&& (!Objects.equals(this.attributes.get(i).getAttributeName(), IEAttributeForm.DEFAULT_NAME))
) {
filledOutAttributes.add(this.attributes.get(i));
++i;
}
return filledOutAttributes;
}
public void initialize() {
for (int i = NUMBER_OF_INITIAL_ATTRIBUTES; i < ATTRIBUTE_MAX_COUNT; ++i) {
this.attributes.add(new IEAttributeForm(i));
}
}
}
Here is the controller for processing the attributes:
@Controller
@RequestMapping("/ie")
public class InformationEntityController {
// ...
@RequestMapping("/create")
String create(@RequestParam(name = "search", required = false, defaultValue = "") String searchText, Model model) {
IEForm infoEntity = new IEForm();
infoEntity.initialize();
this.initializeInfoEntityCreationFormParameters(model, infoEntity, searchText);
return "iecreation/creation";
}
@RequestMapping("/save")
String save(
@Valid @ModelAttribute("form") IEForm dto,
BindingResult bindingResult,
Model model
) {
dto.removeCommaFromAttributeNames();
InfoEntity created = new InfoEntity();
created.setDescription(dto.getDescription());
created.setDisplayName(dto.getName());
created.setVersion(dto.getVersionAlinter());
created.setEditable(dto.isEditable());
created.setActive(dto.isActive());
created.setComplete(dto.isComplete());
created.setNameALINTER(dto.getNameAlinter());
created.setVersionALINTER(dto.getVersionAlinter());
created.setNameCAR(dto.getCarObjectName());
if (!bindingResult.hasErrors()) {
Optional<Application> ieApplication = this.applicationRepository.findById(dto.getApplicationId());
if (ieApplication.isPresent()) {
created.setApplication(ieApplication.get());
}
ArrayList<IEAttribute> attributes = new ArrayList<>();
for (IEAttributeForm processedAttribute : dto.getFilledOutAttributes()) {
IEAttribute addedAttribute = new IEAttribute();
addedAttribute.setType(processedAttribute.getDataType());
addedAttribute.setAttributeName(processedAttribute.getName());
addedAttribute.setPhysicalName(processedAttribute.getPhysicalName());
addedAttribute.setPhysicalLength(processedAttribute.getPhysicalLength());
addedAttribute.setFieldType(processedAttribute.getFieldType());
addedAttribute.setAttributeDescription(processedAttribute.getAttributeDescription());
addedAttribute.setHasHW(processedAttribute.getHasHint());
addedAttribute.setMaxLength(processedAttribute.getMaxLength());
addedAttribute.setEnumName(processedAttribute.getEnumName());
addedAttribute.setPrecision(processedAttribute.getPrecision());
attributes.add(addedAttribute);
}
created.setAttributes(attributes);
this.ieService.saveInfoEntity(created);
return "redirect:/download/ie?infoEntityId=" + created.getId();
}
else {
this.initializeInfoEntityCreationFormParameters(model, dto, "");
return "iecreation/creation";
}
}
}
Finally, here is the TypeScript code for switching between the attributes:
const ADDITIONAL_ATTRIBUTE_COUNT = 100;
let visibleAttributeCount = 0;
let previousAttributeIndex = (-1);
function initializeAttributes() {
visibleAttributeCount = getNumberOfActiveAttributes();
for (let i = 0; i < visibleAttributeCount; ++i) {
addAttribute(i);
}
getTableNode(0).removeClass("d-none");
applyValidationConstraints(0);
previousAttributeIndex = 0;
$("#addAttribute").on("click", function(event): boolean {
event.preventDefault();
addAttribute(visibleAttributeCount);
++visibleAttributeCount;
return false;
});
const totalAttributeCount = ADDITIONAL_ATTRIBUTE_COUNT + visibleAttributeCount;
const INDEX_ID_ATTRIBUTE = 1;
for (let i = 0; i < totalAttributeCount; ++i) {
$("#previousTable-" + i).on("click", function(event) {
event.preventDefault();
let attributeId = extractAttributeId(event.target.attributes[INDEX_ID_ATTRIBUTE].value);
if (attributeId > 0) {
--attributeId;
switchToTable(attributeId);
}
});
$("#nextTable-" + i).on("click", function(event) {
event.preventDefault();
let attributeId = extractAttributeId(event.target.attributes[INDEX_ID_ATTRIBUTE].value);
if (attributeId < (visibleAttributeCount - 1)) {
++attributeId;
switchToTable(attributeId);
}
});
$("#attributeName-" + i).on("keyup", function(event) {
event.preventDefault();
for (let processedAttribute of event.target.attributes) {
if (processedAttribute.name === "id") {
let attributeId = extractAttributeId(processedAttribute.value);
$("#tableHeading-" + attributeId).text(String($(event.target).val()));
}
}
})
$("#attributes-" + i + "_dataType").on("change", function(event) {
event.preventDefault();
for (let processedAttribute of event.target.attributes) {
if (processedAttribute.name === "id") {
let attributeId = extractAttributeId(processedAttribute.value);
applyValidationConstraints(attributeId);
}
}
});
}
}
function getNumberOfActiveAttributes(): number {
let numberOfActiveAttributes = 0;
const attributeRows = $("#attributeList");
attributeRows.children("tr").each(function(index, element) {
const attributeName: string = String($(element).children("td").last().children("input").val());
if ((attributeName != null) && (attributeName !== "")) {
++numberOfActiveAttributes;
}
});
return numberOfActiveAttributes;
}
function addAttribute(attributeIndex: number) {
let rowNode = getAttributeRow(attributeIndex);
rowNode.removeClass("d-none");
$("td:eq(1)>input", rowNode).attr("required", "");
}
function getAttributeRow(attributeNumber: number) {
return $("#attributeList>tr:eq(" + attributeNumber + ")");
}
function extractAttributeId(fullyQualifiedId: string): number {
return parseInt(fullyQualifiedId.substring((fullyQualifiedId.indexOf("-") + 1)));
}
function switchToTable(attributeIndex: number) {
if (previousAttributeIndex !== (-1)) {
getTableNode(previousAttributeIndex).addClass("d-none");
}
previousAttributeIndex = attributeIndex;
let currentTableNode = getTableNode(attributeIndex);
applyValidationConstraints(attributeIndex);
currentTableNode.removeClass("d-none");
}
function getTableNode(attributeIndex: number): JQuery<HTMLElement> {
return $("#table-" + attributeIndex);
}
function applyValidationConstraints(attributeId: number) {
const currentDataTypeName = String($("#attributes-" + attributeId + "_dataType").val());
if (validationRules.has(currentDataTypeName)) {
$(".affectedByDataType-" + attributeId).addClass("d-none");
const dataType = validationRules.get(currentDataTypeName);
if (dataType != null) {
for (let processedAttribute of dataType.attributes) {
makeAttributeVisible(attributeId, processedAttribute);
}
}
} else {
$(".affectedByDataType-" + attributeId).removeClass("d-none");
}
}
function makeAttributeVisible(attributeId: number, processedAttribute: ValidationAttribute) {
$("#attributeRow-" + attributeId + "_" + processedAttribute.id).removeClass("d-none");
const attributeInputNode = $("#attributes-" + attributeId + "_" + processedAttribute.id);
attributeInputNode.attr("type", processedAttribute.type);
if (processedAttribute.pattern != null) {
attributeInputNode.attr("pattern", processedAttribute.type);
} else {
attributeInputNode.removeAttr("pattern");
}
}