@strategies/nudge
TypeScript icon, indicating that this package has built-in type declarations

1.4.0 • Public • Published

Nudge

Installation

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.

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.

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.

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.

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

Answers

All Questions 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:

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 & MultiValueQuestion

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

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.

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

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>
    );
}

Readme

Keywords

none

Package Sidebar

Install

npm i @strategies/nudge

Weekly Downloads

0

Version

1.4.0

License

MIT

Unpacked Size

4.02 MB

Total Files

16

Last publish

Collaborators

  • ncaler
  • scottdpenman
  • tadiraman
  • sasaki-dev
  • arminakvn
  • eyoungberg
  • sasaki-strategies