介绍
虽然反应,但在网络开发社区中已经闻名。 Solidjs是镇上的新酷框架。每个人都对此大肆宣传。在这一系列文章中,您将学习稳固JS及其反应性的来龙去脉。有很多概念要涵盖,所以扣紧!
如果您想观看视频,我已经有了您ð
设置项目
我们将使用pnpm安装软件包。您也可以使用NPM或纱线。您可以通过在终端中运行npm install -g pnpm
来安装PNPM。
创建和服务
npx degit solidjs/templates/ts solid-rest-client-app
cd solid-rest-client-app
pnpm i # or `npm install` or `yarn`
pnpm run dev # Open the app on localhost:3000
# OR
# npm run dev
# yarn dev
运行上面的最后一个命令后,您应该可以看到该应用在http://localhost:3000
上运行添加尾风CSS
按照SolidJS Official Docs或以下说明的说明。
使用以下命令安装软件包:
pnpm add --save-dev tailwindcss postcss autoprefixer
然后运行以下命令以生成尾风配置
npx tailwindcss init -p
替换content
属性以定位tailwind.config.js
文件中的solidjs文件:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
现在在src/index.css
文件中添加以下CSS:
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Existing CSS Code */
添加Nunito字体
在<head></head>
标签内的index.html
中添加nunito字体:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&display=swap" rel="stylesheet">
最后,将nunito字体添加到index.css文件,如下:
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
添加离子
在index.html
中的<head></head>
标签中添加Ionic Fonts (Ionicons),如下:
<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
让我们现在尝试使用图标。更新app.tsx文件以添加Ionicon,如下所示:
const App: Component = () => {
return (
<div class={styles.App}>
<header class={styles.header}>
<img src={logo} class={styles.logo} alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
class={styles.link}
href="https://github.com/solidjs/solid"
target="_blank"
rel="noopener noreferrer"
>
Learn Solid
</a>
<ion-icon name="accessibility-outline"></ion-icon> // <-- Here
</header>
</div>
);
};
您会注意到Typescript目前根本不满意,并且可能显示出这样的内容:
这是因为solidjs和typeScript在一起不了解元素是什么。因为它在JSX中使用。让我们通过提供类型来解决它。在src
文件夹中创建一个名为types
的新文件夹。和一个名为solid-js.d.ts
的新文件。然后在内部添加以下代码:
import "solid-js";
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"ion-icon": any;
}
}
}
之后,您会看到app.tsx和tyspript现在很高兴。查看app,您应该能够在视图上看到可访问性图标。
添加eslint
编码很棒。但是编写好的代码更好。我们将在我们的项目中添加ESLINT,以确保我们的代码在应用程序的整个开发过程中保持不错。抱歉,我内心的软件架构师无法避免执行标准ð!
运行以下命令从终端设置ESLINT:
npx eslint --init
在整个安装中显示以下选项:
To check syntax, find problems, and enforce code style
JavaScript modules (import/export)
-
None of these
(用于框架) -
Yes
(对于“您的项目使用Typescript”) -
Browser
(对于“您的代码运行何处”) -
Standard: https://github.com/standard/eslint-config-standard-with-typescript
(对于“您想遵循哪个样式指南”) -
JavaScript
(对于“您要在哪种格式中使用您的配置文件”) -
JSON
用于使用的文件类型 -
pnpm
安装软件包
然后运行以下内容以安装所需的软件包:
pnpm add --save-dev eslint eslint-plugin-solid @typescript-eslint/parser
完成后,更新.eslintrc
文件以使用以下代码:
{
"env": {
"browser": true,
"es2021": true
},
"overrides": [],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {},
"parser": "@typescript-eslint/parser",
"plugins": ["solid"],
"extends": [
"eslint:recommended",
"standard-with-typescript",
"plugin:solid/typescript"
]
}
导航和路由
我们将开始创建一些页面,并将设置路由。要使用路线,我们将通过在终端中运行以下命令来安装solid-app-router软件包:
pnpm add --save solid-app-router
创建Navbar并添加基本路线
在src
文件夹中创建一个名为components
的新文件夹,然后在src/components
文件夹中创建一个名为Navbar.tsx
的新文件。在文件中添加以下代码:
import { Component } from 'solid-js';
import { Link } from 'solid-app-router';
const Navbar: Component = () => {
return (
<header class="bg-purple-600 text-white py-2 px-8 h-16 flex items-center justify-between">
<Link class="hover:opacity-50 hero" href='/'>REST in Peace</Link>
<div class="flex items-center gap-4">
<Link class="hover:opacity-50" href='/about'>About</Link>
</div>
</header>
)
}
export default Navbar;
现在,在app.tsx文件中添加以下路由配置如下:
import type { Component } from 'solid-js';
import styles from './App.module.css';
import { hashIntegration, Route, Router, Routes } from 'solid-app-router';
import Navbar from './components/Navbar';
const App: Component = () => {
return (
<Router source={hashIntegration()}>
<div class={styles.App}>
<Navbar />
</div>
<Routes>
<Route path="/" component={() => <div>Home Component</div>}></Route>
<Route path="/about" component={() => <div>About Component</div>}></Route>
</Routes>
</Router>
);
};
export default App;
如果您现在查看该应用程序,则应该能够看到带有徽标和链接的Navbar。您可以单击两者以导航到不同的路线。
请注意,我们正在使用<Router>
组件从solid-app-router
包装所有路由配置。在Router
内部,我们具有具有单独的<Route>
元素的Navbar
组件和<Routes>
组件。每个<Route>
元素都定义了路线的工作方式。以下是您可以提供的道具的可能组合:
export declare type RouteProps = {
path: string | string[];
children?: JSX.Element;
data?: RouteDataFunc;
} & ({
element?: never;
component: Component;
} | {
component?: never;
element?: JSX.Element;
preload?: () => void;
});
请注意,每条路线都需要path
。所有其他属性都是可选的。但是我们不能在同一路线上同时提供element
和component
。请注意,最后两个条件之间有一个或操作员:
({
element?: never;
component: Component;
} | {
component?: never;
element?: JSX.Element;
preload?: () => void;
})
因此,目前,我们提供的硬编码功能将JSX作为两种路线的component
支柱。我们将切换到本教程稍后使用element
。
创建家庭和关于组件:
让我们现在可以正确创建主页和关于大约页面。在src
文件夹中创建一个名为pages
的新文件夹。
家庭组件:
在名为Home.tsx
的pages
文件夹中创建一个新文件,并在其中粘贴以下代码:
import { Outlet } from 'solid-app-router';
import { Component } from 'solid-js';
const Home: Component = () => {
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
<button class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8" onClick={() => alert('To be implemented')}>
<div>+</div>
</button>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
)
}
export default Home;
关于组件:
在pages
文件夹中创建一个名为About.tsx
的新文件。然后粘贴其中的以下代码:
import { Component } from 'solid-js';
const About: Component = () => {
return (
<div>
<h2>This is About</h2>
</div>
)
}
export default About;
现在我们已经创建了两个文件,让我们更新App.tsx
文件的<Router>
元素以使用以下路线:
import type { Component } from 'solid-js';
import { hashIntegration, Route, Router, Routes } from 'solid-app-router';
import Navbar from './components/Navbar';
import About from './pages/About'; // <!-- new import
import Home from './pages/Home'; // <!-- new import
const App: Component = () => {
return (
<Router source={hashIntegration()}>
<div class="flex flex-col h-full min-h-screen">
<Navbar></Navbar>
<main class="px-8 py-4 flex-1 flex flex-col h-full">
<Routes>
<Route path="/about" element={<About />} />
<Route path="/" element={<Home />}>
{/* <Route path="/" element={<RestClientIndex />} />
<Route
path="/:id"
element={<RestClient />}
data={fetchSelectedRequest}
/> */}
</Route>
</Routes>
</main>
</div>
</Router>
);
};
export default App;
现在,如果您转到主页,您将看到以下内容:
创建接口
我们正在使用TypeScript ...它非常酷。我们将在此应用中处理一些REST API请求。因此,我们将创建与它们合作所需的接口。
在src
文件夹中创建一个名为interfaces
的新文件夹。然后创建一个名为rest.interfaces.ts
的文件。
最后,在文件中添加以下代码:
interface IRequest {
headers?: {
[key: string]: string;
}[];
method: string;
url: string;
body?: any;
}
export interface IRestRequest {
id: string;
name: string;
description: string;
request: IRequest;
}
export interface IRestResponse {
data: any;
status: number;
headers: any;
}
请注意,我们仅导出此文件中的两个接口。 IRestRequest
和IRestResponse
接口。它们代表我们应用程序上下文中的请求和响应。请注意,IRestRequest
内部使用IRequest
属性的IRequest
接口。此request
属性是我们将在本教程的稍后阶段传递给axios
的实际请求。
创建请求数据
让我们开始创建一些数据。我们将创建一些虚拟请求来使用。更新home.tsx以添加以下请求数组:
import { Outlet } from 'solid-app-router';
import { Component } from 'solid-js';
import { IRestRequest } from '../interfaces/rest.interfaces';
const Home: Component = () => {
const requests: IRestRequest[] = [
{
id: "1",
name: "Get Scores",
description: "Getting scores from server",
request: {
method: "GET",
url: "https://scorer-pro3.p.rapidapi.com/score/game123",
headers: [
{
key: "X-RapidAPI-Host",
value: "API_HOST_FROM_RAPID_API",
},
{
key: "X-RapidAPI-Key",
value: "API_KEY_FROM_RAPID_API",
},
],
},
},
{
id: "2",
name: "Add Score",
description: "Adding scores to server",
request: {
method: "POST",
url: "https://scorer-pro3.p.rapidapi.com/score",
headers: [
{
key: "X-RapidAPI-Host",
value: "API_HOST_FROM_RAPID_API",
},
{
key: "X-RapidAPI-Key",
value: "API_KEY_FROM_RAPID_API",
},
],
body: JSON.stringify({
score: 100,
gameId: "123",
userId: "test123",
}),
},
},
];
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<!-- further code here -->
</div>
)
}
export default Home;
请注意,我们根据提供的接口获得数据。另请注意,每个请求都有此headers
属性。我们不会在本教程中使用它。这只是为了让您了解如何将此应用程序扩展到与请求标头一起使用。或等待我的另一个教程ð!
创建请求元素
现在我们有了数据,让我们通过使用solid-js
中的For
元素在同一Home.tsx
文件中使用它来循环循环请求阵列并呈现一些请求。
如下:
在Home.tsx
文件中更新模板
import { Link, Outlet } from "solid-app-router";
import { Component, For } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
const Home: Component = () => {
{/* ... */}
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
<button
class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
onClick={() => alert("To be implemented")}
>
<div>+</div>
</button>
</div>
{/* 👇 We've added this 👇 */}
<div class="list">
<For each={requests} fallback={<div>Loading...</div>}>
{(item) => (
<Link href={`/${item.id}`} class="relative list__item">
<div
class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
classList={{
"list__item--active": Boolean(
location.pathname === `/${item.id}`
),
}}
>
<div>{item.name}</div>
<div class="text-xs break-all">
{item.request.method} {item.request.url}
</div>
</div>
</Link>
)}
</For>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
);
};
export default Home;
您可以看到,渲染的每个项目都是solid-app-router
的<Link>
元素。单击请求应将您带到请求的详细信息页面。而且我们尚未实施它。每个项目在视图上显示请求的名称,方法和URL。
使用信号创建一个反应性持久商店
由于我们希望我们的商店持久,因此我们将使用@solid-primitives/storage
软件包保存所有请求中的所有请求。如果是空的,我们将使用硬编码/虚拟数据启动商店。让我们首先安装包裹:
pnpm add --save @solid-primitives/storage
现在,在src
文件夹中创建一个名为store.ts
的新文件,并粘贴以下代码:
import { IRestRequest } from "./interfaces/rest.interfaces";
import { createStorageSignal } from "@solid-primitives/storage";
export const [restRequests, setRestRequests] = createStorageSignal<
IRestRequest[]
>(
"requests",
[
{
id: "1",
name: "Get Scores",
description: "Getting scores from server",
request: {
method: "GET",
url: "https://scorer-pro3.p.rapidapi.com/score/game123",
headers: [
{
key: "X-RapidAPI-Host",
value: "API_HOST_FROM_RAPID_API",
},
{
key: "X-RapidAPI-Key",
value: "API_KEY_FROM_RAPID_API",
},
],
},
},
{
id: "2",
name: "Add Score",
description: "Adding scores to server",
request: {
method: "POST",
url: "https://scorer-pro3.p.rapidapi.com/score",
headers: [
{
key: "X-RapidAPI-Host",
value: "API_HOST_FROM_RAPID_API",
},
{
key: "X-RapidAPI-Key",
value: "API_KEY_FROM_RAPID_API",
},
],
body: JSON.stringify({
score: 100,
gameId: "123",
userId: "test123",
}),
},
},
],
{
deserializer: (val): IRestRequest[] => {
if (val === null) {
return [];
}
return JSON.parse(val);
},
serializer: (val) => {
return JSON.stringify(val);
},
}
);
请注意,我们正在使用createStorageSignal
方法与类型IRestRequest[]
来创建存储信号来存储我们的请求数组。请注意,该方法的第一个参数具有"requests"
的值,该值既设置了该商店的名称in App的会话,又是浏览器存储的键(localStorage
)。第二个参数是商店的初始值,我们在这里提供了我们的硬编码/虚拟数据。
第三个参数是存储选项。我们正在传递deserializer
和serializer
的方法,这些方法分别从存储和写入存储时,负责转换数据。
最后,我们将同时导出restRequests
,这是一个可从信号访问值的固体js访问者,也是更新信号值的setRestRequests
方法。
现在我们已经创建了存储空间,让我们在Home.tsx
中使用它:
import { Link, Outlet } from "solid-app-router";
import { Component, For } from "solid-js";
import { restRequests } from "../store"; // <-- importing accessor
const Home: Component = () => {
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
<button
class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
onClick={() => alert("To be implemented")}
>
<div>+</div>
</button>
</div>
<div class="list">
{/* Using the accessor here */}
<For each={restRequests()} fallback={<div>Loading...</div>}>
{(item) => (
<Link href={`/${item.id}`} class="relative list__item">
<div
class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
classList={{
"list__item--active": Boolean(
location.pathname === `/${item.id}`
),
}}
>
<div>{item.name}</div>
<div class="text-xs break-all">
{item.request.method} {item.request.url}
</div>
</div>
</Link>
)}
</For>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
);
};
export default Home;
请注意,当我们将其移至store.ts
文件时,我们已从文件中删除了Home.tsx
的硬编码请求。现在的家庭组件现在看起来很干净ð«§您可以在localstorage中看到持续的请求,如Chrome调试器中的如下:
创建添加请求模式
由于我们现在已经实施了存储,因此让我们创建一种添加/创建更多请求的方法。这就是乐趣开始的地方,因为我们现在开始动态地将内容添加到应用程序中。我们将创建一个模式以添加新请求。为此,在src/components
文件夹中创建一个名为RequestModal.tsx
的新文件。添加以下代码创建基本模式:
import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
interface RequestModalProps extends ComponentProps<any> {
show: boolean;
onModalHide: (id: string | null) => void;
request?: IRestRequest;
}
const RequestModal: Component<RequestModalProps> = (
props: RequestModalProps
) => {
return (
<Show when={props.show}>
<div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
<div
class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl"
>
<h5 class="text-4xl font-bold mb-4">
{(props.request ? "Edit" : "Create") + " Request"}
</h5>
<span class="absolute bottom-9 right-8">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</span>
</div>
</div>
</Show>
);
};
export default RequestModal;
请注意,RequestModal
使用固体JS的<Show>
组件,该组件根据提供给when
属性的条件,显示其中的模板/组件。我们依靠RequestModal
的show
支柱来告诉<Show>
组件何时显示内容。这样我们就可以从组件外部控制RequestModal
以使其可见或隐藏。
现在我们有了这个组件,让我们在Home.tsx
中添加如下:
import { Link, Outlet, useLocation, useNavigate } from "solid-app-router";
import { Component, createSignal, For } from "solid-js";
import RequestModal from "../components/RequestModal";
import { restRequests } from "../store";
const Home: Component = () => {
const [showModal, setShowModal] = createSignal(false);
const navigate = useNavigate();
const location = useLocation();
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div>
<button onClick={() => setShowModal(!showModal())}>Click Me</button>
<RequestModal
show={showModal()}
onModalHide={(id: string | null) => {
setShowModal(!showModal());
}}
/>
</div>
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
<button
class="flex hover:bg-opacity-60 justify-center items-center p-4 bg-purple-600 rounded-full text-white w-8 h-8"
onClick={() => alert("To be implemented")}
>
<div>+</div>
</button>
</div>
<div class="list">
<For each={restRequests()} fallback={<div>Loading...</div>}>
{(item) => (
<Link href={`/${item.id}`} class="relative list__item">
<div
class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
classList={{
"list__item--active": Boolean(
location.pathname === `/${item.id}`
),
}}
>
<div>{item.name}</div>
<div class="text-xs break-all">
{item.request.method} {item.request.url}
</div>
</div>
</Link>
)}
</For>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
);
};
export default Home;
让我们看一下我们在顶部进口的变化的几步之类的步骤。
const Home: Component = () => {
const [showModal, setShowModal] = createSignal(false);
const navigate = useNavigate();
const location = useLocation();
// more code
};
export default Home;
我们创建了一个信号showModal
,我们将用于显示或隐藏模式。 IE。稍后,我们将作为RequestModal
的show
道具传递。我们还创建了两个常数。一次进行导航,一个用于访问位置。
<div>
<button onClick={() => setShowModal(!showModal())}>Click Me</button>
<RequestModal
show={showModal()}
onModalHide={(id: string | null) => {
setShowModal(!showModal());
}}
/>
</div>
在上面的代码中,我们有一个按钮可以切换showModal
信号的值。并且我们使用的是RequestModal
,该RequestModal
具有分配给show
Prop的showModal
的值。您可以看到我们还为onModalHide
Prop提供了功能。当RequestModal
被隐藏时,这将被调用。我们还没有写过它的逻辑。
使用所有这些,当您单击左侧的Click Me
文本时,您应该可以在应用中看到它。
请注意,我们无法关闭模态。我们将稍作处理。
创建IconButton
组件
我们将在整个应用程序中使用一些图标按钮。因此,创建可重复使用的IconButton
组件是一个好主意。在名为IconButton.tsx
的src/components
文件夹中创建一个新文件。然后向其添加以下代码:
import { Component, ComponentProps } from "solid-js";
interface IconButtonProps extends ComponentProps<any> {
onClick: (event: MouseEvent) => void;
label: string;
icon: string;
type?: "reset" | "submit" | "button";
}
const IconButton: Component<IconButtonProps> = ({
onClick,
label,
icon,
type,
}) => {
return (
<button
onclick={onClick}
role="button"
type={type || "button"}
title={label}
class="w-6 h-6 flex transition-all ease-in-out duration-100 hover:scale-125 items-center justify-center text-white bg-purple-600 border border-purple-600 rounded-full hover:bg-purple-700 active:text-white focus:outline-none focus:ring"
>
<span class="sr-only">{label}</span>
<ion-icon name={icon}></ion-icon>
</button>
);
};
export default IconButton;
此组件非常简单。我们正在此处造成一个按钮,并使用<ion-icon>
元素显示目标图标。查看IconButtonProps
接口,以查看我们可以将哪些可能的道具传递给此组件。
让我们用这个新的IconButton
组件替换Home.tsx
文件中的“添加请求”按钮。我们还将从模板中删除“单击我”按钮:
import { Link, Outlet, useLocation, useNavigate } from "solid-app-router";
import { Component, createSignal, For } from "solid-js";
import IconButton from "../components/IconButton";
import RequestModal from "../components/RequestModal";
import { restRequests } from "../store";
const Home: Component = () => {
const [showModal, setShowModal] = createSignal(false);
const navigate = useNavigate();
const location = useLocation();
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div>
<RequestModal
show={showModal()}
onModalHide={(id: string | null) => {
setShowModal(!showModal());
}}
/>
</div>
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
{/* Replaced the Add Request Button with IconButton */}
<IconButton
onClick={() => setShowModal(true)}
icon="add"
label="Add Request"
/>
</div>
<div class="list">
<For each={restRequests()} fallback={<div>Loading...</div>}>
{(item) => (
<Link href={`/${item.id}`} class="relative list__item">
<div
class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
classList={{
"list__item--active": Boolean(
location.pathname === `/${item.id}`
),
}}
>
<div>{item.name}</div>
<div class="text-xs break-all">
{item.request.method} {item.request.url}
</div>
</div>
</Link>
)}
</For>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
);
};
export default Home;
太好了!现在让我们迈向创建添加请求表格。
使用固体JS表单创建请求
我们将使用solid-forms
软件包在本教程中使用表格。我看过多个表格库,但发现这个图书馆很不错。如下安装包:
pnpm add --save solid-forms
实施TextField组件
现在在src/components
文件夹下创建一个新文件,并将其命名为TextField.tsx
。只要我们要显示输入或文本股组件,就会使用此组件。将以下代码添加到创建的文件中:
import { IFormControl } from "solid-forms";
import { Component } from "solid-js";
export const TextField: Component<{
control: IFormControl<string>;
label: string;
placeholder?: string;
type?: string;
rows?: number;
id: string;
class?: string;
valueUpdated?: (val: any) => void;
}> = (props) => {
const type = props.type || "text";
const onInput = (e: { currentTarget: { value: string } }) => {
props.control.markDirty(true);
props.control.setValue(e.currentTarget.value);
};
const onBlur = () => {
props.control.markTouched(true);
if (props.valueUpdated) {
props.valueUpdated(props.control.value);
}
};
return (
<>
<label class="sr-only" for={props.id}>
{props.label}
</label>
{type === "textarea" ? (
<textarea
value={props.control.value}
rows={props.rows || 3}
oninput={onInput}
onblur={onBlur}
placeholder={props.placeholder}
required={props.control.isRequired}
id={props.id}
class={`w-full p-3 text-sm border-gray-200 rounded-lg ${props.class}`}
/>
) : (
<input
type="text"
value={props.control.value}
oninput={onInput}
onblur={onBlur}
placeholder={props.placeholder}
required={props.control.isRequired}
id={props.id}
class={`w-full p-3 text-sm border-gray-200 rounded-lg ${props.class}`}
/>
)}
</>
);
};
除了一些明显的道具外,要考虑的重要实现是模板中<input>
和<textarea>
元素上使用的onblur
和oninput
方法。 TextField
接受control
Prop,该道具应该是固体形式的形式控制。每当<input>
或<textarea>
元素中有输入更改时,都会触发onInput
方法,并通过props.control.setValue
语句设置控件的值。
同样,我们让父组件知道使用valueUpdated
Prop更改了该值。每当我们 blur 从输入或文本方面,如果提供了props.valueUpdated
方法(因为它是可选的道具)。
创建RESTCLIENTFORM
此表单将用于创建和编辑请求。并且它将基于多个TextField
组件。在src/components
文件夹中创建一个名为RestClientForm.tsx
的新文件。然后向其添加以下代码:
import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
export const RestClientForm: Component<{
request?: Partial<IRestRequest>;
formSubmit: Function;
formUpdate?: Function;
actionBtnText: string;
}> = (props) => {
return (
<form
action=""
class="space-y-4"
classList={{}}
onSubmit={(e) => {
e.preventDefault();
}}
>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="name" class="mb-4 block">
Name
</label>
<input placeholder="name" />
</div>
<div>
<label for="url" class="mb-4 block">
URL
</label>
<input placeholder="url" />
</div>
<div>
<label class="my-4 block">Method</label>
<input placeholder="method" />
</div>
</div>
<div>
<label class="my-4 block">Body</label>
<input placeholder="body" />
</div>
<div class="mt-4">
<button
disabled={false}
type="submit"
class="inline-flex items-center disabled:bg-gray-500 justify-center w-full px-5 py-3 text-white bg-purple-600 hover:bg-purple-700 rounded-lg sm:w-auto"
>
<span class="font-medium"> {props.actionBtnText} </span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 ml-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</button>
</div>
</form>
);
};
该组件接受针对formSubmit
Prop的功能,该功能将被调用...好吧...在表单上提交ð!另一个强制性道具是actionBtnText
,因此我们可以在“提交”按钮上显示所需的标签。
如果您仔细查看上述代码,您会注意到我们尚未使用到目前为止的任何TextField
组件。好吧,握住你的马。现在,让我们在RequestModal
组件中使用此RestClientForm
组件,看看一切外观。在RequestModal.tsx
文件中添加RestClientForm
如下:
import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { setRestRequests, restRequests } from "../store";
import { RestClientForm } from "./RestClientForm";
interface RequestModalProps extends ComponentProps<any> {
show: boolean;
onModalHide: (id: string | null) => void;
request?: IRestRequest;
}
const RequestModal: Component<RequestModalProps> = (
props: RequestModalProps
) => {
return (
<Show when={props.show}>
<div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
<div class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl">
<h5 class="text-4xl font-bold mb-4">
{(props.request ? "Edit" : "Create") + " Request"}
</h5>
<RestClientForm
formSubmit={(request: IRestRequest) => {
const id = self.crypto?.randomUUID() || Date.now().toString();
setRestRequests([
...(restRequests() || []),
{
...request,
id,
},
]);
props.onModalHide(id);
}}
actionBtnText={"Save"}
/>
<span class="absolute bottom-9 right-8">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</span>
</div>
</div>
</Show>
);
};
export default RequestModal;
请注意,我们在此处传递针对formSubmit
Prop的功能。如果将request
传递给此功能以响应提交表单,则使用self.crypto?.randomUUID()
(漂亮的Trick Trick TBH)为该请求创建一个唯一的ID,并使用setRestRequests
Update功能将请求添加到请求列表中。请记住,这是信号的一部分,不仅是正常信号,而且是@solid-primitives/storage
包装中的信号。因此,将在应用程序的当前状态和本地存储中更新。最后,您可以看到我们正在调用props.onModalHide(id)
语句以在创建新请求并保存后关闭模式。
但是等等!如果您现在尝试一下,它将无法正常工作。因为我们还没有使用任何控件,也没有使用TextField
组件。因此,让我们首先创建控件。
创建固体形式控件
我们现在将利用solid-forms
的控制工厂。这使我们能够创建一个表格组,我们可以轻松地设法处理表单输入并对其做出反应。
为此,请如下更新RestClientForm.tsx
文件:
import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { createFormGroup, createFormControl } from 'solid-forms';
const controlFactory = () => {
return createFormGroup({
name: createFormControl<string>("New Request", {
required: true,
validators: (val: string) => {
return !val.length ? {isMissing: true} : null;
}
}),
request: createFormGroup({
method: createFormControl<string>("GET"),
body: createFormControl<string>(""),
url: createFormControl<string>(""),
}),
});
};
// rest of the code
controlFactory
方法从实心形式返回一组IFormGroup
的形式。这意味着我们有一个形式组,现在可以在其中具有多个表单控件和表单组。在我们的情况下,我们将name
作为表单控件,因为您可以看到它使用createFormControl
方法。请注意,该方法的第一个参数是初始值,第二个参数是包含required: true
和用于自定义验证的validators
方法的配置。
我们表格组中的request
属性是另一个表单组,其中包含三个表单控件,method
,body
和url
。
现在,我们将使用withControl
方法将RestClientForm
组件用固体表单包装。
在controlFactory
代码下方创建包装组件如下:
import { Component } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { createFormGroup, createFormControl, withControl } from "solid-forms";
import { TextField } from "./TextField";
const controlFactory = () => {
return createFormGroup({
name: createFormControl<string>("New Request", {
required: true,
validators: (val: string) => {
return !val.length ? { isMissing: true } : null;
},
}),
request: createFormGroup({
method: createFormControl<string>("GET"),
body: createFormControl<string>(""),
url: createFormControl<string>(""),
}),
});
};
export const RestClientForm = withControl<
{
request?: Partial<IRestRequest>;
formSubmit: Function;
formUpdate?: Function;
actionBtnText: string;
},
typeof controlFactory
>({
controlFactory,
component: (props) => {
const controlGroup = () => props.control.controls;
const requestControlGroup = () => controlGroup().request.controls;
const request = () => props.request;
return (
<form
action=""
class="space-y-4"
classList={{
"is-valid": props.control.isValid,
"is-invalid": !props.control.isValid,
"is-touched": props.control.isTouched,
"is-untouched": !props.control.isTouched,
"is-dirty": props.control.isDirty,
"is-clean": !props.control.isDirty,
}}
onSubmit={(e) => {
e.preventDefault();
const params = {
...props.control.value,
request: {
...props.control.value.request,
},
};
props.formSubmit(params);
}}
>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="name" class="mb-4 block">
Name
</label>
<TextField
placeholder="name"
id="name"
label="Name"
control={controlGroup().name}
/>
</div>
<div>
<label for="url" class="mb-4 block">
URL
</label>
<TextField
placeholder="url"
id="url"
label="Url"
control={requestControlGroup().url}
/>
</div>
<div>
<label class="my-4 block">Method</label>
<TextField
id="method"
label="Method"
placeholder="method"
control={requestControlGroup().method}
/>
</div>
</div>
<div>
<label class="my-4 block">Body</label>
<TextField
id="body"
type="textarea"
label="Body"
placeholder="body"
control={requestControlGroup().body}
/>
</div>
<div class="mt-4">
<button
disabled={!props.control.isValid}
type="submit"
class="inline-flex items-center disabled:bg-gray-500 justify-center w-full px-5 py-3 text-white bg-purple-600 hover:bg-purple-700 rounded-lg sm:w-auto"
>
<span class="font-medium"> {props.actionBtnText} </span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 ml-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</button>
</div>
</form>
);
},
});
在上面的代码中,您会注意到我们的RestClientForm
组件现在使用withControl
方法,并使用了我们之前创建的controlFactory
。结果,我们获得了使用组件函数中的工厂创建的实体(表格组)。我们收到的表格组为props.control
属性。这是一个实体JS信号。是的!这意味着这是反应性的。
我们将控件分配给两组。
- 使用
controlGroup
访问者的主要组 - 使用
requestControlGroup
访问者的request
对照组
<form
action=""
class="space-y-4"
classList={{
"is-valid": props.control.isValid,
"is-invalid": !props.control.isValid,
"is-touched": props.control.isTouched,
"is-untouched": !props.control.isTouched,
"is-dirty": props.control.isDirty,
"is-clean": !props.control.isDirty,
}}
</form>
最后,我们将每个控件通过control
Prop传递到相应的TextField
组件。请注意如何通过调用登录器然后访问属性来访问控件。例如,requestControlGroup().url
而不是requestControlGroup.url
。
注意:您会注意到我用于
method
的元素是TextField
而不是<select>
组件。这是故意的,不要使教程更复杂。随意自行调整代码ð
如果您现在查看该应用,则应如下:
看起来很酷ð,对吗?尝试立即创建请求。您应该能够看到将其添加到UI以及localStorage
中。这是因为当我们提交表单时,我们从RestClientForm
调用props.formSubmit
方法。这反过
固体JS指令(您的第一个指令)
我们将立即实施指令。指令是像组件这样的实现,但通常没有模板,并且在大多数情况下纯粹与DOM合作。就我们的情况而言,我们将在单击外部时实现关闭RequestModal
的指令。
在名为directives
的src
文件夹中创建一个新文件夹,并在其内部创建一个名为click-outside.directive.ts
的文件。然后向其添加以下代码:
import { Accessor, onCleanup } from "solid-js";
export default function clickOutside(el: Element, accessor: Accessor<any>) {
const onClick = (e: Event) =>
!el.contains(e.target as Node) && accessor()?.();
document.body.addEventListener("click", onClick);
onCleanup(() => document.body.removeEventListener("click", onClick));
}
您可以看到代码很小。但是它做什么? ðÖ
指令本身是具有两个参数的函数。 SolidJS提供了第一个参数,因为该指令被应用于指令。第二个论点是我们提供给指令的价值。在这种情况下,它将是关闭模式的函数。
请注意,我们在document.body
元素上注册了click
事件,该事件应检查我们是否在指令内部或外部单击。而且,如果我们单击室外,它将调用accessor()
以获取要调用的函数,然后通过accessor()?.();
语句调用它。似乎有点混乱。只是克服它ð
我们还使用固体JS的onCleanup
钩子去除事件侦听器,当该指令所应用的元素不再在DOM上。在我们的情况下,当我们打开RequestModal
时,该指令将栩栩如生,活动听众将被注册。当模态关闭时,指令将被销毁并将删除事件听众。
让我们现在使用RequestModal.tsx
中的指令如下:
import { Component, ComponentProps, Show } from "solid-js";
import { IRestRequest } from "../interfaces/rest.interfaces";
import { setRestRequests, restRequests } from "../store";
import { RestClientForm } from "./RestClientForm";
import outsideDirective from "../directives/click-outside.directive"; // <-- new import
// https://github.com/solidjs/solid/discussions/845
const clickOutside = outsideDirective; // <-- remapping to `clickOutside` variable
interface RequestModalProps extends ComponentProps<any> {
show: boolean;
onModalHide: (id: string | null) => void;
request?: IRestRequest;
}
const RequestModal: Component<RequestModalProps> = (
props: RequestModalProps
) => {
return (
<Show when={props.show}>
<div class="fixed z-50 top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.75)]">
<div
class="relative max-h-[85%] overflow-y-auto top-20 bg-gray-200 max-w-md m-auto h- block p-8 pb-8 border-t-4 border-purple-600 rounded-sm shadow-xl"
use:clickOutside={() => { {/** Using the directive here */}
props.onModalHide(null);
}}
>
<h5 class="text-4xl font-bold mb-4">
{(props.request ? "Edit" : "Create") + " Request"}
</h5>
<RestClientForm
...
/>
<span class="absolute bottom-9 right-8">
...
</span>
</div>
</div>
</Show>
);
};
export default RequestModal;
您会注意到Typescript再次不满意,因为它不了解clickOutside
是指令。
让我们让打字稿开心。如下:
,更新types
文件夹中的solid-js.d.ts
文件
import "solid-js";
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"ion-icon": any;
}
interface Directives {
clickOutside?: () => void;
}
}
}
打字稿现在应该很开心!您可能还注意到,我们正在重新映射从outsideDirective
到clickOutside
的变量。这是因为我在代码中链接的打字稿 +固体JS问题。
如果立即打开请求模式,然后单击其外部,您将自动看到模态关闭。是的! ð
删除请求
现在我们可以添加请求,在单击外部时自动关闭模式并持续保存请求,让我们处理删除请求。
要删除请求,我们将在左侧栏中的每个列表项目上添加一个删除按钮。为此,请如下更新Home.tsx
:
// existing imports
import { restRequests, setRestRequests } from "../store";
const Home: Component = () => {
const [showModal, setShowModal] = createSignal(false);
const navigate = useNavigate();
const location = useLocation();
return (
<div class="flex flex-col md:flex-row gap-4 h-full flex-1">
<div>
<RequestModal
show={showModal()}
onModalHide={(id: string | null) => {
setShowModal(!showModal());
}}
/>
</div>
<div class="w-full md:w-1/4 bg-gray-200 min-h-full border-gray-300 border p-4 rounded-lg">
<div class="flex justify-between py-4">
<h1 class="text-sm ">Rest Requests</h1>
{/* Replaced the Add Request Button with IconButton */}
<IconButton
onClick={() => setShowModal(true)}
icon="add"
label="Add Request"
/>
</div>
<div class="list">
<For each={restRequests()} fallback={<div>Loading...</div>}>
{(item) => (
<Link href={`/${item.id}`} class="relative list__item">
<div
class="p-2 hover:bg-gray-300 cursor-pointer pr-12 rounded-lg mb-2"
classList={{
"list__item--active": Boolean(
location.pathname === `/${item.id}`
),
}}
>
<div>{item.name}</div>
<div class="text-xs break-all">
{item.request.method} {item.request.url}
</div>
</div>
{/* Delete Request Button */}
<button
onclick={(e: MouseEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
if (restRequests()?.length) {
const requests = restRequests() || [];
setRestRequests(requests.filter((i) => i.id !== item.id));
if (location.pathname === `/${item.id}`) {
navigate("/");
}
}
}}
class="absolute text-xl hover:scale-125 transition-all ease-in-out duration-100 hover:text-red-700 text-red-600 right-2 top-0 bottom-0 m-auto"
>
<ion-icon name="trash"></ion-icon>
</button>
</Link>
)}
</For>
</div>
</div>
<div class="flex-1 min-h-full">
<Outlet />
</div>
</div>
);
};
export default Home;
如果您在此更改后查看该应用程序,则将在每个请求上查看一个删除按钮。我们将更改它以显示仅在悬停的项目上。
查看删除按钮的onclick
处理程序,请注意,我们使用requests.filter
方法使用请求的id
从数组中删除了请求。然后,我们使用setRestRequests
更新程序功能来持续使用它们。尝试删除按钮删除一些请求。
让我们添加一些CSS,以使删除按钮仅在请求项目的悬停上可见。创建一个名为Home.css
的新文件,并在其中添加以下代码:
.list .list__item ion-icon {
display: none;
}
.list .list__item:hover ion-icon {
display: flex;
}
.list .list__item:hover .list__item--active + ion-icon {
@apply text-white;
}
.list .list__item--active {
@apply bg-purple-600 text-white;
}
最后,在Home.tsx
文件中导入Home.css
文件如下:
// existing imports
import { restRequests, setRestRequests } from "../store";
import "./Home.css";
如果您现在查看该应用程序,我们仅在悬停请求项目上显示删除按钮。
真棒,我们现在能够创建请求,删除请求,保存然后持续保存,并为应用程序提供非常好的UI/UX。随意修改它的外观并使其成为您自己的ð
结论
在本教程中,我们在固体JS方面学到了一些概念。我们学会了:
- 从固体JS 开始
- 信号
- 将数据持续到存储
- 使用
solid-app-router
使用路线 - 使用
solid-forms
处理表格 - 创建指令
我喜欢写这篇文章,希望您从中学到了很多东西。如果您这样做,请对此帖子做出反应并为其添加书签。本教程也可以在YouTube上找到,因此您可以在那里查看Part1和Part2。
如果您想连接,以下是我社交的链接: