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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@welshare/questionnaire

v0.2.3

Published

FHIR Questionnaire components for React with state management and validation

Readme

@welshare/questionnaire

FHIR R4 Questionnaire components for React with state management, validation, and theming.

Installation

npm install @welshare/questionnaire

Quick Start

import { useState, useEffect } from "react";
import {
  QuestionnaireProvider,
  useQuestionnaire,
  QuestionRenderer,
  getVisiblePages,
  type Questionnaire,
} from "@welshare/questionnaire";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function QuestionnairePage() {
  const { questionnaire, isPageValid } = useQuestionnaire();
  const pages = getVisiblePages(questionnaire);
  const [currentPageIndex, setCurrentPageIndex] = useState(0);
  const currentPage = pages[currentPageIndex];

  return (
    <div>
      <h1>{questionnaire.title}</h1>
      {currentPage?.item?.map((item) => (
        <QuestionRenderer key={item.linkId} item={item} />
      ))}
      <button
        onClick={() => setCurrentPageIndex(currentPageIndex + 1)}
        disabled={!isPageValid(currentPage?.item || [])}
      >
        Next
      </button>
    </div>
  );
}

function App() {
  const [questionnaire, setQuestionnaire] = useState<Questionnaire | null>(
    null
  );

  useEffect(() => {
    fetch("/api/questionnaire/your-id")
      .then((res) => res.json())
      .then(setQuestionnaire);
  }, []);

  if (!questionnaire) return <div>Loading...</div>;

  return (
    <QuestionnaireProvider questionnaire={questionnaire}>
      <QuestionnairePage />
    </QuestionnaireProvider>
  );
}

API

QuestionnaireProvider

Props:

  • questionnaire: Questionnaire - FHIR Questionnaire object
  • questionnaireId?: string - Optional ID override (defaults to questionnaire.id)
  • useNestedStructure?: boolean - Nested or flat response structure (default: true)

useQuestionnaire Hook

const {
  questionnaire,
  response,
  updateAnswer,
  updateMultipleAnswers,
  getAnswer,
  getAnswers,
  isPageValid,
  getRequiredQuestions,
  getUnansweredRequiredQuestions,
  markValidationErrors,
  clearValidationErrors,
  hasValidationError,
  debugMode,
  toggleDebugMode,
} = useQuestionnaire();

QuestionRenderer

Props:

  • item: QuestionnaireItem - Questionnaire item to render
  • className?: string - Container CSS classes
  • inputClassName?: string - Input CSS classes
  • choiceClassName?: string - Choice option CSS classes
  • renderRadioInput?: (props: RadioInputProps) => ReactNode - Custom radio renderer
  • renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode - Custom checkbox renderer

Supported Types: choice, boolean, integer, decimal, string, text

BmiForm

A controlled component for collecting height, weight, and calculating BMI. The component acts as an input helper that manages BMI calculation internally and reports changes to the parent via callbacks. Unit system (metric/imperial) is managed internally and automatically clears all fields when switched.

Props:

  • height: number - Current height value (0 when empty)
  • weight: number - Current weight value (0 when empty)
  • bmi: number - Current BMI value (calculated and set by the component, 0 when not calculated)
  • onHeightChange: (value: number, unit: "cm" | "in") => void - Called when height changes, includes current unit
  • onWeightChange: (value: number, unit: "kg" | "lb") => void - Called when weight changes, includes current unit
  • onBmiChange: (value: number) => void - Called when BMI is calculated or cleared
  • className?: string - Optional CSS classes

Features:

  • Controlled numeric values (height, weight, bmi) managed by parent
  • BMI calculation handled internally by the component
  • Unit system managed internally (defaults to metric)
  • Automatically clears all fields (sets to 0) when switching unit systems
  • No unit conversion - values are cleared on unit system change
  • Uses consistent styling with questionnaire components

Example:

import { useState } from "react";
import { BmiForm, getBmiCategory } from "@welshare/questionnaire";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function MyComponent() {
  const [height, setHeight] = useState(0);
  const [weight, setWeight] = useState(0);
  const [bmi, setBmi] = useState(0);

  const handleBmiChange = (value: number) => {
    setBmi(value);

    // Optional: Get BMI category
    if (value) {
      const category = getBmiCategory(value);
      console.log(`BMI Category: ${category}`);
    }
  };

  return (
    <BmiForm
      height={height}
      weight={weight}
      bmi={bmi}
      onHeightChange={(value, unit) => setHeight(value)}
      onWeightChange={(value, unit) => setWeight(value)}
      onBmiChange={handleBmiChange}
    />
  );
}

LegalConsentForm

A self-contained form component for collecting user consent before data submission. This component handles:

  1. Terms & Conditions and Privacy Policy (required) - User must accept to proceed
  2. Study invitation notifications (optional) - User can opt in to receive study invitations

Usage Pattern: This component provides the content for a consent dialog. Applications should wrap this component in their own Dialog/Modal component (e.g., from shadcn/ui, Radix UI, MUI, etc.), similar to how BmiForm is used with input helpers.

Props:

  • initialValues?: Partial<LegalConsentResult> - Initial consent state (for restoring previous preferences)
  • documentLinks?: LegalDocumentLinks - Custom URLs for Terms & Privacy documents
  • onConfirm?: (result: LegalConsentResult) => void - Called when user confirms (only if terms accepted)
  • onCancel?: () => void - Called when user cancels/declines
  • onChange?: (result: LegalConsentResult) => void - Called on any checkbox change (real-time updates)
  • renderCheckbox?: (props: LegalCheckboxProps) => ReactNode - Custom checkbox component renderer
  • confirmButtonLabel?: string - Custom confirm button text (default: "Confirm & Continue")
  • cancelButtonLabel?: string - Custom cancel button text (default: "Cancel")
  • className?: string - Additional CSS classes

Result Object:

interface LegalConsentResult {
  termsAccepted: boolean; // Required - T&Cs and Privacy Policy
  studyConsentAccepted: boolean; // Optional - Study invitations
}

Example with Dialog:

import { useState } from "react";
import {
  LegalConsentForm,
  type LegalConsentResult,
} from "@welshare/questionnaire";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import "@welshare/questionnaire/tokens.css";
import "@welshare/questionnaire/styles.css";

function DataSubmissionFlow() {
  const [showConsent, setShowConsent] = useState(false);

  const handleSubmitData = () => {
    // Show consent dialog before submitting data
    setShowConsent(true);
  };

  const handleConsent = (result: LegalConsentResult) => {
    console.log("Terms accepted:", result.termsAccepted);
    console.log("Study consent:", result.studyConsentAccepted);

    // Proceed with data submission
    submitDataToServer(result);
    setShowConsent(false);
  };

  return (
    <>
      <button onClick={handleSubmitData}>Submit My Data</button>

      <Dialog open={showConsent} onOpenChange={setShowConsent}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Health Data Storage Consent</DialogTitle>
          </DialogHeader>
          <LegalConsentForm
            renderCheckbox={({ id, checked, onCheckedChange, className }) => (
              <Checkbox
                id={id}
                checked={checked}
                onCheckedChange={onCheckedChange}
                className={className}
              />
            )}
            onConfirm={handleConsent}
            onCancel={() => setShowConsent(false)}
          />
        </DialogContent>
      </Dialog>
    </>
  );
}

Custom Document Links:

<LegalConsentForm
  documentLinks={{
    termsUrl: "https://myapp.com/terms",
    privacyUrl: "https://myapp.com/privacy",
  }}
  onConfirm={handleConsent}
  onCancel={handleCancel}
/>

CSS Classes for Styling:

  • .wq-legal-consent-form - Main form container
  • .wq-legal-consent-section - Individual consent section
  • .wq-legal-consent-heading - Section headings
  • .wq-legal-consent-checkbox-row - Checkbox + label row
  • .wq-legal-consent-link - Links to legal documents
  • .wq-legal-consent-info-box - Information box (study consent details)
  • .wq-legal-consent-actions - Button container
  • .wq-button, .wq-button-primary, .wq-button-outline - Button styles

Utilities

Questionnaire Utilities:

  • getVisiblePages(questionnaire) - Get visible page groups
  • calculateProgress(currentIndex, total) - Calculate progress percentage
  • getAllQuestionsFromPage(pageItem) - Get all questions from a page

BMI Helper Functions:

  • calculateBmi(height, weight, unitSystem) - Calculate BMI from height and weight
  • getBmiCategory(bmi) - Get WHO BMI category (Underweight, Normal weight, Overweight, Obese)

Theming

Override CSS custom properties:

:root {
  --wq-color-primary-500: #3b82f6;
  --wq-space-lg: 1.5rem;
  --wq-font-size-base: 1rem;
}

Custom Input Renderers

<QuestionRenderer
  item={item}
  renderRadioInput={(props) => (
    <input type="radio" checked={props.checked} onChange={props.onChange} />
  )}
  renderCheckboxInput={(props) => (
    <input type="checkbox" checked={props.checked} onChange={props.onChange} />
  )}
/>

Renderer Props: linkId, valueCoding, valueInteger, checked, disabled, onChange, label, index

FHIR Extensions

Hidden Questions

{
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden",
      "valueBoolean": true
    }
  ]
}

Media Attachments (Images)

Display images or other media content with question items using the FHIR SDC (Structured Data Capture) extensions.

Item Media (images on questions):

{
  "linkId": "body-diagram",
  "text": "Where is the pain located?",
  "type": "choice",
  "extension": [
    {
      "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemMedia",
      "valueAttachment": {
        "contentType": "image/png",
        "url": "https://example.com/images/body-diagram.png",
        "title": "Body diagram"
      }
    }
  ]
}

Answer Option Media (images on answer options):

{
  "linkId": "skin-condition",
  "text": "Which image best matches your condition?",
  "type": "choice",
  "answerOption": [
    {
      "valueCoding": {
        "code": "mild",
        "display": "Mild"
      },
      "extension": [
        {
          "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
          "valueAttachment": {
            "contentType": "image/jpeg",
            "url": "https://example.com/images/mild.jpg",
            "title": "Mild condition"
          }
        }
      ]
    },
    {
      "valueCoding": {
        "code": "severe",
        "display": "Severe"
      },
      "extension": [
        {
          "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
          "valueAttachment": {
            "contentType": "image/jpeg",
            "url": "https://example.com/images/severe.jpg",
            "title": "Severe condition"
          }
        }
      ]
    }
  ]
}

CSS Classes for Styling:

  • .wq-question-media - Container for question-level images
  • .wq-question-image - Individual question image
  • .wq-choice-option-wrapper - Wrapper around answer option with image
  • .wq-choice-option-image - Individual answer option image

Example Styling:

.wq-question-image {
  max-width: 100%;
  height: auto;
  margin-bottom: 1rem;
  border-radius: 0.5rem;
}

.wq-choice-option-image {
  max-width: 200px;
  height: auto;
  margin-bottom: 0.5rem;
  border-radius: 0.25rem;
}

Slider Controls

{
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-slider-control",
      "extension": [
        { "url": "minValue", "valueInteger": 0 },
        { "url": "maxValue", "valueInteger": 100 },
        { "url": "step", "valueInteger": 1 },
        { "url": "unit", "valueString": "minutes" }
      ]
    }
  ]
}

Exclusive Options

{
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option",
      "valueString": "none-of-the-above-code"
    }
  ]
}

Input Helpers

Input helpers allow you to provide auxiliary UI (like calculators or lookup dialogs) to assist users in filling out form fields. The library detects the extension and calls your renderHelperTrigger function, but you control the dialog/modal implementation.

FHIR Extension:

{
  "linkId": "bmi",
  "text": "Body Mass Index",
  "type": "decimal",
  "extension": [
    {
      "url": "http://codes.welshare.app/StructureDefinition/questionnaire-inputHelper",
      "valueCodeableConcept": {
        "coding": [
          {
            "system": "http://codes.welshare.app/input-helper-type",
            "code": "bmi-calculator",
            "display": "BMI Calculator"
          }
        ],
        "text": "Calculate BMI from height and weight"
      }
    }
  ]
}

Implementation:

import { useState } from "react";
import {
  QuestionRenderer,
  BmiForm,
  type HelperTriggerProps,
} from "@welshare/questionnaire";

function MyQuestionnaireRenderer({ item }) {
  const [showBmiDialog, setShowBmiDialog] = useState(false);
  const [helperCallback, setHelperCallback] = useState<
    ((v: string | number) => void) | null
  >(null);

  const handleHelperTrigger = ({
    helper,
    onValueSelected,
  }: HelperTriggerProps) => {
    if (helper.type === "bmi-calculator") {
      return (
        <button
          type="button"
          onClick={() => {
            setHelperCallback(() => onValueSelected);
            setShowBmiDialog(true);
          }}
        >
          {helper.display || "Calculate"}
        </button>
      );
    }
    return null;
  };

  return (
    <>
      <QuestionRenderer item={item} renderHelperTrigger={handleHelperTrigger} />

      {showBmiDialog && (
        <Dialog onClose={() => setShowBmiDialog(false)}>
          <BmiForm
            onSubmit={({ bmi }) => {
              helperCallback?.(bmi);
              setShowBmiDialog(false);
            }}
          />
        </Dialog>
      )}
    </>
  );
}

Helper Trigger Props:

  • helper: InputHelperConfig - Configuration from the extension
    • type: string - Helper identifier (e.g., "bmi-calculator")
    • display?: string - Display name from the extension
    • description?: string - Description/tooltip text
  • linkId: string - The question's linkId
  • currentValue?: T - Current field value (if any), where T defaults to string | number
  • onValueSelected: (value: T) => void - Callback to update the field

Note: HelperTriggerProps<T> is a generic interface. For decimal questions, it's specialized as HelperTriggerProps<number>, but you can use different types for other question types (e.g., HelperTriggerProps<string> for text inputs).

Note: The library only renders the trigger element you provide. You are responsible for implementing the dialog/modal UI, as this allows you to use your preferred dialog system (e.g., shadcn/ui, Radix UI, MUI, etc.).

License

MIT © Welshare UG (haftungsbeschränkt)