Use `ListBox` from `#headlessui/react` with Mobx? - javascript

Before using ListBox:
store/index.ts
import { action, makeObservable, observable } from 'mobx'
import type { IFrameItStore, TrafficSignal } from '#/types/index'
export class FrameItStore implements IFrameItStore {
trafficSignal: TrafficSignal = {
shape: 'circle',
}
constructor() {
makeObservable(this, {
trafficSignal: observable,
updateTrafficSignal: action.bound,
})
}
updateTrafficSignal({ shape }: TrafficSignal) {
if (shape) this.trafficSignal.shape = shape
}
}
Shape.tsx
import { observer } from 'mobx-react'
import * as React from 'react'
import { useFrameItStore } from '#/store/index'
import type { TrafficSignalShape } from '#/types/index'
export const Shape = observer(() => {
const frameItStore = useFrameItStore()
return (
<>
<label htmlFor="shape" className="mb-1 text-sm font-medium text-blue-gray-500">
Shape
</label>
<select
id="shape"
className="block w-full px-3 py-2 mb-2 bg-white border border-gray-300 rounded-md shadow-sm text-blue-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
value={frameItStore.trafficSignal.shape}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const shape = e.target.value as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
>
<option value="circle">Circle</option>
<option value="square">Square</option>
</select>
</>
)
})
App.tsx
<Shape />
After using ListBox:
Select.tsx
import * as React from 'react'
import { Listbox, Transition } from '#headlessui/react'
import clsx from 'clsx'
import { Selector, Check } from '#/components/icons/index'
type Option = {
id: string
name: string
img: string
}
interface IProps {
label?: string
options: Array<Option>
}
export const Select = ({ label, options }: IProps) => {
const [selectedOption, setSelectedOption] = React.useState<Option>(options[0])
return (
<Listbox value={selectedOption} onChange={setSelectedOption}>
{({ open }) => (
<>
<Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500">
{label}
</Listbox.Label>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="flex items-center">
<img
src={selectedOption.img}
alt={selectedOption.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span className="block ml-3 truncate">{selectedOption.name}</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none">
<Selector />
</span>
</Listbox.Button>
<div className="absolute w-full mt-1 bg-white rounded-md shadow-lg">
<Transition
show={open}
leave="transition duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{options.map((option) => (
<Listbox.Option as={React.Fragment} key={option.id} value={option}>
{({ active, selected }) => (
<li
className={clsx('relative py-2 pl-3 cursor-default select-none pr-9', {
'text-white bg-indigo-600': active,
'text-gray-900': !active,
})}
>
<div className="flex items-center">
<img
src={option.img}
alt={option.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span
className={clsx('ml-3 block truncate', {
'font-semibold': selected,
'font-normal': !selected,
})}
>
{option.name}
</span>
</div>
{selected && (
<span
className={clsx('absolute inset-y-0 right-0 flex items-center pr-4', {
'text-white': active,
'text-indigo-600': !active,
})}
>
<Check />
</span>
)}
</li>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</div>
</>
)}
</Listbox>
)
}
App.tsx
const shapes = [
{
id: '1',
name: 'Circle',
img:
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
{
id: '2',
name: 'Square',
img:
'https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
]
<Select label="Shape" options={shapes} />
How do I convert the After part to use MobX like the Before part?
I tried passing value & onChange as it is in the Before part to Select like:
App.tsx
<Select
label="Shape"
options={shapes}
value={frameItStore.trafficSignal.shape}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const shape = e.target.value as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
/>
Select.tsx
interface IProps {
label?: string
value: any
onChange: (value: any) => void
options: Array<Option>
}
export const Select = ({ label, options, value, onChange }: IProps) => {
const [selectedOption, setSelectedOption] = React.useState<Option>(options[0])
return (
<Listbox value={value} onChange={onChange}>
.
.
.
</Listbox>
)
}
But it doesn't select anything & I don't know what to do of selectedOption?

Alright I solved it. Removed the local hook state & just used the MobX state. Also, had 1 minor issue. I was setting the value as uppercase in the store when the store originally had lowercase values. The uppercase values were only for display in the UI.
Here's the modified code that works:
App.tsx
<Select
label="Shape"
options={shapes}
value={shapes.filter({ name }) => name.toLowerCase() === frameItStore.trafficSignal.shape)[0]}
onChange={(value) => {
const shape = value.toLowerCase() as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
/>
Select.tsx
import * as React from 'react'
import { Listbox, Transition } from '#headlessui/react'
import clsx from 'clsx'
import { Selector, Check } from '#/components/icons/index'
type Option = {
id: string
name: string
img: string
}
interface IProps {
label?: string
value: Option
onChange: (name: string) => void
options: Array<Option>
}
export const Select = ({ label, options, value, onChange }: IProps) => {
return (
<Listbox
value={value}
onChange={(value: Option) => {
onChange(value.name)
}}
>
{({ open }) => (
<>
<Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500">
{label}
</Listbox.Label>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="flex items-center">
<img
src={value.img}
alt={value.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span className="block ml-3 truncate">{value.name}</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none">
<Selector />
</span>
</Listbox.Button>
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg">
<Transition
show={open}
leave="transition duration-100 ease-in"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Listbox.Options
static
className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{options.map((option) => {
return (
<Listbox.Option as={React.Fragment} key={option.id} value={option}>
{({ active, selected }) => {
return (
<li
className={clsx(
'relative py-2 pl-3 cursor-default select-none pr-9',
{
'text-white bg-indigo-600': active,
'text-gray-900': !active,
}
)}
>
<div className="flex items-center">
<img
src={option.img}
alt={option.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span
className={clsx('ml-3 block truncate', {
'font-semibold': selected,
'font-normal': !selected,
})}
>
{option.name}
</span>
</div>
{selected && (
<span
className={clsx(
'absolute inset-y-0 right-0 flex items-center pr-4',
{
'text-white': active,
'text-indigo-600': !active,
}
)}
>
<Check />
</span>
)}
</li>
)
}}
</Listbox.Option>
)
})}
</Listbox.Options>
</Transition>
</div>
</div>
</>
)}
</Listbox>
)
}

Related

React Hook Form not validating nested input field until I call onChange

I have a form which has 2 fields.
Heading
Dynamic List which contains Name & Image
I'm able to validate changes whenever Heading or Name inside dynamic list is changed.
But when I change Image, the validation is failing. It returns true instead of showing error message. I tried adding a text input to test the image. Whenever I call onChange it is working fine.
Here is my code:
import { useForm } from "react-hook-form";
import { zodResolver } from "#hookform/resolvers/zod";
import * as z from "zod";
const schema = z.object({
heading: z
.string()
.min(3, { message: "Min 3 characters" })
.max(40, { message: "Max 40 characters" }),
testimonials: z.array(
z.object({
name: z.string().min(3, { message: "Min 3 characters" }),
image: z.string().url({ message: "Invalid URL" }),
})
),
});
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
const [dataObj, setDataObj] = useState({
"heading": "",
"testimonials": []
});
const fetchData = () => {
// fetch list from server
// bind data
setDataObj(dataFromServer);
};
fetchData();
const listChanged = (type, value, index) => {
let list = dataObj.data.testimonials;
if (type == "name") {
list[index].name = value;
} else if (type == "image") {
list[index].image = value;
}
setDataObj({
...dataObj,
testimonials: list,
});
};
const headingChanged = (value) => {
setDataObj({
...dataObj,
heading: value,
});
};
<form className="mt-10 space-y-6" onSubmit={handleSubmit(onSubmit)}>
<div
className={`relative appearance-none block w-full px-3 py-2 border rounded-md shadow-sm placeholder-gray-400 sm:text-sm ${
errors.heading
? "border-red-400 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
}`}
>
<label
htmlFor="name"
className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400"
>
Section Heading
</label>
<div className="mt-1">
<input
type="text"
autoComplete="name"
{...register("heading", {
required: true,
})}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"
value={dataObj.data.title}
onChange={(e) => headingChanged( e.target.value)}
/>
</div>
{errors.heading && (
<small className="mt-2 text-xs text-red-500">
{errors.heading.message}
</small>
)}
</div>
<div className="mt-2 w-full py-2">
<div>
<div className="mt-2 flex justify-center mx-auto text-left">
<ul role="list" className="w-full">
{dataObj.data.testimonials.map((item, index) => (
<li key={index} className="py-3">
<div className="flex items-center space-x-4 justify-start w-full">
<div className="isolate -space-y-px rounded-md shadow-sm w-full">
<div
className={`appearance-none relative w-full px-3 py-2 border rounded-md rounded-b-none shadow-sm sm:text-sm ${
errors.testimonials?.[index]?.name
? "border-red-400 focus:outline-none focus:ring-red-500 focus:border-red-500"
: "border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
}`}
>
<label
htmlFor="name"
className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400"
>
Name
</label>
<input
{...register(`testimonials.${index}.name`)}
type="text"
onChange={(e) => {
listChanged(index, e.target.value, "name");
}}
value={item.name}
placeholder="New Label"
className="block w-full border-0 p-1.5 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"
/>
<span className="invalid-feedback text-red-500 text-xs">
{errors.testimonials?.[index]?.name?.message}
</span>
</div>
<div className="relative rounded-md rounded-t-none border border-gray-300 px-3 py-2 focus-within:ring-0 focus-within:ring-indigo-600">
<label
htmlFor="name"
className="absolute -top-2 left-2 -mt-px inline-block bg-white px-1 text-xs font-medium text-gray-400"
>
Image
</label>
<input
{...register(`testimonials.${index}.image`)}
type="text"
value={item.image}
/>
<UploadImage
size={`small-round`}
index={index}
imageUrl={item.image}
imageChanged={listChanged}
/>
<span className="invalid-feedback text-red-500 text-xs">
{errors.testimonials?.[index]?.image?.message}
</span>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</div>
</form>;
How do I solve this?
I have created following minimum example on how to use image with useFieldArray to create dynamic form. This has been created in codesandbox and tested for validation only. Let me know if you stuck anywhere or want more information.
SandBoxLink :
import React from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "#hookform/resolvers/zod";
import * as z from "zod";
const MAX_FILE_SIZE = 200000;
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png"];
const schema = z.object({
heading: z
.string()
.min(3, { message: "Min 3 characters" })
.max(40, { message: "Max 40 characters" }),
testimonials: z.array(
z.object({
name: z.string().min(3, { message: "Min 3 characters" }),
testimonial: z.string().min(3, { message: "Min 3 characters" }),
image: z
.any()
.refine(
(files) => files?.[0]?.size <= MAX_FILE_SIZE,
`Max image size is 2MB.`
)
.refine(
(files) => ACCEPTED_IMAGE_TYPES.includes(files?.[0]?.type),
"Only .jpg, .png formats are supported."
)
})
)
});
const App = () => {
const appendValues = {
Name: "",
testimonial: "",
image: []
};
const defaultValues = {
heading: "",
testimonials: [appendValues]
};
const {
register,
handleSubmit,
control,
formState: { errors }
} = useForm({
defaultValues,
resolver: zodResolver(schema)
});
const { fields, append, remove } = useFieldArray({
control,
name: "testimonials"
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="heading">Heading : </label>
<input {...register("heading")} id="heading" />
<p>{errors?.heading?.message}</p>
{fields.map((field, index) => (
<div
key={field.id}
style={{
border: "1px solid black",
padding: "10px"
}}
>
<label htmlFor={`testimonials.${index}.name`}>Name : </label>
<input
id={`test.${index}.name`}
{...register(`testimonials.${index}.name`)}
/>
<p>{errors.testimonials?.[index]?.name?.message}</p>
<br />
<label htmlFor={`testimonials.${index}.testimonial`}>
Testimonial :
</label>
<input
id={`test.${index}.testimonial`}
{...register(`testimonials.${index}.testimonial`)}
/>
<p>{errors.testimonials?.[index]?.testimonial?.message}</p>
<br />
<label htmlFor={`testimonials.${index}.image`}>Image :</label>
<input
id={`test.${index}.image`}
type="file"
{...register(`testimonials.${index}.image`)}
/>
<p>{errors.testimonials?.[index]?.image?.message}</p>
<br />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append(appendValues)}>
append
</button>
<input type="submit" />
</form>
);
};
export default App;

Changing state amongst shared components not working on the first time ReactJS

I'm building out filters for a job board where by I have three types of filters: location, category, and employment type. Switching between the categories filter sorts the job board between the selected category quite fine.
Location and Employment filters is where the issue occurs. They are both dropdown menus whereby the state renders nothing the first time you click them. Only when you click one of the dropdown options again does it actually render. I've attached photos to visually show what the job board looks like and the dropdown options, as well as the devtools open to show you what states render.
Here is the code sandbox for a more interactive look: codesandbox.io/s/mutable-pond-h08wst?file=/src/App.js
App.js
import React, {useState, useEffect} from 'react'
import jobData from "./assets/data.json"
import JobBoardComponent from './components/JobBoardComponent';
import NavBar from './components/NavBar'
import EmailBar from './components/EmailBar'
import BannerPage from "./components/BannerPage"
import FuncFilter from "./components/FuncFilter"
import LocFilter from "./components/LocFilter"
import ConFilter from './components/ConFilter';
import Filters from "./components/Filters"
function App() {
const [jobs, setJobs] = useState([])
const [filtered, setFiltered] = useState([])
const [activeCategory, setActiveCategory] = useState("all")
const [activeLocation, setActiveLocation] = useState("all")
const [activeContract, setActiveContract] = useState("all")
const [selectedId, setSelectedId] = useState("")
const sortedJobs = jobData.sort((a, b) => Number(b.id) - parseFloat(a.id))
useEffect(() => {
setJobs(sortedJobs)
}, [])
function handleClick(id) {
if (selectedId !== id) {
setSelectedId(id)
} else
setSelectedId("")
}
console.log(filtered)
return (
<div className="App w-auto">
<NavBar />
<BannerPage />
<EmailBar />
<Filters
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
<div>
{activeCategory === "all" ? jobs.map((job) => <JobBoardComponent handleClick={handleClick} selected={selectedId === job.id} job={job} key={job.id} />)
: filtered.map((job) => <JobBoardComponent handleClick={handleClick} selected={selectedId === job.id} job={job} key={job.id} />)}
</div>
</div>
);
}
export default App;
FuncFilter
import LocFilter from "./LocFilter"
import ConFilter from "./ConFilter"
function FuncFilter({setActiveCategory, activeCategory, activeLocation, setActiveLocation, activeContract, setActiveContract, setFiltered, jobs}) {
useEffect(() => {
if (activeCategory === "all") {
setFiltered(jobs)
return
}
const filtered = jobs.filter((job) => job.category.includes(activeCategory))
setFiltered(filtered)
}, [activeCategory])
const obj = jobs.map(function(job) {
return job.category
})
function markCategory() {
const marketingCount = []
for (const jobCat of obj) {
if (jobCat === "Marketing") {
marketingCount.push(jobCat)
}
}
return marketingCount.length
}
function cusCategory() {
const cusCount = []
for (const jobCat of obj) {
if (jobCat === "Customer Service") {
cusCount.push(jobCat)
}
}
return cusCount.length
}
function creativeCategory() {
const creativeCount = []
for (const jobCat of obj) {
if (jobCat === "Creative") {
creativeCount.push(jobCat)
}
}
return creativeCount.length
}
function webCategory() {
const webCount = []
for (const jobCat of obj) {
if (jobCat === "Web Development") {
webCount.push(jobCat)
}
}
return webCount.length
}
function salesCategory() {
const salesCount = []
for (const jobCat of obj) {
if (jobCat === "Sales") {
salesCount.push(jobCat)
}
}
return salesCount.length
}
function peopleCategory() {
const peopleCount = []
for (const jobCat of obj) {
if (jobCat === "People & HR") {
peopleCount.push(jobCat)
}
}
return peopleCount.length
}
function opsCategory() {
const opsCount = []
for (const jobCat of obj) {
if (jobCat === "Biz Ops") {
opsCount.push(jobCat)
}
}
return opsCount.length
}
function finCategory() {
const finCount = []
for (const jobCat of obj) {
if (jobCat === "Finance") {
finCount.push(jobCat)
}
}
return finCount.length
}
function proCategory() {
const proCount = []
for (const jobCat of obj) {
if (jobCat === "Product") {
proCount.push(jobCat)
}
}
return proCount.length
}
const resetLocation = event => {
setActiveLocation(1);
};
const resetContract = event => {
setActiveContract(1);
};
const resetCategory = event => {
setActiveCategory(1)
};
//1. Marketing DONE
// 2. Biz Ops DONE
// 3. Sales DONE
// 4. Finance DONE
// 5. Product
// 6. Web Development DONE
// 7. Creative DONE
// 8. People & HR DONE
// 9. Customer Service DONE
return (
<div className="mb-20 -mt-20">
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => setActiveCategory("all")}>All</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Marketing");}}>Marketing ({markCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Customer Service");}}>Customer Service ({cusCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Creative");}}>Creative ({creativeCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Web Development");}}>Web Development ({webCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("People & HR");}}>People & HR ({peopleCategory()})</button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Sales");}}>Sales ({salesCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Biz Ops");}}> Biz Ops ({opsCategory()}) </button>
<button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Finance");}}>Finance ({finCategory()}) </button>
<button className="hidden text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {resetLocation(); resetContract(); setActiveCategory("Product");}}> Product ({proCategory()}) </button>
<LocFilter
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
<ConFilter
jobs={jobs}
setFiltered={setFiltered}
activeLocation={activeLocation}
setActiveLocation={setActiveLocation}
activeCategory={activeCategory}
setActiveCategory={setActiveCategory}
activeContract={activeContract}
setActiveContract={setActiveContract}
/>
</div>
)
}
export default FuncFilter
LocFilter.js
import { Fragment } from 'react'
import { Menu, Transition } from '#headlessui/react'
import { ChevronDownIcon } from '#heroicons/react/solid'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
function LocFilter({setActiveLocation, activeLocation, activeCategory, setActiveCategory, activeContract, setActiveContract, setFiltered, jobs}) {
useEffect(() => {
const filtered = jobs.filter((job) => job.location.includes(activeLocation))
setFiltered(filtered)
}, [activeLocation])
const resetContract = event => {
setActiveContract(1);
};
const resetCategory = event => {
setActiveCategory(1);
};
return (
<Menu as="div" className="mb-20 relative inline-block text-left">
<div>
<Menu.Button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Locations
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
// active category cannot be 0
// if active category is a category, it works to select a location filter
// you cannot click that exact category right after though, as AC is labeled as that category
onClick={() => {resetCategory(); resetContract(); setActiveLocation("USA"); }}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
USA
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetContract(); setActiveLocation("Asia");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Asia
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetContract(); setActiveLocation("Europe");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Europe
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)
}
export default LocFilter
ConFilter,js
import { Fragment } from 'react'
import { Menu, Transition } from '#headlessui/react'
import { ChevronDownIcon } from '#heroicons/react/solid'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
function ConFilter({ setActiveCategory, activeLocation, activeCategory, setActiveContract, setActiveLocation, activeContract, setFiltered, jobs}) {
useEffect(() => {
const filtered = jobs.filter((job) => job.contract.includes(activeContract))
setFiltered(filtered)
}, [activeContract])
const resetLocation = event => {
setActiveLocation(1);
};
const resetCategory = event => {
setActiveCategory(1);
};
return (
<Menu as="div" className="mb-20 relative inline-block text-left">
<div>
<Menu.Button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Employment
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
// active category cannot be 0
// if active category is a category, it works to select a location filter
// you cannot click that exact category right after though, as AC is labeled as that category
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Remote");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Remote
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Part Time");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Part Time
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => {resetCategory(); resetLocation(); setActiveContract("Full Time");}}
className={classNames(
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
'block px-4 py-2 text-sm'
)}
>
Full Time
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)
}
export default ConFilter```

Getting null when I use context.query with getServerSideProps() in Nextjs?

Intro
Basically, I am building an e-commerce website. But I got stuck in this problem while saving the order details after I click payment button (although I had not used any payment gateway).
Problem
After selecting the items in cart , I fill the details in Checkout page and go to pay option to confirm order. In this checkout page I call addorder api which first do some checks(like whether item got out of stock or not) on the product in cart and then create new order.
After creating a new order in order DB (MOngo DB I am using), I goto order page like this with my orderId
res.redirect('/order/' + orderToAdd.orderId, 200);
After clicking the pay button shown in the image above, it redirects to http://localhost:3000/order/814407650978 giving 404 (Not Found).
Thus when I fetch the order details from the MyOrder.js using context.query.orderId, from getServerSideProps() , I am getting null.
export async function getServerSideProps(context) {
if (!mongoose.connections[0].readyState) {
await mongoose.connect(process.env.MONGO_URI);
}
let order = await Order.findById({orderId:context.query.orderId})
console.log("got = ", order);
return {
props: { order: JSON.parse(JSON.stringify(order)) }, // will be passed to the page component as props
};
}
You can see full code below
Code
This is a complete order DB entry, where I am only sending the products object to MyOrder.js
complete order = {
email: 'mohit6#test.com',
orderId: '869972292107',
paymentInfo: 'No payment info',
products: {
'wear-the-chess-king-M-Red': {
qty: 2,
price: 599,
name: 'King Chess Stylish Men’s t-Shirt',
size: 'M',
variant: 'Red'
}
},
address: 'A-22\nVinayak campus',
subtotal: 1198,
status: 'Paid',
_id: new ObjectId("626bd1bd01597b226fc76531"),
createdAt: 2022-04-29T11:53:33.046Z,
updatedAt: 2022-04-29T11:53:33.046Z,
__v: 0
}
Checkout.js
import React, { useState } from "react";
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import { MdOutlinePayment } from "react-icons/md";
import { useRouter } from "next/router";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
// Added payment AiOutlineGateway, but due to no merchant key, it is creating invalid checksum => hence push to different branch in both local & remote
const Checkout = ({ cart, clearCart, subtotal, addToCart, removeFromCart }) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [pincode, setPincode] = useState("");
const [city, setCity] = useState("");
const [statemap, setStatemap] = useState("");
const [disable, setDisable] = useState(true);
const router = useRouter();
const handleChange = async (e) => {
if (e.target.name === "name") {
setName(e.target.value);
} else if (e.target.name === "email") {
setEmail(e.target.value);
} else if (e.target.name === "phone") {
setPhone(e.target.value);
} else if (e.target.name === "address") {
setAddress(e.target.value);
} else if (e.target.name === "pincode") {
setPincode(e.target.value);
if (e.target.value.length == 6) {
let pins = await fetch(`${process.env.NEXT_PUBLIC_HOST}/api/pincode`);
let pinjson = await pins.json();
//console.log(pinjson)
if (Object.keys(pinjson).includes(e.target.value)) {
setCity(pinjson[e.target.value][0]);
setStatemap(pinjson[e.target.value][1]);
} else {
setCity("");
setStatemap("");
}
} else {
setCity("");
setStatemap("");
}
}
if (name && email && phone && address && pincode) setDisable(false);
else setDisable(true);
};
const initiateOrder = async () => {
let oid = Math.floor(Math.random() * Date.now());
const data = {
cart: cart,
subtotal: subtotal,
oid: oid,
email: email,
name: name,
address: address,
pincode: pincode,
phone: phone,
};
//console.log(JSON.stringify(data));
let res = await fetch(`${process.env.NEXT_PUBLIC_HOST}/api/addorder`, {
method: "POST", // or 'PUT'
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
let response = await res.json();
//console.log("response from order- ", response);
if (response.success === true) {
localStorage.setItem("token", response.authToken);
toast.success("Order Added Successfully!", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
setTimeout(() => {
router.push("/order");
}, 2500);
} else {
if(response.error === "err1"){
toast.error("Total price of your cart have changed accidently", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else if(response.error === "err2"){
toast.error("Some items in your cart went out of stock !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else if(response.error === "err5"){
toast.error("Prices of some of the items in your cart have changed !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
else {
toast.error("Error in Adding Order !", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
}
};
return (
<>
<div className="container px-2 sm:m-auto">
<ToastContainer
position="bottom-center"
autoClose={2000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
<h1 className="text-xl md:text-3xl text-center my-8 font-semibold">
Checkout
</h1>
{/* part 1 */}
<h2 className="text-xl my-4 font-semibold">1. Delivery Details</h2>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label htmlFor="name" className="leading-7 text-sm text-gray-600">
Name
</label>
<input
type="name"
id="name"
name="name"
value={name}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div className="px-2 w-1/2">
<div className=" mb-4">
<label
htmlFor="email"
className="leading-7 text-sm text-gray-600"
>
Email
</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
</div>
<div className="px-2 w-full">
<div className=" mb-4">
<label
htmlFor="address"
className="leading-7 text-sm text-gray-600"
>
Address
</label>
<textarea
id="address"
name="address"
value={address}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label
htmlFor="phone"
className="leading-7 text-sm text-gray-600"
>
Phone
</label>
<input
type="text"
id="phone"
name="phone"
value={phone}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
{/* city */}
<div className="px-2 w-1/2">
<div className=" mb-4">
<label
htmlFor="pincode"
className="leading-7 text-sm text-gray-600"
>
Pincode
</label>
<input
type="text"
id="pincode"
name="pincode"
value={pincode}
onChange={handleChange}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
</div>
<div className="mx-auto flex">
<div className="px-2 w-1/2">
<div className="mb-4">
<label
htmlFor="state"
className="leading-7 text-sm text-gray-600"
>
State
</label>
<input
type="text"
id="state"
name="state"
value={statemap}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
onChange={handleChange}
/>
</div>
</div>
<div className="px-2 w-1/2">
<div className=" mb-4">
<label htmlFor="city" className="leading-7 text-sm text-gray-600">
City
</label>
<input
type="text"
id="city"
name="city"
value={city}
className="w-full bg-white rounded border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
onChange={handleChange}
/>
</div>
</div>
</div>
{/* part 2 */}
<h2 className="text-xl my-4 font-semibold">2. Review Cart Items</h2>
<div className="z-10 sideCart p-6 mx-2 my-4 bg-blue-100 border-[1px] border-blue-800 rounded">
<ol className="list-decimal font-semibold">
{Object.keys(cart).length === 0 && (
<div className="mt-5 text-center text-xl font-extralight">
Your Cart is Empty :(
</div>
)}
{
//k is cart {k: {name, price,qty,size,variant} }
Object.keys(cart).map((k) => {
return (
<li key={k}>
<div className="flex item my-5 items-center justify-between">
<div className=" font-semibold">
{cart[k].name} [{cart[k].variant} - {cart[k].size}]
</div>
<div className="w-1/3 font-semibold flex items-center justify-center">
<AiFillPlusCircle
onClick={() =>
addToCart(
k,
1,
cart[k].price,
cart[k].name,
cart[k].size,
cart[k].variant
)
}
className="text-blue-700 text-xl cursor-pointer"
/>
<span className="mx-2 text-sm">{cart[k].qty}</span>
<AiFillMinusCircle
onClick={() =>
removeFromCart(
k,
1,
cart[k].price,
cart[k].name,
cart[k].size,
cart[k].variant
)
}
className="text-blue-700 text-xl cursor-pointer"
/>
</div>
</div>
</li>
);
})
}
</ol>
<span className="subtotal text-xl font-extrabold">
Subtotal : ₹ {subtotal} /-
</span>
</div>
<button
disabled={disable}
onClick={initiateOrder}
className="disabled:bg-blue-300 flex text-white bg-blue-500 border-0 py-2 px-3 focus:outline-none hover:bg-blue-600 rounded text-base mx-2 my-4"
>
<MdOutlinePayment className="m-1" />
Pay ₹ {subtotal}
</button>
</div>
</>
);
};
export default Checkout;
/api/addorder.js
import connectDB from "../../middleware/mongoose";
import Order from "../../models/Order";
import Product from "../../models/Product";
const handler = async (req, res) => {
if (req.method === "POST") {
let product,
sumtotal = 0;
for (let item in req.body.cart) {
//got key(slug) as item
console.log(item);
sumtotal += req.body.cart[item].qty * req.body.cart[item].price;
product = await Product.findOne({ slug: item });
//TODO1: check if cart is tampered or not
if (product.price !== req.body.cart[item].price) {
res.status(500).json({ success: false, error: "err1" });
//console.log("ResT - ",res.json)
return;
}
//TODO2: check if cart items are out of stock
if (product.availableQty < req.body.cart[item].qty) {
res.status(500).json({ success: false, error: "err2" });
return;
}
}
//console.log("sumtotal = ",sumtotal," and subtotal = ",req.body.subtotal)
if (sumtotal != req.body.subtotal) {
res.status(500).json({ success: false, error: "err5" });
//console.log("ResP - ",res)
return;
}
//TODO3: check if details are valid or not
let orderToAdd = new Order({
email: req.body.email,
orderId: req.body.oid,
address: req.body.address,
subtotal: req.body.subtotal,
products: req.body.cart,
status: "Paid",
});
await orderToAdd.save();
let ordered_products = orderToAdd.products;
console.log("Orederd pro = ", ordered_products);
console.log("complete order = ", orderToAdd)
for (let slug in ordered_products) {
await Product.findOneAndUpdate(
{ slug: slug },
{ $inc: { availableQty: -ordered_products[slug].qty } }
);
}
res.redirect('/order/' + orderToAdd.orderId, 200)
} else {
// Handle any other HTTP method
res.status(400).json({ success: false });
}
};
export default connectDB(handler);
MyOrder.js
import React from "react";
import mongoose from "mongoose";
import Order from "../models/Order";
//TODO: To change the orderdetail fecthing resource from cart,subtotal to orders DB (user specific orders)
const MyOrder = ({ order }) => {
console.log("order = ",order)
const products = order.products;
console.log(order,products);
return (
<div>
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-5 py-24 mx-auto">
<div className="lg:w-4/5 mx-auto flex flex-wrap">
<div className="lg:w-1/2 w-full lg:pr-10 lg:py-6 mb-6 lg:mb-0">
<h2 className="text-sm title-font text-gray-500 tracking-widest">
CHESSWEAR.COM
</h2>
<h1 className="text-gray-900 text-3xl title-font font-medium mb-4">
Order Id: 2242434535
</h1>
<p className="leading-relaxed mb-4 text-green-700 rounded-3xl bg-green-100 hover:bg-green-100/70 p-3 border-[1px] border-green-700 inline-flex items-center justify-center ">
Your Order has been successfully placed !
</p>
<div className="flex mb-4 justify-evenly">
<a className="flex-grow w-12 py-2 text-lg px-1">Description</a>
<a className="flex-grow py-2 text-lg px-1">Quantity</a>
<a className="flex-grow py-2 text-lg px-1">Price</a>
</div>
{Object.keys(cart).map((key) => {
return (
<div key={key} className="flex border-t border-gray-200 py-2">
<span className="text-gray-500 w-28 ">
{cart[key].name} ({cart[key].size} - {cart[key].variant})
</span>
<span className="m-auto text-gray-900">
{cart[key].qty}
</span>
<span className="mr-auto mt-auto mb-auto text-gray-900">
₹ {cart[key].price}
</span>
</div>
);
})}
<div className="flex py-2 md:py-4">
<span className="title-font font-medium text-2xl text-gray-900">
Subtotal : <span className="ml-4 text-red-900 font-bold text-3xl leading-tight">₹ {subtotal} /-</span>
</span>
<button className="flex ml-auto text-white bg-blue-500 border-0 py-2 px-6 focus:outline-none hover:bg-blue-600 rounded">
Track Order
</button>
</div>
</div>
<img
alt="ecommerce"
className="lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded"
src="https://dummyimage.com/400x400"
/>
</div>
</div>
</section>
</div>
);
};
export async function getServerSideProps(context) {
if (!mongoose.connections[0].readyState) {
await mongoose.connect(process.env.MONGO_URI);
}
let order = await Order.findById({orderId:context.query.orderId})
console.log("got = ", order);
return {
props: { order: JSON.parse(JSON.stringify(order)) }, // will be passed to the page component as props
};
}
export default MyOrder;
File Structure
Please help me???

Toggle #headlessui/vue Popover component

I'm not entirely sure how to toggle the popover based on a boolean, in my case: if there are search results. I thought I could just use the open attribute. Any help is appreciated!
To sum up this component:
Type in a search term in the input
An API call is being made and results are returned from te API
The PopoverPanel should then be opened
<template>
<form :action="action" method="GET" class="flex-1">
<Popover :open="searchResults.length > 0" class="relative w-full">
<PopoverOverlay
:class="`bg-black ${open ? 'fixed inset-0 opacity-10' : 'opacity-0'}`"
#click="() => emptySearchResults()"
/>
<div class="relative">
<label for="search" class="sr-only">Search...</label>
<input
id="search"
v-model="searchTerm"
type="search"
placeholder="Search..."
class="h-10 w-full px-4 py-2 text-sm rounded-lg placeholder-gray-400 focus:outline-none"
>
</div>
<PopoverPanel class="absolute top-full right-0 w-full mt-3 py-2 px-2 bg-gray-700 rounded-lg shadow-lg">
<a
v-for="result in searchResults"
:key="result.id"
:href="result.url"
class="flex items-center font-semibold p-2 text-sm text-white leading-none rounded-lg hover:bg-gray-600"
>
<span class="mr-3 py-1 px-2 text-xs text-gray-900 leading-none bg-gray-300 rounded-full">
{{ result.module }}
</span>
{{ result.title }}
</a>
</PopoverPanel>
</Popover>
</form>
</template>
<script setup>
import { ref, watch } from 'vue';
import { Popover, PopoverPanel } from '#headlessui/vue';
import debounce from '#/Helpers/debounce';
import xhr from '#/Helpers/xhr';
const searchTerm = ref('');
const searchResults = ref([]);
const getSearchResults = debounce(async (value) => {
if (value === '') return;
const { data } = await xhr.post('search', { q: value });
const { results } = await data;
searchResults.value = results;
}, 250);
const emptySearchResults = () => {
searchTerm.value = '';
searchResults.value = [];
};
watch(searchTerm, (value) => getSearchResults(value));
</script>

is there a way to change form values on submit

i have a situation where there are two buttons that i want acting as a submit.
how can i make the value of the zip code (inside formik) be combined with the value of the option (button onclick), e.g. {"option": "adult", "zip_code": "00000"}
i've found this: https://github.com/formium/formik/issues/467#issuecomment-369833667
but it's for a class component, and doesn't show how to get the button value into formik values...
import React, { useState, useEffect } from 'react';
import { Form, Formik, useField } from 'formik';
import * as Yup from 'yup';
const FORM_INPUT_BOX = "m-2 border border-gray-300 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:border-transparent"
const FORM_INPUT_LABEL = "ml-2 text-gray-700"
const FORM_INPUT_ERROR = "break-normal relative bottom-2 left-2 text-sm text-red-500"
const FORM_DROPDOWN = "m-2 focus:ring-yellow-500 border border-gray-300 focus:border-yellow-500 h-full border-transparent bg-transparent text-gray-500 sm:text-sm rounded"
export const FORM_BUTTON = "ml-2 mr-2 py-1 border border-transparent text-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
const CustomTextInput = ({ label, ...props }) => {
const [field, meta] =useField(props); //using the hook useField, and destructuring
return(
<> {/*so we don't return any div tags - its just a react fragment */}
<label htmlFor={props.id || props.name} className={FORM_INPUT_LABEL}> {label} </label>
<input className={FORM_INPUT_BOX}{...field}{...props} />
{meta.touched && meta.error ? (
<div className={FORM_INPUT_ERROR}>{meta.error}</div>
) : null}
</>
)
}
const ButtonMaker = (props) => {
const {mainprops,userprops,adoptprops,...optionprops} = props
return(
<>
<button className={FORM_BUTTON} id={optionprops.id} onClick={()=>{optionprops.setButtonClick(true);
optionprops.setOption(optionprops.id)}}
>{optionprops.label}</button>
</>
)
};
export default function RegistrationForm(props) {
const {mainprops,userprops,...adoptprops} = props;
const [buttonClick, setButtonClick] = useState(false);
const [option, setOption] = useState('')
const allprops = {
mainprops,
userprops,
adoptprops,
buttonClick,
setButtonClick,
setOption
};
useEffect(()=>{
if (buttonClick){
alert(option)};
},[buttonClick])
// https://regex101.com/
const zipRegExp= /^([0-9]{5})$/;
//zipRegExp: only 5 digits
const zipRegExp_plus4= /^([0-9]{5})(?:[-\s]*([0-9]{4}))?$/;
return (
<Formik
initialValues={{
zip_code : ''
}}
validationSchema={Yup.object({
zip_code: Yup.string()
.min(5,'five digits needed')
.matches(zipRegExp, 'invalid zipcode')
.required('required field'),
})}
onSubmit={ (values, { setSubmitting, resetForm }) => {
setTimeout(() => {
// alert(JSON.stringify(values, null, 2));
resetForm();
setSubmitting(false);
}, 1000)
}}
>
{props => (
<Form className='bg-white rounded px-8 py-8 mt-4 mb-4'>
<div className='flex justify-center align-center'>
<section className='m-2 w-52 flex flex-col border'>
<CustomTextInput label='Enter ZIP Code' name='zip_code' placeholder='zipcode' />
</section>
</div>
<br></br>
<div className='flex justify-center align-center'>
<ButtonMaker {...allprops} groupa='pet' groupb='adopt' id='adult' label='adult'/>
<ButtonMaker {...allprops} groupa='pet' groupb='adopt' id='puppy' label='puppy'/>
</div>
</Form>
)}
</Formik>
)
}
something else i tried:
added this to each button: formkprops={props}
and this to the button maker on click function: formkprops.submitForm()
but it doesn't work.
Try the following:
import React, { useState, useEffect } from 'react';
import { Form, Formik, useField } from 'formik';
import * as Yup from 'yup';
const FORM_INPUT_BOX = "m-2 border border-gray-300 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:border-transparent"
const FORM_INPUT_LABEL = "ml-2 text-gray-700"
const FORM_INPUT_ERROR = "break-normal relative bottom-2 left-2 text-sm text-red-500"
const FORM_DROPDOWN = "m-2 focus:ring-yellow-500 border border-gray-300 focus:border-yellow-500 h-full border-transparent bg-transparent text-gray-500 sm:text-sm rounded"
export const FORM_BUTTON = "ml-2 mr-2 py-1 border border-transparent text-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
const CustomTextInput = ({ label, ...props }) => {
const [field, meta] =useField(props); //using the hook useField, and destructuring
return(
<> {/*so we don't return any div tags - its just a react fragment */}
<label htmlFor={props.id || props.name} className={FORM_INPUT_LABEL}> {label} </label>
<input className={FORM_INPUT_BOX}{...field}{...props} />
{meta.touched && meta.error ? (
<div className={FORM_INPUT_ERROR}>{meta.error}</div>
) : null}
</>
)
}
const ButtonMaker = (props) => {
const {mainprops,userprops,adoptprops,...optionprops} = props
return(
<>
<button className={FORM_BUTTON} id={optionprops.id} onClick={()=>{optionprops.setButtonClick(true);
optionprops.setOption({"option": optionprops.id, "zipcode": optionprops.zip_code})}}
>{optionprops.label}</button>
</>
)
};
export default function RegistrationForm(props) {
const {mainprops,userprops,...adoptprops} = props;
const [buttonClick, setButtonClick] = useState(false);
const [option, setOption] = useState(null)
const allprops = {
mainprops,
userprops,
adoptprops,
buttonClick,
setButtonClick,
setOption
};
useEffect(()=>{
if (buttonClick){
alert(JSON.stringify(option, null, 4))};
},[buttonClick])
// https://regex101.com/
const zipRegExp= /^([0-9]{5})$/;
//zipRegExp: only 5 digits
const zipRegExp_plus4= /^([0-9]{5})(?:[-\s]*([0-9]{4}))?$/;
return (
<Formik
initialValues={{
zip_code : ''
}}
validationSchema={Yup.object({
zip_code: Yup.string()
.min(5,'five digits needed')
.matches(zipRegExp, 'invalid zipcode')
.required('required field'),
})}
onSubmit={ (values, { setSubmitting, resetForm }) => {
setTimeout(() => {
// alert(JSON.stringify(values, null, 2));
resetForm();
setSubmitting(false);
}, 1000)
}}
>
{props => (
<Form className='bg-white rounded px-8 py-8 mt-4 mb-4'>
<div className='flex justify-center align-center'>
<section className='m-2 w-52 flex flex-col border'>
<CustomTextInput label='Enter ZIP Code' name='zip_code' placeholder='zipcode' />
</section>
</div>
<br></br>
<div className='flex justify-center align-center'>
<ButtonMaker {...allprops} groupa='pet' groupb='adopt' id='adult' label='adult' zip_code={props.values.zip_code}/>
<ButtonMaker {...allprops} groupa='pet' groupb='adopt' id='puppy' label='puppy' zip_code={props.values.zip_code}/>
</div>
</Form>
)}
</Formik>
)
}
You needed to create an option property and add a zip_code propery when the actionprops' setoption function is called. In order to display this in the alert, you needed to use the JSON.stringify function to display the option using the alert function.
**Solution for Information Requested In Comments Section by Original Poster **
You can create state management for your application or use a library for this like Redux. Inside the state management, you could have an array of objects similar to the one created above. You can then give whichever component you want access to this array. You would also need a final submit for the user to indicate when they are done, or a required amount of options the user has to choose before they can move on.
References:
StackOverflow. how to alert javascript object. https://stackoverflow.com/a/64941710/8121551. (Accessed 10 October, 2021).

Categories

Resources