Skip to content

Architecture: Modularize components using classes and inheritance #377

Closed
@olemartinorg

Description

@olemartinorg

Description

There are multiple code paths which branch off to do something different depending on the encountered component type. This has historically been needed to implement some custom functionality for "abnormal" components.

Solution

The idea in this issue is to refactor away some complexities in existing code such that we can have cleaner implementations of the generic frontend app, and at the same time concentrate custom overrides for each component in their respective folders. This should also make it easier to implement a new component, as potential overrides are centralized. One way to implement this is to define an abstract class (i.e. BaseComponent), have every component implement their own concrete class from this abstract base class, and then reference instances of these classes in the components map in src/altinn-app-frontend/src/components/index.ts. GenericComponent.tsx can then fetch each react component via components[props.type].getReactComponent() or similar.

Examples from GenericComponent.tsx

This case should instead be handled using a function call, like !component.shouldRenderValidationsGenerically() (quite possibly an overly verbose method name).

const getValidationsForInternalHandling = () => {
  if (
    props.type === 'AddressComponent' ||
    props.type === 'DatePicker' ||
    props.type === 'FileUpload' ||
    props.type === 'FileUploadWithTag' ||
    (props.type === 'Likert' && props.layout === LayoutStyle.Table)
  ) {
    return componentValidations;
  }
  return null;
};

// some components handle their validations internally (i.e merge with internal validation state)
const internalComponentValidations = getValidationsForInternalHandling();
if (internalComponentValidations !== null) {
  passThroughProps.componentValidations = internalComponentValidations;
}

This case should instead be handled using a function call, like component.shouldRenderLabel():

const noLabelComponents = [
  'Header',
  'Paragraph',
  'Image',
  'Submit',
  'ThirdParty',
  'AddressComponent',
  'Button',
  'Checkboxes',
  'RadioButtons',
  'AttachmentList',
  'InstantiationButton',
  'NavigationBar',
  'Likert',
  'Panel',
];

Examples from validation.ts:

In validateEmptyFieldsForLayout, this code should instead look up the function to see if it has implemented any custom 'empty field' validations. Quite possibly every component type should have its own method to figure out if the formData is "empty" or not.

if (
  component.type === 'FileUpload' ||
  component.type === 'FileUploadWithTag'
) {
  // These components have their own validation in validateFormComponents(). With data model bindings enabled for
  // attachments, the empty field validations would interfere.
  continue;
}

The whole function called validateFormComponentsForLayout is filled with custom validation rules for certain components. These should be implemented in the component definitions themselves.

In findLayoutIdsFromValidationIssue(), there is a special case for some components:

// Special handling for FileUpload components
if (c.type === 'FileUpload' || c.type === 'FileUploadWithTag') {
  return c.id === validationIssue.field;
}

Examples in formComponentUtils.ts:

The functions componentValidationsHandledByGenericComponent, getDisplayFormData and getFormDataForComponentInRepeatingGroup include lots of code to make sure different components produce some valid results. These should look up some component definition/declaration instead, where components themselves can override implementations.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions