Skip to content

Commit 5b200aa

Browse files
committed
Merge pull request #38 from g4rcez/forward-state
2 parents a8f40c6 + 07cc3b4 commit 5b200aa

File tree

12 files changed

+98
-160
lines changed

12 files changed

+98
-160
lines changed

.github/workflows/codeql.yml

Lines changed: 0 additions & 77 deletions
This file was deleted.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "brouther",
33
"type": "module",
4-
"version": "4.3.5",
4+
"version": "4.3.6",
55
"source": "./src/index.ts",
66
"types": "./dist/index.d.ts",
77
"main": "./dist/index.js",

playground/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { usePage, Link, useQueryString, usePaths, useErrorPage, NotFoundRoute } from "../../src";
1+
import { Link, NotFoundRoute, Outlet, useErrorPage, usePaths, useQueryString } from "../../src";
22
import { router } from "./routes";
33
import { NotFound } from "./not-found";
44

55
function App() {
6-
const page = usePage();
76
const error = useErrorPage<NotFoundRoute>();
87
const queryString = useQueryString();
98
const paths = usePaths();
@@ -47,7 +46,9 @@ function App() {
4746
</nav>
4847
</header>
4948
<div className="w-full container max-w-lg mx-auto px-4 md:px-0">
50-
{page !== null ? <main className="page">{page}</main> : null}
49+
<main className="page">
50+
<Outlet />
51+
</main>
5152
<NotFound error={error} />
5253
</div>
5354
</div>

playground/src/pages/root.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionProps, createFormPath, Form, InferLoader, jsonResponse, LoaderProps, redirectResponse, useDataLoader } from "../../../src";
1+
import { ActionProps, createFormPath, Form, jsonResponse, LoaderProps, redirectResponse, useDataLoader, useLoadingState } from "../../../src";
22
import { useEffect, useState } from "react";
33
import { Input } from "../components/input";
44

@@ -10,9 +10,11 @@ export const actions = () => ({
1010
post: async (args: ActionProps<Route>) => {
1111
const url = new URL(args.request.url);
1212
const json = await args.request.json();
13+
console.log(args.request);
1314
url.searchParams.set("firstName", json.person.name);
1415
url.searchParams.set("lastName", json.person.surname);
1516
url.searchParams.set("date", json.person.birthday);
17+
await new Promise((res) => setTimeout(res, 3000));
1618
return redirectResponse(url.href);
1719
},
1820
});
@@ -34,6 +36,8 @@ export default function Root() {
3436
console.log("data loader", data?.qs);
3537
}, [data]);
3638

39+
const loading = useLoadingState();
40+
3741
return (
3842
<section className="flex flex-col gap-12">
3943
<h2 className="font-bold text-3xl">Form post action - json</h2>
@@ -43,13 +47,15 @@ export default function Root() {
4347
>
4448
Throw error
4549
</button>
46-
<Form encType="json" method="post" className="flex gap-8 items-end">
47-
<Input defaultValue={qs?.firstName} name={path("person.name")} placeholder="First Name" />
48-
<Input defaultValue={qs?.lastName} name={path("person.surname")} placeholder="Last Name" />
49-
<Input defaultValue={qs?.date} name={path("person.birthday")} type="date" placeholder="Birthday" />
50-
<button className="py-2 px-4 rounded bg-blue-500 text-white font-medium" type="submit">
51-
Submit
52-
</button>
50+
<Form encType="json" method="post">
51+
<fieldset className="flex gap-8 items-end disabled:opacity-40 disabled:bg-gray-400">
52+
<Input defaultValue={qs?.firstName} name={path("person.name")} placeholder="First Name" />
53+
<Input defaultValue={qs?.lastName} name={path("person.surname")} placeholder="Last Name" />
54+
<Input defaultValue={qs?.date} name={path("person.birthday")} type="date" placeholder="Birthday" />
55+
<button className="py-2 px-4 rounded bg-blue-500 text-white font-medium" type="submit">
56+
{loading ? "Saving..." : "Submit"}
57+
</button>
58+
</fieldset>
5359
</Form>
5460
</section>
5561
);

src/brouther/brouther.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type ContextProps = {
2121
navigation: RouterNavigator;
2222
page: X.Nullable<ConfiguredRoute>;
2323
paths: {};
24+
setLoading: (b: boolean) => void;
2425
};
2526

2627
const Context = createContext<ContextProps | undefined>(undefined);
@@ -59,18 +60,18 @@ const findMatches = (config: Base, pathName: string, filter: BroutherProps<any>[
5960
*/
6061
export const Brouther = <T extends Base>({ config, ErrorElement, children, filter }: BroutherProps<T>) => {
6162
const [state, setState] = useState(() => ({
62-
loading: false,
6363
error: null as X.Nullable<BroutherError>,
6464
location: config.history.location,
6565
loaderData: null as X.Nullable<Response>,
6666
matches: findMatches(config, config.history.location.pathname, filter),
6767
}));
68+
const [loading, setLoading] = useState(false);
6869

6970
useEffect(() => {
7071
const result = findMatches(config, state.location.pathname, filter);
7172
const request = async () => {
7273
if (result?.page?.loader) {
73-
setState((p) => ({ ...p, loading: true }));
74+
setLoading(true);
7475
const search = new URLSearchParams(state.location.search);
7576
const qs = transformData(search, mapUrlToQueryStringRecord(result.page.originalPath, fromStringToValue));
7677
const s = (state.location.state as any) ?? {};
@@ -81,11 +82,12 @@ export const Brouther = <T extends Base>({ config, ErrorElement, children, filte
8182
data: result.page.data ?? {},
8283
request: new Request(s.url ?? href, { body: s.body ?? undefined, headers: s.headers }),
8384
});
85+
setLoading(false);
8486
return { loaderData: r, loading: false };
8587
}
8688
};
8789
setState((p) => ({ ...p, matches: result, error: result.error ?? null }));
88-
request().then((x) => setState((prev) => ({ ...prev, ...x })));
90+
request().then((result) => setState((prev) => ({ ...prev, ...result })));
8991
}, [findMatches, state.location.search, state.location.state, config, filter, state.location.pathname]);
9092

9193
useEffect(() => {
@@ -105,11 +107,12 @@ export const Brouther = <T extends Base>({ config, ErrorElement, children, filte
105107
error: state.matches.error ?? state.error,
106108
href,
107109
loaderData: state.loaderData,
108-
loading: state.loading,
110+
loading,
109111
location: state.location,
110112
navigation: config.navigation,
111-
paths: state.matches.params,
112113
page: state.error !== null ? null : state.matches.page,
114+
paths: state.matches.params,
115+
setLoading,
113116
}}
114117
>
115118
<CatchError fallback={Fallback} state={state} setError={setError}>
@@ -214,7 +217,29 @@ export function useDataLoader<T extends DataLoader>(fn: (response: Response) =>
214217
return state;
215218
}
216219

220+
/*
221+
Get current error and the current page that throw the error
222+
@returns string
223+
*/
217224
export const useRouteError = () => {
218225
const router = useRouter();
219226
return [router.error, router.page] as const;
220227
};
228+
229+
/*
230+
Render the page that match with your route
231+
@returns string
232+
*/
233+
export const Outlet = () => {
234+
const page = usePage();
235+
return <Fragment>{page}</Fragment>;
236+
};
237+
238+
/*
239+
Boolean that represents if it's your action/loader process is loading
240+
@returns string
241+
*/
242+
export const useLoadingState = () => {
243+
const router = useRouter();
244+
return router.loading;
245+
};

src/form/form-data-api.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { IParseOptions, IStringifyOptions } from "qs";
21
import { parse, stringify } from "qs";
32

43
const sort = (a: string, b: string) => a.localeCompare(b);

src/form/form.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,40 +49,40 @@ const fromResponse = (ctx: ContextProps, response: Response) => {
4949
export const Form = forwardRef<HTMLFormElement, Props>(function InnerForm(props, externalRef) {
5050
const router = useRouter();
5151
const method = (props.method || "get").toLowerCase() as HttpMethods;
52+
5253
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
54+
router.setLoading(true);
5355
event.preventDefault();
5456
const form = event.currentTarget;
55-
await props.onSubmit?.(event);
57+
if (props.onSubmit) {
58+
await props.onSubmit(event);
59+
}
5660
const page = router.page;
5761
if (method === "get" && page?.loader) {
58-
return fromResponse(
59-
router,
60-
await page.loader({
61-
paths: router.paths,
62-
data: page.data ?? {},
63-
path: router.href as PathFormat,
64-
request: new Request(router.href),
65-
queryString: fetchQs(router.location.search, page.originalPath),
66-
})
67-
);
62+
const body = {
63+
paths: router.paths,
64+
data: page.data ?? {},
65+
path: router.href as PathFormat,
66+
request: new Request(router.href),
67+
queryString: fetchQs(router.location.search, page.originalPath),
68+
};
69+
return fromResponse(router, await page.loader(body));
6870
}
6971
if (page?.actions && method !== "get") {
70-
const actions = await page.actions()
72+
const actions = await page.actions();
7173
if (has(actions, method)) {
7274
const fn = actions[method];
7375
const body = parseFromEncType(props.encType, form);
7476
const headers = new Headers();
7577
if (props.encType) headers.set("Content-Type", props.encType);
76-
return fromResponse(
77-
router,
78-
await fn!({
79-
paths: router.paths,
80-
data: page.data ?? {},
81-
path: router.href as PathFormat,
82-
request: new Request(router.href, { body, method, headers }),
83-
queryString: fetchQs(router.location.search, page.originalPath),
84-
})
85-
);
78+
const response = await fn!({
79+
paths: router.paths,
80+
data: page.data ?? {},
81+
path: router.href as PathFormat,
82+
request: new Request(router.href, { body, method, headers }),
83+
queryString: fetchQs(router.location.search, page.originalPath),
84+
});
85+
return fromResponse(router, response);
8686
}
8787
}
8888
};

src/index.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
11
export type {
2-
ConfiguredRoute,
3-
Route,
2+
ActionProps,
3+
Actions,
44
AnyJson,
55
AnyJsonArray,
6+
ConfiguredRoute,
7+
HttpMethods,
68
Loader,
7-
Actions,
9+
LoaderProps,
810
PathFormat,
11+
Route,
912
RouteData,
10-
HttpMethods,
1113
WithoutGet,
12-
LoaderProps,
13-
ActionProps,
1414
} from "./types";
1515
export type { LinkProps } from "./router/link";
1616
export type { Paths } from "./types/paths";
1717
export type { QueryString } from "./types/query-string";
1818
export type { RouterNavigator } from "./router/router-navigator";
1919
export {
2020
Brouther,
21-
useUrlSearchParams,
22-
usePage,
21+
Outlet,
22+
useBasename,
2323
useDataLoader,
24-
useNavigation,
2524
useErrorPage,
25+
useHref,
26+
useLoadingState,
27+
useNavigation,
28+
usePage,
2629
usePaths,
2730
useQueryString,
28-
useHref,
2931
useRouteError,
30-
useBasename,
32+
useUrlSearchParams,
3133
} from "./brouther/brouther";
3234
export { BroutherError, NotFoundRoute } from "./utils/errors";
3335
export { Form } from "./form/form";
3436
export { Link } from "./router/link";
3537
export {
36-
createRouter,
37-
createMappedRouter,
38-
createMappedRouter as createRouterMap,
39-
createMappedRouter as createRecordRouter,
40-
createRoute,
4138
asyncActions,
42-
asyncLoader,
4339
asyncComponent,
40+
asyncLoader,
41+
createMappedRouter as createRecordRouter,
42+
createMappedRouter as createRouterMap,
43+
createMappedRouter,
44+
createRoute,
45+
createRouter,
4446
} from "./router/router";
4547
export { urlEntity, mergeUrlEntities, createHref, qsToString, transformData, createPaths, type GetPaths } from "./utils/utils";
4648
export { urlSearchParamsToJson, jsonToURLSearchParams, formToJson } from "./form/form-data-api";

0 commit comments

Comments
 (0)