React 파일 관리자를 만들어 봅시다 12장: 진행률 표시줄, 스켈톤 및 오버레이
95754 단어 typescriptmongezjavascriptreact
루트 경로 업데이트
파일 관리자를 로드할 때 파일 관리자 루트 경로를 업데이트하는 것을 잊었으므로 지금 수행하겠습니다.
// FileManager.tsx
// load the given directory path
const load = useCallback(
(path: string, isRoot = false) => {
setIsLoading(true);
+ if (isRoot) {
+ fileManager.setRootPath(path);
+ }
fileManager.load(path).then(node => {
setCurrentDirectoryNode(node);
setIsLoading(false);
if (isRoot) {
setRootDirectoryNode(node);
}
});
},
[fileManager],
);
모달 제거
따라서 모달은 파일 관리자를 격리된 레이아웃으로 표시하는 데 적합하지만 전체 페이지를 차지하도록 해야 하므로 모달을 제거해야 하고 모달에 대한 다른 래퍼 구성 요소를 만들 수 있습니다.
// FileManager.tsx
import { Grid } from "@mantine/core";
import BaseFileManager from "app/file-manager/utils/FileManager";
import { useCallback, useEffect, useRef, useState } from "react";
import Content from "../../Content";
import FileManagerContext from "../../contexts/FileManagerContext";
import { Node } from "../../types/FileManager.types";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
import LoadingProgressBar from "./LoadingProgressBar";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
export default function FileManager({ rootPath }: FileManagerProps) {
const [isLoading, setIsLoading] = useState(true);
const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();
const [rootDirectoryNode, setRootDirectoryNode] = useState<Node>();
const { current: fileManager } = useRef(new BaseFileManager());
// load the given directory path
const load = useCallback(
(path: string, isRoot = false) => {
setIsLoading(true);
if (isRoot) {
fileManager.setRootPath(path);
}
fileManager.load(path).then(node => {
setCurrentDirectoryNode(node);
setIsLoading(false);
if (isRoot) {
setRootDirectoryNode(node);
}
});
},
[fileManager],
);
// load root directory
useEffect(() => {
if (!rootPath) return;
load(rootPath, true);
}, [rootPath, fileManager, load]);
return (
<FileManagerContext.Provider value={fileManager}>
<LoadingProgressBar />
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar rootDirectory={rootDirectoryNode} />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</FileManagerContext.Provider>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
소품 유형도 업데이트하는 것을 잊지 마십시오.
// FileManager.types.ts
import { Node } from "../../types/FileManager.types";
export type FileManagerProps = {
/**
* Root path to open in the file manager
*
* @default "/"
*/
rootPath?: string;
/**
* Callback for when a file/directory is selected
*/
onSelect?: (node: Node) => void;
/**
* Callback for when a file/directory is double clicked
*/
onDoubleClick?: (node: Node) => void;
/**
* Callback for when a file/directory is right clicked
*/
onRightClick?: (node: Node) => void;
/**
* Callback for when a file/directory is copied
*/
onCopy?: (node: Node) => void;
/**
* Callback for when a file/directory is cut
*/
onCut?: (node: Node) => void;
/**
* Callback for when a file/directory is pasted
* The old node will contain the old path and the new node will contain the new path
*/
onPaste?: (node: Node, oldNode: Node) => void;
/**
* Callback for when a file/directory is deleted
*/
onDelete?: (node: Node) => void;
/**
* Callback for when a file/directory is renamed
* The old node will contain the old path/name and the new node will contain the new path/name
*/
onRename?: (node: Node, oldNode: Node) => void;
/**
* Callback for when a directory is created
*/
onCreateDirectory?: (directory: Node) => void;
/**
* Callback for when file(s) is uploaded
*/
onUpload?: (files: Node[]) => void;
/**
* Callback for when a file is downloaded
*/
onDownload?: (node: Node) => void;
};
이제 홈 페이지를 정리하고 파일 관리자를 렌더링해 보겠습니다.
// HomePage.tsx
import Helmet from "@mongez/react-helmet";
import FileManager from "app/file-manager/components/FileManager";
export default function HomePage() {
return (
<>
<Helmet title="home" appendAppName={false} />
<FileManager />
</>
);
}
진행률 표시줄 구성 요소 추가
이전에 언급했듯이 매우 강력하기 때문에 이벤트를 사용하여 파일 관리자가 로드될 때 수신 대기하고 진행률 표시줄을 표시하고 로드가 완료되면 숨길 수 있습니다.
components/LoadingProgressBar.tsx
파일을 만들고 다음 코드를 추가합니다.// LoadingProgressBar.tsx
import useFileManager from "../../hooks/useFileManager";
export default function LoadingProgressBar() {
const fileManager = useFileManager();
return <div>LoadingProgressBar</div>;
}
여기에 멋진 것은 없습니다. 파일 관리자 인스턴스가 해당 이벤트를 수신하도록 해야 합니다.
이제 파일 관리자 구성 요소에서 가져오고 본문에 추가하겠습니다.
// FileManager.tsx
import { Grid, Modal } from "@mantine/core";
import BaseFileManager from "app/file-manager/utils/FileManager";
import { useCallback, useEffect, useRef, useState } from "react";
import Content from "../../Content";
import FileManagerContext from "../../contexts/FileManagerContext";
import { Node } from "../../types/FileManager.types";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";
import LoadingProgressBar from "./LoadingProgressBar";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
export default function FileManager({
open,
onClose,
rootPath,
}: FileManagerProps) {
const [isLoading, setIsLoading] = useState(true);
const [currentDirectoryNode, setCurrentDirectoryNode] = useState<Node>();
const [rootDirectoryNode, setRootDirectoryNode] = useState<Node>();
const { current: fileManager } = useRef(new BaseFileManager());
// load the given directory path
const load = useCallback(
(path: string, isRoot = false) => {
setIsLoading(true);
if (isRoot) {
fileManager.setRootPath(path);
}
fileManager.load(path).then(node => {
setCurrentDirectoryNode(node);
setIsLoading(false);
if (isRoot) {
setRootDirectoryNode(node);
}
});
},
[fileManager],
);
// load root directory
useEffect(() => {
if (!rootPath || !open) return;
load(rootPath, true);
}, [rootPath, fileManager, open, load]);
return (
<FileManagerContext.Provider value={fileManager}>
<Modal size="xl" opened={open} onClose={onClose}>
<LoadingProgressBar />
<Toolbar />
<BodyWrapper>
<Grid>
<Grid.Col span={3}>
<Sidebar rootDirectory={rootDirectoryNode} />
</Grid.Col>
<Grid.Col span={9}>
<Content />
</Grid.Col>
</Grid>
</BodyWrapper>
</Modal>
</FileManagerContext.Provider>
);
}
FileManager.defaultProps = {
rootPath: "/",
};
Sometimes i paste the entire component code, and others i don't so you can see the changes, but you can always check the full code in the github repo.
이제 Mantine Progress Bar을 사용하여 사용해 봅시다.
// LoadingProgressBar.tsx
import { Progress } from "@mantine/core";
import useFileManager from "../../hooks/useFileManager";
export default function LoadingProgressBar() {
const fileManager = useFileManager();
return <Progress size="lg" value={50} striped animate />;
}
그것은 다음과 같아야합니다
이제 진행률 표시줄을 표시하거나 숨기는 논리를 추가해 보겠습니다.
// LoadingProgressBar.tsx
import { Progress } from "@mantine/core";
import { useEffect, useState } from "react";
import useFileManager from "../../hooks/useFileManager";
export default function LoadingProgressBar() {
const fileManager = useFileManager();
const [progress, setProgress] = useState(0);
useEffect(() => {
// let's create an interval that will update progress every 300ms
let interval: ReturnType<typeof setInterval>;
// we'll listen for loading state
const loadingEvent = fileManager.on("loading", () => {
setProgress(5);
interval = setInterval(() => {
// we'll increase it by 10% every 100ms
// if it's more than 100% we'll set it to 100%
setProgress(progress => {
if (progress >= 100) {
clearInterval(interval);
return 100;
}
return progress + 2;
});
}, 100);
});
// now let's listen when the loading is finished
const loadEvent = fileManager.on("load", () => {
// clear the interval
setProgress(100);
setTimeout(() => {
clearInterval(interval);
// set progress to 0
setProgress(0);
}, 300);
});
// unsubscribe events on unmount or when use effect dependencies change
return () => {
loadingEvent.unsubscribe();
loadEvent.unsubscribe();
};
}, [fileManager]);
if (progress === 0) return null;
return <Progress size="lg" value={progress} striped animate />;
}
코드가 약간 복잡해 보이지만 그렇게 어렵지는 않습니다. 100ms마다 진행률을 10%씩 증가시키는 간격을 만들고
loading
및 load
이벤트를 수신하여 간격을 시작하고 중지합니다.효과가 마운트 해제되거나 종속성이 변경되면 이벤트 구독을 취소합니다.
이제 이를 테스트하기 위해
setTimeout
함수에 list
를 추가하여 로딩을 속일 것입니다.// file-manager-service.ts
import FileManagerServiceInterface from "../types/FileManagerServiceInterface";
import fetchNode from "../utils/helpers";
export class FileManagerService implements FileManagerServiceInterface {
/**
* {@inheritDoc}
*/
public list(directoryPath: string): Promise<any> {
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: {
node: fetchNode(directoryPath),
},
});
}, 3000);
});
}
}
이제 진행률 표시줄은 다음과 같아야 합니다.
사이드바가 숨겨져 있습니다. Skelton을 추가해 보겠습니다.
먼저 로딩 상태를 선언한 다음 로딩 이벤트를 수신합니다.
// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useEffect, useMemo, useState } from "react";
import useFileManager from "../../../hooks/useFileManager";
import { Node } from "../../../types/FileManager.types";
import SidebarNode from "./SidebarNode";
export type SidebarProps = {
rootDirectory?: Node;
};
export default function Sidebar({ rootDirectory }: SidebarProps) {
const rootChildren = useMemo(() => {
return rootDirectory?.children?.filter(child => child.isDirectory);
}, [rootDirectory]);
const fileManager = useFileManager();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const loadingEvent = fileManager.on("loading", () => setIsLoading(true));
const loadEvent = fileManager.on("load", () => setIsLoading(false));
return () => {
loadingEvent.unsubscribe();
loadEvent.unsubscribe();
};
}, [fileManager]);
if (isLoading) {
return (
<Card shadow={"sm"}>
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
</Card>
);
}
if (!rootDirectory) return null;
return (
<>
<Card shadow="sm">
<SidebarNode
node={rootDirectory}
navProps={{
p: 0,
}}
icon={<IconHome2 size={16} color="#78a136" />}
/>
{rootChildren?.map(child => (
<SidebarNode
navProps={{
p: 0,
pl: 10,
}}
key={child.path}
icon={<IconFolder size={16} fill="#31caf9" />}
node={child}
/>
))}
</Card>
</>
);
}
꽤 깔끔하죠?
이제 콘텐츠 부분으로 이동하겠습니다.
콘텐츠 로드 상태
콘텐츠 부분에 로드 상태를 추가하고 콘텐츠가 로드될 때 Overlay을 표시합니다.
이전 사이드바에서처럼 로딩 상태를 추가하고 로딩 이벤트를 수신할 것입니다. xD 코드를 복사/붙여넣기만 하면 됩니다.
// Content.tsx
import { Card } from "@mantine/core";
import { useEffect, useState } from "react";
import useFileManager from "../hooks/useFileManager";
export default function Content() {
const fileManager = useFileManager();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const loadingEvent = fileManager.on("loading", () => setIsLoading(true));
const loadEvent = fileManager.on("load", () => setIsLoading(false));
return () => {
loadingEvent.unsubscribe();
loadEvent.unsubscribe();
};
}, [fileManager]);
return (
<>
<Card shadow="sm">
<div>Content</div>
</Card>
</>
);
}
알아차리면 여기에 패턴이 있고, 로딩 상태를 찾고 있고, 로딩 이벤트를 수신하고 있으므로 이를 처리하기 위한 사용자 지정 후크를 만들 수 있습니다.
hooks/useLoading
후크를 만들어 보겠습니다.// hooks/useLoading.ts
import { useEffect, useState } from "react";
import useFileManager from "./useFileManager";
export default function useLoading(): boolean {
const fileManager = useFileManager();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const loadingEvent = fileManager.on("loading", () => setIsLoading(true));
const loadEvent = fileManager.on("load", () => setIsLoading(false));
return () => {
loadingEvent.unsubscribe();
loadEvent.unsubscribe();
};
}, [fileManager]);
return isLoading;
}
이제
Content.tsx
및 Sidebar.tsx
에서 사용하겠습니다.// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useMemo } from "react";
import useLoading from "../../../hooks/useLoading";
import { Node } from "../../../types/FileManager.types";
import SidebarNode from "./SidebarNode";
export type SidebarProps = {
rootDirectory?: Node;
};
export default function Sidebar({ rootDirectory }: SidebarProps) {
const rootChildren = useMemo(() => {
return rootDirectory?.children?.filter(child => child.isDirectory);
}, [rootDirectory]);
const isLoading = useLoading();
if (isLoading) {
return (
<Card shadow={"sm"}>
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
</Card>
);
}
if (!rootDirectory) return null;
...
Content.tsx
에서 동일합니다.// Content.tsx
import { Card } from "@mantine/core";
import useLoading from "../hooks/useLoading";
export default function Content() {
const isLoading = useLoading();
return (
<>
<Card shadow="sm">
<div>Content</div>
</Card>
</>
);
}
이제 오버레이를 만들겠습니다. 하지만 먼저 콘텐츠에 대한 래퍼를 만들어야 오버레이를 배치할 수 있습니다.
Content.styles.tsx
파일을 만들고 다음 코드를 추가합니다.// Content.styles.tsx
import styled from "@emotion/styled";
export const ContentWrapper = styled.div`
label: ContentWrapper;
position: relative;
`;
이제 우리는 그것을 가져옵니다
// Content.tsx
import { Card } from "@mantine/core";
import useLoading from "../hooks/useLoading";
import { ContentWrapper } from "./Content.styles";
export default function Content() {
const isLoading = useLoading();
return (
<>
<Card shadow="sm">
<ContentWrapper>Content</ContentWrapper>
</Card>
</>
);
}
콘텐츠 높이가 작기 때문에 높이를 설정하고 콘텐츠 래퍼에
overflow: auto
를 추가해 보겠습니다.// Content.styles.tsx
import styled from "@emotion/styled";
export const ContentWrapper = styled.div`
label: ContentWrapper;
position: relative;
height: 300px;
overflow: auto;
`;
또한
SidebarWrapper
를 만들고 여기에 overflow: auto
를 추가해 보겠습니다.// Sidebar.styles.tsx
import styled from "@emotion/styled";
export const SidebarWrapper = styled.div`
label: SidebarWrapper;
overflow: auto;
height: 300px;
position: relative;
`;
하나는 로드 중이고 다른 하나는 콘텐츠용입니다.
// Sidebar.tsx
import { Card, Skeleton } from "@mantine/core";
import { IconFolder, IconHome2 } from "@tabler/icons";
import { useMemo } from "react";
import useLoading from "../../../hooks/useLoading";
import { Node } from "../../../types/FileManager.types";
import { SidebarWrapper } from "./Sidebar.styles";
import SidebarNode from "./SidebarNode";
export type SidebarProps = {
rootDirectory?: Node;
};
export default function Sidebar({ rootDirectory }: SidebarProps) {
const rootChildren = useMemo(() => {
return rootDirectory?.children?.filter(child => child.isDirectory);
}, [rootDirectory]);
const isLoading = useLoading();
if (isLoading) {
return (
<Card shadow={"sm"}>
<SidebarWrapper>
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={12} mt={6} width="80%" radius="sm" />
<Skeleton height={8} mt={6} width="60%" radius="xl" />
</SidebarWrapper>
</Card>
);
}
if (!rootDirectory) return null;
return (
<Card shadow="sm">
<SidebarWrapper>
<SidebarNode
node={rootDirectory}
navProps={{
p: 0,
}}
icon={<IconHome2 size={16} color="#78a136" />}
/>
{rootChildren?.map(child => (
<SidebarNode
navProps={{
p: 0,
pl: 10,
}}
key={child.path}
icon={<IconFolder size={16} fill="#31caf9" />}
node={child}
/>
))}
</SidebarWrapper>
</Card>
);
}
이제 다음과 같이 표시됩니다.
콘텐츠 구성 요소로 돌아가서 오버레이를 추가해 보겠습니다.
// Content.tsx
import { Card, LoadingOverlay } from "@mantine/core";
import useLoading from "../hooks/useLoading";
import { ContentWrapper } from "./Content.styles";
export default function Content() {
const isLoading = useLoading();
return (
<>
<Card shadow="sm">
<ContentWrapper>
<LoadingOverlay visible={isLoading} overlayBlur={2} />
</ContentWrapper>
</Card>
</>
);
}
최종 모습은 다음과 같습니다.
로더 작업이 끝났습니다.
이제 진행 상황이 좋습니다. 다음 장에서는 중지하고 코드를 정리하고 파일과 구조를 재구성할 것입니다.
기사 저장소
Github Repository에서 챕터 파일을 볼 수 있습니다.
Don't forget the
main
branch has the latest updated code.
지금 어디 있는지 말해줘
이 시리즈를 저와 함께 후속 조치하는 경우 현재 위치와 어려움을 겪고 있는 부분을 알려주시면 최대한 도와드리겠습니다.
살람.
Reference
이 문제에 관하여(React 파일 관리자를 만들어 봅시다 12장: 진행률 표시줄, 스켈톤 및 오버레이), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/hassanzohdy/lets-create-a-react-file-manager-chapter-xii-progress-bars-skeltons-and-overlays-1ih3텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)