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

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

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Text, Textarea } from "..";
import { Form } from "./Form";
import { Text, Textarea } from "./Inputs";
interface OnSubmitProps {
data: any;

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";

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import { useMemo } from "react";
import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types";

View File

@@ -1,10 +1,10 @@
import clsx from "clsx";
import React, { useMemo } from "react";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element";
import { normalizeOptions } from "../../lib/options";
import { useEffectUpdateSchema } from "../../lib/schema";
import { getValidationRules } from "../../lib/validation";
import { getValidationRules, validate } from "../../lib/validation";
import { NameRequired, OptionsArray, OptionsObjectArray, UniversalInputProps } from "../../types";
import { Fieldset } from "../shared/Fieldset";
import { Help } from "../shared/Help";
@@ -48,6 +48,7 @@ export function Checkbox(props: FormbricksProps) {
id={elemId}
{...register(props.name, {
required: { value: "required" in validationRules, message: "This field is required" },
validate: validate(validationRules),
})}
/>
<Label label={props.label} elemId={elemId} />
@@ -66,7 +67,7 @@ export function Checkbox(props: FormbricksProps) {
<Help help={props.help} elemId={elemId} />
<Options optionsClassName={props.optionsClassName}>
{options.map((option) => (
<Option optionClassName={props.optionClassName}>
<Option key={`${props.name}-${option.value}`} optionClassName={props.optionClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}>
<input

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import clsx from "clsx";
import React, { useMemo } from "react";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element";
import { normalizeOptions } from "../../lib/options";
@@ -66,7 +66,7 @@ export function Radio(props: FormbricksProps) {
<Help help={props.help} elemId={elemId} />
<Options optionsClassName={props.optionsClassName}>
{options.map((option) => (
<Option optionClassName={props.optionClassName}>
<Option key={`${props.name}-${option.value}`} optionClassName={props.optionClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}>
<input

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import { useMemo } from "react";
import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types";

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 { getValidationRules, validate } from "../../lib/validation";
import { NameRequired, UniversalInputProps } from "../../types";
import { Help } from "../shared/Help";
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";
import { Input, InputProps } from "../shared/Input";
interface TextInputUniqueProps {
maxLength?: number;
interface TextUniqueProps {
minLength?: number;
maxLength?: number;
placeholder?: string;
}
type FormbricksProps = TextInputUniqueProps & UniversalInputProps & NameRequired;
type Props = TextUniqueProps & InputProps & UniversalInputProps & NameRequired;
const inputType = "text";
export function Text(props: FormbricksProps) {
const elemId = useMemo(() => getElementId(props.id, props.name), [props.id, props.name]);
export function Text(props: Props) {
useEffectUpdateSchema(props, inputType);
const {
register,
formState: { errors },
} = useFormContext();
const validationRules = getValidationRules(props.validation);
return (
<Outer inputType={inputType} 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="text"
id={elemId}
placeholder={props.placeholder || ""}
aria-invalid={errors[props.name] ? "true" : "false"}
{...register(props.name, {
required: { value: "required" in validationRules, message: "This field is required" },
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`,
},
validate: validate(validationRules),
})}
/>
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
<Messages {...props} />
</Outer>
<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}
/>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema";

View File

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

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface HelpProps {
help?: string;

View File

@@ -1,5 +1,5 @@
import React from "react";
import clsx from "clsx";
import React from "react";
interface InnerProps {
innerClassName?: string;

View File

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

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface LabelProps {
label?: string;

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface LegendProps {
legendClassName?: string;

View File

@@ -1,6 +1,5 @@
import { ErrorMessage } from "@hookform/error-message";
import clsx from "clsx";
import React from "react";
import { useFormContext } from "react-hook-form";
interface HelpProps {

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface OptionProps {
optionClassName?: string;

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface OptionsProps {
optionsClassName?: string;

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import React from "react";
interface OuterProps {
inputType: string;

View File

@@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx";
interface WrapperProps {

View File

@@ -1,3 +1,15 @@
export * from "./components/Form";
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";

View File

@@ -4,6 +4,12 @@ export const getValidationRules = (validation: string | undefined) => {
return validationRules;
}
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)) {
validationRules.required = {};
}
@@ -16,12 +22,27 @@ export const getValidationRules = (validation: string | undefined) => {
if (validationRule.startsWith("min:") && !("min" in validationRules)) {
validationRules.min = { value: validationRule.split(":")[1] };
}
if (validationRule === "url" && !("url" in validationRules)) {
validationRules.url = {};
}
}
return validationRules;
};
export const validate = (validationRules: 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) {
validation.max = (v: string) =>
parseInt(v) <= validationRules.max.value ||
@@ -36,5 +57,13 @@ export const validate = (validationRules: any) => {
validation.number = (v: string) =>
("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;
};

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

View File

@@ -6,7 +6,7 @@
"lib": ["ES2015", "DOM"],
"module": "ESNext",
"target": "ES6",
"jsx": "react",
"jsx": "react-jsx",
"noUnusedLocals": true,
"noUnusedParameters": true
}

12
pnpm-lock.yaml generated
View File

@@ -10,7 +10,7 @@ importers:
turbo: latest
devDependencies:
'@changesets/cli': 2.25.0
prettier: 2.7.1
prettier: 2.8.0
tsx: 3.9.0
turbo: 1.6.3
@@ -1848,7 +1848,7 @@ packages:
fs-extra: 7.0.1
lodash.startcase: 4.4.0
outdent: 0.5.0
prettier: 2.7.1
prettier: 2.8.0
resolve-from: 5.0.0
semver: 5.7.1
dev: true
@@ -2015,7 +2015,7 @@ packages:
'@changesets/types': 5.2.0
fs-extra: 7.0.1
human-id: 1.0.2
prettier: 2.7.1
prettier: 2.8.0
dev: true
/@cnakazawa/watch/1.0.4:
@@ -12447,6 +12447,12 @@ packages:
hasBin: 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:
resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==}
dependencies: