Appearance
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 designerThe 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
| Prop | Contents |
|---|---|
question | The full question definition: identifier, type, section, optional flag, translations and the default settings authored in the designer. |
question_config | The 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. |
answers | All 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. |
locale | The 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:
| Field | Required | Meaning |
|---|---|---|
question | yes | The question the answer belongs to (pass the prop through). |
answer | yes | { 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. |
errors | no | A list of { name, message } validation errors. A non-empty list keeps Next disabled. Emit an empty list when the answer becomes valid again. |
score | no | An 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).
| Export | Use |
|---|---|
UComponent | The bridge to the shared UI library (below). |
MPButton | A 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 validator | A ready-to-use validator (with formats and custom error messages) for structured validation. |
| Date utilities | Calendar date and time types and time-zone helpers used by the date component. |
| Submission helpers and types | Types 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:
| Category | Components |
|---|---|
| Inputs | UInput, UInputNumber, UTextarea, USelect, USlider, USwitch, UCheckbox, URadioGroup, UCalendar, NuxtRating |
| Forms | UForm, UFormField, UFieldGroup |
| Actions | UButton, ULink, UDropdownMenu |
| Layout | UCard, UContainer, UAccordion, UTabs, UTable |
| Feedback | UAlert, UBadge, UProgress, USkeleton, UToast, UTooltip, UModal |
| Media | UIcon, 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
- Create the type. In the Engine Editor choose Add new component, or add an entry to your manifest's
components. Give it aname(the type identifier, used in questionnaires forever, so choose carefully), alabel, aniconand amainFile. - Write the component. Start from the skeleton below. Declare the four props, emit
answer, and useUComponentfor controls. - Describe the settings. Write
Schema.jsonandUISchema.json. Start with the two or three settings you need. - Restore state. Read the existing answer from
answers[question.id]so the value is shown again when the patient navigates back. - Validate. Return errors in the
errorslist while the value is invalid; return an empty list once it is valid. - 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.
- 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
| Symptom | Cause |
|---|---|
| Next never enables | The component emits errors that are never cleared, or never emits answer at all. |
| Value disappears when going back | Initial state is not read from answers. |
| Settings are empty at runtime | Settings read from question instead of question_config, or Schema.json is missing. |
| Component works in preview but not in the mobile app | It loads an external asset (icon, font, script) at runtime instead of bundling it. |
| Wrong language | The component uses the browser language instead of the languages in locale. |
| Logic cannot compare the answer | value_type does not match the shape of value (for example a list reported as string). |