Skip to content

Commit

Permalink
feat(StrapiFormComponents): add StrapiCheckbox (#418)
Browse files Browse the repository at this point in the history
* feat(StrapiFormComponents): add StrapiCheckbox

Co-authored-by: Pram Gurusinga <[email protected]>
  • Loading branch information
chohner and pgurusinga authored Nov 17, 2023
1 parent b691999 commit e69a1e0
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 18 deletions.
4 changes: 4 additions & 0 deletions app/components/PageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import RichText from "./RichText";
import Select from "./inputs/Select";
import Textarea from "./inputs/Textarea";
import NumericList from "./NumericList";
import { renderCheckboxFromStrapi } from "~/services/cms/models/StrapiCheckbox";

type PageContentProps = {
content: Array<StrapiContent>;
Expand Down Expand Up @@ -69,6 +70,7 @@ function cmsToReact(cms: StrapiContent, templateReplacements: Replacements) {
) as StrapiContent;

const key = keyFromElement(replacedTemplate);
// TODO: move from props matching to returning components (see renderCheckboxFromStrapi())
switch (replacedTemplate.__component) {
case "basic.heading":
return <Heading {...getHeadingProps(replacedTemplate)} key={key} />;
Expand All @@ -84,6 +86,8 @@ function cmsToReact(cms: StrapiContent, templateReplacements: Replacements) {
return <RadioGroup {...getRadioGroupProps(replacedTemplate)} key={key} />;
case "form-elements.dropdown":
return <Select {...getDropdownProps(replacedTemplate)} key={key} />;
case "form-elements.checkbox":
return renderCheckboxFromStrapi(replacedTemplate);
case "page.box":
return <Box {...getBoxProps(replacedTemplate)} key={key} />;
case "page.info-box":
Expand Down
49 changes: 38 additions & 11 deletions app/components/inputs/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
import { useField } from "remix-validated-form";
import { useEffect, useState } from "react";
import InputError from "./InputError";
import RichText from "../RichText";

type CheckboxProps = {
name: string;
value?: string; // Defaults to "on", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/checkbox#value
onClick?: () => void;
label?: string;
formId?: string;
required?: boolean;
errorMessage?: string;
};

const Checkbox = ({
name,
value = "on",
onClick,
label,
formId,
required = false,
errorMessage,
}: CheckboxProps) => {
const { error, getInputProps } = useField(name, { formId });
const id = `${name}-${value}`;
const { error, getInputProps, defaultValue } = useField(name, { formId });

Check warning on line 23 in app/components/inputs/Checkbox.tsx

View workflow job for this annotation

GitHub Actions / code-quality / npm run eslint:check

Unsafe array destructuring of a tuple element with an `any` value
const errorId = `${name}-error`;
const className = `ds-checkbox ${error ? "has-error" : ""}`;
// HTML Forms do not send unchecked checkboxes.
// For server-side validation we need a same-named hidden field
// For front-end validation, we need to hide that field if checkbox is checked
// const alreadyChecked = defaultValue === value
const [renderHiddenField, setRenderHiddenField] = useState(
defaultValue !== value,
);
const [jsAvailable, setJsAvailable] = useState(false);
useEffect(() => setJsAvailable(true), []);

return (
<div className="flex">
<div>
{(!jsAvailable || renderHiddenField) && (
<input type="hidden" name={name} value="off" />
)}
<input
{...getInputProps({ type: "checkbox", id, value })}
key={id}
className="ds-checkbox"
aria-describedby={error && `${name}-error`}
onClick={onClick}
{...getInputProps({ type: "checkbox", id: name, value })}
className={className}
aria-describedby={error && errorId}
onClick={() => setRenderHiddenField(!renderHiddenField)}
required={required}
defaultChecked={defaultValue === value}
/>
{label && <label htmlFor={id}>{label}</label>}

{label && (
<label htmlFor={name}>
<RichText markdown={label} />
{error && (
<InputError id={errorId}>{errorMessage ?? error}</InputError>
)}
</label>
)}
</div>
);
};
Expand Down
30 changes: 30 additions & 0 deletions app/services/cms/models/StrapiCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";
import { HasOptionalStrapiIdSchema, HasStrapiIdSchema } from "./HasStrapiId";
import Checkbox from "~/components/inputs/Checkbox";
import { StrapiErrorCategorySchema } from "./StrapiErrorCategory";

export const StrapiCheckboxSchema = z
.object({
__component: z.literal("form-elements.checkbox").optional(),
name: z.string(),
label: z.string(),
isRequiredError: z.object({
data: HasStrapiIdSchema.extend({
attributes: StrapiErrorCategorySchema,
}).nullable(),
}),
})
.merge(HasOptionalStrapiIdSchema);

export const renderCheckboxFromStrapi = (
strapiCheckbox: z.infer<typeof StrapiCheckboxSchema>,
) => (
<Checkbox
name={strapiCheckbox.name}
label={strapiCheckbox.label ?? undefined}
required={strapiCheckbox.isRequiredError.data !== null}
errorMessage={
strapiCheckbox.isRequiredError?.data?.attributes.errorCodes[0].text
}
/>
);
2 changes: 2 additions & 0 deletions app/services/cms/models/StrapiContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import { StrapiDropdownSchema } from "./StrapiDropdown";
import { StrapiTextareaSchema } from "./StrapiTextarea";
import { StrapiNumericListSchema } from "./StrapiNumericList";
import { StrapiNumericListItemSchema } from "./StrapiNumericListItem";
import { StrapiCheckboxSchema } from "./StrapiCheckbox";

export const StrapiContentSchema = z.discriminatedUnion("__component", [
StrapiBoxSchema.required({ __component: true }),
StrapiBoxWithImageSchema.required({ __component: true }),
StrapiHeaderSchema.required({ __component: true }),
StrapiCheckboxSchema.required({ __component: true }),
StrapiHeadingSchema.required({ __component: true }),
StrapiInfoBoxSchema.required({ __component: true }),
StrapiInfoBoxItemSchema.required({ __component: true }),
Expand Down
2 changes: 2 additions & 0 deletions app/services/cms/models/StrapiFormComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { StrapiInputSchema } from "./StrapiInput";
import { StrapiSelectSchema } from "./StrapiSelect";
import { StrapiDropdownSchema } from "./StrapiDropdown";
import { StrapiTextareaSchema } from "./StrapiTextarea";
import { StrapiCheckboxSchema } from "./StrapiCheckbox";

export const StrapiFormComponentSchema = z.discriminatedUnion("__component", [
StrapiInputSchema.required({ __component: true }),
StrapiTextareaSchema.required({ __component: true }),
StrapiSelectSchema.required({ __component: true }),
StrapiDropdownSchema.required({ __component: true }),
StrapiCheckboxSchema.required({ __component: true }),
]);
9 changes: 9 additions & 0 deletions app/services/validation/checkedCheckbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";
import { customRequiredErrorMessage } from "./YesNoAnswer";

export const checkedRequired = z.enum(["on"], customRequiredErrorMessage);

export const checkedOptional = z.enum(
["on", "off"],
customRequiredErrorMessage,
);
27 changes: 20 additions & 7 deletions tests/unit/components/__snapshots__/storybook.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -164,22 +164,35 @@ exports[`Storyshots Button/ButtonContainer Two Buttons 1`] = `
`;

exports[`Storyshots Component/Checkbox Default 1`] = `
<div
className="flex"
>
<div>
<input
name="name"
type="hidden"
value="off"
/>
<input
className="ds-checkbox"
id="name-value"
className="ds-checkbox "
defaultChecked={false}
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
required={false}
type="checkbox"
value="value"
/>
<label
htmlFor="name-value"
htmlFor="name"
>
label
<div
className="rich-text ds-stack-8 "
dangerouslySetInnerHTML={
{
"__html": "<p class="text-lg">label</p>",
}
}
/>
</label>
</div>
`;
Expand Down

0 comments on commit e69a1e0

Please sign in to comment.