Skip to content

Commit a2af11c

Browse files
authored
Merge pull request #51 from kenn/replace_variants
Add `replaceWithFlash` / `replaceWithToast`
2 parents 35d2fb1 + 1715ec7 commit a2af11c

5 files changed

Lines changed: 221 additions & 5 deletions

File tree

README.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ export const {
239239
redirectWithError,
240240
redirectWithInfo,
241241
redirectWithWarning,
242+
replaceWithToast,
243+
replaceWithSuccess,
244+
replaceWithError,
245+
replaceWithInfo,
246+
replaceWithWarning,
242247
dataWithSuccess,
243248
dataWithError,
244249
dataWithInfo,
@@ -248,9 +253,9 @@ export const {
248253

249254
## Utilities
250255

251-
## Redirect important notice!
256+
## Redirect/replace important notice!
252257

253-
If you want to use `throw` with any of the `redirectWith...` utilities you need to `await` the function call. This is because `redirectWith...` utilities have to generate a cookie and set it on the headers and if that is not awaited the headers won't be set properly and the redirect won't work.
258+
If you want to use `throw` with any of the `redirectWith...` or `replaceWith...` utilities you need to `await` the function call. This is because those utilities have to generate a cookie and set it on the headers and if that is not awaited the headers won't be set properly and the redirect won't work.
254259

255260
### redirectWithToast
256261

@@ -318,6 +323,72 @@ export const action = () => {
318323
};
319324
```
320325

326+
### replaceWithToast
327+
328+
Replaces the current entry in the history stack and shows a toast message.
329+
330+
```tsx
331+
import { replaceWithToast } from "remix-toast";
332+
333+
export const action = () => {
334+
return replaceWithToast("/login", {
335+
message: "You need to login to access this page",
336+
description: "description of toast",
337+
type: "error",
338+
});
339+
};
340+
```
341+
342+
### replaceWithSuccess
343+
344+
Replaces the current entry in the history stack and shows a success toast message.
345+
346+
```tsx
347+
import { replaceWithSuccess } from "remix-toast";
348+
349+
export const action = () => {
350+
return replaceWithSuccess("/login", "You are logged in!");
351+
//or with description and message (works for all the other utilities as well)
352+
return replaceWithSuccess("/login", { message: "You are logged in!", description: "description of toast" });
353+
};
354+
```
355+
356+
### replaceWithError
357+
358+
Replaces the current entry in the history stack and shows an error toast message.
359+
360+
```tsx
361+
import { replaceWithError } from "remix-toast";
362+
363+
export const action = () => {
364+
return replaceWithError("/login", "You need to login to access this page");
365+
};
366+
```
367+
368+
### replaceWithInfo
369+
370+
Replaces the current entry in the history stack and shows an info toast message.
371+
372+
```tsx
373+
import { replaceWithInfo } from "remix-toast";
374+
375+
export const action = () => {
376+
return replaceWithInfo("/login", "You need to login to access this page");
377+
};
378+
```
379+
380+
### replaceWithWarning
381+
382+
Replaces the current entry in the history stack and shows a warning toast message.
383+
384+
```tsx
385+
import { replaceWithWarning } from "remix-toast";
386+
387+
export const action = () => {
388+
return replaceWithWarning("/login", "You need to login to access this page");
389+
};
390+
```
391+
321392
### dataWithSuccess
322393

323394
Display a success toast message without a redirection.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "remix-toast",
3-
"version": "3.3.0",
3+
"version": "3.4.0",
44
"description": "Utility functions for server-side toast notifications",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createCookieSessionStorage } from "react-router";
2+
import { createToastUtilsWithCustomSession, getToast, replaceWithSuccess } from "./index";
3+
4+
describe("replaceWith* utilities", () => {
5+
it("marks replacements and flashes toast data", async () => {
6+
const response = await replaceWithSuccess("/next", "Saved!");
7+
8+
expect(response.headers.get("Location")).toBe("/next");
9+
expect(response.headers.get("X-Remix-Replace")).toBe("true");
10+
11+
const setCookie = response.headers.get("Set-Cookie");
12+
expect(setCookie).toBeTruthy();
13+
14+
const request = new Request("http://example.com");
15+
request.headers.set("Cookie", setCookie?.split(";")[0] ?? "");
16+
const { toast } = await getToast(request);
17+
18+
expect(toast).toEqual({ message: "Saved!", type: "success" });
19+
});
20+
21+
it("uses a provided custom session storage", async () => {
22+
const customSession = createCookieSessionStorage({
23+
cookie: {
24+
name: "custom-toast-session",
25+
secrets: ["custom-secret"],
26+
},
27+
});
28+
29+
const { replaceWithError, getToast: getToastFromCustomSession } =
30+
createToastUtilsWithCustomSession(customSession);
31+
32+
const response = await replaceWithError("/oops", "Something went wrong");
33+
const setCookie = response.headers.get("Set-Cookie");
34+
35+
expect(setCookie).toContain("custom-toast-session");
36+
37+
const request = new Request("http://example.com");
38+
request.headers.set("Cookie", setCookie?.split(";")[0] ?? "");
39+
40+
const { toast } = await getToastFromCustomSession(request);
41+
42+
expect(toast).toEqual({ message: "Something went wrong", type: "error" });
43+
});
44+
});

src/index.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type SessionStorage, createCookieSessionStorage, data as dataFn, redirect } from "react-router";
1+
import { type SessionStorage, createCookieSessionStorage, data as dataFn, redirect, replace } from "react-router";
22
import {
33
FLASH_SESSION,
44
type FlashSessionValues,
@@ -49,6 +49,18 @@ async function redirectWithFlash(
4949
});
5050
}
5151

52+
async function replaceWithFlash(
53+
url: string,
54+
flash: FlashSessionValues,
55+
init?: ResponseInit,
56+
customSession?: SessionStorage,
57+
) {
58+
return replace(url, {
59+
...init,
60+
headers: await flashMessage(flash, init?.headers, customSession),
61+
});
62+
}
63+
5264
async function dataWithFlash<T>(
5365
data: T,
5466
flash: FlashSessionValues,
@@ -90,6 +102,18 @@ const redirectWithToastFactory = ({ type, session }: BaseFactoryType) => {
90102
};
91103
};
92104

105+
const replaceWithToastFactory = ({ type, session }: BaseFactoryType) => {
106+
return (
107+
replaceUrl: string,
108+
messageOrToast: string | ToastMessageWithoutType,
109+
init?: ResponseInit,
110+
customSession?: SessionStorage,
111+
) => {
112+
const finalInfo = typeof messageOrToast === "string" ? { message: messageOrToast } : messageOrToast;
113+
return replaceWithFlash(replaceUrl, { toast: { ...finalInfo, type } }, init, customSession ?? session);
114+
};
115+
};
116+
93117
/**
94118
* Helper method used to get the toast data from the current request and purge the flash storage from the session
95119
* @param request Current request
@@ -134,10 +158,17 @@ export const createToastUtilsWithCustomSession = (session: SessionStorage) => {
134158
redirectWithToast: (redirectUrl: string, toast: ToastMessage, init?: ResponseInit) => {
135159
return redirectWithFlash(redirectUrl, { toast }, init, session);
136160
},
161+
replaceWithToast: (replaceUrl: string, toast: ToastMessage, init?: ResponseInit) => {
162+
return replaceWithFlash(replaceUrl, { toast }, init, session);
163+
},
137164
redirectWithSuccess: redirectWithToastFactory({ type: "success", session }),
138165
redirectWithError: redirectWithToastFactory({ type: "error", session }),
139166
redirectWithInfo: redirectWithToastFactory({ type: "info", session }),
140167
redirectWithWarning: redirectWithToastFactory({ type: "warning", session }),
168+
replaceWithSuccess: replaceWithToastFactory({ type: "success", session }),
169+
replaceWithError: replaceWithToastFactory({ type: "error", session }),
170+
replaceWithInfo: replaceWithToastFactory({ type: "info", session }),
171+
replaceWithWarning: replaceWithToastFactory({ type: "warning", session }),
141172
getToast: (request: Request) => getToast(request, session),
142173
};
143174
};
@@ -211,6 +242,24 @@ export const redirectWithToast = (
211242
return redirectWithFlash(redirectUrl, { toast }, init, customSession);
212243
};
213244

245+
/**
246+
* Helper method used to replace the current route with a toast notification
247+
*
248+
* If thrown it needs to be awaited
249+
* @param replaceUrl Replace URL
250+
* @param toast Toast message and it's type
251+
* @param init Additional response options (status code, additional headers etc)
252+
* @returns Returns replace response with toast cookie set
253+
*/
254+
export const replaceWithToast = (
255+
replaceUrl: string,
256+
toast: ToastMessage,
257+
init?: ResponseInit,
258+
customSession?: SessionStorage,
259+
) => {
260+
return replaceWithFlash(replaceUrl, { toast }, init, customSession);
261+
};
262+
214263
/**
215264
* Helper method used to redirect the user to a new page with an error toast notification
216265
*
@@ -222,6 +271,17 @@ export const redirectWithToast = (
222271
*/
223272
export const redirectWithError = redirectWithToastFactory({ type: "error" });
224273

274+
/**
275+
* Helper method used to replace the current route with an error toast notification
276+
*
277+
* If this method is thrown it needs to be awaited, otherwise it can just be returned
278+
* @param replaceUrl Replace url
279+
* @param message Message to be shown as info
280+
* @param init Additional response options (status code, additional headers etc)
281+
* @returns Returns replace response with toast cookie set
282+
*/
283+
export const replaceWithError = replaceWithToastFactory({ type: "error" });
284+
225285
/**
226286
* Helper method used to redirect the user to a new page with a success toast notification
227287
*
@@ -233,6 +293,17 @@ export const redirectWithError = redirectWithToastFactory({ type: "error" });
233293
*/
234294
export const redirectWithSuccess = redirectWithToastFactory({ type: "success" });
235295

296+
/**
297+
* Helper method used to replace the current route with a success toast notification
298+
*
299+
* If this method is thrown it needs to be awaited, otherwise it can just be returned
300+
* @param replaceUrl Replace url
301+
* @param message Message to be shown as info
302+
* @param init Additional response options (status code, additional headers etc)
303+
* @returns Returns replace response with toast cookie set
304+
*/
305+
export const replaceWithSuccess = replaceWithToastFactory({ type: "success" });
306+
236307
/**
237308
* Helper method used to redirect the user to a new page with a warning toast notification
238309
*
@@ -244,6 +315,17 @@ export const redirectWithSuccess = redirectWithToastFactory({ type: "success" })
244315
*/
245316
export const redirectWithWarning = redirectWithToastFactory({ type: "warning" });
246317

318+
/**
319+
* Helper method used to replace the current route with a warning toast notification
320+
*
321+
* If this method is thrown it needs to be awaited, otherwise it can just be returned
322+
* @param replaceUrl Replace url
323+
* @param message Message to be shown as info
324+
* @param init Additional response options (status code, additional headers etc)
325+
* @returns Returns replace response with toast cookie set
326+
*/
327+
export const replaceWithWarning = replaceWithToastFactory({ type: "warning" });
328+
247329
/**
248330
* Helper method used to redirect the user to a new page with a info toast notification
249331
*
@@ -254,3 +336,14 @@ export const redirectWithWarning = redirectWithToastFactory({ type: "warning" })
254336
* @returns Returns redirect response with toast cookie set
255337
*/
256338
export const redirectWithInfo = redirectWithToastFactory({ type: "info" });
339+
340+
/**
341+
* Helper method used to replace the current route with an info toast notification
342+
*
343+
* If this method is thrown it needs to be awaited, otherwise it can just be returned
344+
* @param replaceUrl Replace url
345+
* @param message Message to be shown as info
346+
* @param init Additional response options (status code, additional headers etc)
347+
* @returns Returns replace response with toast cookie set
348+
*/
349+
export const replaceWithInfo = replaceWithToastFactory({ type: "info" });

tsconfig.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,13 @@
103103
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
104104
"skipLibCheck": true /* Skip type checking all .d.ts files. */
105105
},
106-
"exclude": ["./bin/**/*.test.ts", "./dist/**/*", "./bin/mocks/**/*", "./*.config.ts", "test-apps/**"]
106+
"exclude": [
107+
"./bin/**/*.test.ts",
108+
"./dist/**/*",
109+
"./bin/mocks/**/*",
110+
"./*.config.ts",
111+
"test-apps/**",
112+
"**/*.test.ts",
113+
"**/*.test.tsx"
114+
]
107115
}

0 commit comments

Comments
 (0)