Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Solid JS support #24

Merged
merged 4 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/olive-squids-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@examples/playground": patch
"simple-stack-form": patch
---

Add Solid JS template
2 changes: 2 additions & 0 deletions examples/playground/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import simpleStackStream from "simple-stack-stream";
import react from "@astrojs/react";
import node from "@astrojs/node";
import preact from "@astrojs/preact";
import solidJs from "@astrojs/solid-js";
import tailwind from "@astrojs/tailwind";

// https://astro.build/config
Expand All @@ -14,6 +15,7 @@ export default defineConfig({
simpleStackStream(),
react({ include: ["**/react/*"] }),
preact({ include: ["**/preact/*"] }),
solidJs({ include: ["**/solid-js/*"] }),
tailwind(),
],
adapter: node({
Expand Down
2 changes: 2 additions & 0 deletions examples/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@astrojs/node": "^7.0.0",
"@astrojs/preact": "^3.0.1",
"@astrojs/react": "^3.0.7",
"@astrojs/solid-js": "^3.0.3",
"@astrojs/tailwind": "^5.0.3",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
Expand All @@ -25,6 +26,7 @@
"sanitize-html": "^2.11.0",
"simple-stack-form": "^0.1.0",
"simple-stack-stream": "^0.0.3",
"solid-js": "^1.8.7",
"tailwindcss": "^3.0.24",
"zod": "^3.22.4"
},
Expand Down
137 changes: 137 additions & 0 deletions examples/playground/src/components/solid-js/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/** @jsxImportSource solid-js */

// Generated by simple:form

import {
createSignal,
createContext,
useContext,
type ComponentProps,
Show,
For,
} from "solid-js";
import {
type FieldErrors,
type FormState,
type FormValidator,
getInitialFormState,
toSetValidationErrors,
toTrackAstroSubmitStatus,
toValidateField,
validateForm,
formNameInputProps,
} from "simple:form";

export function useCreateFormContext(
validator: FormValidator,
fieldErrors?: FieldErrors,
) {
const initial = getInitialFormState({ validator, fieldErrors });
const [formState, setFormState] = createSignal<FormState>(initial);
return {
value: formState,
set: setFormState,
setValidationErrors: toSetValidationErrors(setFormState),
validateField: toValidateField(setFormState),
trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState),
};
}

export function useFormContext() {
const formContext = useContext(FormContext);
if (!formContext) {
throw new Error(
"Form context not found. `useFormContext()` should only be called from children of a <Form> component.",
);
}
return formContext;
}

type FormContextType = ReturnType<typeof useCreateFormContext>;

const FormContext = createContext<FormContextType | undefined>(undefined);

export function Form(
props: {
validator: FormValidator;
context?: FormContextType;
fieldErrors?: FieldErrors;
} & Omit<ComponentProps<"form">, "method" | "onSubmit">,
) {
const formContext =
props.context ?? useCreateFormContext(props.validator, props.fieldErrors);

return (
<FormContext.Provider value={formContext}>
<form
{...props}
method="post"
onSubmit={async (e) => {
const formData = new FormData(e.currentTarget);
formContext.set((formState) => ({
...formState,
isSubmitPending: true,
submitStatus: "validating",
}));
const parsed = await validateForm({
formData,
validator: props.validator,
});
if (parsed.data) {
return formContext.trackAstroSubmitStatus();
}

e.preventDefault();
e.stopPropagation();
formContext.setValidationErrors(parsed.fieldErrors);
}}
>
<Show when={props.name}>
{(name) => <input {...formNameInputProps} value={name()} />}
</Show>
{props.children}
</form>
</FormContext.Provider>
);
}

export function Input(inputProps: ComponentProps<"input"> & { name: string }) {
const formContext = useFormContext();
const fieldState = () => {
const value = formContext.value().fields[inputProps.name];
if (!value) {
throw new Error(
`Input "${inputProps.name}" not found in form. Did you use the <Form> component?`,
);
}
return value;
};
return (
<>
<input
onBlur={(e) => {
const value = e.target.value;
if (value === "") return;
formContext.validateField(
inputProps.name,
value,
fieldState().validator,
);
}}
onInput={(e) => {
if (!fieldState().hasErroredOnce) return;
const value = e.target.value;
formContext.validateField(
inputProps.name,
value,
fieldState().validator,
);
}}
{...inputProps}
/>
<For each={fieldState().validationErrors}>
{(e) => <p class="text-red-400">{e}</p>}
</For>
</>
);
}
60 changes: 60 additions & 0 deletions examples/playground/src/components/solid-js/Signup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/** @jsxImportSource solid-js */
import { type JSX, Show } from "solid-js";
import { z } from "zod";
import { Form, Input, useFormContext } from "./Form";
import { type FieldErrors, createForm } from "simple:form";

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export const signup = createForm({
username: z
.string()
.min(2)
.refine(async (s) => {
await sleep(400);
return s !== "admin";
}),
optIn: z.boolean().optional(),
});

export default function Signup(props: {
serverErrors?: FieldErrors<typeof signup>;
}) {
return (
<Form
class="flex flex-col gap-2 items-start"
fieldErrors={props.serverErrors}
validator={signup.validator}
name="signupSolid"
>
<FormGroup>
<label for="name">Name</label>
<Input id="name" {...signup.inputProps.username} />
</FormGroup>
<FormGroup>
<label for="optIn">Opt in</label>
<Input id="optIn" {...signup.inputProps.optIn} />
</FormGroup>
<button
type="submit"
class="bg-purple-700 rounded px-5 py-2 disabled:bg-purple-900"
>
Submit
</button>
<Loading />
</Form>
);
}

function FormGroup(props: { children: JSX.Element }) {
return <div class="flex gap-3 items-center">{props.children}</div>;
}

function Loading() {
const formContext = useFormContext();
return (
<Show when={formContext.value().isSubmitPending}>
<p>{formContext.value().submitStatus}</p>
</Show>
);
}
52 changes: 40 additions & 12 deletions examples/playground/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,29 @@ import SignupReact, { signup as signupReact } from "../components/react/Signup";
import SignupPreact, {
signup as signupPreact,
} from "../components/preact/Signup";
import SignupSolid, {
signup as signupSolid,
} from "../components/solid-js/Signup";
import Sanitize, { sanitize } from "../components/Sanitize";
import { ViewTransitions } from "astro:transitions";

const { form } = Astro.locals;

const formResultReact = await form.getDataByName("signupReact", signupReact);
const formResultPreact = await form.getDataByName("signupPreact", signupPreact);

if (formResultReact?.data) {
console.log(formResultReact.data);
}

if (formResultPreact?.data) {
console.log(formResultPreact.data);
}

const formResultSolid = await form.getDataByName("signupSolid", signupSolid);
const sanitizeFormResult = await form.getDataByName("sanitize", sanitize);

if (sanitizeFormResult?.data) {
console.log(sanitizeFormResult.data);
}
[
formResultReact,
formResultPreact,
formResultSolid,
sanitizeFormResult
].forEach((formResult) => {
if (formResult?.data) {
console.log(formResult.data);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactor!

}
});

await new Promise((resolve) => setTimeout(resolve, 400));
---
Expand Down Expand Up @@ -79,7 +82,32 @@ await new Promise((resolve) => setTimeout(resolve, 400));
client:load
/>
</div>

<hr />

<h2>Solid</h2>
{
formResultSolid?.data && (
<div
class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"
role="alert"
>
<strong class="font-bold">Success!</strong>
<span class="block sm:inline">
You have successfully submitted the form.
</span>
</div>
)
}
<div transition:name="solid-form">
<SignupSolid
serverErrors={formResultSolid?.fieldErrors}
client:load
/>
</div>

<hr />

<h2>Sanitize</h2>
<div class="pb-2">
Try pasting this code snippet into each field once and submit
Expand Down
1 change: 1 addition & 0 deletions packages/form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"astro": "^4.0.7",
"preact": "^10.19.3",
"react": "^18.0.0",
"solid-js": "^1.8.7",
"typescript": "^5.3.3",
"zod": "^3.22.4"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/form/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const frameworks = [
label: "Preact",
templateDir: "preact",
},
{
value: "solid-js",
label: "SolidJS",
templateDir: "solid-js",
},
] as const;

type Framework = (typeof frameworks)[number];
Expand Down
Loading
Loading