Closed7

Remix+CloudflareでWebサイトを作る 25(Tanstack Tableを使う・Tailwindのアルファチャンネル適用されない問題・MultiSelectコンポーネント作成)

saneatsusaneatsu

【2024-05-19】Bracket Pair Colorizerをextensions.jsonで非推奨にしておく

https://qiita.com/miruon/items/a45e13db9930dd429d12

extensions.jsonきれいに書こうと思って今使っている拡張機能改めて一つずつ調べていたらこの記事を見つけた。

要はBracket Pair Colorizerをextensions.jsonで非推奨だからVSCodeのデフォルトの機能を使おうねという話。

書いとこう。

.vscode/extensions.json
{
  "unwantedRecommendations": [
    // VSCodeデフォルトの機能を使うこと
    "BracketPairColorDLW.bracket-pair-color-dlw"
  ]
saneatsusaneatsu

【2024-05-23】Hello, Tanstack Table!

とは?

テーブルの実装のための機能を色々揃えているHeadless UIライブラリ。

ページネーション機能を備えた基本的なテーブルを作った。
機能がてんこ盛りすぎた。

例を見て書く

https://dev.classmethod.jp/articles/introduce-tanstack-table/
記事内にスタイルを排除したStackblitzのExampleコードがあるのでそれ見るとだいたいわかった。

https://tanstack.com/table/v8/docs/framework/react/examples/pagination
公式のも参考になった

実装
PagenationDataTable.tsx
import { useMemo, useState } from "react";

import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table";
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
} from "lucide-react";

import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "~/components/ui/select";
import {
  Table as TableUI,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "~/components/ui/table";

import type { ArticleColumn } from "./admin/table/article/column";
import type {
  ColumnDef,
  ColumnFiltersState,
  PaginationState,
  SortingState,
  VisibilityState,
  Table,
} from "@tanstack/react-table";

function PageIndex<T>({ table }: { table: Table<T> }) {
  return (
    <div className="flex items-center">
      <Input
        type="number"
        defaultValue={table.getState().pagination.pageIndex + 1}
        onChange={(e) => {
          const page = e.target.value ? Number(e.target.value) - 1 : 0;
          table.setPageIndex(page);
        }}
        className="border w-16 text-center pr-1 mr-2"
      />
      <span className="text-sm text-muted-foreground">ページへ</span>
    </div>
  );
}

const PAGE_SIZES = [2, 25, 50, 100] as const;

function PageSize<T>({
  table,
  defaultValue,
}: {
  table: Table<T>;
  defaultValue: string;
}) {
  return (
    <Select
      defaultValue={defaultValue}
      onValueChange={(value) => table.setPageSize(Number(value))}
    >
      <SelectTrigger className="w-28">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {PAGE_SIZES.map((pageSize) => (
          <SelectItem
            key={pageSize}
            value={pageSize.toString()}
            className="rounded-lg"
          >
            {pageSize} 件表示
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}

function PagenationButtons<T>({ table }: { table: Table<T> }) {
  return (
    <div className="space-x-1">
      <Button
        variant="outline"
        size="icon"
        onClick={() => table.firstPage()}
        disabled={!table.getCanPreviousPage()}
      >
        <ChevronsLeft className="w-4 h-4" />
      </Button>
      <Button
        variant="outline"
        size="icon"
        onClick={() => table.previousPage()}
        disabled={!table.getCanPreviousPage()}
      >
        <ChevronLeft className="w-4 h-4" />
      </Button>
      <Button
        variant="outline"
        size="icon"
        onClick={() => table.nextPage()}
        disabled={!table.getCanNextPage()}
      >
        <ChevronRight className="w-4 h-4" />
      </Button>
      <Button
        variant="outline"
        size="icon"
        onClick={() => table.lastPage()}
        disabled={!table.getCanNextPage()}
      >
        <ChevronsRight className="w-4 h-4" />
      </Button>
    </div>
  );
}

export function PagenationDataTable<T extends ArticleColumn>({
  data,
  columns,
  pageIndex = 0,
  pageSize = PAGE_SIZES[0], // 1ページに表示するデータの数
}: {
  data: T[];
  columns: ColumnDef<T>[];
  pageIndex?: PaginationState["pageIndex"];
  pageSize?: PaginationState["pageSize"];
  filterColumn?: string;
  filterPlaceholder?: string;
}) {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
  const [rowSelection, setRowSelection] = useState({});

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    onPaginationChange: setPagination,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
      pagination,
    },
  });

  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: pageIndex,
    pageSize: pageSize,
  });
  const from = useMemo(
    () => pagination.pageIndex * pagination.pageSize + 1,
    [pagination.pageIndex, pagination.pageSize]
  );
  const to = useMemo(
    () =>
      Math.min(
        from + pagination.pageSize - 1,
        table.getFilteredRowModel().rows.length
      ),
    [from, table, pagination.pageSize]
  );

  return (
    <div className="w-full">
      <div className="flex items-center py-4">
        <Input
          placeholder="検索"
          className="max-w-sm"
          value={(table.getState().globalFilter as string) ?? ""}
          onChange={(e) => table.setGlobalFilter(e.target.value)}

          // 特定のカラムのみ検索する場合
          // value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
          // onChange={(event) => {
          //   table.getColumn("title")?.setFilterValue(event.target.value);
          // }}
        />
      </div>
      <div className="rounded-xl border">
        <TableUI>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <TableHead key={header.id}>
                      {header.isPlaceholder
                        ? null
                        : flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                    </TableHead>
                  );
                })}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  data-state={row.getIsSelected() && "selected"}
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  データがありません
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </TableUI>
      </div>

      <div className="flex justify-between items-center space-x-2 py-4">
        <PageIndex table={table} />
        <div className="flex space-x-4 items-center">
          <div className="text-sm text-muted-foreground">
            {from} ~ {to} / {table.getFilteredRowModel().rows.length}</div>
          <PageSize table={table} defaultValue={pageSize.toString()} />
          <PagenationButtons table={table} />
        </div>
      </div>
    </div>
  );
}

saneatsusaneatsu

【2024-05-25】shadcnのSelectItemコンポーネントで選択解除するためにvalueに空文字を設定すると無限に再レンダリングされる問題

https://github.com/shadcn-ui/ui/issues/2054

Issueあるけど自動で閉じられてる。

onValueChange()内で、value毎に場合分けをしてみると無限レンダリングは回避されるが、以下のコードだとALLを選択したときに、UI上はなぜか「1つ前に選択していたものが選択されていたことになる」というバグ?があるのでこれも有効ではない。

<Select
  defaultValue=""
  value={columnFilterValue?.toString()}
  onValueChange={(value) => {
    console.log(value);
    column.setFilterValue(value === "ALL" ? "" : value);
  }}
>

ということで普通に <select /><option / > 使って実装した。

https://tailwindui.com/components/application-ui/forms/select-menus

選択肢のUIがデフォルトでは気に食わなくて装飾頑張りたければここらへん見れば助かりそう。

saneatsusaneatsu

【2024-05-25】Tailwindのスラッシュ記法によるアルファチャンネルの適用がうまくいかない

起こったこと

色の設定ちゃんと書くためにTailwind CSSの設定を色々と書き換え中。

https://tailscan.com/colors

Tailwindの色とHSLの対応表役に立った。

tailsind.css
/* 適当に紫色にしてみる */
--primary: 263, 70%, 50%;
tailwind.config.ts
        // デフォルトから変えていない
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },


紫いろはちゃんと反映されている。

className="bg-primary text-primary-foreground hover:bg-primary/90" としている箇所なんだけど、ホバーすると背景が白色になってしまう。
つまり、hover:bg-primary/90 がうまく動いていない。

解決

https://github.com/tailwindlabs/tailwindcss/issues/9143#issuecomment-1802151691

こうすることで反映された。

tailwind.config.ts
        primary: {
-         DEFAULT: "hsl(var(--primary))",
+         DEFAULT: "hsl(var(--primary), <alpha-value>)",
          // DEFAULT: "hsl(var(--primary)/<alpha-value>)", これはうまくいかない
          foreground: "hsl(var(--primary-foreground))",
        },
saneatsusaneatsu

そういや、Lighthouseでログイン後のページを対象に計測したい場合どうすりゃいいんだと思ったけどChrome Extension使わないでDeveloper toolから計測すればいいだけなのか。便利すぎ。

saneatsusaneatsu

【2024-05-26】npx prisma generate してもうまく型定義が更新されない

  1. schema.prisma を更新
  2. npx prisma generateを実行
  3. VSCodeをリロードすると反映された
saneatsusaneatsu

【2024-05-26】shadcnでMultiSelectコンポーネントを作成する

【2024-05-12】初期描画が遅い原因を探る のときに調べたけどAutocomplete(MultipleSelect)実装する。

https://github.com/shadcn-ui/ui/issues/66

結構長いIssueで色々とコードが貼られているが結論以下のがちゃんと動いていて良さそう。
Issueのリンクと、サンプルコードのリンク、スクショを貼っておく。

例1(これを採用)

通常labelを検索してvalueにidとかを入れておきたいのに、検索対象がvalueになってしまっているのでそこだけ直した。

例2

例3

例4

例5

Headless UI使った版。

このスクラップは6ヶ月前にクローズされました