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>
Related
I want to convert comma-separated values into tags. In fact I already have a good part done, but I have a problem. the tags come "added" with the previous ones. If I type in the input "ex1,ex2", the tags "ex1" and "ex1,ex2" will be created, but the tags that should be created are "ex1" and "ex2". look: .
this is my input:
<Input
onChangeText={(text: string) => handleTagChange(text)}
onKeyPress={handleTagKey}
value={inputTag}
/>
and here is the functions:
const handleTagChange = (text: string) => {setInputTag(text);}
const handleTagKey = (event: NativeSyntheticEvent<TextInputKeyPressEventData>) =>
{
const { key } = event.nativeEvent;
const trimmedInput = inputTag.trim();
if(key === ',' && trimmedInput.length && !tags.includes(trimmedInput)) {
event.preventDefault();
setTags((prev) => [...prev, trimmedInput]);
}
}
I thought it would be enough to set an empty string in the input value after setTags (setInputTag("")), but apparently it's not that simple.
would this work for you?
setTags(trimmedInput.split(","));
you must first remove the last comma
for example i am searching "TEST" and in the results "THIS IS A TEST" if it writes i need to color the background of TEST text in this result
here is my code;
this.searchControl.valueChanges
.pipe(
distinctUntilChanged()
)
.subscribe((query: string) => {
this.filteredSubtitles = []
this.docViewerServiceService.getSubTitles
.map(o => o.subTitle)
.forEach(o => {
//searching part
const filtered = o.filter(subtitle => subtitle.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()))
this.filteredSubtitles.push([...filtered]);
//Matching part of the searched word with the word in the string
this.filteredSubtitles.find(o => {
if (o.filter(a => a.includes(query))) {
yes i'm stuck here there is no need for this if maybe but i don't know
}
});
})
//as a whole to divide the words in the string we get and the time range into strings.
this.filteredSubtitles.forEach(findedSubTitles => this.pushFindedTitles = findedSubTitles)
})}
my purpose here is if there is "LEKE" in the Array, enclose it in span tags and change its background.
this.docViewerServiceService.getSubTitles.map((o) => {
return o.subTitle.replace(
new RegExp(query.trim().toLocaleLowerCase(), i),
(match) => {
return `<span>${match}</span>`;
}
);
});
on your template for loop, all the matches will be inside a span tag
<p [innerHTML]="mappedSubtitle"></p>
getLinksByElementAttribute = async (element, attribute) => { /**#type{array}*/ let arrayHTML = [];
return new Cypress.Promise((resolve) => {
cy.get(element).each(($el, index) => {
//cy.log();
arrayHTML[index] = $el.attr(attribute);
resolve(arrayHTML);
});
cy.log(arrayHTML)
});
}
}
I am passing 'a' tag as element and 'href' as attribute and would like to have all the links on webpage which I got using arrayHTML[index] stored in an array together, any help on how to store the links together and return them as one array ?
When I currently run the code then I see the cy.log return value 'Array[52]' in log file for my weblink,means I have 52 'a' elements on my web page, any idea on how I can see actual values of 52 found elements?
That happens because of the asynchronous behavior of cypress. One solution could be to use cy.wrap() and JSON.stringify():
cy.wrap(arrayHTML)
.then((array) => cy.log(JSON.stringify(array)));
More information here: https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Mixing-Async-and-Sync-code
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!
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()))
})