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!!!!
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
};
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.
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.
i'm making a to do list with only javascript and css. When i add the task, it creates an article with a h1 and 2 icons. The check icon and delete icon. When i click the check icon, it add the class that changes the text style to line-through. The problem is that it's applying to all h1, and i want it to apply only to one specific h1.
function TaskName() {
window.taskName = {};
window.taskName.handdleClick = () => {
const $Task = document.querySelectorAll("h1");
$Task.forEach((e) => {
e.classList.toggle("task-check");
};
window.taskName.delete = () => {
ItemRemoval();
};
let $Input = document.getElementById("input-item");
let $task = $Input.value;
return /*html*/ `
<article id='task-article'>
<h1 id='task-name'>${$task}</h1>
<img src='images/check 1.png' alt='Completar tarefa' onclick='taskName.handdleClick()' id='check-icon'>
<img src='images/delete.svg' alt='Deletar tarefa' onclick='taskName.delete()' id='delete-icon'>
</article>
`;
}
Your problem starts at the structure of your code:
there's a function called TaskName that does a lot of things: creating HTML, deleting something from somewhere, handling a click event
you use the global namespace (window) to handle things
What do you need, if you want a Todo app?
a list of todos (probably an array)
a function to add a new Todo to the list of todos
a function to remove a Todo item from the list of todos
a function that sets 1 Todo item to done (OK, usually toggle between done and not done)
Here's a snippet that does this:
// list of todos & list manipulation functions
let todos = []
const addTodo = (newTodo, todos) => [...todos, newTodo]
const removeTodo = (idToRemove, todos) => todos.filter(({ id }) => idToRemove != id)
const toggleTodoDone = (idToToggle, todos) => todos.map(({ id, done, ...rest }) => id == idToToggle ? { id, done: !done, ...rest } : { id, done, ...rest })
const getTodoItem = (label) => ({
id: Date.now(),
done: false,
label,
})
// DOM manipulation & event handling
const input = document.getElementById("input-add-todo")
const btnAdd = document.getElementById("btn-add-todo")
const container = document.getElementById("container")
const resetInput = () => input.value = ''
btnAdd.addEventListener('click', function() {
const label = input.value
if (label) {
const newTodo = getTodoItem(label)
todos = addTodo(newTodo, todos)
updateContainer(container, todos)
resetInput()
}
})
const getTodoHtml = (todo) => {
const doneClass = todo.done ? ' done' : ''
return `
<div
class="todo-item${doneClass}"
data-id="${todo.id}"
>
${todo.label} - ${todo.done}
<button class="remove-todo" data-id="${todo.id}">X</button>
</div>
`
}
const getTodoListHtml = (todos) => todos.map(getTodoHtml).join('')
const registerEventHandlers = (container) => {
const els = document.querySelectorAll('.todo-item')
els.forEach(el => el.addEventListener('click', function() {
const id = el.dataset.id
todos = toggleTodoDone(id, todos)
updateContainer(container, todos)
}))
const btns = document.querySelectorAll('.remove-todo')
btns.forEach(btn => btn.addEventListener('click', function(e) {
e.stopPropagation()
const id = btn.dataset.id
todos = removeTodo(id, todos)
updateContainer(container, todos)
}))
}
const updateContainer = (container, todos) => {
container.innerHTML = getTodoListHtml(todos)
registerEventHandlers(container)
}
.todo-item {
cursor: pointer;
}
.todo-item.done {
text-decoration: line-through;
}
<div>
<input type="text" id="input-add-todo" />
<button id="btn-add-todo">ADD TODO</button>
</div>
<div id="container"></div>
I have this working function to change all children button color on click.
Now im doing a "ClearGame" button, to change all button background color to its original state '#ADC0C4'. How can i do that with key or id from the children?
const newBet: React.FC = () => {
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
totalNumbers.splice(0, spliceRangeJSON);
for (let i = 0; i <= spliceRangeJSON; i++) {
//Looping to change the backgroundColor using Id or Key
}
};
const NumbersParent = (props: any) => {
const [numbersColor, setNumbersColor] = useState('#ADC0C4');
const changeButtonColor = () => {
if (numbersColor === '#ADC0C4') {
setNumbersColor(gamesJson[whichLoteriaIsVar].color);
totalNumbers.push(props.id);
} else {
setNumbersColor('#ADC0C4');
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
return (
<Numbers style={{ backgroundColor: numbersColor }} onClick={changeButtonColor}>
{props.children}
</Numbers>
);
};
return (
<NumbersContainer>
{numbersList.map((num) => (
<NumbersParent key={num} id={num}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
<ClearGame onClick{clearGame}>Clear Game</ClearGame >
);
};
Since you want to modify the state of children from the parent component,
Create a State Object in the parent.
Pass it to the children as prop
You can change it on clear.
Or try something like recoil.
Move the useState and changeButtonColor method to the parent component and make some changes:
const [numbersColor, setNumbersColor] = useState({});
const changeButtonColor = (color) => {
if (!numbersColor[color] || numbersColor[color] === '#ADC0C4') {
setNumbersColor({...numbersColors,
[color]: gamesJson[whichLoteriaIsVar].color
});
totalNumbers.push(props.id);
} else {
setNumbersColor({...numbersColors,
[color]: '#ADC0C4'
});
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
Change the map to that:
<NumbersContainer>
{numbersList.map((num, index) => (
<NumbersParent key={num} id={num} index={index} changeButtonColor={(color) => {changeButtonColor(color)}}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
And on the return of NumbersParent component changes to be like that:
<Numbers style={{ backgroundColor: numbersColor[`color${props.index}`] }} onClick={props.changeButtonColor(`color${props.index}`)}>
{props.children}
</Numbers>
Finally, change clearGame function like this:
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
for (let i = 0; i <= spliceRangeJSON; i++) {
changeButtonColor(`color${i}`)
}
totalNumbers.splice(0, spliceRangeJSON);
};
I have two components:
A parent component with a list of products
A child component a few levels deeper with a product
Child component should change the state in parent component with the help of passed callback from parent to child.
However, I get an error: Rendered more hooks than during the previous render
Code:
// Parent
const ProductsListScreen = () => {
const [localData, setLocalData] = React.useState([
{ id: 1, count: 1 }
]);
const onProductChecked = ({ id, count }) => {
const checkedItemIndex = localData.findIndex((item) => item.id === id);
const checkedItem = checkedItemIndex ? localData[checkedItemIndex] : null;
if (checkedItem) {
// If the input count is equal to item's count in state - set checked to true
if (checkedItem.count == count) {
// Set parent state of checked product to checked true
setLocalTaskItems((prevState) => [
...prevState.slice(0, checkedItemIndex),
{
...prevState[checkedItemIndex],
countIsGood: true,
},
...prevState.slice(idx + 1),
]);
}
}
};
// List of products
return <ProductsList onProductChecked={onProductChecked} />;
};
// Child a few levels deeper - Product
const onContinuePress = (params, callback) => {
// onProductChecked callback
callback(params);
};
const Product = ({ onProductChecked }) => {
const id = 1;
const count = 1;
return (
<Button
onPress={() =>
onContinuePress(
{
id,
count,
},
onProductChecked
)
}
>
Submit
</Button>
);
};
Send localData and setLocalData as props to the child and do onProductChecked in there with props.localData and props.setLocalData
like this
<ProductsList localData={localData} setLocalData={setLocalData} />;