a framework-agnostic library to create user-flows, surveys, and questionnaires
The library was already used for
- Call center software (call scripts)
- Surveys
- Briefings
- Games
- Questionnaires
Because I needed one and Questionnaire
is terrible to type. Let me know if you have a better one!
I have to implement more and more features that behave like questionnaires or surveys. Over the years I tried a couple of things to make it easier for me to implement those features.
Of course, the first approach I tried was to implement a static user flow, for example:
- present first question on first page
- user selects option a
- navigate to the next question
- etc.
That was ok as long as I didn't have to change the flow but when I had to change the flow, I found myself changing big parts of the implementation.
I also tried to use finite state machines, but I ended up with spaghetti state machines most of the time due to regular changes in the user-flow (re-arranging questions, adding questions, skip questions, etc.)
The solution I found to work best for me was a concept of game-development called decision-tree. That way the whole user-flow is based on a static data structure that can be changed without the need of changing the view or the behaviour. This pattern provides a nice separation of:
- data
- behaviour
- presentation
- Framework-agnostic
- View independent
- Linear user-flow
- Branched/merged user-flow
- Loops
- Dependencies between questions and validation
- Re-storing state of the questionnaire from the selected answers
- Navigation
npm i --save quaire
First you need to define the data (decision tree) based on the QuaireItem interface. This can be static data in a JS/TS file, a JSON file that you load on demand or a dynamic JSON from a CMS or backend API.
The structure for questions looks as follows:
import { QuaireComponentType, QuaireItem, QuaireNavigationItem } from 'quaire';
export const items: QuaireItem[] = [
{
id: 1,
resultProperty: 'foo', // property that is used to save the answer
navigationItemId: 1, // association with the navigation entry (optional)
dependsOnResultProperties: [], // dependencies on answers from former questions, based on the result property - not question ID
componentType: QuaireComponentType.SINGLE_SELECT, // component type as indication for custom presentation logic
question: 'Question 1',
description: 'Description 1',
required: true,
selectOptions: [], // options for select components (optional)
rangeOption: {}, // option for range components (optional)
inputOption: {}, // option for input components (optional)
defaultValue: {}, // default value for any kind of component (optional)
nextItemId: {}, // id of the follow up question (optional), usually you want to define this in selectOptions, rangeOption or inputOption
},
// ...
];
The structure for the navigation looks a follows:
export const navigationItems: QuaireNavigationItem[] = [
{
id: 1,
parentId: null, // has no parent
name: 'Category 1',
},
{
id: 2,
parentId: 1, // has a parent (works only for one level)
name: 'Subcategory 1',
},
{
id: 3,
parentId: null,
name: 'Category 2',
},
];
To use the default behavior you need to initialize Quaire
with the data you
defined in the former step.
import { Quaire } from 'quaire';
const q = new Quaire({ items, navigationItems }); // (optional) you can pass an existing result to restore the questionnaire
Next you can get the first active Question and display it in any way you want
let activeQuestion = q.getActiveQuestion(); // first question
let navigation = q.getNavigation(); // initial navigation
let result = q.getResult(); // initial result
// display activeQuestion.question and the related component presentation logic
// Vue.js pseudo code example
<template>
<div>
<template v-if="activeQuestion.componentType === 'SINGLE_SELECT'">
{{ activeQuestion.question }}
// loop through activeQuestion.selectOptions, etc.
<template>
</div>
</template>
After the user selected an answer you can save the answer and get the next question. It's also a good idea to update the navigation and result
onSubmit(value: any) {
q.saveAnswer(value);
activeQuestion = q.getActiveQuestion(); // follow up question
navigation = q.getNavigation(); // update navigation
result = q.getResult(); // update result
// ...
}
You need to identify the end of the user-flow on your own. One way to do it is via the question ID or you create some logic around the result object of the Questionnaire.
onSubmit(value: any) {
// ...
const isValid = q.isValid();
// check if the questionnaire is valid
if(!isValid) {
return;
}
// via ID
if(activeQuestion.id === 3) {
// persist result to the backend, redirect to another page,
// whatever you want after the questionnaire is filled out
}
// via result
if(result.foo && result.bar && result.baz) {
// persist result to the backend, redirect to another page,
// whatever you want after the questionnaire is filled out
}
}
- Linear flow
- Linear flow with a loop
- Linear flow with option question
- Branched flow that merges back into one
- Dependencies between questions and validation
- Re-storing state of the questionnaire from the selected answers
Contributions are always welcome! Please read the contribution guidelines first.