skip to Main Content

How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.

Here is my code base:

const FilterBlock = props => {
    const {
        filterApi,
        filterState,
        filterFrontendInput,
        group,
        items,
        name,
        onApply,
        initialOpen
    } = props;

const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
    filterState,
    items,
    initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);

useEffect(() => {
    const handleClickOutside = event => {
        if (ref.current && !ref.current.contains(event.target)) {
            isExpanded && handleClick();
        }
    };
    document.addEventListener('click', handleClickOutside, true);
    return () => {
        document.removeEventListener('click', handleClickOutside, true);
    };
}, [isExpanded]);

const list = isExpanded ? (
    <Form>
        <FilterList
            filterApi={filterApi}
            filterState={filterState}
            name={name}
            filterFrontendInput={filterFrontendInput}
            group={group}
            items={items}
            onApply={onApply}
        />
    </Form>
) : null;

return (
    <div
        data-cy="FilterBlock-root"
        aria-label={itemAriaLabel}
        ref={ref}
    >
        <Menu.Button
            data-cy="FilterBlock-triggerButton"
            type="button"
            onClick={handleClick}
            aria-label={toggleItemOptionsAriaLabel}
        >
            <div>
                <span>
                    {name}
                </span>
                <svg
                    width="8"
                    height="5"
                    viewBox="0 0 8 5"
                    fill="none"
                    xmlns="http://www.w3.org/2000/svg"
                >
                    <path
                        d="M6.97291 0.193232C7.20854"
                        fill="currentColor"
                    />
                </svg>
            </div>
        </Menu.Button>
        <div>
            <div>
                {list}
            </div>
        </div>
    </div>
);
};

I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?

I am using React and Redux for this project

Thank you for helping me on this!!!!

enter image description here

Update:

Added parent component for FilterBlock.ks:

const FilterSidebar = props => {
    const { filters, filterCountToOpen } = props;
    const [selectedGroup, setSelectedGroup] = useState(null);
    const talonProps = useFilterSidebar({ filters });
    const {
        filterApi,
        filterItems,
        filterNames,
        filterFrontendInput,
        filterState,
        handleApply,
        handleReset
    } = talonProps;

    const filterRef = useRef();
    const classStyle = useStyle(defaultClasses, props.classes);

    const handleApplyFilter = useCallback(
        (...args) => {
            const filterElement = filterRef.current;
            if (
                filterElement &&
                typeof filterElement.getBoundingClientRect === 'function'
            ) {
                const filterTop = filterElement.getBoundingClientRect().top;
                const windowScrollY =
                    window.scrollY + filterTop - SCROLL_OFFSET;
                window.scrollTo(0, windowScrollY);
            }

            handleApply(...args);
        },
        [handleApply, filterRef]
    );

    const result = Array.from(filterItems)
        .filter(
            ([group, items]) =>
                selectedGroup === null ||
                selectedGroup === filterNames.get(group)
        )
        .map(([group, items], iteration) => {
            const blockState = filterState.get(group);
            const groupName = filterNames.get(group);
            const frontendInput = filterFrontendInput.get(group);

            return (
                <FilterBlock
                    key={group}
                    filterApi={filterApi}
                    filterState={blockState}
                    filterFrontendInput={frontendInput}
                    group={group}
                    items={items}
                    name={groupName}
                    onApply={(...args) => {
                        console.log('args: ', ...args);
                        setSelectedGroup(prev =>
                            prev !== null ? null : groupName
                        );
                        return handleApplyFilter(...args);
                    }}
                    initialOpen={iteration < filterCountToOpen}
                    iteration={iteration}
                />
            );
        });

    return (
        <div className="container px-4 mx-auto">
            <Menu
                as="div"
                className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
            >
                {result}
            </Menu>
        </div>
    );
};

Updated added useFilterSideBar.js:

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useHistory, useLocation } from 'react-router-dom';

import { useAppContext } from '@magento/peregrine/lib/context/app';

import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
    getSearchFromState,
    getStateFromSearch,
    sortFiltersArray,
    stripHtml
} from '../FilterModal/helpers';

import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';

const DRAWER_NAME = 'filter';

export const useFilterSidebar = props => {
    const { filters } = props;

    const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
    const { getFilterInputsQuery } = operations;

    const [isApplying, setIsApplying] = useState(false);
    const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
    const [filterState, filterApi] = useFilterState();
    const prevDrawer = useRef(null);
    const isOpen = drawer === DRAWER_NAME;

    const history = useHistory();
    const { pathname, search } = useLocation();

    const { data: introspectionData } = useQuery(getFilterInputsQuery);

    const attributeCodes = useMemo(
        () => filters.map(({ attribute_code }) => attribute_code),
        [filters]
    );

    // Create a set of disabled filters.
    const DISABLED_FILTERS = useMemo(() => {
        const disabled = new Set();
        // Disable category filtering when not on a search page.
        if (pathname !== '/search.html') {
            disabled.add('category_id');
            disabled.add('category_uid');
        }

        return disabled;
    }, [pathname]);

    // Get "allowed" filters by intersection of filter attribute codes and
    // schema input field types. This restricts the displayed filters to those
    // that the api will understand.
    const possibleFilters = useMemo(() => {
        const nextFilters = new Set();
        const inputFields = introspectionData
            ? introspectionData.__type.inputFields
            : [];

        // perform mapping and filtering in the same cycle
        for (const { name } of inputFields) {
            const isValid = attributeCodes.includes(name);
            const isEnabled = !DISABLED_FILTERS.has(name);

            if (isValid && isEnabled) {
                nextFilters.add(name);
            }
        }

        return nextFilters;
    }, [DISABLED_FILTERS, attributeCodes, introspectionData]);

    const isBooleanFilter = options => {
        const optionsString = JSON.stringify(options);
        return (
            options.length <= 2 &&
            (optionsString.includes(
                JSON.stringify({
                    __typename: 'AggregationOption',
                    label: '0',
                    value: '0'
                })
            ) ||
                optionsString.includes(
                    JSON.stringify({
                        __typename: 'AggregationOption',
                        label: '1',
                        value: '1'
                    })
                ))
        );
    };

    // iterate over filters once to set up all the collections we need
    const [
        filterNames,
        filterKeys,
        filterItems,
        filterFrontendInput
    ] = useMemo(() => {
        const names = new Map();
        const keys = new Set();
        const frontendInput = new Map();
        const itemsByGroup = new Map();

        const sortedFilters = sortFiltersArray([...filters]);

        for (const filter of sortedFilters) {
            const { options, label: name, attribute_code: group } = filter;

            // If this aggregation is not a possible filter, just back out.
            if (possibleFilters.has(group)) {
                const items = [];

                // add filter name
                names.set(group, name);

                // add filter key permutations
                keys.add(`${group}[filter]`);

                // TODO: Get all frontend input type from gql if other filter input types are needed
                // See https://github.com/magento-commerce/magento2-pwa/pull/26
                if (isBooleanFilter(options)) {
                    frontendInput.set(group, 'boolean');
                    // add items
                    items.push({
                        title: 'No',
                        value: '0',
                        label: name + ':' + 'No'
                    });
                    items.push({
                        title: 'Yes',
                        value: '1',
                        label: name + ':' + 'Yes'
                    });
                } else {
                    // Add frontend input type
                    frontendInput.set(group, null);
                    // add items
                    for (const { label, value } of options) {
                        items.push({ title: stripHtml(label), value });
                    }
                }
                itemsByGroup.set(group, items);
            }
        }

        return [names, keys, itemsByGroup, frontendInput];
    }, [filters, possibleFilters]);

    // on apply, write filter state to location
    useEffect(() => {
        if (isApplying) {
            const nextSearch = getSearchFromState(
                search,
                filterKeys,
                filterState
            );

            // write filter state to history
            history.push({ pathname, search: nextSearch });

            // mark the operation as complete
            setIsApplying(false);
        }
    }, [filterKeys, filterState, history, isApplying, pathname, search]);

    const handleOpen = useCallback(() => {
        toggleDrawer(DRAWER_NAME);
    }, [toggleDrawer]);

    const handleClose = useCallback(() => {
        closeDrawer();
    }, [closeDrawer]);

    const handleApply = useCallback(() => {
        setIsApplying(true);
        handleClose();
    }, [handleClose]);

    const handleReset = useCallback(() => {
        filterApi.clear();
        setIsApplying(true);
    }, [filterApi, setIsApplying]);

    const handleKeyDownActions = useCallback(
        event => {
            // do not handle keyboard actions when the modal is closed
            if (!isOpen) {
                return;
            }

            switch (event.keyCode) {
                // when "Esc" key fired -> close the modal
                case 27:
                    handleClose();
                    break;
            }
        },
        [isOpen, handleClose]
    );

    useEffect(() => {
        const justOpened =
            prevDrawer.current === null && drawer === DRAWER_NAME;
        const justClosed =
            prevDrawer.current === DRAWER_NAME && drawer === null;

        // on drawer toggle, read filter state from location
        if (justOpened || justClosed) {
            const nextState = getStateFromSearch(
                search,
                filterKeys,
                filterItems
            );

            filterApi.setItems(nextState);
        }

        // on drawer close, update the modal visibility state
        if (justClosed) {
            handleClose();
        }

        prevDrawer.current = drawer;
    }, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);

    useEffect(() => {
        const nextState = getStateFromSearch(search, filterKeys, filterItems);

        filterApi.setItems(nextState);
    }, [filterApi, filterItems, filterKeys, search]);

    return {
        filterApi,
        filterItems,
        filterKeys,
        filterNames,
        filterFrontendInput,
        filterState,
        handleApply,
        handleClose,
        handleKeyDownActions,
        handleOpen,
        handleReset,
        isApplying,
        isOpen
    };
};

Update FilterList component:

const FilterList = props => {
    const {
        filterApi,
        filterState,
        filterFrontendInput,
        name,
        group,
        itemCountToShow,
        items,
        onApply,
        toggleItemOptionsAriaLabel
    } = props;
    const classes = useStyle(defaultClasses, props.classes);
    const talonProps = useFilterList({ filterState, items, itemCountToShow });
    const { isListExpanded, handleListToggle } = talonProps;
    const { formatMessage } = useIntl();

    // memoize item creation
    // search value is not referenced, so this array is stable
    const itemElements = useMemo(() => {
        if (filterFrontendInput === 'boolean') {
            const key = `item-${group}`;
            return (
                <li
                    key={key}
                    className={classes.item}
                    data-cy="FilterList-item"
                >
                    <FilterItemRadioGroup
                        filterApi={filterApi}
                        filterState={filterState}
                        group={group}
                        name={name}
                        items={items}
                        onApply={onApply}
                        labels={labels}
                    />
                </li>
            );
        }

        return items.map((item, index) => {
            const { title, value } = item;
            const key = `item-${group}-${value}`;

            if (!isListExpanded && index >= itemCountToShow) {
                return null;
            }

            // create an element for each item
            const element = (
                <li
                    key={key}
                    className={classes.item}
                    data-cy="FilterList-item"
                >
                    <FilterItem
                        filterApi={filterApi}
                        filterState={filterState}
                        group={group}
                        item={item}
                        onApply={onApply}
                    />
                </li>
            );

            // associate each element with its normalized title
            // titles are not unique, so use the element as the key
            labels.set(element, title.toUpperCase());
            return element;
        });
    }, [
        classes,
        filterApi,
        filterState,
        filterFrontendInput,
        name,
        group,
        items,
        isListExpanded,
        itemCountToShow,
        onApply
    ]);

    const showMoreLessItem = useMemo(() => {
        if (items.length <= itemCountToShow) {
            return null;
        }

        const label = isListExpanded
            ? formatMessage({
                  id: 'filterList.showLess',
                  defaultMessage: 'Show Less'
              })
            : formatMessage({
                  id: 'filterList.showMore',
                  defaultMessage: 'Show More'
              });

        return (
            <li className={classes.showMoreLessItem}>
                <button
                    onClick={handleListToggle}
                    className="text-sm hover_text-indigo-500 transition-colors duration-sm"
                    data-cy="FilterList-showMoreLessButton"
                >
                    {label}
                </button>
            </li>
        );
    }, [
        isListExpanded,
        handleListToggle,
        items,
        itemCountToShow,
        formatMessage,
        classes
    ]);

    return (
        <Fragment>
            <ul className={classes.items}>
                {itemElements}
                {showMoreLessItem}
            </ul>
        </Fragment>
    );
};

FilterList.defaultProps = {
    onApply: null,
    itemCountToShow: 5
};

Update FilterRadioGroup:

const FilterItemRadioGroup = props => {
    const { filterApi, filterState, group, items, onApply, labels } = props;

    const radioItems = useMemo(() => {
        return items.map(item => {
            const code = `item-${group}-${item.value}`;
            return (
                <FilterItemRadio
                    key={code}
                    filterApi={filterApi}
                    filterState={filterState}
                    group={group}
                    item={item}
                    onApply={onApply}
                    labels={labels}
                />
            );
        });
    }, [filterApi, filterState, group, items, labels, onApply]);

    const fieldValue = useMemo(() => {
        if (filterState) {
            for (const item of items) {
                if (filterState.has(item)) {
                    return item.value;
                }
            }
        }

        return null;
    }, [filterState, items]);
    const field = `item-${group}`;
    const fieldApi = useFieldApi(field);
    const fieldState = useFieldState(field);
    useEffect(() => {
        if (field && fieldValue === null) {
            fieldApi.reset();
        } else if (field && fieldValue !== fieldState.value) {
            fieldApi.setValue(fieldValue);
        }
    }, [field, fieldApi, fieldState.value, fieldValue]);

    return (
        <RadioGroup field={field} data-cy="FilterDefault-radioGroup">
            {radioItems}
        </RadioGroup>
    );
};

FilterItemRadioGroup.defaultProps = {
    onApply: null
};

2

Answers


  1. The code seems overly complicated for what it does.

    Could you perhaps make the filter block a state object like this:

    filterGroup = { 
     title: "",
     hasSelectedItems: boolean,
     filterItems: [] 
     }
    

    Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.

    I apologize I don’t have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:

    return(
    { (!!filterValue && (filterGroup === selectedGroup))
       &&
       <FilterGroup />
    }
    );
    

    I find the best results come from using my state values as conditions inside my return statement.
    That way, I don’t have to manage complex logic in the .map() functions or with useEffect.

    Login or Signup to reply.
  2. It looks like FilterSideBar is where you build the the array of FilterBlocks.
    According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?

    So, in your FilterSidebar module, I’d modify your "result" variable to include a check to see if anything has already been selected.

    If something has been selected, then only render the FilterBlock that corresponds to that selected list item.

       const result = Array.from(filterItems)
        .filter(
            ([group, items]) =>
                selectedGroup === null ||
                selectedGroup === filterNames.get(group)
        )
        .map(([group, items], iteration) => {
            const blockState = filterState.get(group);
            const groupName = filterNames.get(group);
            const frontendInput = filterFrontendInput.get(group);
    
            return (
                // This is the conditional rendering
                (!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
                <FilterBlock
                    key={group}
                    ...
                />
                : null
            );
        });
    

    Then you’ll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.

    If no group has a selected filter item, they should all render.

    Also, you’ll have to be sure to clear the selectedGroup whenever there are no selected items.

    If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component’s return method like this:

    {blockState.filterGroupSelected && " " }
    

    It’s a hack but it works, and keeps you from adding calls to useEffect.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search