Skip to content

Commit

Permalink
docs: Form example for demo (#1188)
Browse files Browse the repository at this point in the history
adding a more user ready version of a progressive forms example

<img width="1117" alt="image"
src="https://github.com/user-attachments/assets/7a8cbcf0-d903-44cc-9859-cf849f412311"
/>

---------

Co-authored-by: Tyler <[email protected]>
Co-authored-by: daishi <[email protected]>
  • Loading branch information
3 people authored Jan 26, 2025
1 parent 9150535 commit b998761
Show file tree
Hide file tree
Showing 18 changed files with 401 additions and 68 deletions.
7 changes: 7 additions & 0 deletions examples/06_form-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.env*
*.tsbuildinfo
.cache
.DS_Store
*.pem
24 changes: 24 additions & 0 deletions examples/06_form-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "06_form-demo",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"react-server-dom-webpack": "19.0.0",
"waku": "0.21.17"
},
"devDependencies": {
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"autoprefixer": "10.4.20",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
}
}
7 changes: 7 additions & 0 deletions examples/06_form-demo/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
1 change: 1 addition & 0 deletions examples/06_form-demo/private/message.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from server!
Binary file added examples/06_form-demo/public/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions examples/06_form-demo/src/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { useFormStatus } from 'react-dom';

const SubmitButton = () => {
const { pending } = useFormStatus();
return (
<>
<button
disabled={pending}
type="submit"
className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2"
>
{pending ? 'Pending...' : 'Submit'}
</button>
</>
);
};

export const Form = ({
message,
greet,
}: {
message: Promise<string>;
greet: (formData: FormData) => Promise<void>;
}) => (
<div style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}>
<p>{message}</p>
<form action={greet}>
<div className="flex flex-col gap-1 text-left">
<div>
Name:{' '}
<input
name="name"
required
className="invalid:border-red-500 rounded border px-2 py-1"
/>
</div>
<div>
Email:{' '}
<input
type="email"
name="email"
required
className="invalid:border-red-500 rounded border px-2 py-1"
/>
</div>
<SubmitButton />
</div>
</form>
<h3>This is a client component.</h3>
</div>
);
67 changes: 67 additions & 0 deletions examples/06_form-demo/src/components/ServerForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
async function submitUserProfile(formData: FormData) {
'use server';
const name = formData.get('name');
const age = formData.get('age');
const favoriteColor = formData.get('favoriteColor');
const hobby = formData.get('hobby');
const isSubscribed = formData.get('newsletter') === 'on';

console.log({
name,
age,
favoriteColor,
hobby,
isSubscribed,
});
}

export const ServerForm = () => {
return (
<form action={submitUserProfile} className="space-y-4">
<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label htmlFor="name">Full Name</label>
<input type="text" name="name" id="name" required />
</div>

<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label htmlFor="age">Age</label>
<input type="number" name="age" id="age" min="13" max="120" />
</div>

<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label htmlFor="favoriteColor">Favorite Color</label>
<select name="favoriteColor" id="favoriteColor">
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="purple">Purple</option>
<option value="yellow">Yellow</option>
</select>
</div>

<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label htmlFor="hobby">Favorite Hobby</label>
<input
type="text"
name="hobby"
id="hobby"
placeholder="e.g. Reading, Gaming, Cooking"
/>
</div>

<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label>
<input type="checkbox" name="newsletter" />
Subscribe to newsletter
</label>
</div>

<button
type="submit"
className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2"
>
Save Profile
</button>
</form>
);
};
18 changes: 18 additions & 0 deletions examples/06_form-demo/src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const Footer = () => {
return (
<footer className="p-6 lg:fixed lg:bottom-0 lg:left-0">
<div>
visit{' '}
<a
href="https://waku.gg/"
target="_blank"
rel="noreferrer"
className="mt-4 inline-block underline"
>
waku.gg
</a>{' '}
to learn more
</div>
</footer>
);
};
24 changes: 24 additions & 0 deletions examples/06_form-demo/src/components/funcs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readFile, writeFile } from 'node:fs/promises';
import { unstable_rerenderRoute } from 'waku/router/server';

export const getMessage = async () => {
const data = await readFile('./private/message.txt', 'utf8');
return data;
};

export const greet = async (formData: FormData) => {
'use server';
// simulate a slow server response
await new Promise((resolve) => setTimeout(resolve, 1000));
const currentData = await getMessage();
await writeFile(
'./private/message.txt',
currentData + '\n' + formData.get('name') + ' from server!',
);
unstable_rerenderRoute('/');
};

export const increment = async (count: number) => {
'use server';
return count + 1;
};
19 changes: 19 additions & 0 deletions examples/06_form-demo/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Link } from 'waku';

export const Header = () => {
return (
<header className="flex items-center gap-4 p-6 lg:fixed lg:left-0 lg:top-0">
<h2 className="text-lg font-bold tracking-tight">
<Link to="/">Waku form demo</Link>
</h2>
<a
href="https://github.com/dai-shi/waku/tree/main/examples/06_form-demo"
target="_blank"
rel="noreferrer"
className="text-sm hover:underline"
>
(source)
</a>
</header>
);
};
40 changes: 40 additions & 0 deletions examples/06_form-demo/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '../styles.css';

import type { ReactNode } from 'react';

import { Header } from '../components/header';
import { Footer } from '../components/footer';

type RootLayoutProps = { children: ReactNode };

export default async function RootLayout({ children }: RootLayoutProps) {
const data = await getData();

return (
<div className="font-nunito">
<title>Waku</title>
<meta name="description" content={data.description} />
<link rel="icon" type="image/png" href={data.icon} />
<Header />
<main className="flex items-center justify-center lg:min-h-svh">
{children}
</main>
<Footer />
</div>
);
}

const getData = async () => {
const data = {
description: 'An internet website!',
icon: '/images/favicon.png',
};

return data;
};

export const getConfig = async () => {
return {
render: 'static',
} as const;
};
24 changes: 24 additions & 0 deletions examples/06_form-demo/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Form } from '../components/Form';
import { getMessage, greet } from '../components/funcs';
import { ServerForm } from '../components/ServerForm';

export default function HomePage() {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 p-6">
<div className="bg-slate-100 rounded-md p-4">
<h2 className="text-2xl">Server Form</h2>
<ServerForm />
</div>
<div className="bg-slate-100 rounded-md p-4">
<h2 className="text-2xl">Client Form</h2>
<Form message={getMessage()} greet={greet} />
</div>
</div>
);
}

export const getConfig = async () => {
return {
render: 'dynamic',
} as const;
};
5 changes: 5 additions & 0 deletions examples/06_form-demo/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
10 changes: 10 additions & 0 deletions examples/06_form-demo/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
fontFamily: {
nunito: ['"Nunito"', 'sans-serif'],
'zen-maru-gothic': ['"Zen Maru Gothic"', 'serif'],
},
},
};
15 changes: 15 additions & 0 deletions examples/06_form-demo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"types": ["node", "react/experimental"],
"jsx": "react-jsx"
}
}
2 changes: 1 addition & 1 deletion examples/36_form/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const App = ({ name }: { name: string }) => {
>
<h1>Hello {name}!!</h1>
<h3>This is a server component.</h3>
<Counter increment={increment} />
<Form message={getMessage()} greet={greet} />
<Counter increment={increment} />
<ServerForm />
</div>
</body>
Expand Down
Loading

0 comments on commit b998761

Please sign in to comment.