Skip to content
This repository has been archived by the owner on Oct 28, 2024. It is now read-only.

Commit

Permalink
feat: add todo app example (#167)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominique Wirz <[email protected]>
  • Loading branch information
dwirz and Dominique Wirz authored Feb 20, 2024
1 parent badc61e commit 79f0eb8
Show file tree
Hide file tree
Showing 17 changed files with 529 additions and 16 deletions.
9 changes: 1 addition & 8 deletions packages/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@ This application showcases React Web Components generated using a custom output

- Demonstrates side-by-side comparisons of different rendering strategies: `'wrapper'`, `'server-only'`, and `'use client';`.
- Provides test pages with varying numbers of components (50, 100, 200, and 500) for performance evaluation.
- A TodoMVC example basec on [vogloblinsky/web-components-benchmark](https://github.com/vogloblinsky/web-components-benchmark) to demonstrate the interactive use of `'wrapper'` and `'server-only'`.
- Includes a Storybook environment for isolated component exploration.

## Deployment

Live version: https://stencil-nextjs-example-app.vercel.app (`'wrapper'`, `'server-only'`, and `'use client';`)

Test pages (`'server-only'` approach only, for performance evaluation):
- 50 elements: https://stencil-nextjs-example-app.vercel.app/test/50
- 100 elements: https://stencil-nextjs-example-app.vercel.app/test/100
- 200 elements: https://stencil-nextjs-example-app.vercel.app/test/200
- 500 elements: https://stencil-nextjs-example-app.vercel.app/test/500

Storybook: https://stencil-nextjs-example-app.vercel.app/storybook/index.html (`'use client';`)
17 changes: 13 additions & 4 deletions packages/app/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
/* eslint-disable @next/next/no-before-interactive-script-outside-document */
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import Script from 'next/script';
import { FC, PropsWithChildren } from 'react';

const inter = Inter({
subsets: ['latin'],
display: 'swap',
});

export const metadata: Metadata = {
title: 'Next.js ABC Web Components App',
description: 'This Next.js App demonstrates how to use the ABC Web Components.',
};

export default async function RootLayout({ children }: { children: React.ReactNode }) {
const RootLayout: FC<PropsWithChildren> = async ({ children }) => {
await import('abc-web-components-react-wrapper/server');

return (
<html lang="en">
<html lang="en" className={inter.className}>
<body
style={{
width: '50rem',
Expand All @@ -24,8 +31,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
>
<noscript>Please enable JavaScript to view this website. Especially if you use Firefox.</noscript>
{children}
<Script src="./scripts/polyfills/template-shadowroot.js" strategy="beforeInteractive" />
<Script src="/scripts/polyfills/template-shadowroot.js" strategy="beforeInteractive" />
</body>
</html>
);
}
};

export default RootLayout;
56 changes: 56 additions & 0 deletions packages/app/src/app/navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Section } from './section';
import Link from 'next/link';
import { FC } from 'react';

export const Navigation: FC = () => (
<Section
title="Navigation"
variants={[
{
title: 'Meta',
children: (
<ul style={{ listStyle: 'none', padding: 0 }}>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/storybook/index.html">Storybook</Link>
</li>
</ul>
),
},
{
title: 'Test Server Only Approach',
children: (
<ul style={{ listStyle: 'none', padding: 0 }}>
<li>
<Link href="/test/50">50 Elements</Link>
</li>
<li>
<Link href="/test/100">100 Elements</Link>
</li>
<li>
<Link href="/test/200">200 Elements</Link>
</li>
<li>
<Link href="/test/500">500 Elements</Link>
</li>
</ul>
),
},
{
title: 'Todo App',
children: (
<ul style={{ listStyle: 'none', padding: 0 }}>
<li>
<Link href="/todo/wrapper">Wrapper Approach</Link>
</li>
<li>
<Link href="/todo/server-only">Server Only Approach</Link>
</li>
</ul>
),
},
]}
/>
);
5 changes: 4 additions & 1 deletion packages/app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { ButtonWithWrapper } from '@/components/button/button-with-wrapper';
import { DropdownClientOnly } from '@/components/dropdown/dropdown-client-only';
import { DropdownRSC } from '@/components/dropdown/dropdown-rsc';
import { DropdownWithWrapper } from '@/components/dropdown/dropdown-with-wrapper';
import { FC } from 'react';
import { Navigation } from './navigation';
import { Section } from './section';

const Page = () => (
const Page: FC = () => (
<main style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Navigation />
<Section
title="Buttons"
variants={[
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/app/test/[count]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Navigation } from '@/app/navigation';
import { AccordionRSC } from '@/components/accordion/accordion-rsc';
import { ButtonRSC } from '@/components/button/button-rsc';
import { DropdownRSC } from '@/components/dropdown/dropdown-rsc';
import { notFound, redirect } from 'next/navigation';
import { FC } from 'react';
import { Section } from '../../section';
import { notFound, redirect } from 'next/navigation';

export const generateStaticParams = () => [{ count: '50' }, { count: '100' }, { count: '200' }, { count: '500' }];

Expand All @@ -20,9 +21,10 @@ const Page: FC<{ params: { count: string } }> = ({ params: { count: rawCount } }

return (
<main style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Navigation />
<header>
<h1>Test RSC components</h1>
<p>count: {count}</p>
<h1>Test Server Only Approach</h1>
<p>Components count: {count}</p>
</header>
{Array.from({ length: count }).map((_, index) => (
<Section
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/app/todo/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from 'next';
import './styles.css';
import { Navigation } from '../navigation';
import { FC, PropsWithChildren } from 'react';

export const metadata: Metadata = { title: 'Todo - Next.js ABC Web Components App' };

const Layout: FC<PropsWithChildren> = ({ children }) => (
<>
<Navigation />
<div id="todo">{children}</div>
</>
);

export default Layout;
16 changes: 16 additions & 0 deletions packages/app/src/app/todo/server-only/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { AbcTodoInput } from 'abc-web-components-react-wrapper';
import { WithRSCFallback } from 'abc-web-components-react-wrapper/client';
import { ComponentProps, FC } from 'react';
import { useTodo } from './use-todo';

export const Input: FC<ComponentProps<typeof WithRSCFallback>> = ({ rsc }) => {
const { list, setList } = useTodo();

return (
<WithRSCFallback rsc={rsc}>
<AbcTodoInput onTodoInputSubmit={({ detail }) => setList([...list, { text: detail, checked: false }])} />
</WithRSCFallback>
);
};
33 changes: 33 additions & 0 deletions packages/app/src/app/todo/server-only/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { AbcTodoItem } from 'abc-web-components-react-wrapper';
import { WithRSCFallback } from 'abc-web-components-react-wrapper/client';
import { ComponentProps, FC } from 'react';
import { useTodo } from './use-todo';

export const List: FC<ComponentProps<typeof WithRSCFallback>> = ({ rsc }) => {
const { list, setList } = useTodo();

return (
<WithRSCFallback rsc={rsc}>
{list.map(({ checked, text }, index) => {
const props = { checked, text, index };

return (
<AbcTodoItem
{...props}
key={index}
onTodoItemChecked={({ detail }) => {
const copyList = [...list];
copyList[detail].checked = !copyList[detail].checked;
setList(copyList);
}}
onTodoItemRemove={({ detail }) => {
setList([...list.slice(0, detail), ...list.slice(detail + 1)]);
}}
/>
);
})}
</WithRSCFallback>
);
};
28 changes: 28 additions & 0 deletions packages/app/src/app/todo/server-only/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AbcTodoInputServerOnly, AbcTodoItemServerOnly } from 'abc-web-components-react-wrapper';
import { FC } from 'react';
import { Input } from './input';
import { List } from './list';
import { TodoProvider } from './use-todo';

const list = [
{ text: 'my initial todo', checked: false },
{ text: 'Learn about Web Components', checked: true },
];

const Page: FC = () => (
<TodoProvider list={list}>
<h1>Todos Stencil</h1>
<section>
<Input rsc={<AbcTodoInputServerOnly />} />
<ul id="list-container">
<List
rsc={list.map((props, index) => (
<AbcTodoItemServerOnly key={index} {...props} index={index} />
))}
/>
</ul>
</section>
</TodoProvider>
);

export default Page;
23 changes: 23 additions & 0 deletions packages/app/src/app/todo/server-only/use-todo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';

type TodoItem = { text: string; checked: boolean };

const TodoContext = createContext({} as { list: TodoItem[]; setList: (list: TodoItem[]) => void });

export const TodoProvider: FC<PropsWithChildren<{ list: TodoItem[] }>> = ({ children, list: initialList }) => {
const [list, setList] = useState(initialList);

return <TodoContext.Provider value={{ list, setList }}>{children}</TodoContext.Provider>;
};

export const useTodo = () => {
const context = useContext(TodoContext);

if (!context.list) {
throw new Error('useTodo must be used within a TodoProvider');
}

return context;
};
22 changes: 22 additions & 0 deletions packages/app/src/app/todo/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#todo h1 {
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
}

#todo section {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

#list-container {
margin: 0;
padding: 0;
list-style: none;
border-top: 1px solid #e6e6e6;
}
48 changes: 48 additions & 0 deletions packages/app/src/app/todo/wrapper/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client';

import { AbcTodoInput, AbcTodoItem } from 'abc-web-components-react-wrapper';
import { AbcWrapper } from 'abc-web-components-react-wrapper/client';
import { FC, useState } from 'react';

type TodoItem = { text: string; checked: boolean };
const INITIAL_TODOS: TodoItem[] = [
{ text: 'my initial todo', checked: false },
{ text: 'Learn about Web Components', checked: true },
];

const Page: FC = () => {
const [list, setList] = useState<TodoItem[]>(INITIAL_TODOS);

return (
<AbcWrapper>
<h1>Todos Stencil</h1>
<section>
<AbcTodoInput
onTodoInputSubmit={({ detail }) => {
setList([...list, { text: detail, checked: false }]);
}}
/>
<ul id="list-container">
{list.map(({ checked, text }, index) => (
<AbcTodoItem
key={index}
checked={checked}
text={text}
index={index}
onTodoItemChecked={({ detail }) => {
const copyList = [...list];
copyList[detail].checked = !copyList[detail].checked;
setList(copyList);
}}
onTodoItemRemove={({ detail }) => {
setList([...list.slice(0, detail), ...list.slice(detail + 1)]);
}}
/>
))}
</ul>
</section>
</AbcWrapper>
);
};

export default Page;
Loading

0 comments on commit 79f0eb8

Please sign in to comment.