Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add and improve filter component in strudel-components #110

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions strudel-components/lib/components/CheckboxList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,63 @@ export interface CheckboxOption {
}

interface CheckboxListProps extends Omit<FormGroupProps, 'onChange'> {
values: string[] | number[] | null;
options: CheckboxOption[];
onChange?: (values: CheckboxOptionValue[] | null) => any;
}

export const CheckboxList: React.FC<CheckboxListProps> = ({
options = [],
onChange,
values,
sx,
...rest
}) => {
const [values, setValues] = useState<CheckboxOptionValue[] | null>(null);
const [checkValues, setCheckValues] = useState<CheckboxOptionValue[] | null>(values);

const handleChange = (checked: boolean, value: CheckboxOption['value']) => {
if (values === null && checked) {
setValues([value]);
} else if (values !== null && checked) {
setValues([...values, value]);
} else if (values !== null && !checked) {
const newValues = values.filter((v) => v !== value);
if (checkValues === null && checked) {
setCheckValues([value]);
} else if (checkValues !== null && checked) {
setCheckValues([...checkValues, value]);
} else if (checkValues !== null && !checked) {
const newValues = checkValues.filter((v) => v !== value);
if (newValues.length > 0) {
setValues(newValues);
setCheckValues(newValues);
} else {
setValues(null);
setCheckValues(null);
}
}
};

useEffect(() => {
if (onChange) onChange(values);
if (onChange) onChange(checkValues);
}, [checkValues]);

useEffect(() => {
setCheckValues(values);
}, [values]);

return (
<FormGroup {...rest}>
<FormGroup
sx={{
display: 'inline-flex',
...sx
}}
{...rest}
>
{options.map((option, i) => (
<FormControlLabel
key={`${option}-${i}`}
label={option.label}
control={
<Checkbox
<Checkbox
checked={!!checkValues && checkValues.indexOf(option.value) > -1}
value={option.value}
onChange={(e, checked) => handleChange(checked, option.value)}
sx={{
pr: 1,
pl: 0,
pl: 1,
pb: 0,
pt: 0
}}
Expand Down
87 changes: 87 additions & 0 deletions strudel-components/lib/components/FilterContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { PropsWithChildren, useContext, useEffect, useReducer } from 'react';

export interface FilterState {
activeFilters: { [key: string]: any }
expandedGroup: string | number | boolean;
}

const FilterContextAPI = React.createContext<{state: FilterState; dispatch: React.Dispatch<FilterAction>} | undefined>(undefined);

const initialState: FilterState = {
activeFilters: {},
expandedGroup: false,
}

export type FilterAction =
| { type: 'SET_FILTER', payload: { field: string, value: any } }
| { type: 'SET_ACTIVE_FILTERS', payload:FilterState['activeFilters'] }
| { type: 'SET_EXPANDED_GROUP', payload: FilterState['expandedGroup']; }

function filterReducer(state: FilterState, action: FilterAction): FilterState {
switch (action.type) {
case 'SET_FILTER': {
return {
...state,
activeFilters: { ...state.activeFilters, [action.payload.field]: action.payload.value }
}
}
case 'SET_ACTIVE_FILTERS': {
return {
...state,
activeFilters: action.payload
}
}
case 'SET_EXPANDED_GROUP': {
return {
...state,
expandedGroup: action.payload
}
}
default: {
throw new Error(`Unhandled action type`)
}
}
}

interface FilterContextProps extends PropsWithChildren {
activeFilters?: FilterState['activeFilters'];
onChange?: (filters: FilterState['activeFilters']) => void;
}

export const FilterContext: React.FC<FilterContextProps> = ({
activeFilters = {},
onChange = (filters) => null,
children
}) => {
const [state, dispatch] = useReducer(filterReducer, { ...initialState, activeFilters });
const value = { state, dispatch };

/**
* Emit a change event when state.activeFilters changes
*/
useEffect(() => {
if (onChange) onChange(state.activeFilters);
}, [state.activeFilters]);

/**
* If activeFilters is changed from outside the context (e.g. filters are reset)
* then the new value should be dispatched.
*/
useEffect(() => {
dispatch({ type: 'SET_ACTIVE_FILTERS', payload: activeFilters });
}, [activeFilters]);

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

export const useFilters = () => {
const context = useContext(FilterContextAPI)
if (context === undefined) {
throw new Error('useFilters must be used within a FilterContext')
}
return context
}
Loading