Add Input Types: Checkbox, Email, Number, Password, Phone, Radio, Search, Url | Add validations: accepted, email, url

This commit is contained in:
Matthias Nannt
2022-11-23 13:59:31 +01:00
parent 2a23326dad
commit 493bc3c3af
30 changed files with 356 additions and 80 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@formbricks/react": minor
---
Add Input Types: Checkbox, Email, Number, Password, Phone, Radio, Search, Url | Add validations: accepted, email, url
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Text, Textarea } from "..";
import { Form } from "./Form"; import { Form } from "./Form";
import { Text, Textarea } from "./Inputs";
interface OnSubmitProps { interface OnSubmitProps {
data: any; data: any;
-6
View File
@@ -1,6 +0,0 @@
export * from "./inputs/Button";
export * from "./inputs/Checkbox";
export * from "./inputs/Radio";
export * from "./inputs/Submit";
export * from "./inputs/Text";
export * from "./inputs/Textarea";
@@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import { useMemo } from "react";
import { getElementId } from "../../lib/element"; import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema"; import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types"; import { SVGComponent, UniversalInputProps } from "../../types";
@@ -1,10 +1,10 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useMemo } from "react"; import { useMemo } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element"; import { getElementId } from "../../lib/element";
import { normalizeOptions } from "../../lib/options"; import { normalizeOptions } from "../../lib/options";
import { useEffectUpdateSchema } from "../../lib/schema"; import { useEffectUpdateSchema } from "../../lib/schema";
import { getValidationRules } from "../../lib/validation"; import { getValidationRules, validate } from "../../lib/validation";
import { NameRequired, OptionsArray, OptionsObjectArray, UniversalInputProps } from "../../types"; import { NameRequired, OptionsArray, OptionsObjectArray, UniversalInputProps } from "../../types";
import { Fieldset } from "../shared/Fieldset"; import { Fieldset } from "../shared/Fieldset";
import { Help } from "../shared/Help"; import { Help } from "../shared/Help";
@@ -48,6 +48,7 @@ export function Checkbox(props: FormbricksProps) {
id={elemId} id={elemId}
{...register(props.name, { {...register(props.name, {
required: { value: "required" in validationRules, message: "This field is required" }, required: { value: "required" in validationRules, message: "This field is required" },
validate: validate(validationRules),
})} })}
/> />
<Label label={props.label} elemId={elemId} /> <Label label={props.label} elemId={elemId} />
@@ -66,7 +67,7 @@ export function Checkbox(props: FormbricksProps) {
<Help help={props.help} elemId={elemId} /> <Help help={props.help} elemId={elemId} />
<Options optionsClassName={props.optionsClassName}> <Options optionsClassName={props.optionsClassName}>
{options.map((option) => ( {options.map((option) => (
<Option optionClassName={props.optionClassName}> <Option key={`${props.name}-${option.value}`} optionClassName={props.optionClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}> <Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}> <Inner innerClassName={props.innerClassName}>
<input <input
@@ -0,0 +1,23 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface EmailUniqueProps {
placeholder?: string;
}
type Props = EmailUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "email";
export function Email(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: inputType, formbricks: inputType }}
additionalProps={{ placeholder: props.placeholder }}
{...props}
/>
);
}
@@ -0,0 +1,37 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface InputUniqueProps {
min?: number;
max?: number;
step?: number;
}
type Props = InputUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "number";
export function Number(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: inputType, formbricks: inputType }}
additionalValidation={{
min: {
value: props.min,
message: `The minimum number allowed is ${props.min}`,
},
max: {
value: props.max,
message: `The minimum number allowed is ${props.max}`,
},
}}
additionalProps={{
step: props.step,
}}
{...props}
/>
);
}
@@ -0,0 +1,35 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface PasswordUniqueProps {
minLength?: number;
maxLength?: number;
placeholder?: string;
}
type Props = PasswordUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "password";
export function Password(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: inputType, formbricks: inputType }}
additionalValidation={{
minLength: {
value: props.minLength || 0,
message: `Your answer must be at least ${props.minLength} characters long`,
},
maxLength: {
value: props.maxLength || 524288,
message: `Your answer musn't be longer than ${props.maxLength} characters`,
},
}}
additionalProps={{ placeholder: props.placeholder }}
{...props}
/>
);
}
@@ -0,0 +1,36 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface PhoneUniqueProps {
minLength?: number;
maxLength?: number;
placeholder?: string;
}
type Props = PhoneUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "phone";
const htmlType = "tel";
export function Phone(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: htmlType, formbricks: inputType }}
additionalValidation={{
minLength: {
value: props.minLength || 0,
message: `Your answer must be at least ${props.minLength} characters long`,
},
maxLength: {
value: props.maxLength || 524288,
message: `Your answer musn't be longer than ${props.maxLength} characters`,
},
}}
additionalProps={{ placeholder: props.placeholder }}
{...props}
/>
);
}
@@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useMemo } from "react"; import { useMemo } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element"; import { getElementId } from "../../lib/element";
import { normalizeOptions } from "../../lib/options"; import { normalizeOptions } from "../../lib/options";
@@ -66,7 +66,7 @@ export function Radio(props: FormbricksProps) {
<Help help={props.help} elemId={elemId} /> <Help help={props.help} elemId={elemId} />
<Options optionsClassName={props.optionsClassName}> <Options optionsClassName={props.optionsClassName}>
{options.map((option) => ( {options.map((option) => (
<Option optionClassName={props.optionClassName}> <Option key={`${props.name}-${option.value}`} optionClassName={props.optionClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}> <Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}> <Inner innerClassName={props.innerClassName}>
<input <input
@@ -0,0 +1,35 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface SearchUniqueProps {
minLength?: number;
maxLength?: number;
placeholder?: string;
}
type Props = SearchUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "search";
export function Search(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: inputType, formbricks: inputType }}
additionalValidation={{
minLength: {
value: props.minLength || 0,
message: `Your answer must be at least ${props.minLength} characters long`,
},
maxLength: {
value: props.maxLength || 524288,
message: `Your answer musn't be longer than ${props.maxLength} characters`,
},
}}
additionalProps={{ placeholder: props.placeholder }}
{...props}
/>
);
}
@@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import { useMemo } from "react";
import { getElementId } from "../../lib/element"; import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema"; import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types"; import { SVGComponent, UniversalInputProps } from "../../types";
+20 -50
View File
@@ -1,65 +1,35 @@
import clsx from "clsx";
import React, { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema"; import { useEffectUpdateSchema } from "../../lib/schema";
import { getValidationRules, validate } from "../../lib/validation";
import { NameRequired, UniversalInputProps } from "../../types"; import { NameRequired, UniversalInputProps } from "../../types";
import { Help } from "../shared/Help"; import { Input, InputProps } from "../shared/Input";
import { Inner } from "../shared/Inner";
import { Label } from "../shared/Label";
import { Messages } from "../shared/Messages";
import { Outer } from "../shared/Outer";
import { Wrapper } from "../shared/Wrapper";
interface TextInputUniqueProps { interface TextUniqueProps {
maxLength?: number;
minLength?: number; minLength?: number;
maxLength?: number;
placeholder?: string; placeholder?: string;
} }
type FormbricksProps = TextInputUniqueProps & UniversalInputProps & NameRequired; type Props = TextUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "text"; const inputType = "text";
export function Text(props: FormbricksProps) { export function Text(props: Props) {
const elemId = useMemo(() => getElementId(props.id, props.name), [props.id, props.name]);
useEffectUpdateSchema(props, inputType); useEffectUpdateSchema(props, inputType);
const {
register,
formState: { errors },
} = useFormContext();
const validationRules = getValidationRules(props.validation);
return ( return (
<Outer inputType={inputType} outerClassName={props.outerClassName}> <Input
<Wrapper wrapperClassName={props.wrapperClassName}> type={{ html: inputType, formbricks: inputType }}
<Label label={props.label} elemId={elemId} labelClassName={props.labelClassName} /> additionalValidation={{
<Inner innerClassName={props.innerClassName}> minLength: {
<input value: props.minLength || 0,
className={clsx("formbricks-input", props.inputClassName)} message: `Your answer must be at least ${props.minLength} characters long`,
type="text" },
id={elemId} maxLength: {
placeholder={props.placeholder || ""} value: props.maxLength || 524288,
aria-invalid={errors[props.name] ? "true" : "false"} message: `Your answer musn't be longer than ${props.maxLength} characters`,
{...register(props.name, { },
required: { value: "required" in validationRules, message: "This field is required" }, }}
minLength: { additionalProps={{ placeholder: props.placeholder }}
value: props.minLength || 0, {...props}
message: `Your answer must be at least ${props.minLength} characters long`, />
},
maxLength: {
value: props.maxLength || 524288,
message: `Your answer musn't be longer than ${props.maxLength} characters`,
},
validate: validate(validationRules),
})}
/>
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
<Messages {...props} />
</Outer>
); );
} }
@@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import { useMemo } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element"; import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema"; import { useEffectUpdateSchema } from "../../lib/schema";
@@ -0,0 +1,35 @@
import { useEffectUpdateSchema } from "../../lib/schema";
import { NameRequired, UniversalInputProps } from "../../types";
import { Input, InputProps } from "../shared/Input";
interface UrlUniqueProps {
minLength?: number;
maxLength?: number;
placeholder?: string;
}
type Props = UrlUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "url";
export function Url(props: Props) {
useEffectUpdateSchema(props, inputType);
return (
<Input
type={{ html: inputType, formbricks: inputType }}
additionalValidation={{
minLength: {
value: props.minLength || 0,
message: `Your answer must be at least ${props.minLength} characters long`,
},
maxLength: {
value: props.maxLength || 524288,
message: `Your answer musn't be longer than ${props.maxLength} characters`,
},
}}
additionalProps={{ placeholder: props.placeholder }}
{...props}
/>
);
}
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface HelpProps { interface HelpProps {
help?: string; help?: string;
@@ -1,5 +1,5 @@
import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface InnerProps { interface InnerProps {
innerClassName?: string; innerClassName?: string;
@@ -0,0 +1,60 @@
import clsx from "clsx";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element";
import { getValidationRules, validate } from "../../lib/validation";
import { NameRequired, UniversalInputProps } from "../../types";
import { Help } from "./Help";
import { Inner } from "./Inner";
import { Label } from "./Label";
import { Messages } from "./Messages";
import { Outer } from "./Outer";
import { Wrapper } from "./Wrapper";
export interface InputProps {
additionalValidation?: any;
additionalProps?: any;
}
interface UniqueProps {
type: {
formbricks: string;
html: string;
};
}
type FormbricksProps = UniqueProps & InputProps & UniversalInputProps & NameRequired;
export function Input(props: FormbricksProps) {
const elemId = useMemo(() => getElementId(props.id, props.name), [props.id, props.name]);
const {
register,
formState: { errors },
} = useFormContext();
const validationRules = getValidationRules(props.validation);
return (
<Outer inputType={props.type.formbricks} outerClassName={props.outerClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<Label label={props.label} elemId={elemId} labelClassName={props.labelClassName} />
<Inner innerClassName={props.innerClassName}>
<input
className={clsx("formbricks-input", props.inputClassName)}
type={props.type.html}
id={elemId}
aria-invalid={errors[props.name] ? "true" : "false"}
{...props.additionalProps}
{...register(props.name, {
required: { value: "required" in validationRules, message: "This field is required" },
validate: validate(validationRules),
...props.additionalValidation,
})}
/>
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
<Messages {...props} />
</Outer>
);
}
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface LabelProps { interface LabelProps {
label?: string; label?: string;
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface LegendProps { interface LegendProps {
legendClassName?: string; legendClassName?: string;
@@ -1,6 +1,5 @@
import { ErrorMessage } from "@hookform/error-message"; import { ErrorMessage } from "@hookform/error-message";
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
interface HelpProps { interface HelpProps {
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface OptionProps { interface OptionProps {
optionClassName?: string; optionClassName?: string;
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface OptionsProps { interface OptionsProps {
optionsClassName?: string; optionsClassName?: string;
@@ -1,5 +1,4 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
interface OuterProps { interface OuterProps {
inputType: string; inputType: string;
@@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx"; import clsx from "clsx";
interface WrapperProps { interface WrapperProps {
+13 -1
View File
@@ -1,3 +1,15 @@
export * from "./components/Form"; export * from "./components/Form";
export * from "./components/FormbricksSchema"; export * from "./components/FormbricksSchema";
export * from "./components/Inputs"; // Inputs
export * from "./components/inputs/Button";
export * from "./components/inputs/Checkbox";
export * from "./components/inputs/Email";
export * from "./components/inputs/Number";
export * from "./components/inputs/Password";
export * from "./components/inputs/Phone";
export * from "./components/inputs/Radio";
export * from "./components/inputs/Search";
export * from "./components/inputs/Submit";
export * from "./components/inputs/Text";
export * from "./components/inputs/Textarea";
export * from "./components/inputs/Url";
+29
View File
@@ -4,6 +4,12 @@ export const getValidationRules = (validation: string | undefined) => {
return validationRules; return validationRules;
} }
for (const validationRule of validation.split("|")) { for (const validationRule of validation.split("|")) {
if (validationRule === "accepted" && !("accepted" in validationRules)) {
validationRules.accepted = {};
}
if (validationRule === "email" && !("email" in validationRules)) {
validationRules.email = {};
}
if (validationRule === "required" && !("required" in validationRules)) { if (validationRule === "required" && !("required" in validationRules)) {
validationRules.required = {}; validationRules.required = {};
} }
@@ -16,12 +22,27 @@ export const getValidationRules = (validation: string | undefined) => {
if (validationRule.startsWith("min:") && !("min" in validationRules)) { if (validationRule.startsWith("min:") && !("min" in validationRules)) {
validationRules.min = { value: validationRule.split(":")[1] }; validationRules.min = { value: validationRule.split(":")[1] };
} }
if (validationRule === "url" && !("url" in validationRules)) {
validationRules.url = {};
}
} }
return validationRules; return validationRules;
}; };
export const validate = (validationRules: any) => { export const validate = (validationRules: any) => {
const validation: any = {}; const validation: any = {};
if ("accepted" in validationRules) {
validation.accepted = (v: string | boolean | number) =>
v === true || v === 1 || v === "on" || v === "yes" || `This field must be accepted`;
}
if ("email" in validationRules) {
validation.email = (v: string) =>
("email" in validationRules &&
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
v
)) ||
"Please provide a valid email address";
}
if ("max" in validationRules) { if ("max" in validationRules) {
validation.max = (v: string) => validation.max = (v: string) =>
parseInt(v) <= validationRules.max.value || parseInt(v) <= validationRules.max.value ||
@@ -36,5 +57,13 @@ export const validate = (validationRules: any) => {
validation.number = (v: string) => validation.number = (v: string) =>
("number" in validationRules && /^[+-]?([0-9]*[.])?[0-9]+$/.test(v)) || "Input must be a number"; ("number" in validationRules && /^[+-]?([0-9]*[.])?[0-9]+$/.test(v)) || "Input must be a number";
} }
if ("url" in validationRules) {
validation.url = (v: string) =>
("url" in validationRules &&
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/.test(
v
)) ||
"Please provide a valid url (including http:// or https://)";
}
return validation; return validation;
}; };
+7 -1
View File
@@ -14,7 +14,13 @@ button.formbricks-input {
@apply my-2 inline-flex items-center rounded-md border border-transparent bg-slate-600 px-3 py-2 text-base font-medium leading-4 text-white shadow-sm hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 sm:text-sm; @apply my-2 inline-flex items-center rounded-md border border-transparent bg-slate-600 px-3 py-2 text-base font-medium leading-4 text-white shadow-sm hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 sm:text-sm;
} }
input[type="text"].formbricks-input { input[type="text"].formbricks-input,
input[type="number"].formbricks-input,
input[type="email"].formbricks-input,
input[type="password"].formbricks-input,
input[type="search"].formbricks-input,
input[type="tel"].formbricks-input,
input[type="url"].formbricks-input {
@apply form-input text-base block rounded-md border-gray-300 shadow-sm focus:border-slate-500 focus:ring-slate-500 sm:text-sm; @apply form-input text-base block rounded-md border-gray-300 shadow-sm focus:border-slate-500 focus:ring-slate-500 sm:text-sm;
} }
+1 -1
View File
@@ -6,7 +6,7 @@
"lib": ["ES2015", "DOM"], "lib": ["ES2015", "DOM"],
"module": "ESNext", "module": "ESNext",
"target": "ES6", "target": "ES6",
"jsx": "react", "jsx": "react-jsx",
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true "noUnusedParameters": true
} }
+9 -3
View File
@@ -10,7 +10,7 @@ importers:
turbo: latest turbo: latest
devDependencies: devDependencies:
'@changesets/cli': 2.25.0 '@changesets/cli': 2.25.0
prettier: 2.7.1 prettier: 2.8.0
tsx: 3.9.0 tsx: 3.9.0
turbo: 1.6.3 turbo: 1.6.3
@@ -1848,7 +1848,7 @@ packages:
fs-extra: 7.0.1 fs-extra: 7.0.1
lodash.startcase: 4.4.0 lodash.startcase: 4.4.0
outdent: 0.5.0 outdent: 0.5.0
prettier: 2.7.1 prettier: 2.8.0
resolve-from: 5.0.0 resolve-from: 5.0.0
semver: 5.7.1 semver: 5.7.1
dev: true dev: true
@@ -2015,7 +2015,7 @@ packages:
'@changesets/types': 5.2.0 '@changesets/types': 5.2.0
fs-extra: 7.0.1 fs-extra: 7.0.1
human-id: 1.0.2 human-id: 1.0.2
prettier: 2.7.1 prettier: 2.8.0
dev: true dev: true
/@cnakazawa/watch/1.0.4: /@cnakazawa/watch/1.0.4:
@@ -12447,6 +12447,12 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/prettier/2.8.0:
resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==}
engines: {node: '>=10.13.0'}
hasBin: true
dev: true
/pretty-error/2.1.2: /pretty-error/2.1.2:
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==} resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
dependencies: dependencies: