Merge pull request 'Add formik' (#2) from formik into master

Reviewed-on: #2
This commit is contained in:
Dawid Wesołowski 2020-12-17 17:12:26 +01:00
commit 0a4461bbe4
14 changed files with 427 additions and 194 deletions

View File

@ -12,7 +12,7 @@
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"connected-react-router": "^6.8.0",
"date-fns": "^2.16.1",
"date-fns": "^2.9.0",
"formik": "^2.2.6",
"history": "4.10.1",
"immer": "^8.0.0",
@ -23,7 +23,6 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
"react-hook-form": "^6.13.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.0",

View File

@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
import { useField } from 'formik';
import Grid from '@material-ui/core/Grid';
import {
MuiPickersUtilsProvider,
KeyboardDatePicker
} from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
const DatePickerField = (props) => {
const [field, meta, helper] = useField(props);
const { touched, error } = meta;
const { setValue } = helper;
const isError = touched && error && true;
const { value } = field;
const [selectedDate, setSelectedDate] = useState(null);
useEffect(() => {
if (value) {
const date = new Date(value);
setSelectedDate(date);
}
}, [value]);
const _onChange = (date) => {
if (date) {
setSelectedDate(date);
try {
const ISODateString = date.toISOString();
setValue(ISODateString);
} catch (error) {
setValue(date);
}
} else {
setValue(date);
}
}
return (
<Grid container>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
{...field}
{...props}
value={selectedDate}
onChange={_onChange}
error={isError}
invalidDateMessage={isError && error}
helperText={isError && error}
/>
</MuiPickersUtilsProvider>
</Grid>
);
}
export default DatePickerField

View File

@ -1,27 +0,0 @@
import React from 'react';
import {Button, Grid} from "@material-ui/core";
import {Skeleton} from "@material-ui/lab";
const Loader = () => {
return (
<div>
<Grid item xs={12}>
<Button variant="text" fullWidth>
<Skeleton />
</Button>
</Grid>
<Grid item xs={12}>
<Button variant="text" fullWidth>
<Skeleton />
</Button>
</Grid>
<Grid item xs={12}>
<Button variant="text" fullWidth>
<Skeleton />
</Button>
</Grid>
</div>
);
};
export default Loader;

View File

@ -1,105 +0,0 @@
import React from 'react';
import {Grid, FormControlLabel, Slider, Button, Typography, Radio, RadioGroup, Box} from '@material-ui/core';
import {useInjectReducer} from "redux-injectors";
import { useSelector } from 'react-redux';
import {createStructuredSelector} from "reselect";
import { useFormContext, Controller } from "react-hook-form";
import reducer from "pages/Profile/reducer";
import {
makeSelectGoals,
makeSelectActivities,
makeSelectRatesOfChange,
} from "pages/Profile/selectors";
const stateSelector = createStructuredSelector({
goals: makeSelectGoals(),
activities: makeSelectActivities(),
});
const key = 'profilePage'
const GoalForm = () => {
useInjectReducer({ key, reducer });
const { goals, activities, ratesOfChange } = useSelector(stateSelector)
const { control } = useFormContext()
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Goal
</Typography>
<Grid>
<Grid>
<Controller
name="goal"
control={control}
as={
<RadioGroup aria-label="Your goal">
<Grid container spacing={2} >
{goals.map(({ label, value }) => (
<Grid item xs={4} key={value}>
<Button variant="text" fullWidth>
<FormControlLabel
key={value}
value={value.toString()}
control={<Radio />}
label={label}
/>
</Button>
</Grid>
))}
</Grid>
</RadioGroup>
}
/>
</Grid>
<Grid>
<Typography variant="h6" gutterBottom>
Activity
</Typography>
<Box mx={4}>
<Controller
name="activity"
control={control}
defaultValue={5}
render={({onChange, ...props}) => (
<Slider
{...props}
onChange={(_, value) => onChange(value)}
min={1.2}
step={null}
max={1.9}
marks={activities}
/>
)}
/>
</Box>
</Grid>
<Grid>
<Typography mt={2} variant="h6" gutterBottom>
Rate of change
</Typography>
<Box mx={4}>
<Controller
name="rateOfChange"
control={control}
defaultValue={0}
render={({onChange, ...props}) => (
<Slider
{...props}
onChange={(_, value) => onChange(value)}
min={0}
step={0.1}
max={1}
valueLabelDisplay="auto"
marks={ratesOfChange}
/>
)}
/>
</Box>
</Grid>
</Grid>
</React.Fragment>
);
}
export default GoalForm;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { at } from 'lodash';
import { useField } from 'formik';
import { TextField } from '@material-ui/core';
const InputField = (props) => {
const [field, meta] = useField(props);
const _renderHelperText = () => {
const [touched, error] = at(meta, 'touched', 'error');
if (touched && error) {
return error;
}
}
return (
<TextField
type="text"
error={meta.touched && meta.error && true}
helperText={_renderHelperText()}
{...field}
{...props}
/>
);
}
export default InputField

View File

@ -0,0 +1,39 @@
import React from 'react';
import { at } from 'lodash';
import { useField } from 'formik';
import {
Radio,
FormControl,
FormControlLabel,
FormHelperText
} from '@material-ui/core';
const CheckboxField = ({ label, ...rest }) => {
const [field, meta, helper] = useField({ label, ...rest });
const { setValue } = helper;
const _renderHelperText = () => {
const [touched, error] = at(meta, 'touched', 'error');
if (touched && error) {
return <FormHelperText>{error}</FormHelperText>;
}
}
const _onChange = (e) => {
setValue(e.target.checked);
}
return (
<FormControl {...rest}>
<FormControlLabel
value={field.checked}
checked={field.checked}
control={<Radio {...field} onChange={_onChange} />}
label={label}
/>
{_renderHelperText()}
</FormControl>
);
}
export default CheckboxField

View File

@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import { at } from 'lodash';
import { useField } from 'formik';
import {
InputLabel,
FormControl,
Select,
MenuItem,
FormHelperText
} from '@material-ui/core';
const SelectField = ({ label, data, ...rest }) => {
const [field, meta] = useField({ label, data, ...rest });
const { value: selectedValue } = field;
const [touched, error] = at(meta, 'touched', 'error');
const isError = touched && error && true;
const _renderHelperText = () => {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
return (
<FormControl {...rest} error={isError}>
<InputLabel>{label}</InputLabel>
<Select {...field} value={selectedValue ? selectedValue : ''}>
{data.map((item, index) => (
<MenuItem key={index} value={item.value}>
{item.label}
</MenuItem>
))}
</Select>
{_renderHelperText()}
</FormControl>
);
}
SelectField.defaultProps = {
data: []
};
SelectField.propTypes = {
data: PropTypes.array.isRequired
};
export default SelectField;

View File

@ -0,0 +1,24 @@
import React from 'react';
import { at } from 'lodash';
import { useField } from 'formik';
import {
Slider,
FormControl,
FormControlLabel,
FormHelperText
} from '@material-ui/core';
const SliderField = ({ label, ...rest }) => {
const [field, meta, helper] = useField({ label, ...rest });
const { setValue } = helper;
const _onChange = (e) => {
setValue(e.target.checked);
}
return (
<Slider onChange={_onChange} />
);
}
export default SliderField

View File

@ -0,0 +1,52 @@
import React from 'react';
import {Grid, Typography, Box} from '@material-ui/core';
import {useInjectReducer} from "redux-injectors";
import { useSelector } from 'react-redux';
import {createStructuredSelector} from "reselect";
import reducer from "pages/Profile/reducer";
import {
makeSelectGoals,
makeSelectActivities,
makeSelectRatesOfChange,
} from "pages/Profile/selectors";
import SelectField from 'components/SelectField';
const stateSelector = createStructuredSelector({
goals: makeSelectGoals(),
activities: makeSelectActivities(),
ratesOfChange: makeSelectRatesOfChange(),
});
const key = 'profilePage'
const GoalForm = () => {
useInjectReducer({ key, reducer });
const { goals, activities, ratesOfChange } = useSelector(stateSelector)
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Goal
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<SelectField label="goal" name="goal" data={goals} fullWidth />
</Grid>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Activity
</Typography>
<SelectField label="activity" name="activity" data={activities} fullWidth />
</Grid>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Rate of change
</Typography>
<SelectField label="Rate of Change" name="rateOfChange" data={ratesOfChange} fullWidth />
</Grid>
</Grid>
</React.Fragment>
);
}
export default GoalForm;

View File

@ -0,0 +1,86 @@
import 'date-fns';
import React from 'react';
import {Typography, InputLabel, Grid, InputAdornment, Select, FormControl, MenuItem, TextField, Slider} from '@material-ui/core';
import DatePickerField from 'components/DatePickerField'
import InputField from 'components/InputField'
import {useInjectReducer} from "redux-injectors";
import { useSelector } from 'react-redux';
import {createStructuredSelector} from "reselect";
import reducer from "pages/Profile/reducer";
import {
makeSelectActivities,
makeSelectGenders,
} from "pages/Profile/selectors";
import SelectField from "../../../components/SelectField";
const stateSelector = createStructuredSelector({
activities: makeSelectActivities(),
genders: makeSelectGenders(),
});
const key = 'profilePage'
const PersonalDetailsForm = () => {
useInjectReducer({ key, reducer });
const { genders } = useSelector(stateSelector)
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Personal details
</Typography>
<Grid container spacing={3}>
<Grid item xs={12}>
<SelectField label="gender" name="gender" data={genders} fullWidth />
</Grid>
<Grid item xs={12}>
<DatePickerField
name="birthday"
label="birthday"
format="MM/yy"
views={['year', 'month']}
minDate={new Date('1900/01/01')}
maxDate={new Date()}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<InputField
type="number"
name="height"
label="Height"
variant="outlined"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">cm</InputAdornment>
}}
/>
</Grid>
<Grid item xs={12}>
<InputField
type="number"
label="Current Weight"
name="weight.current"
variant="outlined"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">Kg</InputAdornment>
}}
/>
</Grid>
<Grid item xs={12}>
<InputField
type="number"
label="Goal Weight"
name="weight.goal"
variant="outlined"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">Kg</InputAdornment>
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
}
export default PersonalDetailsForm

View File

@ -0,0 +1,11 @@
import React from 'react';
const ReviewProfileForm = () => {
return (
<div>
ReviewProfileForm
</div>
);
};
export default ReviewProfileForm;

View File

@ -3,13 +3,12 @@ import { makeStyles } from '@material-ui/core/styles';
import {Paper, Stepper, Step, StepLabel, Button, Typography } from '@material-ui/core';
import {useInjectReducer, useInjectSaga} from "redux-injectors";
import { useDispatch } from 'react-redux';
import { useForm, useFormContext, FormProvider } from "react-hook-form";
import GoalForm from 'components/GoalForm';
import PersonalDetailsForm from 'components/PersonalDetailsForm';
import ReviewProfileForm from 'components/ReviewProfileForm';
import { Formik, Form } from 'formik';
import GoalForm from './GoalForm';
import PersonalDetailsForm from './PersonalDetailsForm';
import ReviewProfileForm from './ReviewProfileForm';
import reducer from "./reducer";
import saga from "./saga";
import { updateProfileAction } from './actions'
const useStyles = makeStyles((theme) => ({
layout: {
@ -47,7 +46,7 @@ const useStyles = makeStyles((theme) => ({
const steps = ['Your goal', 'Personal details', 'Review your profile'];
const getStepContent = (step) => {
const renderStepContent = (step) => {
switch (step) {
case 0:
return <GoalForm/>;
@ -60,40 +59,56 @@ const getStepContent = (step) => {
}
}
const formInitialValues = {
gender: '',
goal: '',
birthday: '',
height: '',
weight: {
current: '',
goal: '',
},
rateOfChange: '',
activity: '',
}
const key = 'profilePage'
const ProfilePage = () => {
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const methods = useForm({
defaultValues: {
gender: '',
goal: 0,
birthday: '',
height: 0,
weight: {
current: 0,
goal: 0,
},
rateOfChange: 0,
activity: 0,
}
});
const isLastStep = activeStep === steps.length - 1;
const isFirstStep = activeStep === 0;
const classes = useStyles();
useInjectReducer({ key, reducer });
useInjectSaga({ key, saga });
const dispatch = useDispatch()
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
const submitForm = async (values, actions) => {
console.log(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
const handleNext = () => {
setActiveStep(activeStep + 1);
};
}
const handleSubmit = (values, actions) => {
if (isLastStep) {
submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
const handleBack = () => {
setActiveStep(activeStep - 1);
};
}
const handleSubmitProfile = data => {
console.log('data', data)
const handleNext = () => {
setActiveStep(activeStep + 1);
}
return (
@ -121,29 +136,34 @@ const ProfilePage = () => {
</Typography>
</React.Fragment>
) : (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleSubmitProfile)}>
{getStepContent(activeStep)}
<div className={classes.buttons}>
{activeStep !== 0 && (
<Button onClick={handleBack} className={classes.button}>
back
<Formik
initialValues={formInitialValues}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form id="createProfileForm">
{renderStepContent(activeStep)}
<div className={classes.buttons}>
{activeStep !== 0 && (
<Button onClick={handleBack} className={classes.button}>
back
</Button>
)}
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={handleNext}
type={isLastStep ? 'submit' : 'button'}
className={classes.button}
>
{isLastStep ? 'Create profile' : 'Next'}
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={handleNext}
type={activeStep === steps.length - 1 ? 'submit' : 'button'}
type="submit"
className={classes.button}
>
{activeStep === steps.length - 1 ? 'Save profile' : 'Next'}
</Button>
</div>
</form>
</FormProvider>
)}
</div>
</Form>
)}
</Formik>
)}
</React.Fragment>
</Paper>
</main>

View File

@ -34,19 +34,26 @@ export const initialState = {
goals: [
{
label: 'lose weight',
value: -1,
value: 1,
},
{
label: 'maintain weight',
value: 0,
value: 2,
},
{
label: 'put on weight',
value: 1,
value: 3,
}
],
ratesOfChange: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
genders: ['male', 'female'],
ratesOfChange: [
{ value: 1, label: '0 kg'},
{ value: 2, label: '0.5 kg'},
{ value: 3, label: '1 kg'},
],
genders: [
{value: 1, label: 'male'},
{value: 2, label: 'female'},
],
isLoading: false,
error: {},
gender: '',

View File

@ -4011,7 +4011,7 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.16.1:
date-fns@^2.9.0:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==
@ -9536,11 +9536,6 @@ react-helmet@^6.1.0:
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"
react-hook-form@^6.13.1:
version "6.13.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.13.1.tgz#b9c0aa61f746db8169ed5e1050de21cacb1947d6"
integrity sha512-Q0N7MYcbA8SigYufb02h9z97ZKCpIbe62rywOTPsK4Ntvh6fRTGDXSuzWuRhLHhArLoWbGrWYSNSS4tlb+OFXg==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"