Skip to content

Custom Question Components ​

A question component (also called a renderer) is the part of an engine that draws the answer area for one question type and reports the answer back to the core. Everything around it (question text, progress, Back/Next, validation gating, logic, scoring, languages, theme) is handled by the core. This page describes the contract and walks through creating a component.

Anatomy ​

A component is a Vue 3 single-file component plus two optional JSON files that describe its settings:

my-rating/
├── RatingRenderer.vue   # the component: receives the question, emits the answer
├── Schema.json          # JSON Schema of the type's settings (what the designer stores per question)
└── UISchema.json        # JSON Forms layout for editing those settings in the designer

The component may import anything listed in the engine's shared files (for example a base answer tile or a translation helper) and the helpers exported by the core.

The contract with the core ​

Props the component receives ​

PropContents
questionThe full question definition: identifier, type, section, optional flag, translations and the default settings authored in the designer.
question_configThe effective settings for the current questionnaire language: the default settings, or the per-language override when the designer defined one. Read settings from here, not from question directly.
answersAll answers recorded so far, keyed by question identifier. Use it to restore the current value when the patient navigates back, or to react to earlier answers.
localeThe resolved language context: the patient's interface language and secondary language, the questionnaire language, and the questionnaire's language definition (available languages and default).

Event the component emits ​

A component reports its state with one event, answer, carrying:

FieldRequiredMeaning
questionyesThe question the answer belongs to (pass the prop through).
answeryes{ value, value_type, meta }. value_type is one of string, number, boolean, array. Put the localised and default settings and any component-specific context (option index, numeric value, label) in meta.
errorsnoA list of { name, message } validation errors. A non-empty list keeps Next disabled. Emit an empty list when the answer becomes valid again.
scorenoAn optional numeric score for this answer, when the type carries one.

The core enriches meta with the response language and the questionnaire's language definition before storing the answer, so a component does not need to add those.

When to emit. Emit as soon as the patient has produced a valid, complete answer (a tap on an option, a slider release, leaving a number field). Emitting on every keystroke is fine as long as the last emission before Next reflects the final value. The core takes care of advancing; a component never navigates.

Helpers exported by the core ​

Components import these from the core module (its import name inside an engine is medipal).

ExportUse
UComponentThe bridge to the shared UI library (below).
MPButtonA button with the platform's press feedback and haptics.
useHaptics()Requests haptic feedback from the host device: light/medium/heavy impact, selection start/change/end, success, warning, error. No-op outside the mobile app.
JSON Schema validatorA ready-to-use validator (with formats and custom error messages) for structured validation.
Date utilitiesCalendar date and time types and time-zone helpers used by the date component.
Submission helpers and typesTypes for the answer shape and helpers that infer value_type from a value.

Using the shared UI library ​

The core bundles a complete UI component library, and components should use it rather than bring their own, so that every question type matches the platform's look, supports dark mode and zoom, and works offline. Because the library lives inside the core, a component does not import it directly; it asks the core for a component by name through UComponent:

vue
<UComponent is="USlider" v-model="value" :min="1" :max="10" size="xl" />

UComponent forwards v-model, props, events and slots to the real component, so it behaves exactly like using the component directly.

Available names:

CategoryComponents
InputsUInput, UInputNumber, UTextarea, USelect, USlider, USwitch, UCheckbox, URadioGroup, UCalendar, NuxtRating
FormsUForm, UFormField, UFieldGroup
ActionsUButton, ULink, UDropdownMenu
LayoutUCard, UContainer, UAccordion, UTabs, UTable
FeedbackUAlert, UBadge, UProgress, USkeleton, UToast, UTooltip, UModal
MediaUIcon, UAvatar

Icons must be part of the bundle. The core ships the icons used by the default engine; if your component needs another icon, add it to the engine so that it renders offline.

Styling ​

  • Use utility classes; the core's stylesheet includes the utility framework and the component library's styles, and both colour modes.
  • Use the dark: variants for anything you colour manually, and avoid fixed pixel font sizes so that zoom applies.
  • Respect text direction: the core sets the document direction for RTL languages, so prefer logical properties (start/end) over left/right.
  • Keep the answer area free of its own scrolling containers in paged mode; the page already scrolls.

Translations ​

A component can carry its own message catalogue keyed by locale and read the current interface language from locale. The default engine's shared translation helper resolves a message by exact locale, then by base language, then by a fallback locale. Designers can override or add messages per question through the type's settings, and the helper merges those overrides on top of the component's catalogue, which lets one component serve programmes in languages you did not anticipate.

Settings: Schema and UI Schema ​

Schema.json declares the settings a designer can set for questions of this type (minimum and maximum, options, units, placeholder text). UISchema.json declares how the settings form is laid out. The designer renders that form on the question's settings panel and stores the values in the question definition; at runtime they arrive in question_config, already resolved for the current language.

Keep settings flat where possible, give every property a title and a sensible default, and use enumerations for choices so that the designer shows selects rather than free text.

Create your first component ​

  1. Create the type. In the Engine Editor choose Add new component, or add an entry to your manifest's components. Give it a name (the type identifier, used in questionnaires forever, so choose carefully), a label, an icon and a mainFile.
  2. Write the component. Start from the skeleton below. Declare the four props, emit answer, and use UComponent for controls.
  3. Describe the settings. Write Schema.json and UISchema.json. Start with the two or three settings you need.
  4. Restore state. Read the existing answer from answers[question.id] so the value is shown again when the patient navigates back.
  5. Validate. Return errors in the errors list while the value is invalid; return an empty list once it is valid.
  6. Preview. In the Engine Editor the Mobile tab shows your component inside the core at phone size with a sample question, and the Form tab shows the settings form generated from your schema. With the builder tool, use the preview harness.
  7. Publish the engine (see Building a Custom Engine) and bind a test questionnaire to it.

Minimal skeleton:

vue
<script setup>
import { ref } from "vue";
import { UComponent, useHaptics } from "medipal";

const props = defineProps(["question", "answers", "question_config", "locale"]);
const emit = defineEmits(["answer"]);
const haptics = useHaptics();

const value = ref(props.answers[props.question.id]?.answer?.value ?? null);

const submit = (v) => {
  value.value = v;
  haptics.selectionChanged();
  const errors =
    v < props.question_config.min
      ? [{ name: "rating", message: "Too low" }]
      : [];
  emit("answer", {
    question: props.question,
    answer: {
      value: v,
      value_type: "number",
      meta: { localized_config: props.question_config },
    },
    errors,
  });
};
</script>

<template>
  <UComponent
    is="USlider"
    :model-value="value"
    :min="question_config.min"
    :max="question_config.max"
    @update:model-value="submit"
  />
</template>

Media, attachments and rich content ​

Question text is authored as rich text in the designer and rendered by the core above your component. If your type needs images or other media, reference them by URL from the question settings so that they are part of the questionnaire, not of the engine bundle, and keep in mind the mobile app may be offline.

Common mistakes ​

SymptomCause
Next never enablesThe component emits errors that are never cleared, or never emits answer at all.
Value disappears when going backInitial state is not read from answers.
Settings are empty at runtimeSettings read from question instead of question_config, or Schema.json is missing.
Component works in preview but not in the mobile appIt loads an external asset (icon, font, script) at runtime instead of bundling it.
Wrong languageThe component uses the browser language instead of the languages in locale.
Logic cannot compare the answervalue_type does not match the shape of value (for example a list reported as string).