npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@strategies/nudge

v1.4.0

Published

Nudge survey suite

Downloads

5

Readme

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