Skip to main content

Questionnaire Item Renderer Component

// todo: add link to source code after merge

This documentation covers files located at: Questionnaire Item Renderer

Questionnaire Item Renderer

This component is responsible for rendering individual questionnaire items. A core architectural principle here is the strict separation between Questionnaire Items (the schema/definition) and Response Items (the data/answers).

Rendering Logic

The component evaluates the type of the Questionnaire Item and dynamically renders the corresponding specific UI component.

Handling "Repeat" Logic

While this component is called only once per root item, it manages how repeated elements are handled. It is crucial to distinguish between the two types of repetition within a response:

  • Group Repeats: When a group repeats, the entire item structure is duplicated within the response. This component manages the duplication of these group structures as they do not represent simple user inputs.
  • Input Repeats: For non-group items (standard inputs), the item itself is not duplicated. Instead, additional answer entries are added within the same response item.
Responsibility Mapping

While this component evaluates and provides the repeat configuration for all item types, it only executes the physical duplication for Groups. For standard input items, the specific UI component consumes the provided configuration and manages its own internal answer list.

State & Extension Management

The Item Renderer manages the high-level render state and handles Extension Logics that may be defined for any item type. This includes:

  • Occurrences: Logic for repeat, minOccurs, and maxOccurs.
  • Visibility: Implementation of enableWhen configurations.

These calculated states are provided by this component and consumed by the specific rendered item elements as needed.


Structural Summary

By definition, every questionnaire item is managed by exactly one Item Renderer. However, a single Item Renderer can technically render multiple specific UI instances—this is primarily the case for the Group type, due to its unique repetitive structure.

Visualizing the Tree Structure

The following diagrams illustrate the tree structure and the different handling of repeating elements depending on their type.

1. Group Repetition Flow

In this flow, the Item Renderer manages the repetition logic. Since the item is of type group, the renderer generates two distinct group instances. Each instance then recursively triggers the rendering the items nested child elements.

2. Standard Input Repetition Flow (Non-Group)

For non-group items (e.g., a string type), the Item Renderer creates only a single instance of the specific UI component. This component then manages its own list of multiple answer instances internally. Crucially, it is the individual answer instance that is responsible for triggering the rendering of any further nested child elements.

Important Child Details

It is important to distinguish between item definitions and their instances: Nested child items are defined once at the parent item level, rather than being unique to a specific group or answer instance. Consequently, every instance of a repeating group or answer will render the same predefined set of child items, each maintaining its own independent state.

Structural Conclusions

Based on the definitions and flows described above, we can establish the following core architectural rules:

  • 1:1 Renderer Mapping: Every individual Questionnaire Item (per instance) is managed by exactly one Questionnaire Item Renderer.
  • Recursive Scaling: While a linkId defines a single item in the schema, nested items can exist across multiple instances (due to repeats). Consequently, multiple Item Renderer instances may exist for the same linkId, but they will always reside at the same hierarchical level within their respective tree branches.
  • Root Level Singleton: At the root level of the questionnaire, there is a strict one-to-one relationship: exactly one Item Renderer is initialized for each top-level item definition.

Logic Evaluation & Scope

For the reasons outlined above, it is architecturally optimal to evaluate some item-level logic at the Renderer level.

Key advantages include:

  • Centralized Visibility Logic: Configuration such as enableWhen and general visibility rules are evaluated once at the Renderer level.
  • Instance-Agnostic Configuration: Regardless of how many instances are generated via repeat, the underlying configuration remains unique to the Item definition.
  • Inherited State: By calculating these conditions at the Renderer level, the resulting state is consistently applied to all generated instances, ensuring synchronization and reducing redundant computations.

Um die internen Instanzen unserer spezifischen UI components zu rendern und diese direkt in die korrekte response struktur zu bringen arbeiten wir mit einem eigenen object type MonaQuestionnaireItemNode dieser definiert sich wie folgt:

export type MonaQuestionnaireFormItemAnswerBase = Omit<
QuestionnaireResponseItemAnswer,
| 'item'
| 'valueBoolean'
| 'valueDecimal'
| 'valueInteger'
| 'valueDate'
| 'valueDateTime'
| 'valueTime'
| 'valueString'
| 'valueUri'
| 'valueAttachment'
| 'valueCoding'
| 'valueQuantity'
| 'valueReference'
> & {
id: string;
answerValueWrapper: MonaQuestionnaireAnswerFormGroup;
item?: MonaQuestionnaireItemNode[];
};

export type MonaQuestionnaireBaseNode = {
instanceId: string; // UUID
children: MonaQuestionnaireItemNode[];
};

export type MonaQuestionnaireItemNode = MonaQuestionnaireBaseNode & {
questionnaireItem: QuestionnaireItem;
answerValuesFormArray: FormArray;
answers: WritableSignal<MonaQuestionnaireFormItemAnswerBase[]>;
disabled: Signal<boolean>;
};

// todo link to source code MonaQuestionnaireBaseNode

Every instance rendered within an ItemRenderer is provided with a Node object. This node acts as an enhanced ResponseItem, augmenting the raw data with metadata required specifically for the rendering process.

These nodes serve two purposes:

  1. They represent the current UI render structure of the entire form.
  2. They act as the blueprint for the final QuestionnaireResponse.

Instance Management

The quantity of nodes generated depends on the item type:

  • Group Types: The Renderer generates multiple node instances based on the repeat count, resulting in structural group repetitions.
  • Simple Item Types: Only a single node instance is generated for the entire item.

State & Reactive Forms

As seen in the technical structure, the Disabled State is defined as a Signal. Upon creation of a node instance, this Signal references the disabled state of the current QuestionnaireItemRenderer.

For simple item types, the node includes:

  • answerValuesFormArray: An Angular FormArray containing the raw input values from the specific item instance (it's not the direct value, it is a formGroup wrapper around it).
  • answers: A Signal containing a list of MonaQuestionnaireFormItemAnswerBase objects. This is our internal extension of the standard QuestionnaireResponseItemAnswer, enriched with metadata.

Separation of Concerns: Answers vs. Nested Items

We intentionally separate Child Nodes from Item Answers within the data structure. While Angular Reactive Forms are ideal for managing user input, the FHIR-standardized structure—where nested child items are contained within specific answers—makes a traditional FormGroup hierarchy difficult to maintain and define.

Our Approach:

  1. Decoupling: We treat rendered child nodes and actual item answers as separate entities during the active session.
  2. Reconciliation: The QuestionnaireLogicService combines these two arrays when generating the final response. Since both are maintained as ordered arrays, they share the same index mapping, allowing for a seamless merge of nested items back into their respective answers.

Extensions and Item Flags

To evaluate the functionalities described above, the Questionnaire Item Renderer must extract FHIR extensions from each specific item. This is handled via the helper function XYZ.

Key Metadata Mapping

The component extracts the following properties and extensions from each item:

FeatureConfig KeyLogic
Repeatsitem.repeatsItem Renderer
Requireditem.requiredItem Renderer
Enable Whenitem.enableWhenItem Renderer
Enable When Behaviouritem.enableWhenBehaviourItem Renderer
FeatureExtension URLKey / Value Type
Min Occurrenceshttp://hl7.org/fhir/StructureDefinition/questionnaire-minOccursvalueInteger
Max Occurrenceshttp://hl7.org/fhir/StructureDefinition/questionnaire-maxOccursvalueInteger
SDC Enable Whenhttp://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpressionvalueExpression

All supported extensions of our renderer (item or global level) can be found here

Detailed Logic

Repeatable & Required

  • Repeatable: If an item is repeatable, the renderer can manage multiple node instances. For groups, this allows the creation of several sub-node instances within the renderer.
  • Required: This boolean value is read directly from the FHIR item to drive validation logic. It´s read from node instances of this item.

minOccurs & maxOccurs

  • Extracted via extensions.
  • Initialization: minOccurs determines the minimum number of group nodes created during the initial render (e.g., minOccurs: 2 initializes two group nodes).
  • Constraint: Per FHIR specifications, minOccurs is only active if the element is also marked as required, as validation occurs at the item node level (ReactiveForm).
  • Logic & Validation: * Currently, there is no hard validation enforced by these values. They do not trigger error states if the counts are exceeded or not met.
    • Instead, they are used exclusively to enable or disable UI functions (e.g., hiding the "Add" button when maxOccurs is reached).
    • Specifically, maxOccurs does not restrict the number of elements when a response is being set/loaded via the data layer.
  • UI Controls: Based on these values, the renderer dynamically displays or hides the "Add" and "Remove" buttons for group instances.

Conditional Visibility (enableWhen)

The renderer supports two types of conditional logic:

  1. Standard FHIR enableWhen: Extracted from the item and processed via the questionnaire-enable-when-service. This includes the enableWhenBehavior (all/any).
  2. Extended Logic (FHIRPath): Defined via extensions. These are processed by the questionnaire-logic-service using FHIRPath expressions.
Priority

Extended logic via extensions takes precedence. If a logic extension is defined, the standard enableWhen property is ignored.

Disabled State & Rendering

If any enableWhen configuration exists, the renderer's disabled state is updated based on the service results. If no configuration is present, disabled defaults to false.

  • State Propagation: The disabled value is linked to the nodes, and with this included in the response generation for filtering. This ensures that disabled items are excluded from the final response.
  • Visual Handling: If the renderer is disabled, the host element is hidden via CSS.
  • Note on Child Nodes: Even when disabled, child nodes are still rendered because they must register themselves as items in the response. Specific behavior for child nodes is handled within the respective item components.
  • Performance Optimization: While we currently maintain the entire tree structure for response integrity, a goal should be to remove as much DOM structure as possible via Angular directives (e.g., *ngIf) to improve performance.

Response Management

Since the renderer manages group instances, it is responsible for synchronizing the local state with the FHIR Response.

Data Flow & Nesting

The item-renderer-component receives the entire response layer associated with its current depth. It then filters the data for its own linkId.

  • Instance Management: If ItemA has multiple response entries (ItemA[1], ItemA[2]), the renderer identifies these at the current level and instantiates the corresponding nodes.
  • Recursive Passing: Each generated node is then passed its specific subsection of the response, if it exists.

Core Services

as mentioned above, the renderer relies on two services to evaluate logic and manage state:


Specific Item Components

The renderer is responsible for identifying and instantiating the correct component for a given FHIR item type. It acts as a bridge, passing the established node instance and the corresponding response layer down to the child component.

Core Logic & Inheritance

Most of the logic within these specific item components is derived from the questionnaire-item-base. This base class ensures consistent behavior across different data types (e.g., strings, choices, booleans).

  • Responsibility: While the renderer manages the "shell" (like instances and visibility), the specific item component handles the actual input logic and user interaction.