Skip to content

Commit 83b02fc

Browse files
committed
Create components
1 parent 6726bda commit 83b02fc

File tree

15 files changed

+457
-62
lines changed

15 files changed

+457
-62
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
"@types/node": "^12.0.0",
1111
"@types/react": "^16.9.0",
1212
"@types/react-dom": "^16.9.0",
13+
"downshift": "^5.4.0",
1314
"react": "^16.13.1",
1415
"react-dom": "^16.13.1",
16+
"react-hook-form": "^5.7.2",
1517
"react-scripts": "3.4.1",
1618
"typescript": "~3.7.2"
1719
},

src/App.css

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,106 @@
1-
.App {
2-
text-align: center;
1+
.app {
2+
margin-top: 100px;
33
}
44

5-
.App-logo {
6-
height: 40vmin;
7-
pointer-events: none;
5+
hr {
6+
margin: 0;
7+
border: 1px solid #ececec;
88
}
99

10-
@media (prefers-reduced-motion: no-preference) {
11-
.App-logo {
12-
animation: App-logo-spin infinite 20s linear;
13-
}
10+
.form {
11+
display: grid;
12+
place-content: center;
13+
row-gap: 20px;
1414
}
1515

16-
.App-header {
17-
background-color: #282c34;
18-
min-height: 100vh;
19-
display: flex;
20-
flex-direction: column;
16+
.label {
17+
display: block;
18+
font-size: 12px;
19+
font-weight: 600;
20+
text-transform: uppercase;
21+
margin-bottom: 4px;
22+
}
23+
24+
.input {
25+
width: 250px;
26+
padding: 8px;
27+
border: 1px solid #222;
28+
}
29+
30+
.radio,
31+
.checkbox {
32+
display: inline-flex;
2133
align-items: center;
22-
justify-content: center;
23-
font-size: calc(10px + 2vmin);
24-
color: white;
34+
cursor: pointer;
35+
}
36+
37+
.radio__input,
38+
.checkbox__input {
39+
margin-right: 6px;
40+
}
41+
42+
.radio-group {
43+
display: grid;
44+
grid-auto-flow: column;
45+
justify-content: left;
46+
column-gap: 20px;
47+
}
48+
49+
.dropdown-container {
50+
position: relative;
51+
}
52+
53+
.dropdown-trigger {
54+
display: flex;
55+
justify-content: space-between;
56+
width: 100%;
57+
padding: 8px;
58+
background-color: #fff;
59+
border: 1px solid #222;
60+
border-radius: 0;
61+
font-size: inherit;
62+
text-align: left;
63+
cursor: pointer;
64+
}
65+
66+
.chevron {
67+
transform: rotate(180deg) translateY(2px);
2568
}
2669

27-
.App-link {
28-
color: #61dafb;
70+
.dropdown {
71+
position: absolute;
72+
width: 100%;
73+
margin-top: 2px;
74+
background-color: #fff;
75+
border: 1px solid #222;
76+
}
77+
78+
.dropdown-item {
79+
padding: 8px;
80+
cursor: pointer;
81+
}
82+
83+
.dropdown-item.is-active {
84+
background-color: #ddd;
85+
color: #130d0f;
86+
}
87+
88+
.button {
89+
padding: 8px;
90+
background-color: #222;
91+
color: #fff;
92+
font-size: inherit;
93+
font-weight: 600;
94+
border: 0;
95+
cursor: pointer;
96+
}
97+
98+
.button:disabled {
99+
opacity: 0.7;
100+
pointer-events: none;
29101
}
30102

31-
@keyframes App-logo-spin {
32-
from {
33-
transform: rotate(0deg);
34-
}
35-
to {
36-
transform: rotate(360deg);
37-
}
103+
.error {
104+
color: red;
105+
font-size: 13px;
38106
}

src/App.tsx

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
1-
import React from 'react';
2-
import logo from './logo.svg';
3-
import './App.css';
1+
import React from 'react'
2+
import { Form, Input, Dropdown, RadioGroup, Checkbox, Button } from './components/Form'
3+
import './App.css'
4+
5+
const radioOptions = [
6+
{ label: "Audio", value: "audio" },
7+
{ label: "Text", value: "text" }
8+
]
9+
10+
const dropdownOptions = [
11+
{ text: 'Thinking with Type', value: 'thinking-with-type' },
12+
{ text: 'Grid Systems in Graphic Design', value: 'grid-systems' },
13+
{ text: 'Logo Modernism', value: 'logo-modernism' }
14+
]
15+
16+
const App = () => {
17+
const handleSubmit = (values: any) => console.log(values)
418

5-
function App() {
619
return (
7-
<div className="App">
8-
<header className="App-header">
9-
<img src={logo} className="App-logo" alt="logo" />
10-
<p>
11-
Edit <code>src/App.tsx</code> and save to reload.
12-
</p>
13-
<a
14-
className="App-link"
15-
href="https://reactjs.org"
16-
target="_blank"
17-
rel="noopener noreferrer"
18-
>
19-
Learn React
20-
</a>
21-
</header>
20+
<div className="app">
21+
22+
<Form className="form" onSubmit={handleSubmit}>
23+
<h2>Download Book</h2>
24+
<RadioGroup name="format" label="Format" options={radioOptions} required="Select format" />
25+
<Dropdown name="book" label="Book" options={dropdownOptions} required="Select a book" />
26+
<hr />
27+
<Input name="name" label="Name" required />
28+
<Input
29+
name="email"
30+
label="Email"
31+
type="email"
32+
validation={{
33+
required: true,
34+
pattern: {
35+
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
36+
message: "Invalid email"
37+
}
38+
}}
39+
/>
40+
<hr />
41+
<Checkbox name="terms" label="Accept terms of service" required="Accept terms" />
42+
<Button>Download</Button>
43+
</Form>
2244
</div>
23-
);
45+
)
2446
}
2547

26-
export default App;
48+
export default App

src/components/Form/Button.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react'
2+
import { useFormContext } from 'react-hook-form'
3+
4+
type Button = {
5+
children: React.ReactNode
6+
}
7+
8+
export const Button = ({ children }: Button) => {
9+
const { formState: { dirty, isSubmitting } } = useFormContext()
10+
11+
return (
12+
<button className="button" type="submit" disabled={!dirty || isSubmitting}>
13+
{children}
14+
</button>
15+
)
16+
}

src/components/Form/Checkbox.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react'
2+
import { useFormContext } from 'react-hook-form'
3+
import { Error } from './Error'
4+
5+
type Checkbox = {
6+
label: string
7+
name: string
8+
required?: boolean | string
9+
}
10+
11+
export const Checkbox = ({ label, name, required }: Checkbox) => {
12+
const { register, errors } = useFormContext()
13+
14+
return (
15+
<div>
16+
<label className="checkbox">
17+
<input
18+
type="checkbox"
19+
className="checkbox__input"
20+
name={name}
21+
ref={register({ required })}
22+
/>
23+
{label}
24+
</label>
25+
{errors[name] && <Error message={errors[name].message} />}
26+
</div>
27+
)
28+
}

src/components/Form/Dropdown.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useEffect } from 'react'
2+
import { useFormContext } from 'react-hook-form'
3+
import { useSelect } from 'downshift'
4+
import { Error } from './Error'
5+
6+
type Option = {
7+
text: string
8+
value: string
9+
}
10+
11+
type Dropdown = {
12+
name: string
13+
options: Option[]
14+
label: string
15+
initialValue?: string
16+
placeholder?: string
17+
required?: boolean | string
18+
}
19+
20+
export const Dropdown = ({
21+
name,
22+
options,
23+
label,
24+
initialValue,
25+
placeholder = "Select...",
26+
required
27+
}: Dropdown) => {
28+
const { register, setValue, getValues, errors, clearError } = useFormContext()
29+
const error = errors[name]
30+
31+
const getInitialItem = () => {
32+
const defaultValue = initialValue || getValues()[name]
33+
if (defaultValue) {
34+
return options.find((o: Option) => o.value === defaultValue)
35+
}
36+
}
37+
38+
const {
39+
isOpen,
40+
selectedItem,
41+
selectedItem: dsSelectedItem,
42+
getToggleButtonProps,
43+
getLabelProps,
44+
getMenuProps,
45+
highlightedIndex,
46+
getItemProps
47+
} = useSelect({
48+
items: options,
49+
initialSelectedItem: getInitialItem()
50+
})
51+
52+
useEffect(() => {
53+
if (selectedItem) {
54+
setValue(name, selectedItem.value)
55+
error && clearError(name)
56+
}
57+
}, [selectedItem, name, setValue, error, clearError])
58+
59+
return (
60+
<div className="dropdown-container">
61+
<label className="label" {...getLabelProps()}>{label}</label>
62+
<button className="dropdown-trigger" type="button" {...getToggleButtonProps()}>
63+
<input name={name} type="hidden" ref={register({ required })} />
64+
<div>{selectedItem ? selectedItem.text : placeholder}</div>
65+
<span className="chevron"></span>
66+
</button>
67+
<div {...getMenuProps()}>
68+
{isOpen &&
69+
<div className="dropdown">
70+
{options.map((item: Option, index: number) => {
71+
const isActive = highlightedIndex === index || dsSelectedItem === item
72+
return (
73+
<div
74+
key={`${item.value}${index}`}
75+
className={`dropdown-item ${isActive ? 'is-active' : ''}`}
76+
{...getItemProps({ item, index })}
77+
>
78+
{item.text}
79+
</div>
80+
)
81+
})}
82+
</div>
83+
}
84+
</div>
85+
{error && <Error message={error.message} />}
86+
</div>
87+
)
88+
}

src/components/Form/Error.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react'
2+
3+
type Error = {
4+
message: string
5+
}
6+
7+
export const Error = ({ message }: Error) => (
8+
<div className="error">
9+
{message}
10+
</div>
11+
)
12+

src/components/Form/Form.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import { useForm, FormContext } from 'react-hook-form'
3+
4+
type Form = {
5+
initialValues?: any
6+
children: React.ReactNode
7+
onSubmit: (values: any, actions: any) => void
8+
className?: string
9+
}
10+
11+
export const Form = ({ initialValues, children, onSubmit, className }: Form) => {
12+
const methods = useForm({ defaultValues: initialValues })
13+
14+
return (
15+
<FormContext {...methods}>
16+
<form
17+
className={className}
18+
onSubmit={methods.handleSubmit(onSubmit)}
19+
noValidate
20+
>
21+
{children}
22+
</form>
23+
</FormContext>
24+
)
25+
}

0 commit comments

Comments
 (0)