Skip to content

Commit

Permalink
#102 Updated location form state
Browse files Browse the repository at this point in the history
  • Loading branch information
mjamry committed Feb 18, 2022
1 parent 3f9453c commit 2d2bf51
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 200 deletions.
3 changes: 1 addition & 2 deletions src/TripPlanner.App/ClientApp/src/Common/Dto/ModalDto.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export type ModalDto<T = void> = {
export type ModalDto = {
header: JSX.Element;
body: JSX.Element;
footer?: JSX.Element;
state?: T;
};
13 changes: 5 additions & 8 deletions src/TripPlanner.App/ClientApp/src/components/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import SearchIcon from '@mui/icons-material/Search';
import CircularProgress from '@mui/material/CircularProgress';
import { Button, Popover } from '@mui/material';
import { makeStyles } from '@mui/styles';
import { useSetRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import useLoggerService from '../../Services/Diagnostics/LoggerService';
import useWikiSearch from '../../Common/WikiSearchService';
import SearchResult from './SearchResult';
import { LocationFormStateActions, useLocationFormState } from '../modals/LocationDetailsForm/LocationDetailsFormState';
import { ModalTypes, showModalState } from '../../State/ModalState';
import { locationFormDataState } from '../modals/LocationDetailsForm/LocationDetailsFormState';

const useStyles = makeStyles({
root: {
Expand All @@ -36,8 +36,8 @@ type Props = {
function Search(props: Props) {
const classes = useStyles();
const { name } = props;
const { state: locationState, dispatch: dispatchLocationState } = useLocationFormState();
const [searchValue, setSearchValue] = useState<string>(locationState.location.name || '');
const [locationData, setLocationData] = useRecoilState(locationFormDataState);
const [searchValue, setSearchValue] = useState<string>(locationData.name || '');
const [searchResults, setSearchResults] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const showModal = useSetRecoilState(showModalState);
Expand All @@ -47,10 +47,7 @@ function Search(props: Props) {
const wikiSearch = useWikiSearch();

const updateLocationState = (value: string) => {
dispatchLocationState({
type: LocationFormStateActions.updateLocation,
data: { ...locationState.location, name: value },
});
setLocationData({ ...locationData, name: value });
};

const search = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { LocationFormStateProvider } from './LocationDetailsFormState';
import ModalHeader from '../ModalHeader';

import { ModalDto } from '../../../Common/Dto/ModalDto';
Expand All @@ -15,11 +14,10 @@ type BuilderDto = {
}

const useLocationFormBuilder = () => {
const build = (data: BuilderDto): ModalDto<typeof LocationFormStateProvider> => ({
const build = (data: BuilderDto): ModalDto => ({
header: <ModalHeader title={data.title} />,
body: <LocationDetailsFormBody location={data.location} />,
footer: <LocationDetailsFooter onSubmit={data.onSubmit} />,
state: LocationFormStateProvider,
});

return build;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import Stepper from '@mui/material/Stepper';
import StepComponent from '@mui/material/Step';
import StepButton from '@mui/material/StepButton';
import React, { useEffect } from 'react';

import { LocationFormStateActions, useLocationFormState } from './LocationDetailsFormState';
import { useRecoilState, useSetRecoilState } from 'recoil';
import useStepsCoordinator from './Steps/StepsCoordinator';
import { Step } from './Steps/Step';
import StepsConfiguration from './Steps/StepsConfig';
import LocationDto from '../../../Common/Dto/LocationDto';
import { locationFormDataState, locationFormStepState } from './LocationDetailsFormState';

type BodyProps = {
location: LocationDto;
Expand All @@ -16,28 +16,22 @@ type BodyProps = {
function LocationDetailsFormBody(props: BodyProps) {
const { location } = props;
const steps: Step[] = StepsConfiguration;
const { state, dispatch } = useLocationFormState();
const [formStep, setFormStep] = useRecoilState(locationFormStepState);
const updateLocationData = useSetRecoilState(locationFormDataState);
const coordinator = useStepsCoordinator(steps);

useEffect(() => {
dispatch({
type: LocationFormStateActions.updateLocation,
data: location,
});
updateLocationData(location);
}, []);

const renderStepView = (View: React.ComponentType) => <View />;
const renderStep = () => renderStepView(coordinator.getCurrentView());

const selectStep = (stepIndex: number) => {
dispatch({ type: LocationFormStateActions.setStep, data: stepIndex });
};

const renderStepper = () => (
<Stepper activeStep={state.step} nonLinear alternativeLabel>
<Stepper activeStep={formStep} nonLinear alternativeLabel>
{steps.map((step, index) => (
<StepComponent key={step.title}>
<StepButton onClick={() => selectStep(index)}>{step.title}</StepButton>
<StepButton onClick={() => setFormStep(index)}>{step.title}</StepButton>
</StepComponent>
))}
</Stepper>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Button from '@mui/material/Button';
import React from 'react';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import LocationDto from '../../../Common/Dto/LocationDto';
import { useLocationFormState } from './LocationDetailsFormState';
import { locationFormDataState, locationFormErrorState, locationFormStepState } from './LocationDetailsFormState';
import { Step } from './Steps/Step';
import StepsConfiguration from './Steps/StepsConfig';
import useStepsCoordinator from './Steps/StepsCoordinator';
Expand All @@ -12,8 +13,18 @@ type FooterProps = {

function LocationDetailsFooter({ onSubmit }: FooterProps): JSX.Element {
const steps: Step[] = StepsConfiguration;
const { state } = useLocationFormState();
const coordinator = useStepsCoordinator(steps);
const locationData = useRecoilValue(locationFormDataState);
const resetStep = useResetRecoilState(locationFormStepState);
const resetError = useResetRecoilState(locationFormErrorState);
const resetLocationData = useResetRecoilState(locationFormDataState);

const handleSubmit = () => {
resetStep();
resetError();
resetLocationData();
onSubmit(locationData);
};

const renderPrevious = () => {
if (!coordinator.isFirstStep()) {
Expand All @@ -38,7 +49,7 @@ function LocationDetailsFooter({ onSubmit }: FooterProps): JSX.Element {
size="small"
variant="contained"
onClick={() => coordinator.next()}
disabled={!coordinator.canNext(state.location)}
disabled={!coordinator.canNext(locationData)}
>
Next
</Button>
Expand All @@ -55,8 +66,8 @@ function LocationDetailsFooter({ onSubmit }: FooterProps): JSX.Element {
size="small"
variant="contained"
color="primary"
onClick={() => onSubmit(state.location!)}
disabled={!coordinator.canNext(state.location)}
onClick={() => handleSubmit()}
disabled={!coordinator.canNext(locationData)}
>
Save
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,83 +1,46 @@
import React, {
createContext, useContext, useMemo, useReducer,
} from 'react';
import { atom, selectorFamily } from 'recoil';
import LocationDto, { LocationEmpty } from '../../../Common/Dto/LocationDto';

const enum LocationFormStateActions {
setStep,
updateLocation,
setError,
clearError
}
import { Nullable } from '../../../Common/Dto/Nullable';

type Error = {
[name: string]: string;
}

type State = {
step: number;
location: LocationDto;
errors?: Error;
const enum LocationFormStateActions {
setError,
clearError
}

type Action =
| { type: LocationFormStateActions.setStep, data: number }
| { type: LocationFormStateActions.updateLocation, data: LocationDto }
| { type: LocationFormStateActions.clearError, data: Error }
| { type: LocationFormStateActions.setError, data: Error }

type Dispatch = (action: Action) => void;

const initialState: State = {
step: 0,
location: LocationEmpty,
errors: undefined,
};

const reducer: React.Reducer<State, Action> = (state: State, action: Action) => {
let newState: State = state;

switch (action.type) {
case LocationFormStateActions.setError:
newState = { ...state, errors: { ...state.errors, [action.data.name]: action.data.value } };
break;
case LocationFormStateActions.clearError:
newState = { ...state, errors: { ...state.errors, [action.data.name]: '' } };
break;
case LocationFormStateActions.updateLocation:
newState = { ...state, location: action.data };
break;
case LocationFormStateActions.setStep:
newState = { ...state, step: action.data };
break;
default:
break;
}

return newState;
};

const LocationFormContext = createContext<{ state: State, dispatch: Dispatch}>({
state: initialState,
dispatch: () => undefined,
const locationFormStepState = atom<number>({
key: 'locationForm.step',
default: 0,
});

const useLocationFormState = () => useContext(LocationFormContext);

type Props = {
children: JSX.Element
}
const locationFormErrorState = atom<Nullable<Error>>({
key: 'locationForm.error',
default: undefined,
});

function LocationFormStateProvider({ children }: Props) {
const [state, dispatch] = useReducer<React.Reducer<State, Action>>(reducer, initialState);
const locationFormDataState = atom<LocationDto>({
key: 'locationForm.data',
default: LocationEmpty,
});

const value = useMemo<{state: State, dispatch: Dispatch}>(() => ({ state, dispatch }), [state]);
const updateError = selectorFamily<Nullable<Error>, LocationFormStateActions>({
key: 'locationsForm.updateError',
get: () => ({ get }) => get(locationFormErrorState),
set: (action) => ({ set }, value) => {
if (value instanceof Error) {
if (action === LocationFormStateActions.setError) {
set(locationFormErrorState, value);
} else if (action === LocationFormStateActions.clearError) {
set(locationFormErrorState, undefined);
}
}
},

return (
<LocationFormContext.Provider value={value}>
{children}
</LocationFormContext.Provider>
);
}
});

export { LocationFormStateActions, useLocationFormState, LocationFormStateProvider };
export {
locationFormDataState, locationFormErrorState, locationFormStepState, updateError,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,53 @@ import React from 'react';
import TextField from '@mui/material/TextField';
import L from 'leaflet';
import { Button } from '@mui/material';
import { useRecoilState } from 'recoil';
import LocationFormMapView from '../../../MapView/LocationFormMapView';
import useCoordinatesValidator from '../../../../Common/CoordinatesValidator';
import { useLocationFormState, LocationFormStateActions } from '../LocationDetailsFormState';
import { IStepValidator } from './Step';
import LocationDto from '../../../../Common/Dto/LocationDto';
import useLocationProvider from '../../../../Services/LocationProvider';
import useLocationStepsStyles from './LocationStepsStyles';
import { locationFormDataState } from '../LocationDetailsFormState';

export function LocationCoordinatesStep() {
const classes = useLocationStepsStyles();
const { state, dispatch } = useLocationFormState();
const coordinatesValidator = useCoordinatesValidator();
const locationProvider = useLocationProvider();
const [locationData, setLocationData] = useRecoilState(locationFormDataState);

const handleCoordinatesChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
if (coordinatesValidator.isValid(e.target.value)) {
dispatch({
type: LocationFormStateActions.updateLocation,
data:
{
...state.location,
coordinates: {
...state.location.coordinates,
[e.target.name]: e.target.value,
},
},
setLocationData({
...locationData,
coordinates: {
...locationData.coordinates,
[e.target.name]: e.target.value,
},
});
}
};

const handleMapCoordinatesChanged = (coordinates: L.LatLng) => {
dispatch({
type: LocationFormStateActions.updateLocation,
data:
{
...state.location,
coordinates: {
lat: coordinates.lat,
lon: coordinates.lng,
},
},
setLocationData({
...locationData,
coordinates: {
lat: coordinates.lat,
lon: coordinates.lng,
},
});
};

const handleGetLocation = async () => {
const userLocation = await locationProvider.getLocation();
if (coordinatesValidator.isValid(userLocation.lat)
&& coordinatesValidator.isValid(userLocation.lon)) {
dispatch({
type: LocationFormStateActions.updateLocation,
data:
{
...state.location,
coordinates: {
lat: userLocation.lat,
lon: userLocation.lon,
},
},
setLocationData({
...locationData,
coordinates: {
lat: userLocation.lat,
lon: userLocation.lon,
},
});
}
};
Expand All @@ -75,7 +64,7 @@ export function LocationCoordinatesStep() {
size="medium"
margin="dense"
onChange={handleCoordinatesChanged}
value={state.location?.coordinates.lat || ''}
value={locationData.coordinates.lat || ''}
/>
</div>

Expand All @@ -87,7 +76,7 @@ export function LocationCoordinatesStep() {
size="medium"
margin="dense"
onChange={handleCoordinatesChanged}
value={state.location.coordinates.lon || ''}
value={locationData.coordinates.lon || ''}
/>
</div>
</div>
Expand All @@ -106,7 +95,7 @@ export function LocationCoordinatesStep() {

<div className={classes.formRow}>
<LocationFormMapView
location={state.location}
location={locationData}
onCoordinatesUpdated={handleMapCoordinatesChanged}
/>
</div>
Expand Down
Loading

0 comments on commit 2d2bf51

Please sign in to comment.