npm package: @strategies/nudge
Description: Nudge survey suite
Author: Eric Youngberg
Latest version: 1.4.0

# Nudge

### Installation
```sh
yarn add @strategies/nudge
```

### Usage

#### Provider
Nudge is a contextual data layer that collects `Question` classes and derives the data from their answers.
Because of this contextuality, we must wrap the survey in a provider component.

```tsx
import { Nudge, NudgeConfig } from '@strategies/nudge';

export default function App() {
    return (
        <Nudge config={config as NudgeConfig}>
            <MySurvey />
        </Nudge>
    );
}
```

#### Survey Context
Once you have provided a context, you can access that context through a hook.

The Survey instance is where you can attach metadata, manually save the survey, or control the question flow. Each of the 
behaviors attached to the buttons below will automatically save the survey. Once a survey is completed, the survey is saved,
the `survey.completed` flag will be set to `true` and may not be reset to `false`.

```tsx
import { Button } from '@strategies/ui';
import { useNudgeSurvey, Survey } from '@strategies/nudge';

export default function MySurvey() {
    const survey: Survey = useNudgeSurvey();
    const { questions } = survey;

    useEffect(() => {
        survey.meta = {
            origin: window.location.hostname,
        };
    }, [survey]);
    
    return <>
        <Button onClick={() => questions.previous()}>
            Previous Question
        </Button>

        <Button onClick={() => questions.next()}>
            Next Question
        </Button>

        <Button onClick={() => survey.complete()}>
            Complete Survey
        </Button>
    </>;
}
```

#### Questions
Now that we have outlined the survey scaffolding and lifecycle we can talk about the Questions.

All questions provided to the survey instance will be extensions of the `Question` class. Each question is 
required to have a unique `id` property. Optionally, each question can specify which question is next with the 
`to` property. *If the `to` property is not specified for a question, there is a method on the `Survey` instance that
let's you dynamically choose which question to navigate to. We will cover this in the next section.*

```ts
export class ThirdQuestion extends Question {
    id = 'third';
}

export class SecondQuestion extends Question {
    id = 'second';
}

export class FirstQuestion extends Question {
    id = 'first';
    to = SecondQuestion;
}
```

Then you create an instance of each question to be used by your app through the `QuestionProvider` component.

```tsx
function First() {
    return (
        <QuestionProvider question={FirstQuestion}>
            {(question: FirstQuestion) => question.active && (
                <div className="Question First">
                    {/* use `question` here */}
                </div>
            )}
        </QuestionProvider>
    )
}
```

#### Answers

All `Question`s are required to have an `answer` getter which are collected by the survey each save.
Any data that you wish to save as an answer to the question should be provided in that getter:

```ts
export class SecondQuestion extends Question {
    id = 'second';

    @observable
    value: string = '';

    @action
    setValue(value: string) {
        this.value = value;
    }

    get answer() {
        return {
            value: this.value,
        }
    }
}

export class FirstQuestion extends Question {
    id = 'first';
    to = SecondQuestion;

    @observable
    values: number[] = [];

    @action
    add(value: number) {
        this.values.push(value);
    }

    @action
    remove(value: number) {
        this.values.splice(this.values.indexOf(value), 1);
    }

    get answer() {
        return {
            values: this.values,
        }
    }

}
```

##### SingleValueQuestion<T> & MultiValueQuestion<T>

The above patterns are used so often, Nudge has those question types built-in! 

```ts
export class SecondQuestion extends MultiValueQuestion<number> {
    id = 'second';
}

export class FirstQuestion extends SingleValueQuestion<string> {
    id = 'first';
    to = SecondQuestion;
}
```

### Question Graph

Sometimes the path forward through a survey is determined by how a question has been answered. This is why we don't 
declare the questions in a linear array and use a graph with nodes that we can jump to and around. 

In order to call upon other questions, we just have to query for a reference to an instance of a `Question` class.

```tsx
function Second() {
    const { questions } = useNudgeSurvey();

    const first = questions.use(FirstQuestion);
    const third = questions.use(ThirdQuestion);
    
    return (
        <QuestionProvider question={SecondQuestion}>
            {(second: SecondQuestion) => second.active && (
                <div className="Question Second">
                    <p>You answered {first.value} for the first question</p>

                    <input value={second.value} onChange={e => second.setValue(e.target.value)} />

                    <Button onClick={() => questions.to(third)}>
                        To Question #3
                    </Button>
                </div>
            )}
        </QuestionProvider>
    );
}
```

### Complete Example
```tsx
import { 
    Nudge,
    MultiValueQuestion,
    SingleValueQuestion,
    Question,
    useNudgeSurvey,
} from '@strategies/nudge';


export class ThirdQuestion extends Question {
    id = 'third';

    constructor() {
        super();
        makeObservable(this);
    }

    person: new SingleValueQuestion<string>();

    @observable
    robots: string[] = [];

    @action
    addRobot(robot: string) {
        this.robots.push(robot);
    }

    get answer() {
        return {
            people: this.person.value,
            robots: this.robots,
        };
    }

}

export class SecondQuestion extends MultiValueQuestion<number> {
    id = 'second';
}

export class FirstQuestion extends SingleValueQuestion<string> {
    id = 'first';
    to = SecondQuestion;
}


function First() {
    return (
        <QuestionProvider question={FirstQuestion}>
            {(question: FirstQuestion) => question.active && <Observer>{() => (
                <div className="Question First">
                    {[...'x'.repeat(5)].map((_, i: number) => (
                        <Button onClick={() => question.values.includes(i+1) ? question.remove(i+1) : question.add(i+1)}>
                            {i+1}
                        </Button>
                    ))}
                </div>
            )}</Observer>}
        </QuestionProvider>
    )
}

const Second = observer(function Second() {
    const { questions } = useNudgeSurvey();

    const first = questions.use(FirstQuestion);
    const third = questions.use(ThirdQuestion);
    
    return (
        <QuestionProvider question={SecondQuestion}>
            {(second: SecondQuestion) => second.active && <Observer>{() => (
                <div className="Question Second">
                    <p>You answered {first.value} for the first question</p>

                    <input value={second.value} onChange={e => second.setValue(e.target.value)} />

                    <Button onClick={() => questions.to(third)}>
                        To Question #3
                    </Button>
                </div>
            )}</Observer>}
        </QuestionProvider>
    );
});

function Third() {
    const [robot, setRobot] = useState<string>('');

    return (
        <QuestionProvider question={ThirdQuestion}>
            {(question: ThirdQuestion) => question.active && <Observer>{() => (
                <div className="Question Third">
                    <input value={question.person.value} onChange={e => question.person.setValue(e.target.value)} />

                    <input value={robot} onChange={e => setRobot(e.target.value)} />

                    <Button onClick={() => { question.addRobot(robot); setRobot('') }}>
                        Add Robot
                    </Button>

                    <ul>
                        {question.robots.map((robot: string) => <li key={robot}>{robot}</li>)}
                    </ul>
                </div>
            )}</Observer>}
        </QuestionProvider>
    )
}


function Survey() {
    const survey = useNudgeSurvey();
    const { questions } = survey;

    useEffect(() => {
        survey.meta = {
            origin: window.location.hostname,
        };
    }, [survey]);
    
    return (
        <div className="Survey">
            {survey.complete ? (
                <p>Thank you!</p>
            ) : <>
                <First />
                <Second />
                <Third />
            </>}

            <div className="buttons">
                <Button onClick={() => questions.previous()}>
                    Previous Question
                </Button>

                <Button onClick={() => questions.next()}>
                    Next Question
                </Button>

                <Button onClick={() => survey.complete()}>
                    Complete Survey
                </Button>
            </div>
        </div>
    </>;
}

export default function App() {
    return (
        <Nudge config={config}>
            <Survey />
        </Nudge>
    );
}
```