Skip to content

Commit

Permalink
Merge pull request #14 from EAVFW/origin/kba/input-properties
Browse files Browse the repository at this point in the history
Origin/kba/input properties
  • Loading branch information
KasperBaun authored Apr 12, 2024
2 parents f99eb99 + 4c8448d commit 745ed72
Show file tree
Hide file tree
Showing 17 changed files with 136 additions and 162 deletions.
91 changes: 46 additions & 45 deletions packages/core/src/components/error-popup/ErrorPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,73 @@
"use client";
import React, { useState, useEffect } from 'react';
import { useDelayedClickListener } from "../../hooks";
import { useQuickForm } from "../../state/QuickFormContext";
import { quickformtokens } from '../../style/quickFormTokensDefinition';

type ErrorPopupProps = {
readonly message: string;
};

export const ErrorPopup: React.FC<ErrorPopupProps> = ({ message }: ErrorPopupProps) => {
export const ErrorPopup: React.FC<ErrorPopupProps> = ({ message }) => {
const [isVisible, setIsVisible] = useState(false);
const { dispatch, state } = useQuickForm();
const [opacity, setOpacity] = useState(0);
const { dispatch } = useQuickForm();

/**
* DISCUSS - What cases is there for resetting error and can it be handled in reducer all alone?.
* When an error is shown, upon next answer it can be cleared.
* Possible a dissmis button - but i dont think it should automatically just remove when clicked.
*/


//const resetErrorMessage = () => {
// if (state.errorMsg !== "") {
// dispatch({ type: "SET_ERROR_MSG", msg: "" })
// }
//}

// useDelayedClickListener(resetErrorMessage);
const resetErrorPopup = () => {
dispatch({ type: 'SET_ERROR_MSG', msg: "" });
setIsVisible(false);
setOpacity(0);
}

useEffect(() => {
let timer: NodeJS.Timeout;
if (message) {
setIsVisible(true);
setTimeout(() => setIsVisible(false), 350);
setOpacity(0);
setTimeout(() => setOpacity(1), 10);

timer = setTimeout(() => {
resetErrorPopup();
}, 3000);
} else {
resetErrorPopup();
}

return () => {
clearTimeout(timer);
setOpacity(0);
};
}, [message]);

if (message === "") {
return <></>;
}
if (!isVisible) return null;

const errorStyle: React.CSSProperties = {
alignItems: 'flex-end',
animation: isVisible ? 'slide-up 0.35s linear 1 forwards' : '',
backgroundColor: quickformtokens.error,
borderRadius: '3px',
color: quickformtokens.onError,
display: 'flex',
fontSize: '1.5rem',
marginTop: '15px',
padding: '8px 12px',
width: 'max-content',
};

const mobileErrorStyle: React.CSSProperties = {
...errorStyle,
fontSize: '1.75rem',
marginTop: '22px',
const backdropStyle: React.CSSProperties = {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
};

const imgStyle: React.CSSProperties = {
marginRight: '4px',
const toastStyle: React.CSSProperties = {
padding: '20px',
borderRadius: '10px',
backgroundColor: quickformtokens.error,
color: quickformtokens.onError,
fontSize: '16px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
opacity: opacity,
transition: 'opacity 0.5s ease-in',
};

return (
<div style={window.innerWidth <= 599 ? mobileErrorStyle : errorStyle}>
{/* If there's an image you want to include inside the error message */}
{/* <img src="path_to_your_image" alt="Error" style={imgStyle} /> */}
{message}
<div style={backdropStyle} onClick={resetErrorPopup}>
<div style={toastStyle} onClick={(e) => e.stopPropagation()}>
{message}
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion packages/core/src/components/question/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const Question: React.FC<QuestionProps> = ({ model, style }) => {
questionModel={model}
{...model.inputProperties ?? {}}
/>
{model.validationResult?.message !== "" && <ErrorMessage message={model.validationResult?.message} />}
{typeof (model.validationResult?.message) !== "undefined" && model.validationResult?.message !== "" && <ErrorMessage message={model.validationResult?.message} />}
</div>
);
}
6 changes: 3 additions & 3 deletions packages/core/src/hooks/useFocusableQuestion.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from "react";
import { useEffect, useRef } from "react";
import { useQuickForm } from "../state";


Expand All @@ -10,8 +10,8 @@ export const useFocusableQuestion = <T extends HTMLElement>(questionkey: string,
if (ref.current && isFirstQuestionInCurrentSlide(questionkey)) {
ref.current.focus(options);
}
}, [ref,questionkey]);
}, [ref, questionkey]);


return ref;
}
36 changes: 0 additions & 36 deletions packages/core/src/model/InputType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,23 @@ export type InputProps<TProps = InputPropertiesTypes> = {

const Email = "email";
const Multilinetext = "multilinetext";
const Radio = "radio";
const Slider = "slider";
const Text = "text";
const Buttons = "buttons";
const Phone = "phone";

export interface InputTypeMap {
[Buttons]: ButtonsProperties;
[Phone]: PhoneProperties;
[Email]: EmailProperties;
[Multilinetext]: MultilineProperties;
[Radio]: RadioProperties;
[Slider]: SliderProperties;
[Text]: TextProperties;
}

export type InputPropertiesTypes =
ButtonsProperties |
EmailProperties |
MultilineProperties |
RadioProperties |
SliderProperties |
TextProperties |
PhoneProperties |
{};

export type ButtonsProperties = {
inputType: typeof Buttons;
options: {
key: string | undefined;
label: string;
}
defaultValue?: string;
}


export type PhoneProperties = {
inputType: typeof Phone;
defaultValue?: number;
Expand All @@ -60,23 +41,6 @@ export type MultilineProperties = {
defaultValue?: string;
}

export type RadioProperties = {
inputType: typeof Radio;
options: {
[key: string]: string;
}
defaultValue?: boolean;
direction?: "horizontal" | "vertical";
}

export type SliderProperties = {
inputType: typeof Slider;
min: number;
max: number;
step: number;
defaultValue?: number;
}

export type TextProperties = {
inputType: typeof Text;
defaultValue?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/model/QuickFormModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EndingModel, IntroModel, QuestionModel, SlideModel, SubmitModel } from "./index";
import { EndingModel, IntroModel, SlideModel, SubmitModel } from "./index";

export type QuickFormModel = {
intro?: IntroModel;
Expand Down
17 changes: 6 additions & 11 deletions packages/core/src/model/json-definitions/JsonDataModels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ButtonsProperties, EmailProperties, MultilineProperties, RadioProperties, SliderProperties, TextProperties } from "../InputType";
import { EmailProperties, MultilineProperties, TextProperties } from "../InputType";

type QuickFormQuestionDefinition = {

Expand Down Expand Up @@ -49,7 +49,7 @@ type QuickFormQuestionDefinition = {

/**
* All questions support conditional rendering, allowing one to specify a rule and a engine to execute it.
* TODO: the rule should be of type any, because its the engine (type) that knows its data type.
* The rule is of type any, because its the engine (type) that knows its data type.
*/
visible?: {
engine: string;
Expand All @@ -68,12 +68,7 @@ type QuickFormQuestionDefinition = {
*
* TODO - need to be able to extend this in a meaning full way.
*/
export type QuestionJsonModel =
QuickFormQuestionDefinition |
QuickFormQuestionDefinition & ButtonsProperties |
// QuickFormQuestionDefinition & DropDownProperties |
QuickFormQuestionDefinition & EmailProperties |
QuickFormQuestionDefinition & MultilineProperties |
QuickFormQuestionDefinition & RadioProperties |
QuickFormQuestionDefinition & SliderProperties |
QuickFormQuestionDefinition & TextProperties;
export type QuestionJsonModel = QuickFormQuestionDefinition
| QuickFormQuestionDefinition & EmailProperties
| QuickFormQuestionDefinition & MultilineProperties
| QuickFormQuestionDefinition & TextProperties;
3 changes: 2 additions & 1 deletion packages/core/src/services/QuickFormServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { InputPropertiesTypes, QuestionModel, QuickFormModel } from "../model";
import { QuickFormDefinition } from "../model";
import { QuestionJsonModel } from "../model/json-definitions/JsonDataModels";
import { InputComponentType } from "./defaults/DefaultInputTypeResolver";
import { QuickformState } from "../state";

export type HeadingNumberDisplayProvider = () => boolean;
export type QuickFormModelTransformer = (data: QuickFormDefinition, payload: any) => QuickFormModel;
export type QuestionTransformer = (key: string, question: QuestionJsonModel, value?: any, visible?: { type: string; rule: string; }) => QuestionModel;
export type InputTypePropertiesTransformer = (questionJsonModel: QuestionJsonModel) => InputPropertiesTypes | undefined;
export type RegisterInputTypeComponent = (key: string, component: InputComponentType) => void;
export type InputValidator = <TProps extends InputPropertiesTypes>(questionModel: QuestionModel<TProps>) => Promise<ValidationResult>;
export type InputValidator = <TProps extends InputPropertiesTypes>(questionModel: QuestionModel<TProps>, state: QuickformState) => Promise<ValidationResult>;
export interface IQuickFormLogger {
log(body: string, ...args: any[]): void;
warn(body: string, ...args: any[]): void;
Expand Down
23 changes: 2 additions & 21 deletions packages/core/src/services/defaults/DefaultInputTypeResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FC } from "react";
import { RadioProperties, SliderProperties, ButtonsProperties, InputPropertiesTypes, InputProps } from "../../model";
import { InputPropertiesTypes, InputProps } from "../../model";
import { QuestionJsonModel } from "../../model/json-definitions/JsonDataModels";
import { registerQuickFormService } from "../QuickFormServices";

Expand All @@ -19,23 +19,7 @@ const parseInputProperties = (questionJsonModel: QuestionJsonModel): InputProper
.map(([key, schema]) => [key, questionJsonModel[key as keyof QuestionJsonModel] ?? getDefaultValue(schema)])) as InputPropertiesTypes;
}

const inputTypePropertiesMap: { [key: string]: () => InputPropertiesTypes } = {
buttons: () => ({
inputType,
options: (questionJsonModel as QuestionJsonModel & ButtonsProperties).options
}),
radio: () => ({
inputType,
options: (questionJsonModel as QuestionJsonModel & RadioProperties).options,
direction: (questionJsonModel as QuestionJsonModel & RadioProperties).direction
}),
slider: () => ({
inputType,
min: (questionJsonModel as QuestionJsonModel & SliderProperties).min,
max: (questionJsonModel as QuestionJsonModel & SliderProperties).max,
step: (questionJsonModel as QuestionJsonModel & SliderProperties).step
}),
};
const inputTypePropertiesMap: { [key: string]: () => InputPropertiesTypes } = {};

return inputType in inputTypePropertiesMap ? inputTypePropertiesMap[inputType]() : {};
};
Expand Down Expand Up @@ -85,9 +69,6 @@ const ThrowIfUsed: InputComponentType = (props) => { throw new Error("Not regist
const inputComponents: InputComponentDictionary = {
text: ThrowIfUsed,
none: ThrowIfUsed,
dropdown: ThrowIfUsed,
slider: ThrowIfUsed,
toggle: ThrowIfUsed,
multilinetext: ThrowIfUsed
};

Expand Down
31 changes: 11 additions & 20 deletions packages/core/src/services/defaults/DefaultInputValidator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ValidationResult } from "../../model/ValidationResult";
import { InputPropertiesTypes, QuestionModel, SliderProperties } from "../../model";
import { InputPropertiesTypes, QuestionModel } from "../../model";
import { registerQuickFormService } from "../QuickFormServices";
import { QuickformState } from "../../state";

const validateText = (output: any): ValidationResult => {
const text = typeof output === 'string' ? output.trim() : '';
Expand Down Expand Up @@ -36,8 +37,8 @@ const validateEmail = (output: any): ValidationResult => {
};

const validatePhone = async (output: any): Promise<ValidationResult> => {
// Wait for 4 seconds
await new Promise(resolve => setTimeout(resolve, 4000));
// Wait for 2 seconds to demo
await new Promise(resolve => setTimeout(resolve, 2000));

const phoneRegex = /^[0-9]{8,}$/;
const valid = typeof output === 'string' && phoneRegex.test(output);
Expand All @@ -49,29 +50,18 @@ const validatePhone = async (output: any): Promise<ValidationResult> => {
};
};


const validateSlider = (output: any, properties: SliderProperties): ValidationResult => {
const valid = typeof output === 'number' && output >= properties.min && output <= properties.max;
return {
isValid: valid,
message: valid ? "" : `Value must be a number between ${properties.min} and ${properties.max}.`,
validatedOutput: output,
};
};

type ValidatorMap = {
[inputType: string]: (output: any, properties?: any) => Promise<ValidationResult>;
[inputType: string]: ValidatorFunction<any, any, QuestionModel<any>, QuickformState>;
};

const validatorMap: ValidatorMap = {
email: (output: any) => Promise.resolve(validateEmail(output)),
phone: (output: any) => Promise.resolve(validatePhone(output)),
slider: (output: any, properties: SliderProperties) => Promise.resolve(validateSlider(output, properties)),
text: (output: any) => Promise.resolve(validateText(output)),
multilinetext: (output: any) => Promise.resolve(validateMultilineText(output))
};

const validateQuestionOutput = async <TProps extends InputPropertiesTypes>(questionModel: QuestionModel<TProps>): Promise<ValidationResult> => {
const validateQuestionOutput = async <TProps extends InputPropertiesTypes>(questionModel: QuestionModel<TProps>, state: QuickformState): Promise<ValidationResult> => {
const validator = validatorMap[questionModel.inputType];
if (!validator) {
// This is to support if no validation is created for inputtype.. defaults to validated..
Expand All @@ -82,14 +72,15 @@ const validateQuestionOutput = async <TProps extends InputPropertiesTypes>(quest
isValidating: false,
timestamp: new Date().getTime()
});
// return Promise.resolve({ isValid: false, message: `No validator available for inputType: ${questionModel.inputType}`, validatedOutput: questionModel.output });
}

return await validator(questionModel.output, questionModel.inputProperties);
return await validator(questionModel.output, questionModel.inputProperties, questionModel,state);
};

export const registerInputTypeValidator = (key: string, validator: (output: any, properties?: any) => Promise<ValidationResult>) => {
validatorMap[key] = validator;
export type ValidatorFunction<TAnswer, TInputProps, TQuestionModel extends QuestionModel<TInputProps>, TQuickFormState extends QuickformState> = (output: TAnswer, properties: TInputProps, questionModel: TQuestionModel, state: TQuickFormState) => Promise<ValidationResult>;

export const registerInputTypeValidator = <TAnswer, TInputProps, TQuestionModel extends QuestionModel<TInputProps>, TQuickFormState extends QuickformState>(key: string, validator: ValidatorFunction<TAnswer, TInputProps, TQuestionModel, TQuickFormState>) => {
validatorMap[key] = validator as ValidatorFunction<any, any, QuestionModel<any>, QuickformState>;
};

registerQuickFormService("inputValidator", validateQuestionOutput);
Loading

0 comments on commit 745ed72

Please sign in to comment.