I've got a form with several multi-select components on it and the options for some of them are dependent on which options are selected in others. The problem I'm running into is when I check and uncheck options in the select that is depended on, the other select seems to end up in a state where some but not all options are selected, but it shows all options as selected.
import React, { FC, useState, useMemo, useCallback } from "react";
import styled from "styled-components/macro";
import MultiSelect from "@khanacademy/react-multi-select";
import { IoIosCalendar } from "react-icons/io";
import { FiDownload } from "react-icons/fi";
import Form from "./Form";
import Row from "./Row";
import useAsyncEffect from "../utils/useAsyncEffect";
import getTeachers from "../api/teachers/getTeachers";
import Teacher from "shared/lib/types/Teacher";
import capitalizeFirst from "shared/lib/utils/capitalizeFirst";
import getAssignmentCategories from "../api/assignments/getAssignmentCategories";
import AssignmentCategory from "shared/lib/types/AssignmentCategory";
import getAllUnits from "../api/units/getAllUnits";
import Unit from "shared/lib/types/Unit";
import Assignment from "shared/lib/types/Assignment";
import getAllAssignments from "../api/assignments/getAllAssignments";
import LabeledSwitch from "./LabeledSwitch";
import DateInput from "./DateInput";
import Column from "./Column";
import BlockButton from "./BlockButton";
import ResponseCsvOptions from "shared/lib/types/ResponseCsvOptions";
interface Props {
onSubmit(value: Value): any;
}
interface Data {
teachers: Teacher[];
categories: AssignmentCategory[];
units: Unit[];
assignments: Assignment[];
}
export type Value = ResponseCsvOptions;
const CsvExportForm: FC<Props> = props => {
const { onSubmit, ...rest } = props;
const [data, setData] = useState<Data | null>(null);
const [value, setValue] = useState<Value>({
startDate: null,
endDate: null,
teacherIds: [],
unitIds: [],
assignmentIds: [],
categoryIds: [],
includeTeacherUnits: false
});
useAsyncEffect(async () => {
const teachers = await getTeachers();
const categories = await getAssignmentCategories();
const units = await getAllUnits();
const assignments = await getAllAssignments();
setData({
teachers,
categories,
units,
assignments
});
}, []);
const teacherOptions = useMemo(() => {
if (!data) {
return [];
}
return data.teachers.map(teacher => ({
value: teacher.id,
label: `${capitalizeFirst(teacher.firstName)} ${capitalizeFirst(
teacher.lastName
)}`
}));
}, [data]);
const unitOptions = useMemo(() => {
if (!data) {
return [];
}
return data.units
.filter(unit => {
// Skip units that belong to unselected teachers
if (unit.teacherId && !value.teacherIds.includes(unit.teacherId)) {
return false;
}
// Skip units that belong to unselected categories
if (unit.categoryId) {
return value.categoryIds.includes(unit.categoryId);
} else {
// Only show teacher units if the teacher authored switch is on
return value.includeTeacherUnits;
}
})
.map(unit => ({
value: unit.id,
label: unit.name
}));
}, [data, value]);
const assignmentOptions = useMemo(() => {
if (!data) {
return [];
}
return data.assignments
.filter(assignment => {
return value.unitIds.includes(assignment.unitId);
})
.map(assignment => ({
value: assignment.id,
label: `${assignment.title} (${assignment.subTitle})`
}));
}, [data, value]);
const handleTeachersChange = useCallback(
(teacherIds: number[]) => {
setValue(value => {
if (!data) {
return value;
}
const newUnitIds = value.unitIds.filter(unitId => {
const unit = data.units.find(unit => unit.id === unitId);
if (!unit) {
return false;
}
if (unit.teacherId) {
return (
value.includeTeacherUnits && teacherIds.includes(unit.teacherId)
);
}
return true;
});
return { ...value, teacherIds, unitIds: newUnitIds };
});
},
[data]
);
const handleUnitsChange = useCallback((unitIds: number[]) => {
setValue(value => ({ ...value, unitIds }));
}, []);
const handleAssignmentsChange = useCallback((assignmentIds: number[]) => {
setValue(value => ({ ...value, assignmentIds }));
}, []);
const handleCategoriesChange = useCallback((categoryId: number) => {
setValue(value => {
const { categoryIds } = value;
let newCategoryIds;
if (categoryIds.includes(categoryId)) {
newCategoryIds = categoryIds.filter(id => id !== categoryId);
} else {
newCategoryIds = [...categoryIds, categoryId];
}
return {
...value,
categoryIds: newCategoryIds
};
});
}, []);
const handleTeacherUnitsChange = useCallback((teacherUnits: boolean) => {
setValue(value => ({ ...value, includeTeacherUnits: teacherUnits }));
}, []);
const handleSubmit = useCallback(() => {
onSubmit(value);
}, [onSubmit, value]);
if (!data) {
return null;
}
return (
<Form {...rest} onSubmit={handleSubmit}>
<SectionLabel>Date Range</SectionLabel>
<Section>
<DateInputColumn>
<DateInputLabel>Start</DateInputLabel>
<StyledDateInput placeholder="mm/dd/yyyy" />
<IoIosCalendar size={28} />
</DateInputColumn>
<DateInputColumn>
<DateInputLabel>End</DateInputLabel>
<StyledDateInput placeholder="mm/dd/yyyy" />
<IoIosCalendar size={28} />
</DateInputColumn>
</Section>
<SectionLabel>Products</SectionLabel>
<Section>
{data.categories.map(category => (
<LabeledSwitch
key={category.id}
checked={value.categoryIds.includes(category.id)}
onChange={() => handleCategoriesChange(category.id)}
>
{category.name}
</LabeledSwitch>
))}
<LabeledSwitch
checked={value.includeTeacherUnits}
onChange={handleTeacherUnitsChange}
>
TEACHER AUTHORED
</LabeledSwitch>
</Section>
<SectionLabel>Units</SectionLabel>
<Section>
<MultiSelect
selected={value.unitIds}
options={unitOptions}
onSelectedChanged={handleUnitsChange}
/>
</Section>
<SectionLabel>Assignments</SectionLabel>
<Section>
<MultiSelect
selected={value.assignmentIds}
options={assignmentOptions}
onSelectedChanged={handleAssignmentsChange}
/>
</Section>
<SectionLabel>Teachers</SectionLabel>
<Section>
<MultiSelect
selected={value.teacherIds}
options={teacherOptions}
onSelectedChanged={handleTeachersChange}
/>
</Section>
<DownloadButton>
<FiDownload size={20} />
Download CSV
</DownloadButton>
</Form>
);
};
export default styled(CsvExportForm)`
.multi-select {
width: 459px;
}
.dropdown-heading {
border-radius: 8px !important;
}
.dropdown-heading-dropdown-arrow {
background-color: #979797;
width: 35px !important;
padding-right: 0 !important;
}
.dropdown-heading-dropdown-arrow > span {
border-color: white transparent transparent !important;
}
.dropdown[aria-expanded="true"] .dropdown-heading-dropdown-arrow > span {
border-color: transparent transparent white !important;
}
`;
const SectionLabel = styled("h3")`
color: #000000;
font-family: Lato;
font-size: 21px;
line-height: 25px;
margin-bottom: 20px;
font-weight: normal;
`;
const Section = styled(Row)`
padding-left: 23px;
margin-bottom: 32px;
${LabeledSwitch} + ${LabeledSwitch} {
margin-left: 40px;
}
`;
const StyledDateInput = styled(DateInput)`
input {
height: 37px;
width: 219px;
border: 1px solid #979797;
border-radius: 8px;
background-color: #ffffff;
padding-left: 8px;
}
input::placeholder {
color: #000000;
font-size: 14px;
line-height: 17px;
}
`;
const DateInputColumn = styled(Column)`
position: relative;
& + & {
margin-left: 21px;
}
svg {
pointer-events: none;
position: absolute;
right: 13px;
top: 24px;
}
`;
const DateInputLabel = styled("label")`
color: #7e7e7e;
font-size: 14px;
font-weight: 900;
margin-bottom: 3px;
margin-left: 8px;
`;
const DownloadButton = styled(BlockButton)`
width: 160px;
background-color: #000000;
color: #fff;
font-size: 14px;
font-weight: 500;
margin-left: 23px;
svg {
margin-right: 8px;
}
`;