11import type { LoaderArgs } from "@remix-run/node" ;
2+ import type { FormEventHandler } from "react" ;
3+ import { useRef } from "react" ;
24import { json , redirect } from "@remix-run/node" ;
35import {
46 Form ,
@@ -14,13 +16,26 @@ import { mockProducts } from "~/lib/product.server";
1416export const loader = ( { request } : LoaderArgs ) => {
1517 const { searchParams } = new URL ( request . url ) ;
1618
17- const schema = z . object ( {
18- name : z . string ( ) . optional ( ) ,
19- minPrice : z . coerce . number ( ) . min ( 1 ) . optional ( ) ,
20- maxPrice : z . coerce . number ( ) . min ( 1 ) . optional ( ) ,
21- page : z . coerce . number ( ) . min ( 1 ) . optional ( ) ,
22- size : z . coerce . number ( ) . min ( 5 ) . max ( 10 ) . step ( 5 ) . optional ( ) ,
23- } ) ;
19+ const schema = z
20+ . object ( {
21+ name : z . string ( ) . optional ( ) ,
22+ minPrice : z . coerce . number ( ) . gt ( 0 ) . optional ( ) ,
23+ maxPrice : z . coerce . number ( ) . gt ( 0 ) . optional ( ) ,
24+ page : z . coerce . number ( ) . min ( 1 ) . step ( 1 ) . optional ( ) ,
25+ size : z . coerce . number ( ) . min ( 5 ) . max ( 10 ) . step ( 5 ) . optional ( ) ,
26+ } )
27+ . refine (
28+ ( { minPrice, maxPrice } ) => {
29+ if ( minPrice && maxPrice && minPrice > maxPrice ) {
30+ return false ;
31+ }
32+ return true ;
33+ } ,
34+ {
35+ message : "Max price cannot be less than min price" ,
36+ path : [ "maxPrice" ] ,
37+ } ,
38+ ) ;
2439
2540 // filter out empty string values from query params
2641 // otherwise zod will throw while coercing them to number
@@ -29,8 +44,18 @@ export const loader = ({ request }: LoaderArgs) => {
2944 ) ;
3045
3146 if ( ! parseResult . success ) {
32- console . log ( parseResult . error ) ;
33- throw new Error ( "Invalid query params" ) ;
47+ return json ( {
48+ products : [ ] ,
49+ searchParams : {
50+ name : searchParams . get ( "name" ) || "" ,
51+ minPrice : searchParams . get ( "minPrice" ) || "" ,
52+ maxPrice : searchParams . get ( "maxPrice" ) || "" ,
53+ page : 1 ,
54+ size : searchParams . get ( "size" ) === "10" ? 10 : 5 ,
55+ } ,
56+ fieldErrors : parseResult . error . flatten ( ) . fieldErrors ,
57+ totalPageCount : 1 ,
58+ } ) ;
3459 }
3560
3661 const { name, minPrice, maxPrice, page, size } = parseResult . data ;
@@ -87,6 +112,7 @@ export const loader = ({ request }: LoaderArgs) => {
87112 page : pagination . page ,
88113 size : pagination . size ,
89114 } ,
115+ fieldErrors : null ,
90116 totalPageCount,
91117 } ,
92118 {
@@ -97,39 +123,83 @@ export const loader = ({ request }: LoaderArgs) => {
97123 ) ;
98124} ;
99125
126+ const errorTextStyle : React . CSSProperties = {
127+ fontWeight : "bold" ,
128+ color : "red" ,
129+ marginInline : 0 ,
130+ marginBlock : "0.25rem" ,
131+ } ;
132+
100133export default function ProductsView ( ) {
101134 const loaderData = useLoaderData < typeof loader > ( ) ;
102135 const submit = useSubmit ( ) ; // used for select onChange
103136 const navigation = useNavigation ( ) ;
104137 const isLoading = navigation . state === "loading" ;
105138
139+ // Debounced onChange handler to submit the form after a delay
140+ // Create a ref to hold the debounce timer
141+ const debounceTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
142+ const formRef = useRef < HTMLFormElement > ( null ) ;
143+ const onChangeHandler : FormEventHandler < HTMLFormElement > = ( event ) => {
144+ // On input change, clear the previous debounce timer first
145+ if ( debounceTimerRef . current ) {
146+ clearTimeout ( debounceTimerRef . current ) ;
147+ }
148+
149+ // Set a new debounce timer to trigger submission after a delay
150+ debounceTimerRef . current = setTimeout ( ( ) => {
151+ submit ( formRef . current ) ;
152+ } , 300 ) ; // Adjust the debounce delay as needed (in milliseconds)
153+ } ;
154+
106155 return (
107156 < div >
108157 < h1 > Products</ h1 >
109158
110- < Form method = "get" >
159+ < Form method = "get" ref = { formRef } onChange = { onChangeHandler } >
111160 { /* Filters */ }
112- < label htmlFor = "name" > Product Name:</ label >
113- < input
114- type = "text"
115- id = "name"
116- name = "name"
117- defaultValue = { loaderData . searchParams . name }
118- /> { " " }
119- < label htmlFor = "minPrice" > Min Price:</ label >
120- < input
121- type = "number"
122- id = "minPrice"
123- name = "minPrice"
124- defaultValue = { loaderData . searchParams . minPrice }
125- /> { " " }
126- < label htmlFor = "maxPrice" > Max Price:</ label >
127- < input
128- type = "number"
129- id = "maxPrice"
130- name = "maxPrice"
131- defaultValue = { loaderData . searchParams . maxPrice }
132- /> { " " }
161+ < div >
162+ < label htmlFor = "name" > Product Name:</ label >
163+ < input
164+ type = "text"
165+ id = "name"
166+ name = "name"
167+ defaultValue = { loaderData . searchParams . name }
168+ />
169+ { loaderData ?. fieldErrors ?. name ?. map ( ( error , index ) => (
170+ < p style = { errorTextStyle } key = { `name-error-${ index } ` } >
171+ { error }
172+ </ p >
173+ ) ) }
174+ </ div >
175+ < div >
176+ < label htmlFor = "minPrice" > Min Price:</ label >
177+ < input
178+ type = "number"
179+ id = "minPrice"
180+ name = "minPrice"
181+ defaultValue = { loaderData . searchParams . minPrice }
182+ />
183+ { loaderData ?. fieldErrors ?. minPrice ?. map ( ( error , index ) => (
184+ < p style = { errorTextStyle } key = { `min-price-error-${ index } ` } >
185+ { error }
186+ </ p >
187+ ) ) }
188+ </ div >
189+ < div >
190+ < label htmlFor = "maxPrice" > Max Price:</ label >
191+ < input
192+ type = "number"
193+ id = "maxPrice"
194+ name = "maxPrice"
195+ defaultValue = { loaderData . searchParams . maxPrice }
196+ />
197+ { loaderData ?. fieldErrors ?. maxPrice ?. map ( ( error , index ) => (
198+ < p style = { errorTextStyle } key = { `max-price-error-${ index } ` } >
199+ { error }
200+ </ p >
201+ ) ) }
202+ </ div >
133203 < button type = "submit" disabled = { isLoading } >
134204 Search
135205 </ button >
@@ -152,41 +222,52 @@ export default function ProductsView() {
152222 </ ul >
153223 < hr />
154224 { /* Pagination */ }
155- < span >
156- Page { loaderData . searchParams . page } of { loaderData . totalPageCount }
157- </ span > { " " }
158- < button
159- type = "submit"
160- name = "page"
161- value = { loaderData . searchParams . page - 1 }
162- disabled = { isLoading || loaderData . searchParams . page === 1 }
163- >
164- Prev
165- </ button > { " " }
166- < button
167- type = "submit"
168- name = "page"
169- value = { loaderData . searchParams . page + 1 }
170- disabled = {
171- isLoading ||
172- loaderData . searchParams . page === loaderData . totalPageCount
173- }
174- >
175- Next
176- </ button > { " " }
177- < label htmlFor = "size" > Items per Page:</ label >
178- < select
179- id = "size"
180- name = "size"
181- defaultValue = { loaderData . searchParams . size }
182- onChange = { ( event ) => {
183- submit ( event . currentTarget . form ) ;
184- } }
185- disabled = { isLoading }
186- >
187- < option value = { 5 } > 5</ option >
188- < option value = { 10 } > 10</ option >
189- </ select >
225+ < div >
226+ < span >
227+ Page { loaderData . searchParams . page } of { loaderData . totalPageCount }
228+ </ span > { " " }
229+ < button
230+ type = "submit"
231+ name = "page"
232+ value = { loaderData . searchParams . page - 1 }
233+ disabled = { isLoading || loaderData . searchParams . page === 1 }
234+ >
235+ Prev
236+ </ button > { " " }
237+ < button
238+ type = "submit"
239+ name = "page"
240+ value = { loaderData . searchParams . page + 1 }
241+ disabled = {
242+ isLoading ||
243+ loaderData . searchParams . page === loaderData . totalPageCount
244+ }
245+ >
246+ Next
247+ </ button >
248+ { loaderData ?. fieldErrors ?. page ?. map ( ( error , index ) => (
249+ < p style = { errorTextStyle } key = { `page-error-${ index } ` } >
250+ { error }
251+ </ p >
252+ ) ) }
253+ </ div >
254+ < div >
255+ < label htmlFor = "size" > Items per Page:</ label >
256+ < select
257+ id = "size"
258+ name = "size"
259+ defaultValue = { loaderData . searchParams . size }
260+ disabled = { isLoading }
261+ >
262+ < option value = { 5 } > 5</ option >
263+ < option value = { 10 } > 10</ option >
264+ </ select >
265+ { loaderData ?. fieldErrors ?. size ?. map ( ( error , index ) => (
266+ < p style = { errorTextStyle } key = { `size-error-${ index } ` } >
267+ { error }
268+ </ p >
269+ ) ) }
270+ </ div >
190271 </ Form >
191272 < hr />
192273 < Link to = "/" prefetch = "intent" >
0 commit comments