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

refactor(docs): autocomplete dx #3934

Merged
merged 11 commits into from
Nov 6, 2024
6 changes: 1 addition & 5 deletions apps/docs/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"files": ["*.ts", "*.tsx"],
"parserOptions": {
"project": ["apps/docs/tsconfig(.*)?.json"],
"ecmaFeatures": {
Expand All @@ -22,10 +22,6 @@
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
34 changes: 34 additions & 0 deletions apps/docs/content/components/autocomplete/async-filtering.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useAsyncList} from "@react-stately/data";

export default function App() {
let list = useAsyncList({
async load({signal, filterText}) {
let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
let json = await res.json();

return {
items: json.results,
};
},
});

return (
<Autocomplete
className="max-w-xs"
inputValue={list.filterText}
isLoading={list.isLoading}
items={list.items}
label="Select a character"
placeholder="Type to search..."
variant="bordered"
onInputChange={list.setFilterText}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}
41 changes: 41 additions & 0 deletions apps/docs/content/components/autocomplete/async-filtering.raw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useAsyncList} from "@react-stately/data";

type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};

export default function App() {
let list = useAsyncList<SWCharacter>({
async load({signal, filterText}) {
let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
let json = await res.json();

return {
items: json.results,
};
},
});

return (
<Autocomplete
className="max-w-xs"
inputValue={list.filterText}
isLoading={list.isLoading}
items={list.items}
label="Select a character"
placeholder="Type to search..."
variant="bordered"
onInputChange={list.setFilterText}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}
78 changes: 2 additions & 76 deletions apps/docs/content/components/autocomplete/async-filtering.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,5 @@
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useAsyncList} from "@react-stately/data";

export default function App() {
let list = useAsyncList({
async load({signal, filterText}) {
let res = await fetch(\`https://swapi.py4e.com/api/people/?search=\${filterText}\`, {signal});
let json = await res.json();

return {
items: json.results,
};
},
});

return (
<Autocomplete
className="max-w-xs"
inputValue={list.filterText}
isLoading={list.isLoading}
items={list.items}
label="Select a character"
placeholder="Type to search..."
variant="bordered"
onInputChange={list.setFilterText}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}`;

const AppTs = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useAsyncList} from "@react-stately/data";

type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};

export default function App() {
let list = useAsyncList<SWCharacter>({
async load({signal, filterText}) {
let res = await fetch(\`https://swapi.py4e.com/api/people/?search=\${filterText}\`, {signal});
let json = await res.json();

return {
items: json.results,
};
},
});

return (
<Autocomplete
className="max-w-xs"
inputValue={list.filterText}
isLoading={list.isLoading}
items={list.items}
label="Select a character"
placeholder="Type to search..."
variant="bordered"
onInputChange={list.setFilterText}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}`;
import App from "./async-filtering.raw.jsx?raw";
import AppTs from "./async-filtering.raw.tsx?raw";

const react = {
"/App.jsx": App,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";

export function usePokemonList({fetchDelay = 0} = {}) {
const [items, setItems] = React.useState([]);
const [hasMore, setHasMore] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(false);
const [offset, setOffset] = React.useState(0);
const limit = 10; // Number of items per page, adjust as necessary

const loadPokemon = async (currentOffset) => {
const controller = new AbortController();
const {signal} = controller;

try {
setIsLoading(true);

if (offset > 0) {
// Delay to simulate network latency
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
}

const res = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
{signal},
);

if (!res.ok) {
throw new Error("Network response was not ok");
}

let json = await res.json();

setHasMore(json.next !== null);
// Append new results to existing ones
setItems((prevItems) => [...prevItems, ...json.results]);
} catch (error) {
if (error.name === "AbortError") {
// eslint-disable-next-line no-console
console.log("Fetch aborted");
} else {
// eslint-disable-next-line no-console
console.error("There was an error with the fetch operation:", error);
}
} finally {
setIsLoading(false);
}
};

React.useEffect(() => {
loadPokemon(offset);
}, []);

const onLoadMore = () => {
const newOffset = offset + limit;

setOffset(newOffset);
loadPokemon(newOffset);
};

return {
items,
hasMore,
isLoading,
onLoadMore,
};
}

export default function App() {
const [isOpen, setIsOpen] = React.useState(false);
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});

const [, scrollerRef] = useInfiniteScroll({
hasMore,
isEnabled: isOpen,
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
onLoadMore,
});

return (
<Autocomplete
className="max-w-xs"
defaultItems={items}
isLoading={isLoading}
label="Pick a Pokemon"
placeholder="Select a Pokemon"
scrollRef={scrollerRef}
variant="bordered"
onOpenChange={setIsOpen}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}
107 changes: 107 additions & 0 deletions apps/docs/content/components/autocomplete/async-loading-items.raw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from "react";
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";

export type Pokemon = {
name: string;
url: string;
};

export type UsePokemonListProps = {
/** Delay to wait before fetching more items */
fetchDelay?: number;
};

export function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
const [items, setItems] = React.useState<Pokemon[]>([]);
const [hasMore, setHasMore] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(false);
const [offset, setOffset] = React.useState(0);
const limit = 10; // Number of items per page, adjust as necessary

const loadPokemon = async (currentOffset: number) => {
const controller = new AbortController();
const {signal} = controller;

try {
setIsLoading(true);

if (offset > 0) {
// Delay to simulate network latency
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
}

let res = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
{signal},
);

if (!res.ok) {
throw new Error("Network response was not ok");
}

let json = await res.json();

setHasMore(json.next !== null);
// Append new results to existing ones
setItems((prevItems) => [...prevItems, ...json.results]);
} catch (error) {
if (error.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("There was an error with the fetch operation:", error);
}
} finally {
setIsLoading(false);
}
};

React.useEffect(() => {
loadPokemon(offset);
}, []);

const onLoadMore = () => {
const newOffset = offset + limit;

setOffset(newOffset);
loadPokemon(newOffset);
};

return {
items,
hasMore,
isLoading,
onLoadMore,
};
}

export default function App() {
const [isOpen, setIsOpen] = React.useState(false);
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});

const [, scrollerRef] = useInfiniteScroll({
hasMore,
isEnabled: isOpen,
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
onLoadMore,
});

return (
<Autocomplete
className="max-w-xs"
defaultItems={items}
isLoading={isLoading}
label="Pick a Pokemon"
placeholder="Select a Pokemon"
scrollRef={scrollerRef}
variant="bordered"
onOpenChange={setIsOpen}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
);
}
Loading