Remix+CloudflareでWebサイトを作る 38(Selectで未選択状態にする、Prismaでランダムに取得、URLのクエリに関するコンテクスト作成、Biomeの導入時にエラー)
<Select >
コンポーネントで、未選択状態にしたい
【2024-10-20】shadcnの
UIのパターン
- パターン1: どこかにキャンセルボタンを用意する
- パターン2: 選択中のItemをもう一度クリックしたらキャンセルされる
- パターン3: そもそも選択肢の中に未選択状態にする選択肢を用意する
- 選択肢をいじるだけなので一番楽そう
パターン1(うまく動かず)
fakerで適当な会社名を入れている。
会社が選択状態になると、選択しているItemの右側にバツ印のアイコンが表示されるようにした。
以下は元のnpx shadcn@latest add select
をしてからの変更点。
ざっくり、{children}
を囲んであとは呼び出し元で<SelectTrigger cancell>
とすればOKでは?と思ったけど、この場合<Button>
をクリックしてもonClickが発火せずに選択肢が開く状態になってしまうのでダメだった。
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
+ import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { cancell?: boolean }
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
+ <div className="flex items-center">
{children}
+ {props.cancell && (
+ <Button
+ variant="ghost"
+ className="h-6 w-6 ml-2 hover:bg-none"
+ size="icon"
+ >
+ <X className="h-4 w-4 opacity-50" />
+ </Button>
+ )}
+ </div>
<SelectPrimitive.Icon asChild>
<ChevronDown className="ml-1 h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
結論
一旦パターン3の選択肢に未選択のものを入れておくので回避しておく。
パターン2が一番気持ち良いんだけどな。今後やりたいものとしてバックログに入れておこう。
【2024-10-23】Prismaでランダムに複数の要素を取得する方法
ここを参考に書いた。
function randomPick<T>(array: T[]): T {
const randomIndex = Math.floor(Math.random() * array.length);
return array[randomIndex];
}
export async function getRandomUsers() {
const count = await client.user.count({
where: {
isArchived: false,
},
});
const take = 3;
const skip = Math.max(0, Math.floor(Math.random() * count) - take); // 0以上、count-take 以下のランダムな数を取得
const orderBy = randomPick(["id", "name", "email"]);
const orderDir = randomPick(["asc", "desc"]);
const users = await client.user.findMany({
where: {
isArchived: false,
},
take,
skip,
orderBy: { [orderBy]: orderDir },
select: {
id: true,
name: true,
email: true,
},
});
return users;
}
cloudflare/wrangler-action@v3
の outputs.deployment-url
をクリックしてもデプロイ先ではなく、リポジトリに遷移するようになっている
【2024-10-24】
前まで機能していたはずでは?なんでだ?
ToDo
【2024-10-25】DatTableのフィルタリングによってURLのクエリを更新してデータを取ってきたいのでコンテクストを作る
背景
DataTableでテキストを入力したり、Dropdownから選択したりすることで色々とフィルタリングをしたい。
shadcnで作っているけどUIのイメージはPrimeVueを参考にしている。
以前使ったことがあるけどカラムごとにフィルタリングするコンポーネントがあるのが好み。
以下は、shadcnのDataTableを拡張したexampleだけど、こんな感じでフィルタリング箇所が全部まとまってるUIもあるけど、カラムごとにあるのが直感的な気がして好きなんだよな。
やりたいこと
- 上のPrimeVueの例でいうとNameカラムのinputや、Agentカラムのselectをいじる
- URLのクエリパラメーターが更新される
- loaderが走る
- クエリパラメーターを元にフィルタリング
- 取得したデータを表示
実装
2で更新するときに、いろいろなコンポーネントで現在のクエリパラメータを更新する処理をconst navigate = useNavigate();
で書くのがしんどかったのでコンテクストを使った。
import { useLocation, useNavigate } from "@remix-run/react";
import React, { createContext, useContext, useEffect, useState } from "react";
export type Query = {
key: string;
value: string;
};
type QueryContextType = {
query: URLSearchParams;
upsertQuery: (newQuery: Query) => void;
deleteQuery: (key: string) => void;
};
const QueryContext = createContext<QueryContextType>({
query: new URLSearchParams(),
upsertQuery: () => {},
deleteQuery: () => {},
});
export const useQuery = () => {
return useContext(QueryContext);
};
function QueryProvider({ children }: { children: React.ReactNode }) {
const location = useLocation();
const navigate = useNavigate();
const [query, setQueryState] = useState(new URLSearchParams(location.search));
useEffect(() => {
setQueryState(new URLSearchParams(location.search));
}, [location.search]);
/**
* クエリを追加・更新する
*/
const upsertQuery = ({ key, value }: Query) => {
const newQuery = new URLSearchParams(query);
newQuery.set(key, String(value));
setQueryState(newQuery);
navigate(`${location.pathname}?${newQuery.toString()}`, { replace: true });
};
/**
* クエリを削除する
*/
const deleteQuery = (key: string) => {
const newQuery = new URLSearchParams(query);
if (newQuery.has(key)) {
newQuery.delete(key);
setQueryState(newQuery);
navigate(`${location.pathname}?${newQuery.toString()}`, {
replace: true,
});
}
};
return (
<QueryContext.Provider value={{ query, upsertQuery, deleteQuery }}>
{children}
</QueryContext.Provider>
);
}
export default QueryProvider;
呼び出し側では以下の様な感じで使うとURLが http://localhost:5173/hoge?name=<YOUR_INPUT>
という感じになる。
const { upsertQuery, deleteQuery } = useQuery();
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
if (value) {
upsertQuery({ key: "name", value }); // クエリを追加 or 更新
} else {
deleteQuery("name"); // なにもない場合はクエリから削除
}
};
monorepo構成にしてService Bindingsの対応したほうが良いと思ってるんだけど面倒でできない。
気が向いたらやりたいけどあと半年は気が向きそうにない。
The package "punycode" wasn't found on the file system but is built into node.
と怒られた。
【2024-10-25】Biome導入してeslintのパッケージ消したらやったこと
以下を全部消した。
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.0",
"eslint-plugin-react-hooks": "^4.6.2",
エラー内容
デプロイ時に以下のエラーが発生。
"markdown-it": "^13.0.2",
でエラーが発生している。
markdown-it パッケージが punycode という Node.js のネイティブモジュールに依存しているため発生しています。Cloudflare Workers はブラウザライクな環境のため、ネイティブ Node.js モジュールを直接使用できません。
🚀 Running Wrangler Commands
✘ [ERROR] Build failed with 1 error:
✘ [ERROR] Could not resolve "punycode"
../node_modules/markdown-it/lib/index.js:14:27:
14 │ var punycode = require('punycode');
╵ ~~~~~~~~~~
The package "punycode" wasn't found on the file system but is built into node. Are you trying to bundle for node? You can use "platform: 'node'" to do that, which will remove this error.
原因探る
1つずつ消していってどこでエラーが発生するのか探ってみると、eslint-plugin-react-hooks
がpackage.jsonに残っていれば発生しなかった。
理由がわからん...。
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.0",
"eslint-plugin-react-hooks": "^4.6.2", // これがあればこのエラーが発生しない
その他
biome は Prettier と違う点としてデフォルトがインデントタブです。
インデントにタブを使うことはアクセシビリティ上の利点があるようです。
知らなかった。
とはいえGitHubでdiff見る時スペース4つ分になるのが気に食わないのでデフォルトの設定は使わずにスペースに変えます。
lintを実行したら、Found 984 errors.
...。
明日は少しずつ対象となるディレクトリ分けてlintで書き換え実施していこう。
てか実行速度が早すぎる。
強めのこだわりが無いのでインデントがスペースなこと以外はBiome様の仰せのままに行く。
見た記事