Skip to content

Commit

Permalink
persist tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
layerok committed Jun 30, 2024
1 parent 6db37b4 commit 074daee
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 114 deletions.
42 changes: 38 additions & 4 deletions src/components/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,63 @@ import { Sidebar } from "src/components/Sidebar/Sidebar";
import { Tabs, TabsApi } from "src/components/Tabs/Tabs.tsx";

import { TabStoreKey } from "src/constants/tabs.constants.ts";
import { useTabbedNavigation } from "src/lib/tabs";
import { TabbedNavigationMeta, useTabbedNavigation } from "src/lib/tabs";

import { useCallback, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import * as routes from "src/constants/routes.constants.ts";

import { usePersistTabs } from "src/lib/tabs/persist.ts";
import { localStorageDriver } from "src/lib/storage/local-storage.ts";

const persistStoreKey = {
name: "main-tabs",
version: "1.0",
};

export function AppLayout() {
const apiRef = useRef<TabsApi>();
const navigate = useNavigate();

const { getTabsProps } = useTabbedNavigation({
const { getTabsFromStorage, persistTabs } =
usePersistTabs<TabbedNavigationMeta>({
storage: localStorageDriver,
storageKey: persistStoreKey,
});

const [tabs, setTabs] = useState(getTabsFromStorage() || []);

const [startPinnedTabs, setStartPinnedTabsChange] = useState<string[]>([]);

const { activeTabId, setActiveTabId } = useTabbedNavigation({
tabs,
onTabsChange: setTabs,
startPinnedTabs,
key: TabStoreKey.Main,
onCloseAllTabs: () => {
navigate(routes.homeRoute);
},
resolveTabMeta: useCallback(() => ({}), []),
});

useEffect(() => {
return persistTabs(tabs);
}, [tabs, persistTabs]);

return (
<div className="layout">
<Sidebar />
<div className={"content"}>
<header className={"header"}>John Doe</header>
<Tabs apiRef={apiRef} {...getTabsProps()} />
<Tabs
apiRef={apiRef}
tabs={tabs}
onTabsChange={setTabs}
onStartPinnedTabsChange={setStartPinnedTabsChange}
startPinnedTabs={startPinnedTabs}
hasControlledActiveTabId
activeTabId={activeTabId}
onActiveTabIdChange={setActiveTabId}
/>
<Outlet />
</div>
</div>
Expand Down
46 changes: 46 additions & 0 deletions src/lib/storage/create-storage-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export type StorageKey = {
version: string;
name: string;
};

export const createStorageDriver = (storage: Storage) => {
const get = <V>(key: StorageKey): V | null => {
const value = storage.getItem(key.name);
if (!value) {
return null;
}
try {
const result = JSON.parse(value);
if (result?.version !== key.version) {
remove(key);
return null;
}
return result?.state;
} catch (e) {
// todo: log this
return null;
}
};

const set = (key: StorageKey, value: any) => {
storage.setItem(
key.name,
JSON.stringify({
state: value,
version: key.version,
}),
);
};

const remove = (key: StorageKey) => {
storage.removeItem(key.name);
};

return {
get,
set,
remove,
};
};

export type StorageDriver = ReturnType<typeof createStorageDriver>;
9 changes: 9 additions & 0 deletions src/lib/storage/local-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createStorageDriver } from "src/lib/storage/create-storage-driver.ts";

export const localStorageDriver = createStorageDriver(localStorage);

export const {
get: getFromLocalStorage,
set: setToLocalStorage,
remove: removeFromLocalStorage,
} = localStorageDriver;
32 changes: 32 additions & 0 deletions src/lib/storage/memory-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createStorageDriver } from "src/lib/storage/create-storage-driver.ts";

const memoryStorageMap = new Map();

export const memoryStorage: Storage = {
getItem: (storageKey: string) => {
return memoryStorageMap.get(storageKey);
},
setItem(key: string, value: string) {
return memoryStorageMap.set(key, value);
},
removeItem(key: string) {
memoryStorageMap.delete(key);
},
clear() {
memoryStorageMap.clear();
},
get length() {
return memoryStorageMap.size;
},
key(index: number) {
return [...memoryStorageMap.keys()][index];
},
};

export const memoryStorageDriver = createStorageDriver(memoryStorage);

export const {
get: getFromMemoryStorage,
set: setToMemoryStorage,
remove: removeFromMemoryStorage,
} = memoryStorageDriver;
37 changes: 37 additions & 0 deletions src/lib/tabs/persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TabModel, ValidTabMeta } from "src/lib/tabs/tabs.types.ts";
import {
StorageDriver,
StorageKey,
} from "src/lib/storage/create-storage-driver.ts";
import { useCallback } from "react";

export const usePersistTabs = <Meta extends ValidTabMeta = ValidTabMeta>({
storageKey,
storage,
}: {
storageKey: StorageKey;
storage: StorageDriver;
}) => {
return {
getTabsFromStorage: useCallback(
() => storage.get<TabModel<Meta>[]>(storageKey) || [],
[storageKey, storage],
),
persistTabs: useCallback(
(tabs: TabModel<Meta>[]) => {
const onUnload = () => {
// save on window close
// it doesn't make sense for memory storage
storage.set(storageKey, tabs);
};
window.addEventListener("unload", onUnload);
return () => {
window.removeEventListener("unload", onUnload);
// save on unmount
storage.set(storageKey, tabs);
};
},
[storageKey, storage],
),
};
};
65 changes: 25 additions & 40 deletions src/lib/tabs/tabbed-navigation-2.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useDataRouterContext } from "../../hooks/useDataRouterContext.tsx";
import { useMatches, useNavigate } from "react-router-dom";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect } from "react";
import { RouterState, AgnosticDataRouteMatch } from "@remix-run/router";
import { last, replaceAt, insertAt } from "src/utils/array-utils.ts";
import { TabModel, ValidTabMeta } from "src/lib/tabs/tabs.types.ts";
Expand All @@ -17,7 +17,7 @@ type ValidID<Params extends ValidParams = ValidParams> =
| string
| { (props: { params: Params }): string };

type TabConfig<
export type TabConfig<
Params extends ValidParams = ValidParams,
ID extends ValidID<Params> = ValidID<Params>,
> = {
Expand All @@ -26,43 +26,36 @@ type TabConfig<
title: ({ params }: { params: Params }) => string;
};

export class Tab<
Params extends ValidParams = ValidParams,
ID extends ValidID<Params> = ValidID<Params>,
> {
routeId: string;
id: ID;
title: ({ params }: { params: Params }) => string;
constructor(props: TabConfig<Params, ID>) {
const { id, routeId, title } = props;
this.id = id;
this.routeId = routeId;
this.title = title;
}
}
type TabsChangeCallback<Meta extends ValidTabMeta = ValidTabMeta> = (
tabs:
| TabModel<TabbedNavigationMeta & Meta>[]
| {
(
prevTabs: TabModel<TabbedNavigationMeta & Meta>[],
): TabModel<TabbedNavigationMeta & Meta>[];
},
) => void;

export const useTabbedNavigation2 = <
Meta extends ValidTabMeta = ValidTabMeta,
Params extends ValidParams = ValidParams,
>(options: {
config: Tab<Params>[];
initialTabs?: TabModel<TabbedNavigationMeta & Meta>[];
initialStartPinnedTabs?: string[];
config: TabConfig<Params>[];
onTabsChange?: TabsChangeCallback<Meta>;
tabs: TabModel<TabbedNavigationMeta & Meta>[];
startPinnedTabs: string[];
onCloseAllTabs: () => void;
resolveTabMeta: (match: AgnosticDataRouteMatch) => Meta;
}) => {
const {
resolveTabMeta,
onCloseAllTabs,
initialTabs = [],
initialStartPinnedTabs = [],
onTabsChange,
tabs = [],
startPinnedTabs,
config,
} = options;
const [tabs, setTabs] =
useState<TabModel<TabbedNavigationMeta & Meta>[]>(initialTabs);
const [startPinnedTabs, setStartPinnedTabs] = useState<string[]>(
initialStartPinnedTabs,
);

const { router } = useDataRouterContext();
const navigate = useNavigate();

Expand Down Expand Up @@ -91,10 +84,13 @@ export const useTabbedNavigation2 = <
);
return [def, match];
})
.filter(([def]) => !!def) as unknown as [Tab, AgnosticDataRouteMatch][];
.filter(([def]) => !!def) as unknown as [
TabConfig,
AgnosticDataRouteMatch,
][];

pairs.forEach(([def, match]) => {
setTabs((prevTabs) => {
onTabsChange?.((prevTabs) => {
const tab = prevTabs.find(
(tab) =>
tab.id ===
Expand Down Expand Up @@ -134,7 +130,7 @@ export const useTabbedNavigation2 = <
});
});
},
[resolveTabMeta, startPinnedTabs, config],
[resolveTabMeta, startPinnedTabs, config, onTabsChange],
);

useEffect(() => {
Expand All @@ -153,18 +149,7 @@ export const useTabbedNavigation2 = <
})?.pathname;

return {
getTabsProps: () => ({
activeTabId,
tabs,
onTabsChange: setTabs,
onActiveTabIdChange: setActiveTabId,
hasControlledActiveTabId: true,
startPinnedTabs,
onStartPinnedTabsChange: setStartPinnedTabs,
}),
setTabs,
setActiveTabId,
tabs,
activeTabId,
};
};
Loading

0 comments on commit 074daee

Please sign in to comment.