Templates, styling and JavaScript
Overriding templates
The extension uses standard TYPO3 Fluid template path merging. Copy any file
from the extension's Resources/ tree into your site package at
the path configured in View paths, and TYPO3 uses your version
instead. For purely visual changes no PHP is involved at all.
Layouts/
└── Default.html
Templates/
└── Questionnaire/
├── Intro.html
├── Question.html
└── Result.html
Partials/
├── AnswerTypes/
│ ├── SingleChoice.html
│ ├── MultipleChoice.html
│ ├── YesNo.html
│ ├── Scale.html
│ └── Informational.html
├── AdviceBlock.html
├── ButtonLabel.html
└── Progress.html
Button resolves one button label: it renders the FlexForm
override when it is filled and the translated standard text otherwise.
Progress. renders the progress bar and is shared by the question
steps and the result page.
Custom ViewHelper namespace
The extension provides one custom ViewHelper. Declare its namespace in any template that uses it:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:pnq="http://typo3.org/ns/ProudNerds/PnQuestionnaire/ViewHelpers"
data-namespace-typo3-fluid="true">
| ViewHelper | Description |
|---|---|
pnq: | Returns true when needle exists in haystack, comparing as
strings. Used in the answer type partials to restore the checked
state of previously given answers |
Key template variables
Question.html
| Variable | Type | Description | |
|---|---|---|---|
questionnaire | Questionnaire | The questionnaire record | |
question | Question | The current question | |
progress | array | Step X of Y | |
progress | int | 0–100: the share of the questionnaire behind the visitor, see [progress_mode](#confval-typoscript-progress-mode) | |
prev | `int | null` | UID of the previous question, null on the first |
current | string | Previously stored answer values for this question | |
has | bool | true when at least one answer is in the session — shows the Start
over button | |
answer | string | Partial path, e.g. Answer | |
scale | int | Array from scale min to max (radio scale only) | |
scale | string | radio or range, from the question's Scale display field | |
scale | int | Midpoint of the scale range, used as the default slider position |
Result.html
| Variable | Type | Description | |
|---|---|---|---|
questionnaire | Questionnaire | The questionnaire record | |
result | `ResultPage | null` | The matched result page; null means no catch-all is configured |
advice | Advice | The visible advice blocks, already filtered | |
total | float | The calculated score, 0. when no scores are set | |
show | bool | From the plugin settings | |
show | bool | From the plugin settings | |
answer | array | Pre-built list of question and answers for the summary |
Form field naming
Answer inputs must use this exact
name
attribute for Extbase to map them
to
process:
name="tx_pnquestionnaire_questionnaire[answers][]"
And the hidden question UID field:
name="tx_pnquestionnaire_questionnaire[questionUid]"
Heading levels, and why the RTE needs Heading 4 and Heading 5
The result page builds a heading hierarchy from three sources, only two of which the templates control:
| Level | Rendered by | Content |
|---|---|---|
h2 | Result. | Result page headline |
h3 | Advice | A group heading (condition_), or a block title
that is not in a group |
h4 | Advice | The title of a block that points at a group heading |
h5 | The editor, inside the content field | A sub-heading within a block |
That last row is the catch: the deepest level comes out of a rich text field, so
it is the RTE configuration — not the extension — that decides whether an editor
can produce a valid document. An installation therefore needs both Heading 4
and Heading 5 available in the preset used for
tx_:
- Heading 5 for a sub-heading inside a grouped block, whose title is
already an
h4. - Heading 4 for a sub-heading inside an ungrouped block, whose title is
an
h3.
Which one an editor needs depends on whether the block sits in a group, and that
can change after the fact — moving a block into a group shifts its title from
h3 to h4, so any sub-heading in its body has to move down a level too.
Offer both and the editor can always pick the level one step below the block
title.
If the preset stops at Heading 3, every sub-heading an editor creates lands at
the same level as — or above — the title of the block it belongs to, which
breaks the document outline (WCAG 1.3.1). The extension deliberately does not
set
richtext on the field to force this: that would override
whatever preset the site assigns through page TSconfig, for every installation.
editor:
config:
heading:
options:
- { model: 'paragraph', title: 'Paragraph' }
- { model: 'heading2', view: 'h2', title: 'Heading 2' }
- { model: 'heading3', view: 'h3', title: 'Heading 3' }
- { model: 'heading4', view: 'h4', title: 'Heading 4' }
- { model: 'heading5', view: 'h5', title: 'Heading 5' }
Note
CKEditor replaces this list as a whole rather than merging it, so when
overriding it in a separate file, repeat the entries you want to keep. Also
check that h5 is present in the allowTags list of the processing
configuration — otherwise the tag is stripped on save. The default
EXT: allows h1
through h6.
Styling
The extension ships a minimal CSS file
(Resources/) that only defines the progress
bar. Everything else is unstyled — apply all visual design in your site package.
The progress bar fill uses currentColor, which inherits from the nearest
element with an explicit color value:
.pn-questionnaire__progress-fill {
color: #your-brand-color;
}
BEM class reference
| Class | Element |
|---|---|
.pn- | Root wrapper, also carries the data- attribute |
.pn- | Intro screen container |
.pn- | Introduction text area |
.pn- | Start button wrapper |
.pn- | Text below the start button |
.pn- | Question step container, carries data- |
.pn- | Progress indicator wrapper, from the Progress partial |
.pn- | Grey track of the progress bar |
.pn- | Coloured fill; width set inline |
.pn- | "Step X of Y" on a question, "Completed" on the result page |
.pn- | Dynamic steps note below the bar |
.pn- | Embedded tt_ element |
.pn- | Question text area |
.pn- | Help text below the question |
.pn- | The answer form |
.pn- | Fieldset wrapping all answer inputs |
.pn- | Wrapper for one answer option |
.pn- | Modifier for radio inputs |
.pn- | Modifier for checkbox inputs |
.pn- | Modifier for yes/no inputs |
.pn- | Radio input |
.pn- | Checkbox input |
.pn- | Label of an answer option |
.pn- | Scale question wrapper |
.pn- | Modifier: radio button display |
.pn- | Modifier: range slider display |
.pn- | Row of scale radio buttons |
.pn- | One scale value |
.pn- | Label of one radio scale value |
.pn- | Wrapper for slider input and output |
.pn- | Min endpoint label |
.pn- | Max endpoint label |
.pn- | The range input element |
.pn- | The output element showing the live value |
.pn- | Client-side validation message, hidden by default |
.pn- | Asterisk after the text of a required question |
.pn- | Previous / Next / Start over wrapper |
.pn- | Base button class |
.pn- | Primary action button |
.pn- | Secondary action button |
.pn- | Start over button |
.pn- | Result container |
.pn- | Modifier when no result page matched |
.pn- | Result headline |
.pn- | Score display |
.pn- | Result body text |
.pn- | Call-to-action button wrapper |
.pn- | Start over button on the result page |
.pn- | Advice blocks container |
.pn- | One advice block |
.pn- | Modifier for always-visible blocks |
.pn- | Modifier for score-range blocks |
.pn- | Modifier for specific-answer blocks |
.pn- | Modifier for scale-range blocks |
.pn- | Advice block headline |
.pn- | Advice block body text |
.pn- | Answer summary container |
.pn- | One question and answer pair |
.pn- | Question text in the summary |
.pn- | Answer text in the summary |
.pn- | Copy button next to the retrieval link |
.pn- | Live region reporting the copy result |
JavaScript
All behaviour is progressive enhancement: the questionnaire works without JavaScript through standard HTML form submissions.
Questionnaire.js
Loaded via
<f: in Layouts/ and
initialised on DOMContentLoaded by looking for [data-questionnaire].
| Feature | Description |
|---|---|
| Scroll to container | Scrolls the questionnaire into view when a step is active |
| Required validation | Validates that at least one non-hidden input is filled before allowing submission |
| Error message | Reads the error text from the data- attribute on the
form |
| Submit lock | Disables the submit button after a valid submission, preventing double posts |
A range input always has a value, so required validation passes automatically for scale slider questions.
ScaleRange.js
Loaded conditionally, only when a scale question renders as a range slider. TYPO3 deduplicates the asset, so it is safe with multiple scale questions on one page.
| Feature | Description |
|---|---|
| Live value display | Keeps the output element next to the slider in sync while the visitor drags |
| Initial sync | Syncs on load in case the browser normalises the server-rendered value |
The output element is server-rendered with the correct initial value, so the slider is usable without JavaScript.
To replace the JavaScript, point
<f: in your layout
override at a different file, or load your own script that initialises a class
targeting [data-questionnaire].
Adding a language
- Copy
Resources/to a new file namedPrivate/ Language/ locallang. xlf {language-in the same directory.code}. locallang. xlf - Add
target-to thelanguage=" {language- code}" <file>element. - Add a
<target>element with the translation for each<trans-.unit>
TYPO3 picks the translation up automatically, based on the active frontend
language. The Dutch translation (nl.) can serve as a
reference; German (de.) is shipped as well.
The backend labels live in locallang_ and follow the same pattern.