Skip to content

Commit

Permalink
Merge pull request #159 from gutentag2012/fix/next-type-inference
Browse files Browse the repository at this point in the history
Fix/next type inference
  • Loading branch information
gutentag2012 authored Dec 29, 2024
2 parents 04633da + 39a947d commit 0c56c40
Show file tree
Hide file tree
Showing 35 changed files with 1,408 additions and 53 deletions.
41 changes: 41 additions & 0 deletions examples/react/next-form-signals/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
26 changes: 26 additions & 0 deletions examples/react/next-form-signals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Form Signals Example | Next Form Signals

This is an example application that demonstrates how to use the `form-signals` library within Next.js.

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/gutentag2012/form-signals/tree/main/examples/react/next-form-signals?startScript=example&title=Form%20Signals%20|%20Next%2Ejs%20Example)

> [!NOTE]
> For now I did not find a way to use the `@preact/signals-react-transform` package to automatically track signals, that is why you will find the manual use of the `useSignals()` hook.
## Quick Start

Install the dependencies:

```bash
pnpm install
```

Run the example:

```bash
pnpm example
```

## Relevant Files

- [`components/form/LoginForm.tsx`](./components/form/LoginForm.tsx): The main form components for the example.
Binary file added examples/react/next-form-signals/app/favicon.ico
Binary file not shown.
72 changes: 72 additions & 0 deletions examples/react/next-form-signals/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
font-family: Arial, Helvetica, sans-serif;
}

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
34 changes: 34 additions & 0 deletions examples/react/next-form-signals/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'

const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})

const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
)
}
10 changes: 10 additions & 0 deletions examples/react/next-form-signals/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { LoginForm } from '@/components/form/LoginForm'

export default function Home() {
return (
<main className="mx-auto max-w-xl">
<h1 className="mt-8 text-center font-bold text-3xl">Login</h1>
<LoginForm />
</main>
)
}
21 changes: 21 additions & 0 deletions examples/react/next-form-signals/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
25 changes: 25 additions & 0 deletions examples/react/next-form-signals/components/form/ErrorText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useFieldContext, useFieldGroupContext } from '@formsignals/form-react'

export const ErrorText = () => {
const field = useFieldContext()
if (field.isValidating.value)
return <p className="text-[0.8rem] opacity-70">Validating...</p>
if (field.isValid.value) return null
return (
<p className="font-medium text-[0.8rem] text-destructive">
{field.errors.value.join(', ')}
</p>
)
}

export const ErrorTextGroup = () => {
const group = useFieldGroupContext()
if (group.isValidating.value)
return <p className="text-[0.8rem] opacity-70">Validating...</p>
if (group.isValid.value) return null
return (
<p className="font-medium text-[0.8rem] text-destructive">
{group.errors.value.join(', ')}
</p>
)
}
100 changes: 100 additions & 0 deletions examples/react/next-form-signals/components/form/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use client'
import { ErrorText } from '@/components/form/ErrorText'
import { Button } from '@/components/ui/button'
import { InputForm } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useForm, useFormContext } from '@formsignals/form-react'
import { ZodAdapter } from '@formsignals/validation-adapter-zod'
import { useSignals } from '@preact/signals-react/runtime'
import { z } from 'zod'

export function LoginForm() {
useSignals()

const form = useForm({
validatorAdapter: ZodAdapter,
defaultValues: {
username: '',
password: '',
passwordRepeat: '',
},
onSubmit: (values) => {
if (
!window.confirm(
`Submitted login for ${values.username}. Do you want to reset the form?`,
)
)
return
form.reset()
},
})

return (
<form.FormProvider>
<form
className="flex flex-col gap-2"
onSubmit={async (e) => {
e.preventDefault()
e.stopPropagation()
await form.handleSubmit()
}}
>
<form.FieldProvider
name="username"
validator={z.string().min(3)}
validatorOptions={{ validateOnChangeIfTouched: true }}
>
<div>
<Label htmlFor="username">Username</Label>
<InputForm id="username" />
<ErrorText />
</div>
</form.FieldProvider>
<form.FieldProvider
name="password"
validator={z.string().min(8)}
validatorOptions={{
validateOnChangeIfTouched: true,
}}
>
<div>
<Label htmlFor="password">Password</Label>
<InputForm id="password" type="password" />
<ErrorText />
</div>
</form.FieldProvider>
<form.FieldProvider
name="passwordRepeat"
validateMixin={['password']}
validator={z
.tuple([z.string(), z.string()])
.refine(
([check, password]) => check === password,
'Passwords did not match',
)}
validatorOptions={{
validateOnChangeIfTouched: true,
}}
>
<div>
<Label htmlFor="password-repeat">Password (repeat)</Label>
<InputForm id="password-repeat" type="password" />
<ErrorText />
</div>
</form.FieldProvider>

<FormSubmitButton />
</form>
</form.FormProvider>
)
}

function FormSubmitButton() {
const form = useFormContext()

return (
<Button className="mt-2" type="submit" disabled={!form.canSubmit.value}>
Login
</Button>
)
}
57 changes: 57 additions & 0 deletions examples/react/next-form-signals/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'

import { cn } from '@/lib/utils'

const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = 'Button'

export { Button, buttonVariants }
Loading

0 comments on commit 0c56c40

Please sign in to comment.