React app - Load selected array in Material UI Table not rendering checks - javascript

So I have been working hard for days and searching on how to do this. I have a Material UI table in my React App. I want to load a table where if my user has entries in the selected array it will prerender the checks in the DOM. The selected array is populated with the entries I want but my table which uses a onClick I think needs an event to trigger the DOM to render the check. This is relevant part of my table body.
<TableBody>
{this.props.competitorData.map(competitor => {
const isSelected = this.props.isSelected(competitor.key);
return (
<TableRow
hover
onClick={() => this.props.handleClick(competitor)}
role="checkbox"
aria-checked={isSelected}
tabIndex={-1}
key={competitor.key}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={isSelected} />
</TableCell>
I have a toggle that loads my table. It fills the selected the array with the subset of data I want trigger in componentWillMount. (it's 2 tables, tier1 and tier 2).
componentWillMount() {
this.renderChecks(this.props.team)
}
renderChecks(team) {
const { selected1 } = this.state;
const { selected2 } = this.state;
let newSelected1 = [];
let newSelected2 = [];
team.map(teammate => {
if (teammate.tier === "1") {
newSelected1 = newSelected1.concat(selected1, teammate.key)
} else if (teammate.tier === "2") {
newSelected2 = newSelected2.concat(selected2, teammate.key)
}
this.setState({ selected1: newSelected1 });
this.setState({ selected2: newSelected2 });
})
}
Essentially I need a way to render isSelected based of another list that is the smaller list (team is a subset of competitorData) that has the same keys. Ive tried so many things it's to many to list here. Im looking for help on what to do to make this work because nothing has worked and Im not sure what direction I should be going on in at this point. I've tried a lot of things that seem to cause instability in the render. Essentially I've tried to make the isSelected more state based but setting and resetting that state with inline functions like
{() => this.myFunctionThatUpdatesIsSelectedState(Key)}
These blow up in render. Sometimes cause an ugly infinite loop.
Update
Based on #Eld0w post below this does render my subset of checks.
checkKeys(val) {
return this.props.team.some(teammate => {
return val.key === teammate.competitorKey;
});
}
getCompetitors = () => {
const { competitorData, team } = this.props;
return competitorData.map(
value => ({
value,
isSelected: this.checkKeys(value)
})
)
}
Tables looks like this now.
<TableBody>
{this.getCompetitors().map(competitor => {
console.log('MYCOMPETITOR2::', competitor);
return (
<TableRow
hover
onClick={event => this.props.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
There is small issues I didn't see coming. Now my table renders only the preselected checks since im not using my previous isSelected function which was:
isSelected1 = key => this.state.selected1.indexOf(key) !== -1;
Basically i need to render the existing checks but maintain the standard isSelected function somewhere in the process as well. If I think of something or post anything about it I'll update here as well. Further input is obviously welcome.
I think i need to load my team into my selected array then run my standard isSelected function. But this is where I seem to run into trouble since that is state based. Render goes crazy on me.
Final Update
So it was late last night. I just needed to change the criterion to make this whole thing work. I load my team array in the local state selected array. Then performed isSelected property check on my competitor. Now it loads my preselected and the user can then edit selects in the table from that point.
Final Solution
Load the preselect team into the local selected state array.
componentWillMount() {
this.renderChecks(this.props.team);
}
I have tiered tables. That is just some business logic (not important here). teammate.competitorKey is the key I store that is same key as the larger table, which is competitorData. I need that to get the compares to work.
renderChecks(team) {
const { selected } = this.state;
let newSelected = [];
team.map(teammate => {
if (teammate.tier === '1') {
newSelected = newSelected.concat(selected, teammate.competitorKey)
this.setState({ selected: newSelected });
}
})
}
getCompetitor can now just verify the value key exist in the array using includes
getCompetitors = () => {
const { competitorData, team } = this.props;
console.log('THISSTATESELECTED:::', this.state.selected)
return competitorData.map(
value => ({
value,
isSelected: this.state.selected.includes(value.key)
})
)
}
And Final Table looks like
<TableBody>
{this.getCompetitors().map(competitor => {
return (
<TableRow
hover
onClick={event => this.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
I know this is a lot of writing but is spent a lot of time trying to get all this working. I hope it helps someone looking to do. I will look into making this more redux worth and possibly going the reselect route to optimize but for now im going to enjoy a working table for a day. Thank you again #Eld0w !!

So basically, you want to add an isSelected props to your competitors array depending on another array's values. Avoid using state, it's only props combinations.
The straightforward solution
Instead of mapping directly your competitor's array, map on a function returning an array.
getCompetitors = () => {
const { competitors, team } = this.props;
return competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
}
Basically, what this does is destructuring the object and adding a new property isSelected thanks to the spread operator ... (ES6)
In your render then call this.getCompetitors().map(competitor => ...) instead of this.props.competitors.
Optimize this solution
You will want to use a plugin such as reselect to avoid any useless render operation.
const competitorSelector = createSelector(
props => props.competitors,
props => props.team,
(competitors, team) => competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
)
and then use it like this in your render :
this.competitorSelector(this.props)
You will also need to use competitor.isSelected instead of isSelected since the property is now part of your competitor's properties.

Related

React Material-UI Column Search Selection

I am currently using Material-UI data tables and have a search routine that is similar to this Codesandbox example, which is just searching on the Name/Food column alone.
My current code is as follows:
const [filterFn, setFilterFn] = useState({ fn: items => { return items; } })
const handleSearch = e => {
let target = e.target;
setInput(target.value)
setFilterFn({
fn: items => {
if (target.value == "")
return items;
else
return items.filter(x => x.name.toLowerCase().includes(target.value))
}
})
}
And within my current search input, I am calling handleSearch as follows:
<Controls.Input
id="name"
label="Search Name"
value={input}
autoFocus={true}
className={classes.searchInput}
InputProps={{
startAdornment: (<InputAdornment position="start">
<Search />
</InputAdornment>)
}}
onChange={handleSearch}
/>
With this, I now need a means of being able to search on any of the columns within this data table, i.e, Calories, Carbs or Protein and not just on the Food.
I was thinking of proving the user a drop-down selection list of all the available columns and then being able to use that column to perform their search but unsure how to achieve this as new to material-ui.
Can anyone please assist with how to achieve this or possibly point me to examples or other packages that still use material-ui as keen to get this going.
Here's the similar case.
Material-UI XGrid multiple select with checkbox 'select all rows checkbox' to select filtered rows only?
And if you can look through the applyFilter function, then I am pretty sure that you could implement what you want by comparing searchedVal inside a loop of keys.
That's an idea for it.
So in your case, I'd like to code this way.
const requestSearch = (originalRows: [], searchedVal: string): [] => {
return originalRows.filter((row) => {
let matches = true;
if (searchedVal) {
const properties = ['Name', 'Calories', 'Carbs', 'Protein'];
// include keys what you want to search
let containsVal = false;
properties.forEach((property) => {
if (originalRows[property].toLowerCase().includes(searchedVal.toLowerCase())) {
containsVal = true;
}
});
if (!containsVal) {
matches = false;
}
}
return matches;
});
};
I believe this is not tricky for Material-UI, just a search function in JavaScript.

Dynamic input value in React

So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.

How to limit access for User and show specific menu parts?

I have a component where through map I show all menu parts. How can I limit access(make it hidden) if user right for that part of menu is equal to 0?
const Aside: React.FunctionComponent = () => {
const[hasRight, setHasRight] = useState(false);
useEffect( () => {
getUserContext()
.then((response) => {
Object.keys(response.data.acl.rights).forEach(role => {
const right = response.data.acl.rights[role];
Object.keys(right).forEach(val => {
right[val] > 0 ?
setHasRight(true) :
setHasRight(false);
});
});
return response.data.acl;
})
}, [hasRight]);
return (
<div className="Aside">
{hasRight?
pages.map((page, index) => {
return (
<Link className="Aside__link" key={index} to={page.link}>
<img className="Aside__link-image" src={page.img} alt="page__img" />
<p>{page.title}</p>
</Link>
);
}): null}
</div>
);
};
In this part I hide all menu if it does not have access. I need to check each menu item to see if there is access to it.
Each user has many different rights for many different pages, so instead of creating hasRight as a boolean state, you need to store a keyed object showing whether they have rights for each page. I'm assuming that if the user has multiple roles, they would have rights if any of their roles have rights.
type UserRights = Record<string, boolean>;
const [userRights, setUserRights] = useState<UserRights>({});
On the rendering side of things, we need to check whether they have the right for each page separately. Instead of a ternary with null, we can use array.filter() to filter the pages before mapping them to JSX.
const canView = (page: PageConfig): boolean => {
return userRights[page.acl];
}
<div className="Aside">
{pages.filter(canView).map((page, index) => {
Prior to the response being loaded, there should be no viewable pages.
Inside our useEffect, we need to change the setState callbacks to support our keyed object format. Instead of using Object.keys to iterate, we can use Object.values and Object.entries (provided that your tsconfig has lib: es2017 or newer). I think this is correct, but you may need to tweak based on your response structure.
useEffect(() => {
getUserContext()
.then((response: Response) => {
Object.values(response.data.acl.rights).forEach(roleRights => {
Object.entries(roleRights).forEach(([name, number]) => {
setUserRights(existing => ({
...existing,
[name]: existing[name] || number > 0 // true if true true for any role
}))
});
});
})
}, []);
The dependency for the useEffect should be the user id or something like that, otherwise use an empty array [] to run it once.
Typescript Playground Link

How to sort React components based on specific value?

I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.

Filtering Components on React Native

I'm trying to make a filtering checkbox list options to filter out the news to show on a feed. My app isn't yet getting information from database so I'm basically creating by hand to test the methods.
The variables I have are topics (which is a state prop), and feedNews, topics is an array with the topics that the user wishes to see in the feed, while feedNews are all the news components that exist to show in the feed.
e.g. of topics
this.state = {
topics: ['News-1','News-2','News-3']
}
e.g. of components for feedNews
const feedNews = [
{name:'News-1', comp: <FeedNews key={101} name="News-1" />},
{name:'News-2', comp: <FeedNews key={102} name="News-2" />},
{name:'News-3', comp: <FeedNews key={103} name="News-3" />},
{name:'News-1', comp: <FeedNews key={104} name="News-1" />},
{name:'News-3', comp: <FeedNews key={105} name="News-3" />}
]
Note: the keys on each component was just for a test
Basically what I have on my render of the class is calling a function that returns the filtered array of components:
filterFeedNews(){
let topics = this.state.topics;
return feedNews.filter((item) => {
return topics.indexOf(item.name)!==-1;
}).map(item => item.comp);
}
Now this function works everytime I open the app for the first time, but if I actually change the topics array with my filter options (list of checkboxes), sometimes there are options that disappear and they weren't the options that I had chosen. Btw, the update filter function is this:
updateFilter(newsName, value){
let newNewsTopics = this.state.topics;
if(value){
newNewsTopics.push(newsName);
}else{
newNewsTopics.splice(newNewsTopics.indexOf(newsName),1);
}
this.props.dispatch(setNewsFilter(newNewsTopics));
this.setState({
newsTopics: newNewsTopics,
});
}
this function is called by bind within each checkbox as action (because of the name of the props)
onClick(){
this.action(this.name, !this.state.value);
this.setState({
value: !this.state.value,
});
}
My question is, what am I doing wrong on those functions, or is it "normal" for this to happen in react-native
PS: if there's only one news per topic, there is no problem. This only doesn't work when there are more than one news per topic
UPDATE
I'm not sure if this is the problem, but if the problem is being ScrollView instead of ListView for filtering and rendering purposes
render(){
return (
<View style={styles.root}>
<Toolbar home={true} options={true} title='Feed' actionOptions={this.optionsAction.bind(this)}/>
<View style={styles.flexContainer}>
<ScrollView style={styles.flexContainer}>
{this.filterFeedNews()}
</ScrollView>
</View>
<View style={styles.spacing} />
{this.options()}
</View>
);
}
Solution
So basically the main wasn't even the filtering, the problem was more in terms of the rendering of the components. With the help of a colleague I had to change a bit the structure of what I posted above.
What changed:
const feedNews = [
{name:'News-1', ...otherProps},
{name:'News-2', ...otherProps},
{name:'News-3', ...otherProps},
{name:'News-1', ...otherProps},
{name:'News-3', ...otherProps}
];
added a dataSource to my state
dataSource: ds.cloneWithRows(feedNews),
my filter feed function was changed as well to adapt to the new way of thought
filterFeedNews(){
let topics = this.state.newsTopics;
let feed = feedNews.filter((item) => {
return topics.includes(item.name);
});
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.setState({dataSource: ds.cloneWithRows(feed)});
}
my update filter action had to be changed as well
updateFilter(newsName, value){
let newNewsTopics = this.state.topics;
if(value){
newNewsTopics.push(newsName);
}else{
newNewsTopics.splice(newNewsTopics.indexOf(newsName), 1);
}
this.props.dispatch(setNewsFilter(newNewsTopics));
this.setState({
newsTopics: newNewsTopics,
}, () => this.filterFeedNews());
}
and my render instead of the ScrollView it now has a ListView
<ListView
style={styles.flexContainer}
dataSource={this.state.dataSource}
removeClippedSubviews={false}
renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
enableEmptySections={true}
/>
Pro: it doesn't have the problems I had with my previous approach
Con: LayoutAnimation that I used everytime the component is updated doesn't work with the ListView, so the user only has feedback of the filtration if the news in the feed have their pictures
Solution maintaining ScrollView
In case if I want to keep my initial approach with Scroll View, my solution was basically this
updateFilter(newsName, value){
let newNewsTopics = this.state.topics;
if(value){
newNewsTopics.push(newsName);
}else{
newNewsTopics.splice(newNewsTopics.indexOf(newsName), 1);
}
this.props.dispatch(setNewsFilter(newNewsTopics));
this.setState({
newsTopics: newNewsTopics,
});
}
filterFeedNews(){
let topics = this.state.topics;
let i = 0;
return feedNews.filter((item) => {
return topics.includes(item.name);
}).map((item) => {
return (<FeedNews key={++i} name={item.name}/>);
});
}
Where this.state.topics maintains the structure (an array of strings), whereas feedNews basically turns into an array of objects like in the example above for the previous solution, and then it's converted with the function filterFeedNews using filter and map o "convert" into an array of components.
In a way, the result is exactly as the same as ListView, the original animation that had isn't "working", but that's because of how the implementation is done.
Solution for the Animation
The problem I had that was causing all the problems I talked above was actually because of LayoutAnimation, everytime I "deleted" a news feed with the filtering options the Layout Animation ended up deleting more than the news feed from the specific category.
I apologize for this since I thought LayoutAnimation wasn't the problem and like that I didn't post that part of the code.
Basically for this deleting problem doesn't occur, for now the solution is to make this:
LayoutAnimation.configureNext({
duration: 300,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: { type: LayoutAnimation.Types.easeInEaseOut },
});
If you are asking why I didn't put the delete, that's because in LayouAnimation delete doesn't work well on Android, only on iOS
Again, sorry for wasting your time without putting all the information
If I am not mistaken, setState does not update the state in react immediately. So in order to prevent that, I would suggest you to call the function on the callback, like the following:
this.setState({
value: !this.state.value,
}, ()=> {
this.action(this.name, this.state.value);
});
Does that makes sense?

Categories

Resources