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

docs: Form example for demo #1188

Merged
merged 12 commits into from
Jan 26, 2025
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
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('/');
Copy link
Owner

Choose a reason for hiding this comment

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

It's nice to have this in this example, but it makes me wonder if this example is more than just "form demo".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would you rename it to? 🤔

Copy link
Owner

Choose a reason for hiding this comment

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

No immediate ideas. 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"form demo" seems fitting to me I think. rerenderRoute can be used for other things, but progressive forms will be a very normal use of it.

};

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
Loading