-
-
Notifications
You must be signed in to change notification settings - Fork 280
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
CommandItem not updating on asynchronous changes #267
Comments
+1 experiencing the same issue. In my case, the items are only "updated" only when the popover closes and then reopened or the search box value is deleted (empty) |
@pacocoursey any help with that? |
Hi @ImamJanjua, since the options are updated when the It should end up with something like this: Kapture.2024-05-30.at.06.35.25.mp4interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}[];
onOpenChange?: ((open: boolean) => void) | undefined;
onValueChange?: ((search: string) => void) | undefined;
value?: string | undefined;
defaultValue?: string | undefined;
isLoading?: boolean;
}
const TableFacetedFilter = <TData, TValue>({
column,
title,
options,
onOpenChange,
onValueChange,
value,
defaultValue,
isLoading,
}: DataTableFacetedFilterProps<TData, TValue>) => {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
// If it's loading, replace it with lookalike component
if (isLoading) {
return (
<Popover onOpenChange={onOpenChange}>
<PopoverTrigger asChild className="w-full">
<Button variant="outline" className="border-dashed">
<PlusCircledIcon className="mr-2 h-4 w-4" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 0 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-full p-0 popover-content-width-same-as-its-trigger"
align="start"
>
<div className="flex items-center border-b px-3">
<Icons.Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input // emulate the Command.Input
className="flex h-10 w-full rounded-md bg-transparent py-3 px-0 text-sm !outline-none placeholder:text-muted-foreground !ring-0 !border-0"
placeholder={title}
defaultValue={value}
autoFocus // keep the input focused
/>
</div>
<div className="flex flex-col items-center justify-center py-6">
<Icons.Spinner className="h-4 w-4 animate-spin" />
<p className="text-center text-sm">Fetching data…</p>
</div>
</PopoverContent>
</Popover>
);
}
// After the loading finished, re-render the options again
return (
<Popover onOpenChange={onOpenChange}>
<PopoverTrigger asChild className="w-full">
<Button variant="outline" className="border-dashed">
<PlusCircledIcon className="mr-2 h-4 w-4" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 0 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-full p-0 popover-content-width-same-as-its-trigger"
align="start"
>
<Command>
<CommandInput
placeholder={title}
onValueChange={onValueChange}
value={value}
defaultValue={defaultValue}
autoFocus
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options?.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value);
} else {
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(
filterValues.length ? filterValues : undefined
);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}; |
+1, same issue here |
+1, same issue here. |
I have two forms of solving this problem. First it's just not use the Input provided by Solution 1 - Best solution for my use caseconst [currentData, setCurrentData] = useState(initialData);
const searchRoleByTerm = async (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target!.value
setIsLoading(true);
try {
const response = await getServerRolesCategoriesAndChannels(
guildId,
"roles",
{
query: value,
}
);
setCurrentData(response.data.roles);
setIsLoading(false);
} catch (error) {
errorToast(
"Unable to search by this term. Please try again or contact the management team"
);
console.log(error);
setIsLoading(false);
}
};
<Command>
<div className="flex items-center border-b border-primary/10 px-3">
<HiSearch className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
onChange={handleDebouncedRoleSearch}
placeholder="Search a role..."
className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-defaultText font-medium disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
<CommandList>...</CommandList>
</Command>
Solution 2This one you have some problems and I don't recommend using it. This one consists of a key on the Command element, that changes when a new result comes, making the Command re-render const [currentData, setCurrentData] = useState(initialData);
const [currentCommandId, setCurrentCommandId] = useState(
Math.random() * 1000
);
const searchRoleByTerm = async (value: string) => {
setIsLoading(true);
try {
const response = await getServerRolesCategoriesAndChannels(
guildId,
"roles",
{
query: value,
}
);
setCurrentData(response.data.roles);
setIsLoading(false);
setCurrentCommandId(Math.random() * 1000); // This updates Command element
} catch (error) {
errorToast(
"Unable to search by this term. Please try again or contact the management team"
);
console.log(error);
setIsLoading(false);
}
};
<Command key={currentCommandId}>
<CommandInput
onValueChange={onValueChange}
/>
<CommandList>...</CommandList>
</Command> But you will see that the input lose the value typed beforeFor that you can set it's value directly after Command has been updated. With something like this: // ...rest of the states
const inputRef = useRef<HTMLInputElement | null>(null);
const searchRoleByTerm = async (value: string) => {
setIsLoading(true);
try {
// ... fetch result
setCurrentCommandId(Math.random() * 1000);
setTimeout(() => {
inputRef.current!.value = value;
inputRef.current!.defaultValue = value;
}, 10);
// This updates the input value after 10ms
// PS: State changes are asynchronous, so if the input updates before Command element re-render, it won't update the input after
} catch (error) {
errorToast(
"Unable to search by this term. Please try again or contact the management team"
);
console.log(error);
setIsLoading(false);
}
};
<Command key={currentCommandId}>
<CommandInput
ref={inputRef}
onValueChange={onValueChange}
/>
<CommandList>...</CommandList>
</Command> If it doesn't work for someone I would happy to help, I'm also available on Discord as |
Well this is annoying My case was for hiding / showing the CommandList content instead of having a empty state view, (using this as a select input with auto complete, withoyt popover) Ended up having a "use client";
import React, { useEffect } from "react";
import { Link1Icon, PersonIcon, PlusIcon } from "@radix-ui/react-icons";
import { Command as CommandPrimitive } from "cmdk";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { useState } from "react";
import { useEditorState } from "../editor";
import { SupabaseLogo } from "@/components/logos";
import { SYSTEM_GF_CUSTOMER_UUID_KEY } from "@/k/system";
import { cn } from "@/utils";
import { PrivateEditorApi } from "@/lib/private";
import { GridaSupabase } from "@/types";
const Input = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
Input.displayName = "CommandInput";
export function NameInput({
autoFocus,
value,
onValueChange,
}: {
autoFocus?: boolean;
value?: string;
onValueChange?: (value: string) => void;
}) {
const ref = React.useRef<React.ElementRef<
typeof CommandPrimitive.Input
> | null>(null);
const [state] = useEditorState();
const [open, setOpen] = useState<boolean>(false);
const [focus, setFocus] = useState<boolean>(false);
const [tableSchema, setTableSchema] = useState<
GridaSupabase.SupabaseTable["sb_table_schema"] | undefined
>();
useEffect(() => {
if (state.connections.supabase) {
PrivateEditorApi.SupabaseConnection.getConnectionTable(
state.form_id
).then(({ data }) => {
setTableSchema(data.data.sb_table_schema);
});
}
}, [state.form_id, state.connections.supabase]);
useEffect(() => {
setOpen(focus && !!value);
}, [value, focus]);
useEffect(() => {
// https://github.com/pacocoursey/cmdk/issues/267
if (open || (!open && !value)) {
ref.current?.focus();
}
}, [open, ref, value]);
const onSelect = (val: string) => {
onValueChange?.(val);
setOpen(false);
setFocus(false);
};
return (
<Command key={String(open)} className="rounded-lg border">
<Input
required
autoFocus={autoFocus}
ref={ref}
placeholder="field_name"
value={value}
onValueChange={onValueChange}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
/>
<CommandList>
{open && (
<>
{value && (
<>
<CommandGroup>
<CommandItem key={"current"} onSelect={onSelect}>
<PlusIcon className="mr-2 h-4 w-4" />
<span>{value}</span>
</CommandItem>
</CommandGroup>
</>
)}
<CommandSeparator />
<CommandGroup heading="System">
<CommandItem
key={SYSTEM_GF_CUSTOMER_UUID_KEY}
onSelect={onSelect}
>
<PersonIcon className="mr-2 h-4 w-4" />
<span>{SYSTEM_GF_CUSTOMER_UUID_KEY}</span>
</CommandItem>
</CommandGroup>
{state.connections.supabase && (
<>
<CommandSeparator />
<CommandGroup
heading={
<>
<SupabaseLogo className="inline w-4 h-4 me-1 align-middle" />{" "}
Supabase
</>
}
>
{Object.keys(tableSchema?.properties ?? {}).map((key) => {
// const property = tableSchema?.properties[key];
return (
<CommandItem key={key} onSelect={onSelect}>
<Link1Icon className="mr-2 h-4 w-4" />
<span>{key}</span>
</CommandItem>
);
})}
</CommandGroup>
</>
)}
</>
)}
</CommandList>
</Command>
);
} Final behaviour: Screen.Recording.2024-06-16.at.12.34.10.AM.mov |
@ImamJanjua @hisamafahri @TalalBadreddine @guilhermeseckert @TheRafaelFarias @softmarshmallow I've come across a similar issue, and it has been resolved by adding the props Like this : My guess is that the Command component is trying to filter its CommandItem with the CommandInput's value, but it's happening way before we populate the items with our asynchronous data, so it has a weird behavior I hope that this will help people that have this issue too (I have found this solution thanks to this codepen) |
Thanks - encountered this issue while implementing an autosuggest/autocomplete following this comment from a shadcn-ui issue as a starting point. Adding |
Hi, as u can see in the video i am trying to implement a component which on input change gets the addres predictions from server. Its using PopOver from shadcn for the for the popover.
Now the thing is that when the first adresses arrive than it will render it but on chnage of the input value, it gets the new predictions but it will not render them?
Bildschirmaufnahme.2024-05-27.um.13.07.23.mov
The text was updated successfully, but these errors were encountered: