Skip to content

Instantly share code, notes, and snippets.

@palanisamym14
Created November 24, 2021 13:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save palanisamym14/935366e4874b9a76e199e3e661f96b8f to your computer and use it in GitHub Desktop.
Save palanisamym14/935366e4874b9a76e199e3e661f96b8f to your computer and use it in GitHub Desktop.
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import classNames from 'classnames';
import Icon from '../../Icon';
import { filter, find, remove } from 'lodash';
import { usePopper } from 'react-popper';
export type OptionProps = {
label?: string;
value?: string;
};
export type MultiSelectProps = {
name: string;
options?: OptionProps[];
initialValue?: any;
placeholder?: string;
validator?: any;
onSelect?: (v: any) => void;
onInputChange?: (v: any) => void;
isLoading?: boolean;
};
const MultiSelect = React.forwardRef<HTMLElement, MultiSelectProps>(
(
{
name,
initialValue = [],
placeholder = '',
onSelect = () => null,
validator,
onInputChange = () => null,
isLoading = false,
options = [],
},
ref
) => {
const filterInitialOptions = (_values: Array<string>) => {
if (!_values) {
return [];
}
return filter(options, (o: any) => _values?.includes(o.value));
};
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'bottom',
modifiers: [
{
name: 'flip',
options: {
fallbackPlacements: ['top'],
},
},
],
});
const [error, setError] = useState(null);
const [showOptions, setShowOptions] = useState(false);
const [selectedOptions, setSelectedOptions] = useState<any[]>(
filterInitialOptions(initialValue)
);
const [value, setValue] = useState('');
// @ts-ignore
useImperativeHandle(ref, () => ({
name,
overrideValue: (v: any) => setSelectedOptions(filterInitialOptions(v)),
getValue: () => ({
[name]: selectedOptions ? selectedOptions : [],
}),
getError: () => ({ [name]: null }),
checkError: () => {
try {
validator.validateSync(
selectedOptions?.length ? selectedOptions : ''
);
return false;
} catch (e: any) {
if (e.errors && e.errors[0]) {
setError(e.errors[0]);
return true;
}
return false;
}
},
}));
const onChange = (e: any) => {
setValue(e.currentTarget.value);
onInputChange(e.currentTarget.value);
};
useEffect(() => {
onSelect(selectedOptions);
setValue('');
setError(null);
}, [selectedOptions]);
const inputStyle = classNames(
'text-base leading-normal text-gray-900 outline-none placeholder-gray-500 px-3 py-2 bg-white',
{
'border-error focus:border-red': error,
'focus:border-primary': !error,
}
);
const listStyle = classNames(
'w-full right-0 flex flex-col mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg z-50 overflow-auto ring-1 ring-black ring-opacity-5 max-h-56',
{
block: showOptions,
hidden: !showOptions,
}
);
const optionStyle = (active: boolean) =>
classNames(
'cursor-pointer select-none relative py-2 pl-3 pr-9 text-gray-900 hover:bg-blue-700 hover:text-white',
{
'text-gray-900': !active,
'text-white bg-primary': active,
}
);
const optionLabelStyle = (selected: boolean) =>
classNames('flex items-center truncate', {
'font-normal': !selected,
'font-bold': selected,
});
const onOptionSelect = (option: any) => {
const _option = find(selectedOptions, { value: option.value });
if (!_option) {
setSelectedOptions((prev: any) => [...prev, option]);
}
setValue('');
};
const OptionWrapper = () => (
<>
<div
ref={popperElement}
style={{ ...styles.popper, width: 'calc(100% - 0px)' }}
{...attributes.popper}
>
<ul className={listStyle}>
{filter(options, (o: any) =>
!value
? true
: o?.label?.toLowerCase().includes(value.toLowerCase())
).map((option: any, idx: number) => (
<li key={idx} className={optionStyle(false)}>
<button
className="flex items-center w-full"
onClick={() => onOptionSelect(option)}
>
<span className={optionLabelStyle(false)}>
{option.label}
</span>
</button>
</li>
))}
</ul>
</div>
</>
);
const onBlur = () => {
setTimeout(() => {
setShowOptions(false);
}, 200);
};
const onCloseChip = (item: any) => {
const __selectedOptions = [...selectedOptions];
remove(__selectedOptions, {
value: item.value,
});
setSelectedOptions(__selectedOptions);
};
const inputRef = useRef<HTMLInputElement>(null);
return (
<React.Fragment>
<div className="w-full relative">
<div
className="shadow border rounded-md border-gray-300 flex flex-wrap p-2"
onClick={() => {
if (inputRef?.current) {
inputRef?.current.focus();
}
}}
ref={referenceElement}
>
{selectedOptions?.map((item: any, idx: number) => (
<div
key={idx}
className="inline-flex space-x-0.5 items-center justify-center py-0.5 pl-2.5 pr-1 bg-indigo-100 rounded-full m-1"
>
<p className="text-sm font-medium leading-tight text-center text-indigo-800">
{item.label}
</p>
<div
className="flex items-start justify-start p-1 rounded-full cursor-pointer"
onClick={() => onCloseChip(item)}
>
<Icon name="x" className="w-4" />
</div>
</div>
))}
<input
className={inputStyle}
placeholder={selectedOptions?.length ? '' : placeholder}
onChange={onChange}
onFocus={() => setShowOptions(true)}
onBlur={onBlur}
value={value}
ref={inputRef}
/>
<div className="absolute inset-y-0 right-2 flex items-center text-base leading-normal text-gray-500">
<span className="flex-center flex-col w-5 h-5">
<Icon
name="chevron-up"
className="w-full flex justify-self-end text-gray-400"
/>
<Icon
name="chevron-down"
className="w-full flex justify-self-start text-gray-400"
/>
</span>
</div>
</div>
<OptionWrapper />
</div>
{error && <div className="error-text">{error}</div>}
<></>
</React.Fragment>
);
}
);
MultiSelect.displayName = 'MultiSelect';
export default MultiSelect;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment