Home > Software design >  Creating a string and converting it to JSX
Creating a string and converting it to JSX

Time:06-13

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?

CodePudding user response:

You can add click handlers to those spans using Vanilla JS as following:

  1. 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" >@${mention.username}</span>`)
  })
  return message
}
  1. 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

CodePudding user response:

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>

CodePudding user response:

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