diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f5729c..6f804b2 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -59,7 +59,7 @@ Before you submit your Pull Request (PR) consider the following guidelines: - Make your changes in a new git branch: ```shell - git checkout -b my-fix-branch next + git checkout -b my-fix-branch main ``` - Create your patch, **including appropriate test cases**. @@ -89,7 +89,7 @@ Before you submit your Pull Request (PR) consider the following guidelines: - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): ```shell - git rebase next -i + git rebase main git push -f ``` @@ -106,10 +106,10 @@ from the main (upstream) repository: git push origin --delete my-fix-branch ``` -- Check out the next branch: +- Check out the main branch: ```shell - git checkout next -f + git checkout main -f ``` - Delete the local branch: @@ -118,10 +118,10 @@ from the main (upstream) repository: git branch -D my-fix-branch ``` -- Update your next branch with the latest upstream version: +- Update your main branch with the latest upstream version: ```shell - git pull --ff upstream next + git pull --ff upstream main ``` ## Coding Rules diff --git a/README.md b/README.md index 02923ab..757e77a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # quaire -a framework-agnostic library to create user-flows, surveys, and questionnaires +A framework-agnostic library to create user flows, surveys, and questionnaires. -# What type of application can I build with quaire? +You describe the flow as data (a decision tree). quaire takes care of the behavior: +which question comes next, which answers are valid, and what the navigation looks like. +You only build the view. -The library was already used for +# What can I build with quaire? + +The library is already used for: - Call center software (call scripts) - Surveys @@ -14,190 +18,266 @@ The library was already used for # Why the name? -Because I needed one and `Questionnaire` is terrible to type. Let me know if you have a better one! +Because I needed one, and `Questionnaire` is hard to type. Let me know if you have a better one! -# What use-case does it try to solve? +# What problem does it solve? -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. +I often have to build features that behave like questionnaires or surveys. +Over the years, I tried a few things to make this easier. -Of course, the first approach I tried was to implement a static user flow, for example: +The first approach was a static user flow, for example: -- present first question on first page -- user selects option a +- show the first question on the first page +- the 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. +This worked well until the flow changed. Then I had to change 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.) +I also tried finite state machines, but most of the time I ended up with spaghetti state machines, +because the flow changed often (questions were re-arranged, added, skipped, 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: +The solution that worked best for me is a concept from game development: the decision tree. +The whole user flow is a static data structure. You can change it without changing the view or the behavior. +This gives you a clean separation of: - data -- behaviour +- behavior - presentation -# Features? - -- Framework-agnostic -- View independent -- [Linear user-flow](https://github.com/devCrossNet/quaire/tree/main/examples/linear-flow) -- [Branched/merged user-flow](https://github.com/devCrossNet/quaire/tree/main/examples/branched-and-merged-flow) -- [Loops](https://github.com/devCrossNet/quaire/tree/main/examples/linear-flow-with-loop) -- [Dependencies between questions and validation](https://github.com/devCrossNet/quaire/tree/main/examples/dependencies-between-questions) -- [Re-storing state of the questionnaire from the selected answers](https://github.com/devCrossNet/quaire/blob/main/examples/restore-state.spec.ts) -- Navigation +# Features -# How does it work? +- Framework-agnostic and view-independent +- Written in TypeScript, types included +- No runtime dependencies +- Linear flows, branches, merges, and loops +- Optional questions that can be skipped +- Options that depend on former answers +- Validation that resets answers when they are no longer valid +- Navigation with categories and subcategories +- Restore the state from an existing result +- Typed result +- Extensible with custom component types and custom data -## Installation +# Installation ```shell -npm i --save quaire +npm i quaire ``` -## Define quaire data +# Getting started -First you need to define the data (decision tree) based on the -[QuaireItem type](https://github.com/devCrossNet/quaire/blob/main/src/types.ts#L46). 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. +## 1. Define the questions -The structure for questions looks as follows: +The questions are a list of [QuaireItem](https://github.com/devCrossNet/quaire/blob/main/src/types.ts) objects. +This can be static data in a JS/TS file, a JSON file that you load on demand, +or JSON from a CMS or backend API. -```js -import { QuaireComponentType, QuaireItem, QuaireNavigationItem } from 'quaire'; +```ts +import { QuaireComponentType, QuaireItem } 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 + resultProperty: 'foo', // property in the result that stores the answer + navigationItemId: 1, // navigation entry of this question (optional) + dependsOnResultProperties: [], // result properties of former questions that change the options (see below) + componentType: QuaireComponentType.SINGLE_SELECT, // tells your view which component to render 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 + selectOptions: [ + { label: 'Option 1', value: 'option 1', nextItemId: 2 }, + { label: 'Option 2', value: 'option 2', nextItemId: 3 }, + ], + defaultValue: 'option 1', // (optional) }, // ... ]; ``` -The structure for the navigation looks a follows: +### Component types -```js -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', +The component type defines where quaire finds the next question: + +| Component type | Options | Next question | +| ---------------- | ------------------------------------------------ | ----------------------------------- | +| `SINGLE_SELECT` | `selectOptions: [{ label, value, nextItemId }]` | `nextItemId` of the selected option | +| `RANGE_SLIDER` | `rangeOption: { range, nextItemId }` | `rangeOption.nextItemId` | +| `INPUT` | `inputOption: { type, placeholder, nextItemId }` | `inputOption.nextItemId` | +| any other string | - | `nextItemId` of the item | + +If there is no next question, the active question stays the same. +You can add your own component types, see [Custom component types](https://github.com/devCrossNet/quaire/tree/main/examples/custom-component-types). + +### Options that depend on former answers + +Use `dependsOnResultProperties` when the options of a question depend on the answer of a former question. +The options are then nested by the result property and the answer: + +```ts +{ + id: 2, + resultProperty: 'bar', + dependsOnResultProperties: ['foo'], + // ... + selectOptions: { + foo: { + 'option 1': [{ label: 'Option 1.1', value: 'option 1.1', nextItemId: 3 }], + 'option 2': [{ label: 'Option 2.1', value: 'option 2.1', nextItemId: 3 }], + }, }, +} +``` + +This works the same way for `rangeOption`, `inputOption`, and `defaultValue`. +With more than one dependency, the objects are nested in the same order, for example +`{ foo: { 'option 1': { bar: { 'option 1.1': [...] } } } }`. +See [Dependencies between questions](https://github.com/devCrossNet/quaire/tree/main/examples/dependencies-between-questions). + +## 2. Define the navigation (optional) + +```ts +import { QuaireNavigationItem } from 'quaire'; + +export const navigationItems: QuaireNavigationItem[] = [ + { id: 1, parentId: null, name: 'Category 1' }, // has no parent + { id: 2, parentId: 1, name: 'Subcategory 1' }, // has a parent (only one level is supported) + { id: 3, parentId: null, name: 'Category 2', icon: 'phone' }, ]; ``` -A navigation item without an own question is shown only if it has children with questions. -For a parent item, `hasValue` and `isValid` are derived from its children. +`getNavigation()` returns the navigation items with these additional properties: + +| Property | Description | +| --------------- | -------------------------------------------------------------------------------- | +| `value` | Answer of the question. For select components, the option label | +| `active` | The active question belongs to this item (or one of its children) | +| `hasValue` | The question of this item (or one of its children) has an answer | +| `isValid` | The question of this item is valid. Without own question: all children are valid | +| `componentType` | Component type of the question | +| `subNavigation` | Children of a parent item | + +A parent item without its own question is shown only if it has children with questions. Navigation items without any question are not shown. +See [Navigation](https://github.com/devCrossNet/quaire/tree/main/examples/navigation). -## Use quaire behaviour +## 3. Use quaire -To use the default behavior you need to initialize `Quaire` with the data you -defined in the former step. +Create a `Quaire` instance with your data: -```js +```ts import { Quaire } from 'quaire'; -const q = new Quaire({ items, navigationItems }); // (optional) you can pass an existing result to restore the questionnaire +const q = new Quaire({ items, navigationItems }); ``` -Next you can get the first active Question and display it in any way you want +Get the active question and show it in any way you want: -```js +```ts let activeQuestion = q.getActiveQuestion(); // first question -let navigation = q.getNavigation(); // initial navigation -let result = q.getResult(); // initial result +let navigation = q.getNavigation(); +let result = q.getResult(); // {} +``` -// display activeQuestion.question and the related component presentation logic -// Vue.js pseudo code example +```vue +