add radio buttons to react lib

This commit is contained in:
Matthias Nannt
2022-11-22 17:45:27 +01:00
parent 9311dc0f6f
commit d8d48f14f8
17 changed files with 255 additions and 73 deletions

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3001",
"build": "next build",
"postbuild": "next-sitemap",
"start": "next start",

View File

@@ -1,4 +1,5 @@
export * from "./inputs/Button";
export * from "./inputs/Radio";
export * from "./inputs/Submit";
export * from "./inputs/Text";
export * from "./inputs/Textarea";

View File

@@ -5,6 +5,8 @@ import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types";
import ButtonComponent from "../shared/ButtonComponent";
import { Help } from "../shared/Help";
import { Outer } from "../shared/Outer";
import { Wrapper } from "../shared/Wrapper";
interface ButtonInputUniqueProps {
PrefixIcon?: SVGComponent;
@@ -21,11 +23,11 @@ export function Button(props: FormbricksProps) {
useEffectUpdateSchema(props, inputType);
return (
<div className={clsx("formbricks-outer", props.outerClassName)} data-type={inputType}>
<div className={clsx("formbricks-wrapper", props.wrapperClassName)}>
<Outer inputType={inputType} outerClassName={props.outerClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<ButtonComponent elemId={elemId} {...props} />
</div>
{props.help && <Help help={props.help} elemId={elemId} />}
</div>
</Wrapper>
<Help help={props.help} elemId={elemId} />
</Outer>
);
}

View File

@@ -0,0 +1,85 @@
import clsx from "clsx";
import React, { 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 { NameRequired, OptionsArray, OptionsObjectArray, 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";
interface RadioInputUniqueProps {
options?: OptionsArray | OptionsObjectArray;
}
type FormbricksProps = RadioInputUniqueProps & UniversalInputProps & NameRequired;
const inputType = "radio";
export function Radio(props: FormbricksProps) {
const elemId = useMemo(() => getElementId(props.id, props.name), [props.id, props.name]);
const options = useMemo(() => normalizeOptions(props.options), [props.options]);
useEffectUpdateSchema(props, inputType);
const {
register,
formState: { errors },
} = useFormContext();
const validationRules = getValidationRules(props.validation);
if (!options || options.length === 0) {
return (
<Outer inputType={inputType} outerClassName={props.outerClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}>
<input
className={clsx("formbricks-input", props.inputClassName)}
type="radio"
id={elemId}
{...register(props.name, {
required: { value: "required" in validationRules, message: "This field is required" },
})}
/>
<Label label={props.label} elemId={elemId} />
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} />
<Messages {...props} />
</Outer>
);
}
return (
<Outer inputType={inputType} outerClassName={props.outerClassName}>
<fieldset className="formbricks-fieldset" name={props.name}>
<legend className="formbricks-legend">{props.label}</legend>
<Help help={props.help} elemId={elemId} />
<div className="formbricks-options">
{options.map((option) => (
<div className="formbricks-option">
<Wrapper wrapperClassName={props.wrapperClassName}>
<Inner innerClassName={props.innerClassName}>
<input
className={clsx("formbricks-input", props.inputClassName)}
type="radio"
id={`${props.name}-${option.value}`}
value={option.value}
disabled={option?.config?.disabled}
{...register(props.name)}
/>
<Label label={option.label} elemId={`${props.name}-${option.value}`} />
</Inner>
</Wrapper>
</div>
))}
</div>
</fieldset>
<Messages {...props} />
</Outer>
);
}

View File

@@ -1,10 +1,11 @@
import clsx from "clsx";
import React, { useMemo } from "react";
import { getElementId } from "../../lib/element";
import { useEffectUpdateSchema } from "../../lib/schema";
import { SVGComponent, UniversalInputProps } from "../../types";
import ButtonComponent from "../shared/ButtonComponent";
import { Help } from "../shared/Help";
import { Outer } from "../shared/Outer";
import { Wrapper } from "../shared/Wrapper";
interface SubmitInputUniqueProps {
PrefixIcon?: SVGComponent;
@@ -20,11 +21,11 @@ export function Submit(props: FormbricksProps) {
useEffectUpdateSchema(props, inputType);
return (
<div className={clsx("formbricks-outer", props.outerClassName)} data-type={inputType}>
<div className={clsx("formbricks-wrapper", props.wrapperClassName)}>
<Outer inputType={inputType} outerClassName={props.outerClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<ButtonComponent type="submit" elemId={elemId} {...props} />
</div>
{props.help && <Help help={props.help} elemId={elemId} />}
</div>
</Wrapper>
<Help help={props.help} elemId={elemId} />
</Outer>
);
}

View File

@@ -6,8 +6,11 @@ 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";
interface TextInputUniqueProps {
maxLength?: number;
@@ -30,12 +33,12 @@ export function Text(props: FormbricksProps) {
const validationRules = getValidationRules(props.validation);
return (
<div className={clsx("formbricks-outer", props.outerClassName)} data-type={inputType}>
<div className={clsx("formbricks-wrapper", props.wrapperClassName)}>
<Label label={props.label} elemId={elemId} />
<div className={clsx("formbricks-inner", props.innerClassName)}>
<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("form-input", "formbricks-input", props.inputClassName)}
className={clsx("formbricks-input", props.inputClassName)}
type="text"
id={elemId}
placeholder={props.placeholder || ""}
@@ -53,10 +56,10 @@ export function Text(props: FormbricksProps) {
validate: validate(validationRules),
})}
/>
</div>
</div>
{props.help && <Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />}
<Messages errors={errors} {...props} />
</div>
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
<Messages {...props} />
</Outer>
);
}

View File

@@ -5,8 +5,11 @@ 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";
interface TextareaInputUniqueProps {
cols?: number;
@@ -31,10 +34,10 @@ export function Textarea(props: TextareaProps) {
const validationRules = getValidationRules(props.validation);
return (
<div className="formbricks-outer" data-type={inputType}>
<div className="formbricks-wrapper">
<Label label={props.label} elemId={elemId} />
<div className="formbricks-inner">
<Outer inputType={inputType} outerClassName={props.outerClassName}>
<Wrapper wrapperClassName={props.wrapperClassName}>
<Label label={props.label} elemId={elemId} labelClassName={props.labelClassName} />
<Inner innerClassName={props.innerClassName}>
<textarea
className="formbricks-input"
id={elemId}
@@ -55,10 +58,10 @@ export function Textarea(props: TextareaProps) {
validate: validate(validationRules),
})}
/>
</div>
</div>
{props.help && <Help help={props.help} elemId={elemId} />}
<Messages errors={errors} {...props} />
</div>
</Inner>
</Wrapper>
<Help help={props.help} elemId={elemId} helpClassName={props.helpClassName} />
<Messages {...props} />
</Outer>
);
}

View File

@@ -2,12 +2,15 @@ import clsx from "clsx";
import React from "react";
interface HelpProps {
help: string;
help?: string;
elemId: string;
helpClassName?: string;
}
export function Help({ help, elemId, helpClassName }: HelpProps) {
if (!help) {
return null;
}
return (
<div className={clsx("formbricks-help", helpClassName)} id={`help-${elemId}`}>
{help}

View File

@@ -0,0 +1,11 @@
import React from "react";
import clsx from "clsx";
interface InnerProps {
innerClassName?: string;
children: React.ReactNode;
}
export function Inner({ innerClassName, children }: InnerProps) {
return <div className={clsx("formbricks-inner", innerClassName)}>{children}</div>;
}

View File

@@ -1,15 +1,17 @@
import clsx from "clsx";
import React from "react";
interface LabelProps {
label?: string;
elemId: string;
labelClassName?: string;
}
export function Label({ label, elemId }: LabelProps) {
export function Label({ label, elemId, labelClassName }: LabelProps) {
return (
<>
{typeof label !== "undefined" && (
<label className="formbricks-label" htmlFor={elemId}>
<label className={clsx("formbricks-label", labelClassName)} htmlFor={elemId}>
{label}
</label>
)}

View File

@@ -1,49 +1,36 @@
import { ErrorMessage } from "@hookform/error-message";
import clsx from "clsx";
import React from "react";
import { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
import { useFormContext } from "react-hook-form";
interface HelpProps {
name: string;
errors?: Partial<
FieldErrorsImpl<{
[x: string]: any;
}>
>;
messagesClassName?: string;
messageClassName?: string;
}
export function Messages({ errors, messagesClassName, messageClassName, name }: HelpProps) {
export function Messages({ messagesClassName, messageClassName, name }: HelpProps) {
const {
formState: { errors },
} = useFormContext();
return (
<>
{/* <ul className={clsx("formbricks-messages", messagesClassName)}>
{console.log(messages)}
<li
className={clsx("formbricks-message", messageClassName)}
id="input_1-rule_required"
data-message-type="validation">
FormKit Input is required.
</li>
</ul> */}
<ErrorMessage
errors={errors}
name={name}
render={({ messages }) =>
messages &&
Object.entries(messages).map(([type, message]) => (
<ul className={clsx("formbricks-messages", messagesClassName)}>
<li
className={clsx("formbricks-message", messageClassName)}
id={`${name}-${type}`}
data-message-type={type}
role="alert">
{message}
</li>
</ul>
))
}
/>
</>
<ErrorMessage
errors={errors}
name={name}
render={({ messages }) =>
messages &&
Object.entries(messages).map(([type, message]) => (
<ul className={clsx("formbricks-messages", messagesClassName)}>
<li
className={clsx("formbricks-message", messageClassName)}
id={`${name}-${type}`}
data-message-type={type}
role="alert">
{message}
</li>
</ul>
))
}
/>
);
}

View File

@@ -0,0 +1,22 @@
import clsx from "clsx";
import React from "react";
import { FieldErrorsImpl, useFormContext } from "react-hook-form";
import { Help } from "./Help";
import { Messages } from "./Messages";
interface OuterProps {
inputType: string;
outerClassName?: string;
children: React.ReactNode;
}
export function Outer({ inputType, outerClassName, children }: OuterProps) {
const {
formState: { errors },
} = useFormContext();
return (
<div className={clsx("formbricks-outer", outerClassName)} data-type={inputType}>
{children}
</div>
);
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import clsx from "clsx";
interface WrapperProps {
wrapperClassName?: string;
children: React.ReactNode;
}
export function Wrapper({ wrapperClassName, children }: WrapperProps) {
return <div className={clsx("formbricks-wrapper", wrapperClassName)}>{children}</div>;
}

View File

@@ -0,0 +1,25 @@
import { OptionsArray, OptionsObjectArray } from "../types";
export const normalizeOptions = (options?: OptionsArray | OptionsObjectArray) => {
if (!options) {
return undefined;
}
const normalizedOptions = [];
if (Array.isArray(options)) {
for (const option of options) {
if (typeof option === "string") {
normalizedOptions.push({
label: option,
value: option,
});
} else if (typeof option === "object" && !Array.isArray(option)) {
normalizedOptions.push({
label: option.label,
value: option.value,
config: option.config || {},
});
}
}
}
return normalizedOptions;
};

View File

@@ -7,17 +7,21 @@
}
.formbricks-label {
@apply block text-base font-medium text-gray-700 font-sans sm:text-sm;
@apply text-base font-medium text-gray-700 font-sans sm:text-sm;
}
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.formbricks-input {
input[type="text"].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;
}
input[type="radio"].formbricks-input {
@apply h-4 w-4 border-gray-300 text-slate-600 focus:ring-slate-500 mr-2;
}
textarea.formbricks-input {
@apply form-textarea text-base font-sans block rounded-md border-gray-300 shadow-sm focus:border-slate-500 focus:ring-slate-500 sm:text-sm;
}
@@ -37,3 +41,11 @@ textarea.formbricks-input {
.formbricks-message {
@apply font-sans text-base sm:text-sm text-red-500;
}
.formbricks-legend {
@apply text-base font-medium text-gray-700 font-sans sm:text-sm;
}
.formbricks-fieldset {
@apply border-gray-50 rounded-lg max-w-md;
}

View File

@@ -6,6 +6,7 @@ export interface UniversalInputProps {
name?: string;
label?: string;
validation?: string;
labelClassName?: string;
outerClassName?: string;
wrapperClassName?: string;
innerClassName?: string;
@@ -18,3 +19,16 @@ export interface UniversalInputProps {
export interface NameRequired {
name: string;
}
export type OptionsArray = string[];
export interface OptionsObject {
label: string;
value: string;
config?: {
validation?: string;
disabled?: boolean;
};
}
export type OptionsObjectArray = OptionsObject[];

View File

@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
format: ["cjs", "esm"],
entry: ["src/index.tsx", "src/components/*.tsx"],
entry: ["src/index.tsx"],
clean: true,
splitting: true,
dts: true,