I have a problem of find a value in an array inside another array, and use the result to setState()
This is the initialState:
this.state =
{
initialStudents:[
{name:"str1",tags;["str","str",...],...},
{name:"str2",tags;["str","str",...],...},
...
],
students: [
{name:"str1",tags;["str","str",...],...},
{name:"str2",tags;["str","str",...],...},
...
]
}
The code i use to find the tags:
findTag = (tags, target) => {
tags.filter(tag => {
return tag.toLowerCase().search(target.toLowerCase()) !== >-1;
});
};
filterTag = e => {
let updatedList = this.state.initialStudents;
updatedList = updatedList.filter(student => {
return this.findTag(student.tags, e.target.value);
});
this.setState({ students: updatedList });
};
The filterTag does not update the students state
To solve your problem, I made a few edits and put them all in this working codesandbox example.
First, I changed your findTag function to something like this:
// pass in the tags from the student, and the target tag you're searching for.
// -> return true if 1 or more matching tag, false otherwise
findTag = (tags, targetTag) => {
// make sure you return something!
return tags.filter(tag => {
// check if current tag in arr matches target tag (case insensitive)
return tag.toLowerCase() === targetTag.toLowerCase();
}).length > 0; // check if there's 1 or more matching tag
};
Next, I updated the filterTag function in a few ways:
Immutably copy this.state.initialStudents into the local updatedList array. This is necessary so you don't mess up the current state before running this.setState!
Pass the value of the input via this.state.filterTag instead of e.target.value. This way, you'd update the filter when you click the button instead of on every time you press a key.
Here's how these changes look:
filterTag = e => {
// immutably copy initial student data
let updatedList = this.state.initialStudents
.map(student => ({
name: student.name,
tags: [...student.tags]
}))
// remove students w/out filter tag
.filter(student => {
return this.findTag(student.tags, this.state.filterTag);
});
// update state with new student list
this.setState({ students: updatedList });
};
A few other improvements I made:
Instead of manually setting data in initialStudents and students, I made them immutably copy the same data set from the const initialStudents data set. This could be done in the componentDidMount lifecycle method if you're fetching students from a database.
I fixed your student object declarations - you put tags;["str"...] which is invalid - the semicolon ; should be a normal colon :
I changed some "str" values to "str2" to make them unique between students
Let me know if you have questions about the codesandbox or anything else :D Hope it helps!
Related
I have a situation where I need to parse a string, break, rejoin and then generate jsx from it. I'm somewhat successful but I have to make it clickable as well so that I can navigate on click but the problem is onClick remains a string even after conversion.
Here's the details:
I get a string in this format:
Some Text #(username)[a_long_user_id_to_navigate_to] some more text
I first get all the mentions from text and convert it to object of usernames with their IDs.
and then:
const createMessageWithActionableMentions = (message, mentions) => {
mentions.map(mention => {
message = message.replace('#'+'['+mention.username+']'+'('+mention.id+')',`<span style="color:${Colors.colorPrimary}; cursor: pointer" onClick={props.history.push('/some-route')}>#${mention.username}</span>`)
})
return message
}
the message argument in above function contains the raw string and the mentions is an array of objects as follows:
mentions = [
{
username: 'abcd_1234',
id: 'abcd_defg_hijk'
},
{
username: 'efgh_1234',
id: 'wxyz_defg_jklm'
}
...so on
]
Here's what I do when I add this text to view:
<p dangerouslySetInnerHTML={{__html: message}}></p>
Here's what I see on webpage:
Some text #username some more text
Here's what I see on Inspector:
<span style="color:#03F29E; cursor: pointer" onclick="{props.history.push('/some-route')}">#username</span>
Now the question is how do I handle this part onclick="{props.history.push('/some-route')} to work the react way. also, am I doing it correctly or there's a better way?
You can add click handlers to those spans using Vanilla JS as following:
Assign a class to those spans, for example user-mention
const createMessageWithActionableMentions = (message, mentions) => {
mentions.map(mention => {
message = message.replace('#'+'['+mention.username+']'+'('+mention.id+')',`<span style="color:${Colors.colorPrimary}; cursor: pointer" class="user-mention">#${mention.username}</span>`)
})
return message
}
Add event listeners to those spans. If you are using function components with hooks, put it inside a useEffect like this, and make sure this effect hook is called after the spans are appended to the DOM.
useEffect(() => {
[...document.getElementsByClassName('user-mention')].forEach((element) => {
element.addEventListener('click', () => {
props.history.push('/some-route');
})
})
}, [/* Your dependencies */])
Note that if the spans change (increase/decrease in number, for example), you need to handle event listeners when they change as well. If this becomes cumbersome, you can use Event delegation
You could do it like this:
const createMessageWithActionableMentions = (message, mentions) => {
// Will return a list of JSX elements
let output = [message];
// For every mention
mentions.map(
mention=>`#[${mention.username}](${mention.id})`
).forEach(mention => {
// For every string we have output so far
output = output.map(
substring => {
// Exclude existing JSX elements or strings that do not contain the mention
if (typeof substring!=="string" || !substring.includes(mention)) return [substring]
// We know the mention exists in this specific string segment so we need to find where it is so we can cut it out
const index = substring.indexOf(mention)
// Split the string into the part before, the part after, and the JSX element
return [
substring.substring(0,index),
<span style={{color:Colors.colorPrimary; cursor: "pointer"}} onClick={()=>props.history.push('/some-route')}>{mention.username}</span>,
substring.substring(index+mention.length)
]
}
// Reduce the nested array of arrays back into a single array
).reduce((a,b)=>[...a,...b], [])
})
return output
}
No Inner html shenanigens needed. Just use it like this:
<p>{message}</p>
I figured it out, this can be solved using the array, I simply first broken the message into array of substrings:
message = message.split(' ')
Then mapped it to find mentions in those substrings and replaced the respective substr with <span>:
message.map( (substr, index) => {
if(isMention(substr))){
found = mentions.find(mention => mention.username == substr.split('[').pop().split(']')[0])
if(found) {
message[index] = <span style={{color:Colors.colorPrimary, cursor: "pointer"}}>{found.username}</span>
}
}
})
The final code
const createMessageWithActionableMentions = (message, mentions) => {
let found = ''
message = message.split(' ')
message.map( (substr, index) => {
if(isMention(substr))){
found = mentions.find(mention => mention.username == substr.split('[').pop().split(']')[0])
if(found) {
message[index] = <span style={{color:Colors.colorPrimary, cursor: "pointer"}}>{found.username}</span>
}
}
})
return message
}
Then rendered it as jsx
<p>{message}</p>
I have a problem how to update(in this case Add or Remove Cargo) my multidimensional array based on the selected index. The parent array has been successful, but I am confused for the child how to process the data when clicking the Add Cargo button to add cargo and the Remove Cargo button to delete all cargo based on the selected index.
Please help. This is my codesandbox code
Sorry for the view, maybe is not good enough
You will need to provide index to the functions to add and/or delete cargos. In the function update the nested fields by mapping over the inputFields. While calling the handleAddCargo, pass the index and while calling handleRemoveCargo, pass the index as well as finalIndex which is the cargo array index.
const handleAddCargo = (parentFiledIndex) => {
const updatedInputFields = inputFields.map((item, i) => {
if(parentFiledIndex === i){
return {...item, cargo: item.cargo.concat({
cargoId: '',
cargoDescription: "",
cargoHsCode: ""
})}
}else{
return item
}
});
setInputFields(updatedInputFields);
console.log("add by its index cargo here");
};
const handleRemoveCargo = (parentFiledIndex, cargoIndex) => {
const updatedInputFields = inputFields.map((item, i) => {
if(parentFiledIndex === i){
return {...item, cargo: item.cargo.filter((cargo, c) => c !== cargoIndex)}
}else{
return item
}
});
setInputFields(updatedInputFields);
console.log("remove by its index cargo here");
};
updated working solution is here
https://codesandbox.io/s/reverent-bose-c2nkk
quick note -
try not to use array indexes while rendering lists. For eg, use some library to generate unique id
I am trying to implement a filter function that is able to search in two separate JSON fields when a user types in a search bar. Searching the whole JSON returns errors and if I repeat this function, the two similar functions cancel each other out.
My current filter function:
let filteredOArt = origArt.filter((origAItem) => {
return origAItem.authors.toLowerCase().includes(this.state.search.toLowerCase())
});
I want to be able to have the search look within the "authors" field as well as a "description" field.
Before the React render, I have this function listening to the state:
updateSearch(event) {
this.setState({ search: event.target.value })
}
Then my search function is in an input field in the React return:
<h6>Search by author name: <input type="text" value={this.state.search} onChange={this.updateSearch.bind(this)} /></h6>
You can tweak the function a bit like this
let filteredOArt = origArt.filter((origAItem) => {
return (
(origAItem.authors.toLowerCase().includes(this.state.search.toLowerCase())||
(origAItem.description.toLowerCase().includes(this.state.search.toLowerCase())
)
)
});
You actually can do a filter for both fields.
Given you have your searchValue and your array with the objects you could filter this way:
const filterByAuthorOrDescription = (searchValue, array) =>
array.filter(
item =>
item.authors.toLowerCase().includes(searchValue.toLowerCase()) ||
item.description.toLowerCase().includes(searchValue.toLowerCase())
);
const filtered = filterByAuthorOrDescription(this.state.search, articles);
filtered will now contain an array of objects that contain your searchValue in either description or authors that you can map through.
You could use some to check if the filter is positive for at least one field :
let filteredOArt = origArt.filter(origAItem => ['authors', 'description'].some(field => origAItem.[field].toLowerCase().includes(this.state.search.toLowerCase())))
Just iterate over the different field names you want to use.
Some will return true if any of the fields mentioned contains your string and avoid repetitions in your code.
Long syntax :
origArt.filter(origAItem => {
return ['authors', 'description'].some(field => origAItem.[field].toLowerCase().includes(this.state.search.toLowerCase()))
})
I have a pretty simple custom component: two select lists with buttons to move the options from the available (left) list to the selected (right) list. Naturally, the moved element should no longer show up on the list it was moved from. Though both buttons successfully add the element to the target, it doesn't remove from the source, because when I pass the reduced array of items to setState, the render still returns with the original list.
EDIT posting most of the component code for clarification. The problem methods are the addItems and removeItems, where setState is called. In both cases, whichever array property is being reduced/filtered is the one not updating; the one being added to always updates properly.
... imports
interface JoinedListState {
availableItems: ListItem[]
selectedItems: ListItem[]
}
export class JoinedList extends React.Component<JoinedListState, any>{
// Create new arrays of the proper available and selected then set the new
// state
private addItems(newItems: ListItem[]) {
let oldSelected = this.props.selectedItems;
oldSelected.push.apply(oldSelected, newItems);
let newSelected = oldSelected.sort((a, b) => {
let nameA = a.value.toUpperCase();
let nameB = b.value.toUpperCase();
if (nameA < nameB) {
return -1
}
return 1
});
let newAvailable = this.props.availableItems
.slice(0) // updated on recommendation of Sasha Kos
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
});
}
// Create new arrays of the proper available and selected then set the
//new state
private removeItems(removedItems: ListItem[]) {
.. same approach as addItems
let newSelected = this.props.selectedItems.filter((item) => {
// return only the items whose id does not exist on the newly
//removed items list
return removedItems.findIndex(i => i.id == item.id) == -1
})
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
})
}
// Get the selected items by querying the DOM and send them to function
// to update state
addSelected(event: React.FormEvent<HTMLButtonElement>) {
// Code removed for brevity: uses the event object to find the
//selected objects and builds a ListItem array called 'selected'
//to pass to addItems
this.addItems(selected)
}
removeSelected(event: React.FormEvent<HTMLButtonElement>) {
// Code removed for brevity: uses the event object to find the
//selected objects and builds a ListItem array called 'selected'
//to pass to addItems
this.removeItems(selected)
}
render() {
let aItems = this.renderOptionList(this.props.availableItems),
sItems = this.renderOptionList(this.props.selectedItems);
return (
<div className='joined-list-container'>
<select key='available_list' className='available-list form-
control' multiple>
{aItems}
</select>
<span className='button-container'>
<button key='button1' className='btn btn-success'
onClick={this.addSelected.bind(this)}>
<span className='glyphicon glyphicon-chevron-right'>
</span>
</button>
<button key='button2' className='btn btn-danger'
onClick={this.removeSelected.bind(this)}>
<span className='glyphicon glyphicon-chevron-left'>
</span>
</button>
</span>
<select key='selected_list' className='selected-list form-
control' multiple>
{sItems}
</select>
</div>
)
}
renderOptionList(items: ListItem[]) {
return items.map((item, idx) => {
let key = `${item.value}_${idx}`
return (
<option value={item.id} key={key}>{item.value}</option>
)
})
}
}
(Sorry for any flawed formatting, posting was tricky)
When this kicks off the new render, the selectedItems list is properly updated with the new item(s), but the availableItems is always the original array (yes I've ensured that the newAvailable array is properly filtered down), and even when I try
this.setState({
availableItems: [],
selectedItems: newSelected
})
I get the original availableItems array on the next render.
Is there some nuance to returning similar-but-shorter arrays to state via setState? I can't find anything referencing this behavior, and not sure what I'm missing.
Thanks
This is the issue:
let oldSelected = this.props.selectedItems;
oldSelected.push.apply(oldSelected, newItems);
You are updating this.props.selectedItems here, but for availableItems:
let newAvailable = this.props.availableItems
.slice(0) // updated on recommendation of Sasha Kos
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
Here, you do not directly update this.props.availableItems. The reason this matters is that when you call setState and render is triggered these methods:
let aItems = this.renderOptionList(this.props.availableItems),
sItems = this.renderOptionList(this.props.selectedItems);
are using this.props to return arrays, NOT this.state. this.props.selectedItems has changed, and thus returns a different array, while this.props.availableItems has not changed.
tl;dr - use this.state instead of this.props when passing the arrays to your renderOptionList method.
According to mozilla docs Array.prototype.filter should create new array, but described symptoms says that you just get 2 references to one array so there is no rerender. So please try this
let newAvailable = this.props.availableItems
.slice(0) /* clones your array */
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
});
I'm using Redux and Immutable.js in my React-based project (built on React Boilerplate) and I'm looking for an idiomatic way to update or add to an Immutable.js List.
My current setup. State initially looks like this:
const initialState = fromJS({
accounts: [],
activeAccount: null,
loadedAccounts: [],
});
I have an Immutable Record for an account object:
const account = new Record({
description: '',
id: '',
name: '',
status: '',
});
And my reducer:
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ACCOUNT_SUCCESS:
return state
.set('activeAccount', new account(action.account))
default:
return state;
}
}
This works fine - when a LOAD_ACCOUNT_SUCCESS action is fired, activeAccount is updated to the value of action.account.
I can amend this so that every new LOAD_ACCOUNT_SUCCESS action pushes the newly-loaded account data to loadedAccounts instead:
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ACCOUNT_SUCCESS:
const loadedAccount = new account(action.account);
return state
.update('loadedAccounts', (loadedAccounts) => loadedAccounts.push(loadedAccount));
default:
return state;
}
}
However, at the moment loading the same account data twice will result in new Records being pushed to my List each time (duplicating data). What I want to do instead is either add action.account to loadedAccounts (as happens now) or update the Record in the List if there is a matching ID. I'm looking at similar questions and the lamentable Immutable.js documentation and I can't see how to do this: no syntax I've tried works as I expect here.
So, what do you need here is nested update.
At first, you have to check your list of loadedAccounts whether it has this account or not.
Secondly, you have to change activeAccount field.
And, lastly, add (or update) account to loadedAccounts.
The caveat here is how you pass account property. If you derive it from somewhere and pass around as a Record, you can just compare by === (or by .equals()), but it seems that it is just a plain javascript object – I'll suppose it later.
In terms of code it would be something like:
// we can do it by different ways, it is just one of them
const listWithLoadedAccounts = state.get('loadedAccounts');
const isAccountAlready = Boolean(
listWithLoadedAccounts.filter(
account => account.get('id') === action.account.id
).size
);
const patchedState = state.set('activeAccount', action.account.id);
return isAccountAlready
? patchedState.updateIn(['loadedAccounts'], list => list.map(account => account.get('id') === account.action.id ? new account(action.account) : account))
: patchedState.updateIn(['loadedAccounts'], list => list.concat(new account(action.account)))
It is not the ideal code, something can be deduplicated, but you get the idea – always use deep merge / update if you need to change nested fields or data structures.
You also can set new field directly, like:
const oldList = state.get('loadedAccounts');
const newList = oldList.concat(action.account);
const patchedState = state.set('loadedAccounts', newList);
But I personally find that it is not that flexible and also not consistent, because it is quite common operation to perform deep merge.
i hope this example will help, i am creating a new immutable list and first performing an update and then adding a new element. i am not passing the object which i want to replace with, but you can also pass your existing object, Also in update method you have access to current item
class Test {
a = null;
b = null;
constructor(a,b){
this.a=a;
this.b=b;
}
}
$("#test").html("");
function logme(item){
$("#test").append("<br/>"+JSON.stringify(item));
}
function logmeNewLine(){
$("#test").append("<br/>");
}
function listAddUpadte(key){
var index= list.get('data').findIndex(listing => {
return listing.a === key;
});
logme(' found index (-1 for not found) : ' + index);
if(index >= 0){
logme("upadte");
list = list.set("data",list.get("data").update(index,function(item){
return new Test(key,"go");
}));
}else {
logme("add");
list = list.set("data",list.get("data").push(new Test(key,"go")));
}
list.get('data').forEach(item=>{
logme(item);
});
}
var list = Immutable.fromJS({
"data":[new Test(6,"abhi"),new Test(4,"raj"),new Test(1,"ajay")]
});
logme("intial data");
list.get('data').forEach(item=>{
logme(item);
});
logmeNewLine();
logme("testing replace with a = 4 ")
logmeNewLine();
listAddUpadte(4);
logmeNewLine();
logme("testing add with a = 8 ")
logmeNewLine();
listAddUpadte(8);
logmeNewLine();
logmeNewLine();
logmeNewLine();
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.2/immutable.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="test"></div>
Since this is the top search result for what is in the title of the question
https://immutable-js.github.io/immutable-js/docs/#/List/push
An example of how to add to a list using immutable.js:
let oldList = List([ 1, 2, 3, 4 ])
let newList = oldList.push(5)
https://immutable-js.com/docs/v4.0.0/List/#insert()
insert()
Returns a new List with value at index with a size 1 more than this List. Values at indices above index are shifted over by 1.
insert(index: number, value: T): List
Discussion
This is synonymous with list.splice(index, 0, value).
List([ 0, 1, 2, 3, 4 ]).insert(6, 5)
// List [ 0, 1, 2, 3, 4, 5 ]