Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8da382b

Browse files
committedJan 20, 2022
Hook up formik to create form
1 parent 5016b9a commit 8da382b

File tree

11 files changed

+475
-25
lines changed

11 files changed

+475
-25
lines changed
 

‎site/api.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,44 @@ export interface Project {
99
}
1010

1111
export namespace Project {
12+
const testProject1: Project = {
13+
id: "test-terraform-1",
14+
icon: "https://www.datocms-assets.com/2885/1620155117-brandhcterraformverticalcolorwhite.svg",
15+
name: "Terraform Project 1",
16+
description: "Simple terraform project that deploys a kubernetes provider",
17+
}
18+
19+
const testProject2: Project = {
20+
id: "test-echo-1",
21+
name: "Echo Project",
22+
description: "Project built on echo provisioner",
23+
}
24+
25+
const allProjects = [testProject1, testProject2]
26+
1227
export const getAllProjectsInOrg = (_org: string): Promise<Project[]> => {
13-
const project1: Project = {
14-
id: "test-terraform-1",
15-
icon: "https://www.datocms-assets.com/2885/1620155117-brandhcterraformverticalcolorwhite.svg",
16-
name: "Terraform Project 1",
17-
description: "Simple terraform project that deploys a kubernetes provider",
18-
}
28+
return Promise.resolve(allProjects)
29+
}
1930

20-
const project2: Project = {
21-
id: "test-echo-1",
22-
name: "Echo Project",
23-
description: "Project built on echo provisioner",
31+
export const getProject = async (_org: string, projectId: string): Promise<Project> => {
32+
const matchingProjects = allProjects.filter((p) => p.id === projectId)
33+
34+
if (matchingProjects.length === 0) {
35+
throw new Error(`No project matching ${projectId} found`)
2436
}
2537

26-
return Promise.resolve([project1, project2])
38+
return matchingProjects[0]
39+
}
40+
41+
export const createWorkspace = (name: string): Promise<string> => {
42+
return Promise.resolve("test-workspace")
2743
}
44+
}
45+
46+
export namespace Workspace {
47+
export type WorkspaceId = string
2848

29-
export const createProject = (name: string): Promise<string> => {
49+
export const createWorkspace = (name: string, projectTemplate: string): Promise<WorkspaceId> => {
3050
return Promise.resolve("test-workspace")
3151
}
3252
}

‎site/components/Form/FormSection.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import FormHelperText from "@material-ui/core/FormHelperText"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import Typography from "@material-ui/core/Typography"
4+
import { Style } from "@material-ui/icons"
5+
import React from "react"
6+
7+
export interface FormSectionProps {
8+
title: string
9+
description?: string
10+
}
11+
12+
export const useStyles = makeStyles((theme) => ({
13+
root: {
14+
display: "flex",
15+
flexDirection: "row",
16+
// Borrowed from PaperForm styles
17+
maxWidth: "852px",
18+
width: "100%",
19+
borderBottom: `1px solid ${theme.palette.divider}`,
20+
},
21+
descriptionContainer: {
22+
maxWidth: "200px",
23+
flex: "0 0 200px",
24+
display: "flex",
25+
flexDirection: "column",
26+
justifyContent: "center",
27+
alignItems: "flex-start",
28+
marginTop: theme.spacing(2),
29+
marginBottom: theme.spacing(2),
30+
},
31+
descriptionText: {
32+
fontSize: "0.9em",
33+
lineHeight: "1em",
34+
color: theme.palette.text.secondary,
35+
marginTop: theme.spacing(1),
36+
},
37+
contents: {
38+
flex: 1,
39+
marginTop: theme.spacing(2),
40+
marginBottom: theme.spacing(2),
41+
},
42+
}))
43+
44+
export const FormSection: React.FC<FormSectionProps> = ({ title, description, children }) => {
45+
const styles = useStyles()
46+
47+
return (
48+
<div className={styles.root}>
49+
<div className={styles.descriptionContainer}>
50+
<Typography variant="h5" color="textPrimary">
51+
{title}
52+
</Typography>
53+
{description && (
54+
<Typography className={styles.descriptionText} variant="body2" color="textSecondary">
55+
{description}
56+
</Typography>
57+
)}
58+
</div>
59+
<div className={styles.contents}>{children}</div>
60+
</div>
61+
)
62+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
2+
import { FormikLike } from "../../util/formik"
3+
import React from "react"
4+
import { PasswordField } from "./PasswordField"
5+
import { FormFieldProps } from "./types"
6+
7+
/**
8+
* FormTextFieldProps extends form-related MUI TextFieldProps with Formik
9+
* props. The passed in form is used to compute error states and configure
10+
* change handlers. `formFieldName` represents the key of a Formik value
11+
* that's associated to this component.
12+
*/
13+
export interface FormTextFieldProps<T>
14+
extends Pick<
15+
TextFieldProps,
16+
| "autoComplete"
17+
| "autoFocus"
18+
| "children"
19+
| "className"
20+
| "disabled"
21+
| "fullWidth"
22+
| "helperText"
23+
| "id"
24+
| "InputLabelProps"
25+
| "InputProps"
26+
| "inputProps"
27+
| "label"
28+
| "margin"
29+
| "multiline"
30+
| "onChange"
31+
| "placeholder"
32+
| "required"
33+
| "rows"
34+
| "select"
35+
| "SelectProps"
36+
| "style"
37+
| "type"
38+
| "variant"
39+
>,
40+
FormFieldProps<T> {
41+
/**
42+
* eventTransform is an optional transformer on the event data before it is
43+
* processed by formik.
44+
*
45+
* @example
46+
* <FormTextField
47+
* eventTransformer={(str) => {
48+
* return str.replace(" ", "-")
49+
* }}
50+
* />
51+
*/
52+
eventTransform?: (value: string) => unknown
53+
/**
54+
* isPassword uses a PasswordField component when `true`; otherwise a
55+
* TextField component is used.
56+
*/
57+
isPassword?: boolean
58+
/**
59+
* displayValueOverride allows displaying a different value in the field
60+
* without changing the actual underlying value.
61+
*/
62+
displayValueOverride?: string
63+
}
64+
65+
/**
66+
* Factory function for creating a formik TextField
67+
*
68+
* @example
69+
* interface FormValues {
70+
* username: string
71+
* }
72+
*
73+
* // Use the factory to create a FormTextField associated to this form
74+
* const FormTextField = formTextFieldFactory<FormValues>()
75+
*
76+
* const MyComponent: React.FC = () => {
77+
* const form = useFormik<FormValues>()
78+
*
79+
* return (
80+
* <FormTextField
81+
* form={form}
82+
* formFieldName="username"
83+
* fullWidth
84+
* helperText="A unique name"
85+
* label="Username"
86+
* placeholder="Lorem Ipsum"
87+
* required
88+
* />
89+
* )
90+
* }
91+
*/
92+
export const formTextFieldFactory = <T,>(): React.FC<FormTextFieldProps<T>> => {
93+
const component: React.FC<FormTextFieldProps<T>> = ({
94+
children,
95+
disabled,
96+
displayValueOverride,
97+
eventTransform,
98+
form,
99+
formFieldName,
100+
helperText,
101+
isPassword = false,
102+
InputProps,
103+
onChange,
104+
type,
105+
...rest
106+
}) => {
107+
const isError = form.touched[formFieldName] && Boolean(form.errors[formFieldName])
108+
109+
// Conversion to a string primitive is necessary as formFieldName is an in
110+
// indexable type such as a string, number or enum.
111+
const fieldId = FormikLike.getFieldId<T>(form, String(formFieldName))
112+
113+
const Component = isPassword ? PasswordField : TextField
114+
const inputType = isPassword ? undefined : type
115+
116+
return (
117+
<Component
118+
{...rest}
119+
disabled={disabled || form.isSubmitting}
120+
error={isError}
121+
helperText={isError ? form.errors[formFieldName] : helperText}
122+
id={fieldId}
123+
InputProps={isPassword ? undefined : InputProps}
124+
name={fieldId}
125+
onBlur={form.handleBlur}
126+
onChange={(e) => {
127+
if (typeof onChange !== "undefined") {
128+
onChange(e)
129+
}
130+
131+
const event = e
132+
if (typeof eventTransform !== "undefined") {
133+
// TODO(Grey): Asserting the type as a string here is not quite
134+
// right in that when an input is of type="number", the value will
135+
// be a number. Type asserting is better than conversion for this
136+
// reason, but perhaps there's a better way to do this without any
137+
// assertions.
138+
event.target.value = eventTransform(e.target.value) as string
139+
}
140+
form.handleChange(event)
141+
}}
142+
type={inputType}
143+
value={displayValueOverride || form.values[formFieldName]}
144+
>
145+
{children}
146+
</Component>
147+
)
148+
}
149+
150+
// Required when using an anonymous factory function
151+
component.displayName = "FormTextField"
152+
return component
153+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import IconButton from "@material-ui/core/IconButton"
2+
import InputAdornment from "@material-ui/core/InputAdornment"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
5+
import VisibilityOffOutlined from "@material-ui/icons/VisibilityOffOutlined"
6+
import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined"
7+
import React, { useCallback, useState } from "react"
8+
9+
type PasswordFieldProps = Omit<TextFieldProps, "InputProps" | "type">
10+
11+
export const PasswordField: React.FC<PasswordFieldProps> = ({ variant = "outlined", ...rest }) => {
12+
const styles = useStyles()
13+
const [showPassword, setShowPassword] = useState<boolean>(false)
14+
15+
const handleVisibilityChange = useCallback(() => setShowPassword((showPassword) => !showPassword), [])
16+
const VisibilityIcon = showPassword ? VisibilityOffOutlined : VisibilityOutlined
17+
18+
return (
19+
<TextField
20+
{...rest}
21+
type={showPassword ? "text" : "password"}
22+
variant={variant}
23+
InputProps={{
24+
endAdornment: (
25+
<InputAdornment position="end">
26+
<IconButton aria-label="toggle password visibility" onClick={handleVisibilityChange} size="small">
27+
<VisibilityIcon className={styles.visibilityIcon} />
28+
</IconButton>
29+
</InputAdornment>
30+
),
31+
}}
32+
/>
33+
)
34+
}
35+
36+
const useStyles = makeStyles({
37+
visibilityIcon: {
38+
fontSize: 20,
39+
},
40+
})

‎site/components/Form/Title.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ export interface TitleProps {
1010
const useStyles = makeStyles((theme) => ({
1111
title: {
1212
textAlign: "center",
13-
marginBottom: theme.spacing(10),
14-
15-
[theme.breakpoints.down("sm")]: {
16-
gridColumn: 1,
17-
},
13+
marginTop: theme.spacing(5),
14+
marginBottom: theme.spacing(5),
1815

1916
"& h3": {
2017
marginBottom: theme.spacing(1),

‎site/components/Form/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export { Title } from "./Title"
1+
export * from "./Title"
2+
export * from "./FormSection"

‎site/components/Form/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { FormikLike } from "../../util/formik"
2+
3+
/**
4+
* FormFieldProps are required props for creating form fields using a factory.
5+
*/
6+
export interface FormFieldProps<T> {
7+
/**
8+
* form is a reference to a form or subform and is used to compute common
9+
* states such as error and helper text
10+
*/
11+
form: FormikLike<T>
12+
/**
13+
* formFieldName is a field name associated with the form schema.
14+
*/
15+
formFieldName: keyof T
16+
}
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.