How to pass a ref for a component using map() - javascript

I have a card component that's generated with data from a graphql query and map(). I want to be able to add a ref to the last card generated so I can use the InteserctionObserver API to load more cards when the user scrolls to the last card on the screen, how can I pass a ref to the last card?
this is the code I have
const IndexPage = ({ data }) => {
const [currentIndex, setIndex] = useState(3)
const [element, setElement] = useState(null)
console.log("element", element)
return (
<div className="article-cards">
{data.allStrapiArticle.nodes
.slice(0, currentIndex)
.map((article, index, cards) => {
return (
<Card
id={article.id}
title={article.title}
category={article.categories}
author={article.author.name}
content={article.content}
imgUrl={article.cover.publicURL}
date={article.created_at}
slug={article.slug}
ref={index == currentIndex ? setElement : null}
/>
)
})}
</div>
)

const IndexPage = ({ data }) => {
const [currentIndex, setIndex] = useState(3)
const element = React.useRef(null)
React.useEffect(() => {
console.log("new Element", element.current)
}, [element.current])
return (
<div className="article-cards">
{data.allStrapiArticle.nodes
.slice(0, currentIndex)
.map((article, index, cards) => {
return (
<Card
id={article.id}
title={article.title}
category={article.categories}
author={article.author.name}
content={article.content}
imgUrl={article.cover.publicURL}
date={article.created_at}
slug={article.slug}
ref={index == currentIndex ? element : null}
/>
)
})}
</div>
)

Related

Make other block disappear when chose a value

How can I make other filter button disappear when picked 1 value.
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 inside filter block the other block will disappear. 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 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 [selectedBlock, setSelectedBlock] = useState();
const filtersList = useMemo(
() =>
Array.from(filterItems, ([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
if (selectedBlock) {
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={handleApplyFilter}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
id={selectedBlock}
onSelected={setSelectedBlock}
/>
);
}
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={handleApplyFilter}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
id={selectedBlock}
onSelected={setSelectedBlock}
/>
);
}),
[
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
filterCountToOpen,
handleApplyFilter
]
);
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"
>
{filtersList}
</Menu>
</div>
);
};
console.log(filterItems) and it gave me this output:
Map(3) {'markforged_printer_type' => Array(3),
'markforged_material_filter' => Array(7), 'markforged_parts_filter' =>
Array(7)} [[Entries]] 0 : {"markforged_printer_type" => Array(3)} 1 :
{"markforged_material_filter" => Array(7)} 2 :
{"markforged_parts_filter" => Array(7)}
Updated Answer
From the changes you provided, you are using useMemo() and useCallback(). Those kinds of optimizations in general are not necessary to be made or even decrease performance in some cases. Check this article from Kent C. Dodds (others can be easily found about the theme) to explain some issues with it.
About the changes, as a suggestion, you could use the .map()/.filter() functions instead Array.from().
You are splitting logic about rendering different components with the useMemo(), and this could be changed into one component instead of this whole logic inside the Parent component. (For my suggestion this will be not the case)
As a guide to your code, you could use something like this:
const FilterSidebar = ({ filters, filterCountToOpen }) => {
// here you have the state to control if there is a block selected
const [selectedGroup, setSelectedGroup] = useState();
const {
// only those are needed for this example
filterItems,
handleApplyFilter
} = useFilterSidebar({ filters });
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"
>
{filterItems.map(([group, items], iteration) => {
const groupName = filterNames.get(group);
if (selectedGroup !== null && selectedGroup !== groupName) {
// returning null here should not render anything for this list item
return null;
}
return (
<FilterBlock
// pass all your extra props here
// but the important one is the `onApply`
onApply={(...args) => {
setSelectedGroup((prev) => prev !== null ? null : groupName);
return handleApplyFilter(...args);
}}
/>
);
}}
</Menu>
</div>
);
};
If you see any null on your screen, you could use first the .filter() and then the .map() or combine both with a single .reduce(). It should be something like this:
{filterItems
.filter(([group, items]) => selectedGroup === null || selectedGroup === filterNames.get(group))
.map(([group, items], iteration) => {
const groupName = filterNames.get(group);
return (
<FilterBlock
// pass all your extra props here
// but the important one is the `onApply`
onApply={(...args) => {
setSelectedGroup((prev) => prev !== null ? null : groupName);
return handleApplyFilter(...args);
}}
/>
);
}}
With your update, it is possible to see that you can select by the group (instead of the block which it was called before). Also, you can just add a little change to your onApply prop and that will save and re-render the list. If the selectedGroup is already there, removing the filter will show the other sections. Eventually, you'll need to trim this logic to accommodate other things such as selecting more than one filter and checking for that and so on.
Original Answer
From what you described I'm assuming what you want is: You have 3 FilterBlocks on your screen. Once a user selects one checkbox inside one opened "select" (that you are calling FilterBlock), you want the other FilterBlocks disappear from the screen and just the single FilterBlock with the selected option to stay at the screen (the other 2 will be hidden).
If that's your case, there are some possible options to achieve that but the easiest one is controlling this on a Parent Component: You can pass a prop from the parent component named something like onSelected, give an id to each FilterBlock, and when one filter is selected inside, you trigger that callback with the id from that FilterBlock.
const Parent = () => {
const [selectedBlock, setSelectedBlock] = useState();
if (selectedBlock) {
return <FilterBlock id={selectedBlock} onSelected={setSelectedBlock} />
}
return (
<>
<FilterBlock id="filter-block-1" onSelected={setSelectedBlock} />
<FilterBlock id="filter-block-2" onSelected={setSelectedBlock} />
<FilterBlock id="filter-block-2" onSelected={setSelectedBlock} />
</>
)
}
const FilterBlock = ({ id, onSelected }) => (
return (
<>
<button onClick={() => onSelected(id)}>Select filter block {id}</button>
<button onClick={() => onSelected()}>Unselect filter block {id}</button>
</>
);

Cannot interact with MUI stepper and change a value

I want to use an MUI stepper to replace a Select component. The select component is used to indicate the status of the document the user is working in (New, In Progress, Complete, etc.). I have managed to display the correct status in the stepper, but I cannot interact with it to move the status forward or back.
This is my stepper file. I am passing the status value through props:
export default function IntakeStatusBar(props) {
const { status } = props;
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const steps = ["New", "In Progress", "Completed"];
useEffect(() => {
if (status === "In Progress") {
setActiveStep(1);
} else if (status === "Completed") {
setActiveStep(2);
} else setActiveStep(0);
}, [status, activeStep]);
const handleStep = (step) => () => {
setActiveStep(step);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label, index) => (
<Step key={label}>
<StepButton onClick={handleStep(index)}>{label}</StepButton>
</Step>
))}
</Stepper>
</div>
);
}
This is where I call and display the stepper:
export default function IntakeDetails() {
const [details, setDetails] = useState("");
const onTextChange = (e) => {
var id = e.target.id ? e?.target.id : e?.target.name;
var value = e.target.value;
setDetails({ ...details, [id]: value });
}
....
return (
<IntakeStatusBar status={details?.Status} onChange={onTextChange} />
// This is the Select drop down menu I have been using
<TextField
label="Status"
error={requiredField && details?.Status?.length <= 0}
value={details?.Status}
disabled={!(adminRole && isSolutionsTab && details?.Status !== "In Plan")}
select
onChange={onTextChange}
>
{details.StatusList?.map((choice) => {
return (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
);
})}
</TextField>
)
}
This is what the status field looks like in JSON:
{
Status: "New"
}
besides changing this:
<StepButton onClick={() => handleStep(index)}>{label}</StepButton>
you have to change this:
const handleStep = (step) => {
setActiveStep(step);
};
and set Stepper to nonLinear if you want user to click on steps:
<Stepper nonLinear activeStep={activeStep} alternativeLabel>
I also commented out useEffect since I had no idea what its purpose is and it's messing with activeStep state.

How to filter items in a ResourceList

I am trying to filter ResourceItems in my ResourceList by their tag. For exmaple, if a user searches for the tag "Sports", all items with the this tag should be returned.
I have been utilising this example to produce this, but it doesn't actually have any functionality when the user enters a tag to filter by.
This is my code so far, in which I don't get any items back:
const GetProductList = () => {
// State setup
const [taggedWith, setTaggedWith] = useState(null);
const [queryValue, setQueryValue] = useState(null);
// Handled TaggedWith filter
const handleTaggedChange = useCallback(
(value) => setTaggedWith(value),
[],
);
const handleTaggedRemove = useCallback(() => setTaggedWith(null), []);
const handleQueryRemove = useCallback(() => setQueryValue(null), []);
const handleFilterClear = useCallback(() => {
handleTaggedRemove();
handleQueryRemove();
}, [handleQueryRemove, handleTaggedRemove]);
const filters = [
{
key:'taggedWith',
label:'Tagged With',
filter: (
<TextField
label="Tagged With"
value={taggedWith}
onChange={handleTaggedChange}
labelHidden
/>
),
shortcut: true,
}
];
const appliedFilters = !isEmpty(taggedWith)
? [
{
key: 'taggedWith',
label: disambiguateLabel('taggedWith', taggedWith),
onRemove: handleTaggedRemove,
},
]
: [];
const filterControl = (
<Filters
queryValue={queryValue}
filters={filters}
appliedFilters={appliedFilters}
onQueryChange={setQueryValue}
onQueryClear={handleQueryRemove}
onClearAll={handleFilterClear}
children={() => {
<div>Hello World</div>
}}
>
<div>
<Button onClick={() => console.log('New Filter Saved')}>Save</Button>
</div>
</Filters>
)
// Execute GET_PRODUCTS GQL Query
const { loading, error, data } = useQuery(GET_PRODUCTS);
if (loading) return "Loading products...";
if (error) return `Error = ${error}`;
// Return dropdown menu of all products
return (
<Frame>
<Page>
<Layout>
<Layout.Section>
<DisplayText size="large">WeBuy Valuation Tool</DisplayText>
</Layout.Section>
<Layout.Section>
<Card>
<Card.Section>
<div>
<Card>
<ResourceList
resourceName={{singular: 'product', plural: 'products'}}
items={data ? data.products.edges : ""}
renderItem={renderItem}
filterControl={filterControl}
>
</ResourceList>
</Card>
</div>
</Card.Section>
</Card>
</Layout.Section>
</Layout>
</Page>
</Frame>
)
function renderItem(item) {
const { id, title, images, tags } = item.node;
const media = (
<Thumbnail
source={ images.edges[0] ? images.edges[0].node.originalSrc : '' }
alt={ images.edges[0] ? images.edges[0].node.altText : '' }
/>
);
const resourceItem = (
<ResourceItem
id={id}
accessibilityLabel={`View details for ${title}`}
media={media}
>
<Stack>
<Stack.Item fill>
<h3><TextStyle variation="strong">{title}</TextStyle></h3>
<h2>{tags}</h2>
</Stack.Item>
<Stack.Item>
<AddMetafield id={id} />
<DeleteMetafield id={id} />
</Stack.Item>
</Stack>
</ResourceItem>
);
tags ? tags.forEach(tag => {
if (tag == "Sports") {
console.log("has tag")
return resourceItem
}
}) : console.log("Return")
}
function disambiguateLabel (key, value) {
switch(key) {
case 'taggedWith' :
return `Tagged with ${value}`;
default:
return value;
}
}
function isEmpty(value) {
if (Array.isArray(value)) {
return value.length === 0;
} else {
return value === '' || value == null;
}
}
}
I Couldn't find any documentation on how to implement this, and I expected it to be built-in, but I ended up doing something like this:
//add "filter.apply" function to the filter object
const filters = [{...apply:(c:Item)=>...},isApplied=...]
const appliedFilters = filters.filter(f=>f.isApplied);
let filteredItems = pageData.Items;
for (const filter of appliedFilters){
filteredItems = filteredItems.filter(filter.apply)
}
if(queryValue && queryValue.length > 0){
filteredItems = filteredItems.filter((c:FBComment)=>c.text.includes(queryValue))
}

Add to favourites and view from favourites with React Hooks?

I have a state
const [ideas, setIdeas] = useState([{title:"test", favourite:false]);
Component Idea.jsx returns props.title and a button "fav".
App.jsx maps through the idea[] and renders each idea.title in
<Item title = {idea.title}/>
on the page.
Problem:
Every time when "fav" is clicked I want to toggle ideas[index].favourite.
How to change a value of favourite only for an idea that was clicked?
How to add this exact idea to the array favourites[]?
App.jsx
function App() {
const [ideas, setIdeas] = useState([{title:"test",
favourite:false}]);
const [isClicked, setIsClicked] = useState(false)
function showAllIdeas () {
setIsClicked(prevValue => {
return !prevValue
}
)
}
function mapIdeas(){return ideas.map((ideaItem, index) => {
return (<Idea
key = {index}
id = {index}
title = {ideaItem.title}
/>
);
})}
return ( <div>
<Fab color="primary" onClick={showAllIdeas}>{expandText()}</Fab>
{isClicked && mapIdeas()}
</div>)
}
Item.jsx
function Idea(props) {
const [isClicked, setIsClicked] = useState(false)
function handleClick(){
setIsClicked(prevValue => {
return !prevValue
})
}
console.log(isClicked)
return(
<div className={"idea-list" } ><p>{props.title} {isClicked ?
<StarIcon onClick={handleClick}/> :<StarBorderIcon onClick=.
{handleClick}/>}</p>
</div>
)
}
const handleFavToggle = (index) => {
setItems(items=> {
const data = [...items]
data[index] = {...data[index],favourite: !data[index].favourite }
return data
})
}
<Item key={index} title={item.title} index={index} handleFavToggle={handleFavToggle}/>
In item component you have to handle click with handleFavToggle and pass all params

Changing the order of an item in a list using ReactJS

I have created a list using material UI and reactjs, and when a new element is added to the list the new element will go on top of the list.
I have a requirement where when I click on an element on the list the element should be strike-through and that now that element should be listed on the bottom of the list.
I was able to strike-through the element when clicked, but I am confused as to how to bring the element to the bottom of the list
How should I approach this problem?
The code of the listlayout.js is presented here, In this code, the added items are listed, and I need to find the way to change the list order when an element is stricked
app.js
class App extends Component {
constructor(props) {
super(props);
this.state={
items:[],
newItem:{
id:'',
itemText:''
},
updateItem:false
};
this.handleInput = this.handleInput.bind(this);
this.addItem = this.addItem.bind(this);
}
handleInput = e =>{
this.setState({
newItem:{
id:1 + Math.random(),
itemText: e.target.value
}
});
};
addItem = e =>{
e.preventDefault();
const typedItem = this.state.newItem;
if(typedItem.itemText !==""){
const typedItems=[...this.state.items,typedItem];
this.setState({
items:typedItems,
newItem:{
id:'',
itemText: ''
},
updateItem:false
})
}
};
render() {
return (
<div >
<HeaderBar/>
<ListLayout items={this.state.items}
/>
</div>
);
}
}
export default App;
ListLayout.js
const ToDoList = props => {
const clearList = props.clearList;
const deleteItem = props.deleteItem;
const updateItem = props.updateItem;
const strikeList = props.strikeList;
const listItems = item => {
return <div key={item.id}>{item.itemText}</div>;
};
const completed = id => {
document.getElementById(id).style.textDecoration = "line-through";
return true;
};
const strikeTextMethod = id => {
completed(id);
};
return (
<div>
<Grid container justify="center" alignContent="center">
<Grid item xs={12} md={6}>
<Typography variant="h6" className={classes.title}>
To do List
</Typography>
<div className={classes.demo}>
<List dense={dense}>
{items
.slice(0)
.reverse()
.map(x => (
<ListItem
key={x.id}
button
id={x.id}
onClick={() => strikeTextMethod(x.id)}
divider
>
<ListItemText primary={listItems(x)} />
<ListItemSecondaryAction></ListItemSecondaryAction>
</ListItem>
))}
</List>
</div>
</Grid>
</Grid>
<br />
</div>
);
};
export default ToDoList;
You have to mainatain the strike event for each item in the array. You can add an additional property to the array items, like isStriked or status.. something like that.
Then you can sort them accordingly..
Your code doesn't seem to be the entire solution. I don't see the definition of items as an example.
but something like this could be a workaround.
const ToDoList = props => {
const [items, setItems] = React.useState(props.items || []); // Initial values
// Maybe you need to these lines to sync the items state.
React.useEffect(
() => {
setItems(items)
},
[props.items]
)
const completed = id => {
document.getElementById(id).style.textDecoration = "line-through";
return true;
};
const strikeTextMethod = id => {
const index = items.findIndex(x => x.id === id);
const newItems = [items[index], ...items.slice(0, index - 1), ...items.slice(index + 1)]
setItems(newItems);
completed(id);
};
return (
)
}

Categories

Resources