这是用户在 2024-5-4 23:15 为 https://www.remixfast.com/blog/remix-modal-route#modal-route 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Remix Modal Route 混音模态路线

Learn how to develop and display route in a Modal.
了解如何在模态中开发和显示路线。

Manish Dalal - Updated on 11 Mar 2024 - Originally Posted on 18 Sep 2022. - 6 min read
Manish Dalal - 更新于 2024 年 3 月 11 日 - 最初发布于 2022 年 9 月 18 日。- 阅读 6 分钟

In this blog post, we will cover how to display a route in Modal, aka Modal Route, in Remix. We will be using shadcn/ui (Radix) Dialog/Modal component, but you can easily replace it with your preferred Modal implementation! Lets see how to do this.
在这篇博文中,我们将介绍如何在 Remix 中显示 Modal 中的路线(又名 Modal Route)。我们将使用 shadcn/ui (Radix) 对话框/模态组件,但您可以轻松地将其替换为您喜欢的模态实现!让我们看看如何做到这一点。

Content 内容

Stater Project 斯塔特项目

Start by creating a new Remix project using create-remix:
首先使用 create-remix 创建一个新的 Remix 项目:

npx create-remix@latest modal-route
npx create-remix@latest modal-route

This will create basic remix project.
这将创建基本的混音项目。

Next install Tailwind CSS
接下来安装 Tailwind CSS

npm install -D tailwindcss
npx tailwindcss init
npm install -D tailwindcss
npx tailwindcss init

Next Run the shadcn-ui init command to setup your project for adding shadcn/UI components
接下来运行 shadcn-ui init 命令来设置项目以添加 shadcn/UI 组件

npx shadcn-ui@latest init
npx shadcn-ui@latest init

You will be asked a few questions to configure components.json:
系统会询问您几个问题来配置 Components.json:

Would you like to use TypeScript (recommended)? yes
Which style would you like to use?  Default
Which color would you like to use as base color?  Slate
Where is your global CSS file?  app/tailwind.css
Do you want to use CSS variables for colors?  yes
Where is your tailwind.config.js located?  tailwind.config.js
Configure the import alias for components:  ~/components
Configure the import alias for utils:  ~/lib/utils
Are you using React Server Components?  no
Would you like to use TypeScript (recommended)? yes
Which style would you like to use?  Default
Which color would you like to use as base color?  Slate
Where is your global CSS file?  app/tailwind.css
Do you want to use CSS variables for colors?  yes
Where is your tailwind.config.js located?  tailwind.config.js
Configure the import alias for components:  ~/components
Configure the import alias for utils:  ~/lib/utils
Are you using React Server Components?  no

And finally add tailwind.css to the app. In app/root.tsx file, import the tailwind.css file:
最后将 tailwind.css 添加到应用程序中。在 app/root.tsx 文件中,导入tailwind.css文件:

import styles from './tailwind.css';
 
export const links: LinksFunction = () => [
  { rel: 'stylesheet', href: styles },
  ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []),
];
import styles from './tailwind.css';
 
export const links: LinksFunction = () => [
  { rel: 'stylesheet', href: styles },
  ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []),
];

We will use the popular list-detail route pattern to display widgets. We will show user list of widgets and when user clicks an item, show the selected item details in a Modal view.
我们将使用流行的列表详细信息路由模式来显示小部件。我们将显示小部件的用户列表,并且当用户单击某个项目时,在模态视图中显示所选项目的详细信息。

First lets add some code to retrieve list of widgets.
首先让我们添加一些代码来检索小部件列表。

Create app\models folder and add widget.ts file. Copy following code into app\models\widget.ts:
创建 app\models 文件夹并添加 widget.ts 文件。将以下代码复制到 app\models\widget.ts

export interface Widget {
  widgetId: number;
  widgetName: string;
  widgetNumber: string;
}
 
export function getNewWidget() {
  return {
    widgetId: undefined,
    widgetName: '',
    widgetNumber: '',
  };
}
export interface Widget {
  widgetId: number;
  widgetName: string;
  widgetNumber: string;
}
 
export function getNewWidget() {
  return {
    widgetId: undefined,
    widgetName: '',
    widgetNumber: '',
  };
}

Here we are declaring Widget model type and getNewWidget function to return empty widget.
这里我们声明 Widget 模型类型和 getNewWidget 函数以返回空小部件。

Next add widget.server.ts file to app\models. In real project, this is where you will be calling your backend/database. For this example, we will just create dummy data.
接下来将 widget.server.ts 文件添加到 app\models 。在实际项目中,这是您调用后端/数据库的地方。对于本示例,我们将仅创建虚拟数据。

import { Widget } from '~/models/widget';
 
// get list of widgets
export async function getList() {
  const widgets: Widget[] = [];
  for (let i = 0; i < 33; i++) {
    widgets.push({
      widgetId: i + 1,
      widgetName: `Widget ${i + 1}`,
      widgetNumber: `W-${i + 1}`,
    });
  }
  return widgets;
}
 
// get a single widget
export async function get(id: number) {
  return {
    widgetId: id,
    widgetName: `Widget ${id}`,
    widgetNumber: `W-${id}`,
  };
}
import { Widget } from '~/models/widget';
 
// get list of widgets
export async function getList() {
  const widgets: Widget[] = [];
  for (let i = 0; i < 33; i++) {
    widgets.push({
      widgetId: i + 1,
      widgetName: `Widget ${i + 1}`,
      widgetNumber: `W-${i + 1}`,
    });
  }
  return widgets;
}
 
// get a single widget
export async function get(id: number) {
  return {
    widgetId: id,
    widgetName: `Widget ${id}`,
    widgetNumber: `W-${id}`,
  };
}

Ok, now that we have a way to get data, lets show the list of widgets UI.
好的,现在我们有了获取数据的方法,让我们显示小部件 UI 列表。

Modal Route

Display Item List 显示项目列表

We will show users a list of items and when they click an item, show the selected item details in a Modal view, this way preserving the user context.
我们将向用户显示项目列表,当他们单击某个项目时,在模态视图中显示所选项目的详细信息,这样可以保留用户上下文。

Create app\routes\widget.tsx as shown below:
创建 app\routes\widget.tsx 如下所示:

import { json } from "@remix-run/node";
import { Link, Outlet, useLoaderData } from "@remix-run/react";
 
import { Widget } from "~/models/widget";
import * as widgetDb from "~/models/widget.server";
 
export async function loader() {
  const widgetList: Widget[] = await widgetDb.getList();
  return json({ widgetList });
}
 
export default function WidgetRoute() {
  const { widgetList } = useLoaderData<typeof loader>();
  return (
    <div className="h-full overflow-auto">
      {widgetList.map((w) => (
        <div key={w.widgetId} className="m-4 rounded-md border p-4">
          <div className="text-large font-semibold">{w.widgetName}</div>
        </div>
      ))}
    </div>
  );
}
import { json } from "@remix-run/node";
import { Link, Outlet, useLoaderData } from "@remix-run/react";
 
import { Widget } from "~/models/widget";
import * as widgetDb from "~/models/widget.server";
 
export async function loader() {
  const widgetList: Widget[] = await widgetDb.getList();
  return json({ widgetList });
}
 
export default function WidgetRoute() {
  const { widgetList } = useLoaderData<typeof loader>();
  return (
    <div className="h-full overflow-auto">
      {widgetList.map((w) => (
        <div key={w.widgetId} className="m-4 rounded-md border p-4">
          <div className="text-large font-semibold">{w.widgetName}</div>
        </div>
      ))}
    </div>
  );
}

Route file has loader method to get list of widgets and WidgetRoute component to display list of widgets.
路由文件有 loader 方法来获取小部件列表和 WidgetRoute 组件来显示小部件列表。

We are using hook useLoaderData get widgetList that was sent from our loader and just iterate over it to display list of widgets.
我们使用钩子 useLoaderData 获取从加载器发送的 widgetList,然后迭代它以显示小部件列表。

Start project with npm run dev and go to http://localhost:3000/widget
npm run dev 开始项目并转到 http://localhost:3000/widget

You should see first 20 widgets 🚀 You can even turn off JavaScript and it will still show first 20 widgets thanks to Remix SSR! ✨
您应该会看到前 20 个小部件🚀 您甚至可以关闭 JavaScript,但由于 Remix SSR,它仍然会显示前 20 个小部件! ✨

Display Item Detail 显示项目详细信息

When user selects an item from the widget list, we want to display the selected item details.
当用户从小部件列表中选择一个项目时,我们希望显示所选项目的详细信息。

We will use Link component to let user select an item. The to property of link component will let user navigate to item detail by changing URL from /widget to /widget/${id}
我们将使用 Link 组件让用户选择一个项目。链接组件的 to 属性将允许用户通过将 URL 从 /widget 更改为 /widget/${id} 来导航到项目详细信息

And to show item details, add an Outlet component. Remix docs define Outlet as "A component rendered inside of a parent route that shows where to render the matching child route". Basically, as the name implies, it provides an outlet to render child route inside the parent route!
要显示项目详细信息,请添加 Outlet 组件。 Remix 文档将 Outlet 定义为“在父路由内部呈现的组件,显示在何处呈现匹配的子路由”。基本上,顾名思义,它提供了一个在父路由内渲染子路由的出口!

Add <Outlet \> component just below <div className="h-full overflow-auto"> in app\routes\widget.tsx file and wrap each Widget in a Link to navigate to /widget/:id detail route.
app\routes\widget.tsx 文件中的 <div className="h-full overflow-auto"> 下方添加 <Outlet \> 组件,并将每个小部件包装在链接中以导航到 /widget/:id 详细路由。

export default function WidgetRoute() {
  const { widgetList } = useLoaderData<typeof loader>();
  return (
    <div className="h-full overflow-auto">
      <Outlet />
      {widgetList.map((w) => (
        <Link key={w.widgetId} to={`${w.widgetId}`}>
          <div className="m-4 rounded-md border p-4">
            <div className="text-large font-semibold">{w.widgetName}</div>
          </div>
        </Link>
      ))}
    </div>
  );
}
export default function WidgetRoute() {
  const { widgetList } = useLoaderData<typeof loader>();
  return (
    <div className="h-full overflow-auto">
      <Outlet />
      {widgetList.map((w) => (
        <Link key={w.widgetId} to={`${w.widgetId}`}>
          <div className="m-4 rounded-md border p-4">
            <div className="text-large font-semibold">{w.widgetName}</div>
          </div>
        </Link>
      ))}
    </div>
  );
}

And remember to import the Outlet component
并记得导入 Outlet 组件

import { Link, Outlet, useLoaderData } from '@remix-run/react';
import { Link, Outlet, useLoaderData } from '@remix-run/react';

Ok, now lets display the widget details. Add app\routes\widget.$id.tsx as follows:
好的,现在让我们显示小部件的详细信息。添加 app\routes\widget.$id.tsx 如下:

import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useNavigate } from "@remix-run/react";
 
import { getNewWidget } from "~/models/widget";
import * as widgetDb from "~/models/widget.server";
 
export async function loader({ params, request }: LoaderFunctionArgs) {
  const id = +(params?.id || 0);
  if (!id || id == 0) {
    return json({ widget: getNewWidget() });
  }
  //
  const data = await widgetDb.get(id);
  if (!data) {
    throw new Response("Widget not found", { status: 404 });
  }
  return json({ widget: data });
}
 
export default function WidgetDetailRoute() {
  const { widget } = useLoaderData<typeof loader>();
  return (
    <div className=" rounded-md border p-4">
      <div className="text-large font-semibold">{widget.widgetName}</div>
      <div>{widget.widgetNumber}</div>
    </div>
  );
}
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useNavigate } from "@remix-run/react";
 
import { getNewWidget } from "~/models/widget";
import * as widgetDb from "~/models/widget.server";
 
export async function loader({ params, request }: LoaderFunctionArgs) {
  const id = +(params?.id || 0);
  if (!id || id == 0) {
    return json({ widget: getNewWidget() });
  }
  //
  const data = await widgetDb.get(id);
  if (!data) {
    throw new Response("Widget not found", { status: 404 });
  }
  return json({ widget: data });
}
 
export default function WidgetDetailRoute() {
  const { widget } = useLoaderData<typeof loader>();
  return (
    <div className=" rounded-md border p-4">
      <div className="text-large font-semibold">{widget.widgetName}</div>
      <div>{widget.widgetNumber}</div>
    </div>
  );
}

Just like typical Remix route file, we have a loader and WidgetDetailRoute component. Loader retrieves widget id from URL params and gets widget details for that id. useLoaderData hook is then used to get widget detail in WidgetDetailRoute component and display details.
就像典型的 Remix 路由文件一样,我们有一个加载器和 WidgetDetailRoute 组件。加载器从 URL 参数中检索小部件 ID,并获取该 ID 的小部件详细信息。然后使用 useLoaderData 钩子获取 WidgetDetailRoute 组件中的小部件详细信息并显示详细信息。

Start project with npm run dev and go to http://localhost:3000/widget and select a widget, you should see URL change to /widget/:id and UI will display the selected widget details at top of the page, where the <Outlet/> component was placed.
使用 npm run dev 启动项目,转到 http://localhost:3000/widget 并选择一个小部件,您应该看到 URL 更改为 /widget/:id 并且 UI 将在页面顶部显示所选小部件的详细信息,其中放置了 <Outlet/> 组件。

Now that we have the basic item detail route working, lets display Widget details in a Modal!
现在我们已经有了基本的项目详细信息路由,让我们在模态中显示小部件详细信息!

Start by installing Dialog component
首先安装Dialog组件

npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dialog

Next, change WidgetDetailRoute function in file widget.$id.tsx as follows:
接下来,更改文件 widget.$id.tsx 中的 WidgetDetailRoute 函数,如下所示:

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog";
 
export default function WidgetDetailRoute() {
 
  const { widget } = useLoaderData<typeof loader>();
  const navigate = useNavigate();
 
  const handleClose = () => {
    navigate(-1);
  };
 
  return (
    <Dialog
      open={true}
      onOpenChange={(open: boolean) => {
        open ? () => {} : handleClose();
      }}
    >
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Widget</DialogTitle>
        </DialogHeader>
        <div className="grid gap-4 py-4">
          <div className="grid grid-cols-4 items-center gap-4">
            {widget.widgetName}
          </div>
          <div className="grid grid-cols-4 items-center gap-4">
            {widget.widgetNumber}
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog";
 
export default function WidgetDetailRoute() {
 
  const { widget } = useLoaderData<typeof loader>();
  const navigate = useNavigate();
 
  const handleClose = () => {
    navigate(-1);
  };
 
  return (
    <Dialog
      open={true}
      onOpenChange={(open: boolean) => {
        open ? () => {} : handleClose();
      }}
    >
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Widget</DialogTitle>
        </DialogHeader>
        <div className="grid gap-4 py-4">
          <div className="grid grid-cols-4 items-center gap-4">
            {widget.widgetName}
          </div>
          <div className="grid grid-cols-4 items-center gap-4">
            {widget.widgetNumber}
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}

We are using Radix Dialog/Modal component to display Widget Details. When the route renders in the Outlet, UI will show Dialog as we have it open by default. When user closes dialog, we navigate back to the previous parent route using useNavigate hook.
我们使用 Radix Dialog/Modal 组件来显示小部件详细信息。当路由在 Outlet 中呈现时,UI 将显示 Dialog,因为我们默认打开它。当用户关闭对话框时,我们使用 useNavigate 钩子导航回之前的父路由。

Update file for appropriate imports and start project with npm run dev and go to http://localhost:3000/widget and select a widget, you should see URL change from /widget to /widget/:id and UI will display the selected widget details in a Modal Dialog on top of existing list of widgets.
更新文件以进行适当的导入并使用 npm run dev 启动项目,然后转到 http://localhost:3000/widget 并选择一个小部件,您应该看到 URL 从 /widget 更改为 /widget/:id 并且 UI 将在模态对话框中显示所选小部件的详细信息现有小部件列表的顶部。

Next Steps 下一步

Hopefully you now have a good idea of how to display route details in a Modal. Fundamentally, with routing, we can decide when (nested route), where (Outlet), and how to display the route (Modal).
希望您现在已经了解如何在模态中显示路线详细信息。从根本上来说,通过路由,我们可以决定何时(嵌套路由)、何处(Outlet)以及如何显示路由(模态)。

Find Source code with a ready to run project @ GitHub
在 GitHub 上查找可运行项目的源代码

RemixFast 快速混音

RemixFast will auto generate List-Modal Detail route with full integration to database and integrated UI, security and host of other features. Go beyond traditional coding and experience 10x faster Remix app development with RemixFast!
RemixFast 将自动生成列表-模态详细信息路线,并完全集成到数据库和集成的 UI、安全性和其他功能。使用 RemixFast 超越传统编码并体验 10 倍快的 Remix 应用程序开发!

18 Mar 2024 2024 年 3 月 18 日

Remix Kanban Board  混音看板

Learn how to develop a Kanban board with drag-and-drop, workflow with error handling and optimistic UI.
了解如何开发具有拖放功能的看板、具有错误处理功能的工作流程和乐观的 UI。

By using RemixFast, you agree to our Cookie Policy.
使用 RemixFast 即表示您同意我们的 Cookie 政策。