Skip to content

Commit

Permalink
Allow numerical input using a slider (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishnuravi authored Feb 25, 2024
1 parent 0b8fa83 commit a5a7d65
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 14 deletions.
86 changes: 72 additions & 14 deletions src/components/Question/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IExtentionType, IItemProperty, IQuestionnaireItemType } from '../../typ

import { updateItemAction } from '../../store/treeStore/treeActions';
import { isRecipientList } from '../../helpers/QuestionHelper';
import { createMarkdownExtension, removeItemExtension } from '../../helpers/extensionHelper';
import { createMarkdownExtension, removeItemExtension, setItemExtension, hasExtension } from '../../helpers/extensionHelper';
import { isItemControlInline, isItemControlReceiverComponent, isItemControlHighlight } from '../../helpers/itemControl';

import Accordion from '../Accordion/Accordion';
Expand All @@ -39,6 +39,7 @@ import {
canTypeHaveSublabel,
getItemDisplayType,
} from '../../helpers/questionTypeFeatures';
import SliderSettings from './SliderSettings/SliderSettings';

interface QuestionProps {
item: QuestionnaireItem;
Expand Down Expand Up @@ -98,6 +99,7 @@ const Question = (props: QuestionProps): JSX.Element => {
const isDecimal = props.item.type === IQuestionnaireItemType.decimal;
const isQuantity = props.item.type === IQuestionnaireItemType.quantity;
const isDecimalOrQuantity = isDecimal || isQuantity;
const isSlider = hasExtension(props.item, IExtentionType.itemControl);

// Adds instructions for the user
const instructionType = (): JSX.Element => {
Expand Down Expand Up @@ -193,19 +195,36 @@ const Question = (props: QuestionProps): JSX.Element => {
</FormField>
)}
{(isNumber) && (
<FormField>
<SwitchBtn label={t('Allow decimals')} value={isDecimalOrQuantity} onChange={() => {
const newItemType = isDecimal || isQuantity
? IQuestionnaireItemType.integer
: IQuestionnaireItemType.decimal;
dispatchUpdateItem(IItemProperty.type, newItemType)
<>
<FormField>
<SwitchBtn label={t('Allow decimals')} value={isDecimalOrQuantity} onChange={() => {
const newItemType = isDecimal || isQuantity
? IQuestionnaireItemType.integer
: IQuestionnaireItemType.decimal;
dispatchUpdateItem(IItemProperty.type, newItemType)

// remove max decimal places extension if toggling off
if (newItemType === IQuestionnaireItemType.integer) {
removeItemExtension(props.item, IExtentionType.maxDecimalPlaces, props.dispatch);
}
}} />
</FormField>
// remove slider-related extensions if toggling decimals on, as sliders currently only support integers
if (newItemType === IQuestionnaireItemType.decimal) {
const extensionsToRemove = [
IExtentionType.itemControl,
IExtentionType.questionnaireSliderStepValue,
IExtentionType.minValue,
IExtentionType.maxValue
]
removeItemExtension(
props.item,
extensionsToRemove,
props.dispatch
);
}

// remove max decimal places extension if toggling decimals off
if (newItemType === IQuestionnaireItemType.integer) {
removeItemExtension(props.item, IExtentionType.maxDecimalPlaces, props.dispatch);
}
}} />
</FormField>
</>
)}
{(isDecimalOrQuantity) && (
<FormField>
Expand Down Expand Up @@ -236,6 +255,45 @@ const Question = (props: QuestionProps): JSX.Element => {
/>
)}
</FormField>
<br />
{!isDecimalOrQuantity &&
<FormField>
<SwitchBtn
label={t('Display as a slider')}
value={isSlider}
onChange={(() => {
const newExtension = {
url: IExtentionType.itemControl,
valueCodeableConcept: {
coding: [
{
system: "http://hl7.org/fhir/questionnaire-item-control",
code: "slider",
display: "Slider"
}
]
}
};
if (!isSlider) {
setItemExtension(props.item, newExtension, props.dispatch);
} else {
const extensionsToRemove = [
IExtentionType.itemControl,
IExtentionType.questionnaireSliderStepValue,
IExtentionType.minValue,
IExtentionType.maxValue
]
removeItemExtension(
props.item,
extensionsToRemove,
props.dispatch
);
}
})}
/>
</FormField>
}
{isSlider && <SliderSettings item={props.item} /> }
{/* Sublabel is not currently supported
{canTypeHaveSublabel(props.item) && (
<FormField label={t('Sublabel')} isOptional>
Expand All @@ -258,7 +316,7 @@ const Question = (props: QuestionProps): JSX.Element => {
{respondType()}
</div>
<div className="question-addons">
{canTypeBeValidated(props.item) && (
{canTypeBeValidated(props.item) && !isSlider && (
<Accordion title={t('Add validation')}>
<ValidationAnswerTypes item={props.item} />
</Accordion>
Expand Down
141 changes: 141 additions & 0 deletions src/components/Question/SliderSettings/SliderSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TreeContext } from '../../../store/treeStore/treeStore';
import { removeItemExtension, setItemExtension } from '../../../helpers/extensionHelper';
import FormField from '../../FormField/FormField';
import { QuestionnaireItem } from '../../../types/fhir';
import { IExtentionType } from '../../../types/IQuestionnareItemType';

interface SliderSettingsProp {
item: QuestionnaireItem;
}

const SliderSettings = ({ item }: SliderSettingsProp): JSX.Element => {
const { t } = useTranslation();
const { dispatch } = useContext(TreeContext);
const [minValue, setMinValue] = useState(item.extension?.find((x) => x.url === IExtentionType.minValue)?.valueInteger);
const [maxValue, setMaxValue] = useState(item.extension?.find((x) => x.url === IExtentionType.maxValue)?.valueInteger);
const [stepValue, setStepValue] = useState(item.extension?.find((x) => x.url === IExtentionType.questionnaireSliderStepValue)?.valueInteger);
const [errors, setErrors] = useState({ minValue: false, maxValue: false, stepValue: false });
const [errorMessage, setErrorMessage] = useState('');

const validate = () => {
let newErrors = { minValue: false, maxValue: false, stepValue: false };
let isValid = true;

let errorMessage = '';

if (minValue == null || maxValue == null) {
newErrors.minValue = newErrors.maxValue = true;
errorMessage += t('Both min and max values must be filled in.');
isValid = false;
} else if (maxValue <= minValue) {
newErrors.minValue = newErrors.maxValue = true;
errorMessage += t('Max value must be greater than min value.');
isValid = false;
} else if (stepValue == null || stepValue <= 0 || stepValue > (maxValue - minValue)) {
newErrors.stepValue = true;
errorMessage += t('Step value must be a positive number and less than the difference between max and min values.');
isValid = false;
} else if ((maxValue - minValue) % stepValue !== 0) {
newErrors.stepValue = true;
errorMessage += t('It must be possible to reach the max value from the min value using the step value.');
isValid = false;
}

setErrors(newErrors);
if (isValid) {
setErrorMessage('');
} else {
setErrorMessage(errorMessage);
}
return isValid;
};

const handleMinValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) {
setMinValue(undefined);
removeItemExtension(item, 'minValue', dispatch);
} else {
const value = parseInt(event.target.value);
setMinValue(value);
const extension = {
url: IExtentionType.minValue,
valueInteger: value,
};
setItemExtension(item, extension, dispatch);
}
};


const handleMaxValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) {
setMaxValue(undefined);
removeItemExtension(item, 'maxValue', dispatch);
} else {
const value = parseInt(event.target.value);
setMaxValue(value);
const extension = {
url: IExtentionType.maxValue,
valueInteger: value,
};
setItemExtension(item, extension, dispatch);
}
};

const handleStepValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) {
setStepValue(undefined);
removeItemExtension(item, 'questionnaireSliderStepValue', dispatch);
} else {
const value = parseInt(event.target.value);
setStepValue(value);
const extension = {
url: IExtentionType.questionnaireSliderStepValue,
valueInteger: value,
};
setItemExtension(item, extension, dispatch);
}
};

const handleBlur = () => {
validate();
};

return (
<>
<div className="horizontal equal">
<FormField label={t('Slider min value')}>
<input
type="number"
value={minValue ?? ''}
onBlur={handleBlur}
onChange={handleMinValueChange}
style={{ borderColor: errors.minValue ? 'red' : undefined }}
></input>
</FormField>
<FormField label={t('Slider max value')}>
<input
type="number"
value={maxValue ?? ''}
onBlur={handleBlur}
onChange={handleMaxValueChange}
style={{ borderColor: errors.maxValue ? 'red' : undefined }}
></input>
</FormField>
<FormField label={t('Slider step value')}>
<input
type="number"
value={stepValue ?? ''}
onBlur={handleBlur}
onChange={handleStepValueChange}
style={{ borderColor: errors.stepValue ? 'red' : undefined }}
></input>
</FormField>
</div>
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
</>
);
}

export default SliderSettings;
5 changes: 5 additions & 0 deletions src/helpers/itemControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum ItemControlType {
year = 'year',
receiverComponent = 'receiver-component',
dynamic = 'dynamic',
slider = 'slider'
}

export const createItemControlExtension = (itemControlType: ItemControlType): Extension => {
Expand Down Expand Up @@ -87,6 +88,10 @@ export const isItemControlInline = (item?: QuestionnaireItem): boolean => {
return item?.type === IQuestionnaireItemType.text && getItemControlType(item) === ItemControlType.inline;
};

export const isItemControlSlider = (item?: QuestionnaireItem): boolean => {
return getItemControlType(item) === ItemControlType.slider;
};

export const getHelpText = (item: QuestionnaireItem): string => {
if (!isItemControlHelp(item)) {
return '';
Expand Down
1 change: 1 addition & 0 deletions src/types/IQuestionnareItemType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export enum IExtentionType {
optionReference = 'http://ehelse.no/fhir/StructureDefinition/sdf-optionReference',
presentationbuttons = 'http://helsenorge.no/fhir/StructureDefinition/sdf-presentationbuttons',
questionnaireUnit = 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit',
questionnaireSliderStepValue = 'http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue',
regEx = 'http://hl7.org/fhir/StructureDefinition/regex',
repeatstext = 'http://ehelse.no/fhir/StructureDefinition/repeatstext',
maxOccurs = 'http://hl7.org/fhir/StructureDefinition/questionnaire-maxOccurs',
Expand Down

0 comments on commit a5a7d65

Please sign in to comment.