Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to replace parts of a string with a component? #3386

Closed
binarykitchen opened this issue Mar 12, 2015 · 40 comments
Closed

How to replace parts of a string with a component? #3386

binarykitchen opened this issue Mar 12, 2015 · 40 comments

Comments

@binarykitchen
Copy link

I have this code which is obviously not working:

    _.each(matches, (match) ->
      string = string.replace(match, ->
        <span className="match" key={i++}>{match}</span>
      )
    )

Because that would result into a string mixed with objects. Bad I know. But how can I add React components inside a string? All I want is to highlight parts of a string with a react component. A tough case to crack I guess.

@bloodyowl
Copy link
Contributor

something like this should work :

const escapeRE = new RegExp(/([.*+?^=!:$(){}|[\]\/\\])/g)
const safeRE = (string) => {
  return string.replace(escapeRE, "\\$1")
}

class Hightlight extends Component {
  static propTypes = {
    match : PropTypes.string,
    string : PropTypes.string,
  }

  render() {
    return (
      <span
        dangerouslySetInnerHTML={{
          __html : string.replace(safeRE(this.props.match), "<strong className\"match\">$1</strong>")
        }} />
    )
  }
}

@binarykitchen
Copy link
Author

Yes, I've been thinking of abusing dangerouslySetInnerHTML but I do not like this idea. This method labelled as dangerously for good reasons.

Is there no way to teach React to parse jsx within a string? Have React parse i.E.

var example = "this one word <span className="match" key={i++}>hello</span> is highlighted"

and return this in a render method?

@syranide
Copy link
Contributor

@binarykitchen You can construct React elements dynamically and that's what you need to do here, you just can't do it with only string replacement.

@binarykitchen
Copy link
Author

@syranide Yes, I already know that. My point is: wouldn't this be a nice JSX/React feature? Parse a string and turn any tags inside into child React components.

@sophiebits
Copy link
Collaborator

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
  parts[i] = <span className="match" key={i}>{parts[i]}</span>;
}
return <div>{parts}</div>;

Let me know if that's unclear.

@sophiebits
Copy link
Collaborator

Parsing JSX at runtime is error-prone and slow, and can easily have security implications – it wouldn't inherently be any safer than dangerouslySetInnerHTML is.

@bloodyowl
Copy link
Contributor

that can be a bit of an issue with type case

— mlb

On 12 mars 2015, at 21:37, Ben Alpert notifications@github.com wrote:

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
parts[i] = {parts[i]};
}
return

{parts}
;
Let me know if that's unclear.


Reply to this email directly or view it on GitHub.

@iansinnott
Copy link

I ran into this issue as well when trying to highlight parts of a string programatically (i.e. replace with <span> tags). I ended up creating a module based on @spicyj's solution above. For anyone interested: iansinnott/react-string-replace

@oztune
Copy link

oztune commented Apr 28, 2016

@iansinnott Thanks for sharing that. I started using it then realized I'm going to need the full String.prototype.replace API (needed access to the individual matching groups for a regexp within the replace function). I realized the change will be more than a simple patch (and require an API change) so I created a new repo: https://github.com/oztune/string-replace-to-array

@EfogDev
Copy link

EfogDev commented Mar 10, 2017

If react-string-replace doesn't fit your requirements:

https://github.com/EfogDev/react-process-string

@richardwestenra
Copy link

The following worked for me as way to highlight keyword(s) within a string of text, without
dangerously setting HTML:

/**
 * Find and highlight relevant keywords within a block of text
 * @param  {string} label - The text to parse
 * @param  {string} value - The search keyword to highlight
 * @return {object} A JSX object containing an array of alternating strings and JSX
 */
const formatLabel = (label, value) => {
  if (!value) {
    return label;
  }
  return (<span>
    { label.split(value)
      .reduce((prev, current, i) => {
        if (!i) {
          return [current];
        }
        return prev.concat(<b key={value + current}>{ value }</b>, current);
      }, [])
    }
  </span>);
};

formatLabel('Lorem ipsum dolor sit amet', 'dolor');
// <span>Lorem ipsum <b>dolor</b> sit amet</span>

@EliecerC
Copy link

EliecerC commented Dec 14, 2017

I needed something like this for mentions matching some pattern so it can wrap multiple values and did some changes to the @richardwestenra solution. Maybe it will be useful for someone.

/**
 * Find and highlight mention given a matching pattern within a block of text
 * @param {string} text - The text to parse
 * @param {array} values - Values to highlight
 * @param {RegExp} regex - The search pattern to highlight 
 * @return {object} A JSX object containing an array of alternating strings and JSX
 */
const formatMentionText = (text, values, regex) => { 
    if (!values.length)
        return text;

    return (<div>
        {text.split(regex)
            .reduce((prev, current, i) => {
                if (!i)
                    return [current];

                return prev.concat(
                    values.includes(current)  ?
                        <mark key={i + current}>
                            {current}
                        </mark>
                        : current
                );
            }, [])}
    </div>);
};

const text = 'Lorem ipsum dolor sit amet [[Jonh Doe]] and [[Jane Doe]]';
const values = ['Jonh Doe', 'Jane Doe'];
const reg = new RegExp(/\[\[(.*?)\]\]/); // Match text inside two square brackets

formatMentionText(text, values, reg);
// Lorem ipsum dolor sit amet <mark>Jonh Doe</mark> and <mark>Jane Doe</mark>

@rmtngh
Copy link

rmtngh commented Jun 21, 2018

I had a similar issue and I solved it with react portals, to easily find the node, I replaced the string with a div and rendered my react component using createPortal into that div.

@oztune
Copy link

oztune commented Jun 21, 2018

@rmtngh I'm curious why you'd need to do that. It seems like it might be overkill. Are you interlacing non-react code?

Regarding solutions to this problem, I'd like to re-recommend string-replace-to-array (I'm the author). It's been battle and time-tested on multiple environments. It's also quite small (the entire library is this file https://github.com/oztune/string-replace-to-array/blob/master/string-replace-to-array.js).

Here's a usage example with regex:

import replace from 'string-replace-to-array'

// The API is designed to match the native 'String.replace',
// except it can handle non-string replacements.
replace(
  'Hello Hermione Granger...',
  /(Hermione) (Granger)/g,
  function (fullName, firstName, lastName, offset, string) {
    return <Person firstName={ firstName } lastName={ lastName } key={ offset } />
  }
)

// output: ['Hello ', <Person firstName="Hermione" lastName="Granger" key={ 0 } />, ...]

@rmtngh
Copy link

rmtngh commented Jun 22, 2018

@oztune Thanks for your reply, In this case I have a string of html markup coming from a wysiwyg editor of a CMS and I'd like to place react components in certain places inside that markup.
Do you think it would be better to use a library instead of using portals? I think what portal does is to just render the component inside another node instead of nearest node which wouldn't be more expensive than rendering a normal component and the only extra step is to replace the string which can be done with native js replace.
Am I missing anything? Will there be any advantages in using the library instead?

@oztune
Copy link

oztune commented Jun 22, 2018

@rmtngh I'd say it depends on what you're trying to do. If you're just trying to sprinkle some React components in different areas of your DOM tree, and the tree isn't already being rendered with React, portals are the way to go. The method I mentioned is most useful when the parts you're trying to replace are already rendered inside of a React component.

@wojtekmaj
Copy link

wojtekmaj commented Jul 12, 2018

Here's the code that returns an array of nodes given text and pattern:

const highlightPattern = (text, pattern) => {
  const splitText = text.split(pattern);

  if (splitText.length <= 1) {
    return text;
  }

  const matches = text.match(pattern);

  return splitText.reduce((arr, element, index) => (matches[index] ? [
    ...arr,
    element,
    <mark>
      {matches[index]}
    </mark>,
  ] : [...arr, element]), []);
};

@alex9pro1
Copy link

alex9pro1 commented Sep 5, 2018

Thank you @wojtekmaj
My version for groups in pattern:

const replacePatternToComponent = (text, pattern, Component) => {
  const splitText = text.split(pattern);
  const matches = text.match(pattern);

  if (splitText.length <= 1) {
    return text;
  }

  return splitText.reduce((arr, element) => {
      if (!element) return arr;

      if(matches.includes(element)) {
        return [...arr, Component];
      }

      return [...arr, element];
    },
    []
  );
};

const string = 'Foo [first] Bar [second]';
const pattern = /(\[first\])|(\[second\])/g;

replacePatternToComponent(string, pattern, <Component />);

@pedrodurek
Copy link

pedrodurek commented Jan 10, 2019

Approach using typescript:

const formatString = (
  str: string,
  formatingFunction?: (value: number | string) => React.ReactNode,
  ...values: Array<number | string>
) => {
  const templateSplit = new RegExp(/{(\d)}/g);
  const isNumber = new RegExp(/^\d+$/);
  const splitedText = str.split(templateSplit);
  return splitedText.map(sentence => {
    if (isNumber.test(sentence)) {
      const value = values[Number(sentence)];
      return Boolean(formatingFunction) ? formatingFunction(value) : value;
    }
    return sentence;
  });
};

Example of use:

const str = '{0} test {1} test';
formatString(str, value => <b>{value}</b>, value1, value2);

Result:
<b>value1</b> test <b>value2</b> test

@Dashue
Copy link

Dashue commented Jan 28, 2019

@rmtngh could you provide a sample of how you achieved the markup enrichment through createportal? I'm looking to do the same.

@rmtngh
Copy link

rmtngh commented Jan 29, 2019

@Dashue The idea is to create some targets for your react components inside your markup, depending on the HTML string and how much control you have over it,
If you can change it on the server side, you may want to give an ID to your elements. Something like :
<div id="component_target_1"></div>
And inside react, look for \id="(component_target_\d)"\g with regexp, store the ids (state.targets) and render your components:

let mappedComponents
if(this.state.targets && this.state.targets.length){
			mappedComponents = this.state.targets.map((id,index)=><MyComponent id={id} key={id} />)
		}

Here is a sample of "MyComponent":

import React from 'react'
import ReactDOM from 'react-dom'

export default class MyComponent extends React.Component {
  constructor(props) {
    super(props)
  }
  getWrapper(){
    return document.getElementById(this.props.id)
  }
  render() {
    if(!this.getWrapper()) return null
   
    const content = <div>
        Hello there! I'm rendered with react.
      </div>

      return ReactDOM.createPortal(
        content,
        this.getWrapper(),
      );
  }
}

If you don't have much control over the HTML string, you still can use the same approach, you might need to find some elements and inject a target element into the string.

@ykadosh
Copy link

ykadosh commented Jun 26, 2019

Great thread guys!

I created a simple React Text Highlighter pen that implements some of the ideas in this thread (with some extra magic...), hopefully future visitors can find it useful.

@nullhook
Copy link

nullhook commented Aug 5, 2019

Here's a solution using map,
forking @sophiebits solution here:

const reg = new RegExp(/(\bmoo+\b)/, 'gi');
const parts = text.split(reg);
return <div>{parts.map(part => (part.match(reg) ? <b>{part}</b> : part))}</div>;

@djD-REK
Copy link

djD-REK commented Aug 9, 2019

Beautiful solution @rehat101

@djD-REK
Copy link

djD-REK commented Aug 9, 2019

Here's what worked best in my case -- splitting by the space character and looking for specific words.

It's a little messy in the HTML but who cares 😂

💗 RegExp and map make this soooo clean. 💗💗💗

image

const Speech = ({ speech, speeches, setSpeeches, i, text }) => {

const highlightRegExp = new RegExp(
    /you|can|do|it|puedes|hacerlo|pingüinos|son|bellos/,
    "gi"
  );
  const delineator = " ";
  const parts = text.split(delineator);

  return (
        <div>
          {parts.map(part =>
            part.match(highlightRegExp) ? <b>{part + " "}</b> : part + " "
          )}
        </div>
  );
};

djD-REK added a commit to djD-REK/learn-penguin that referenced this issue Aug 9, 2019
Currently magic numbers for the RegExp at code line 276, but it works.  Sourced from @rehat101 at facebook/react#3386
@nilsbeuth
Copy link

Beautiful @rehat101

@stikut
Copy link

stikut commented Nov 24, 2019

If react-string-replace doesn't fit your requirements:

https://github.com/EfogDev/react-process-string

Brilliant! I find it extremely useful since most of the times you actually need to access the original string

@SalehAly01
Copy link

I refactored the solution of @nullhook a little bit to make it accept variable from a query and memic the behavior of Google search autocomplete

const highlightQueryText = (text: string, filterValue: string) => {
const reg = new RegExp(`(${filterValue})`, 'gi');
const textParts = text.split(reg);
return (
           <span style={{ fontWeight: 600 }}>
               {textParts.map(part => (part.match(reg) ? <span style={{ fontWeight: 'normal' }}>{part}</span> : part))}
           </span>
       );
   };

@Lazercat
Copy link

so I found a nice combination of implementing DOMPurify to sanitize html strings with html-react-parser to find targeted markup in it and convert it to JSX components - I use Rebass with passed props as an example.. that came out quite nicely, the two packages will help you to avoid the overhead of rolling your own parser, I just wouldnt recommend this parser package without sanitization. For those struggling with this still maybe scope out this codepen for what it's worth: https://codesandbox.io/s/eager-wiles-5hpff. Feedback on any improvements much appreciated too.

@djD-REK
Copy link

djD-REK commented Feb 27, 2020 via email

@elron
Copy link

elron commented Mar 27, 2020

You can use the split function with a regex and then replace the captured parts, like so:

var parts = "I am a cow; cows say moo. MOOOOO.".split(/(\bmoo+\b)/gi);
for (var i = 1; i < parts.length; i += 2) {
  parts[i] = <span className="match" key={i}>{parts[i]}</span>;
}
return <div>{parts}</div>;

Let me know if that's unclear.

This works great. Any way to get it to work in Angular? I've posted a question about it, using your code:
https://stackoverflow.com/questions/60889171/wrap-certain-words-with-a-component-in-a-contenteditable-div-in-angular

@luigbren
Copy link

Hello friends, I need your help please, I am using React Native and what I need to do is from a text that I extract from the DB to apply a format (font color, link) to make @mentions, when searching if in the text finds 1 single match makes replacement all good, but! If there are several @mentions in the text, it throws me an error.

/////Text example: hey what happened @-id:1- everything ok ?? and you @-id:2- @-id:3- @-id:4-
//// listusers its array, example: [idusuario: "1", usuario: "@luigbren", format: "@-id:1-".....]

const PatternToComponent = (text, usuarios,) => {
    let mentionsRegex = new RegExp(/@-(id:[0-9]+)-/, 'gim');
    let matches = text.match(mentionsRegex);
    if (matches && matches.length) {
        matches = matches.map(function (match, idx) {
            let usrTofind = matches[idx]
            //////////////////////////////////////////////////////////////////
            const mentFormat = listusers.filter(function (item) { 
                const itemData = item.format;
                return itemData.indexOf(usrTofind) > -1;
            });
            if (mentFormat.length > 0) {
                let idusuario = mentFormat[0].idusuario
                let replc = mentFormat[0].usuario

                console.log(usrTofind) //// here find @-id:1-
                console.log(replc)   //// here is @luigbren for replace
 
                ////////// now here replace part of the string, @-id:1- with a <Text> component 
                ///////// with @luigbren and the link, this is repeated for every @mention found

                parts = text.split(usrTofind);
                for (var i = 1; i < parts.length; i += 2) {
                  parts[i] = <Text key={i} style={{ color: '#00F' }} onPress={() => { alert('but this is'); }}>{replc}</Text>;
                }
                return text = parts;
                /////////////////////////////////////////////////////////////////////
            } else {
                return text
            }
        });
    } else {
        return text
    }
    return text
};

in this way the code works well for me only when in the text there is only one mention, example 'hey what happened @-id:1- everything ok ??' , but when placing more than one mention it gives me an error, example: 'hey what happened @-id:1- everything ok ?? @-id:2- @-id:3-' ... Error: TypeError: text.split is not a function and if instead of placing parts = text.split(usrTofind); I place parts = text.toString().split(usrTofind); it gives me an [Object Object] error

@sadi304
Copy link

sadi304 commented Apr 4, 2020

You can use html-react-parser to parse the string, then replace by node name with jsx components. This type of replacement will allow you to use react elements dynamically by replacing strings.

      parse(body, {
        replace: domNode => {
          if (domNode.name === 'select') { // or
            return React.createElement(
              Select, // your react component
              { },
              domToReact(domNode.children)
            );
          }
        }

@Pedroxam
Copy link

Pedroxam commented Apr 25, 2020

Someone tell me how can replace react-router-dom in string?

Example:

var text = "Are You okay @sara ?";

var href= <Link to={{
  pathname: '/user/sara',
}}> @sara </Link>;

var replace = text.replace("@sara", href);

//output : Are You okey [Object Object] ?

i realy don't know who is [Object Object] ? i say "Are you Okay @sara", but this code called someone else !

@ghost
Copy link

ghost commented Apr 29, 2020

I have almost the same question in react-native, I appreciate if anyone can help me

my question

@kas-elvirov
Copy link

Nothing worked for me except this package https://www.npmjs.com/package/regexify-string

@iksent
Copy link

iksent commented Jul 20, 2020

I am using this function:

export const replaceWithHtml = (
  text: string,
  textToReplace: string,
  replaceValue: any,
): any[] => {
  const delimiter = '|||||'
  return text && text.includes(textToReplace)
    ? text
        .replace(textToReplace, `${delimiter}${textToReplace}${delimiter}`)
        .split(delimiter)
        .map((i) => {
          if (i === textToReplace) {
            return replaceValue
          } else {
            return i || null
          }
        })
    : text
}

Like that:

const text = 'This is an [icon]'
replaceWithHtml(
      text,
      '[icon]',
      <i className="icon-cogs" />,
)

@tscok
Copy link

tscok commented Jul 23, 2020

I just wanted a simple interpolation tool, to replace certain keywords with certain elements.

To reduce the number of elements in my output I'm using a React.Fragment.

const interpolate = (text, values) => {
  const pattern = /([$0-9]+)/g
  const matches = text.match(pattern)
  const parts = text.split(pattern)

  if (!matches) {
    return text
  }

  return parts.map((part, index) => (
    <Fragment key={part + index}>{matches.includes(part) ? values[part] : part}</Fragment>
  ))
}

// ...

<p>
  {interpolate('$1 with $2 is fun!', {
    $1: <em>Playing</em>,
    $2: <strong>JavaScript</strong>,
  })}
</p>

// <p><em>Playing</em> with <strong>JavaScript</strong> is fun!</p>

Thanks @sophiebits for the idea on split() 🙏

@TobiasWehrum
Copy link

TobiasWehrum commented Sep 15, 2020

Based on @pedrodurek's solution, here is something for my specific needs (translations with readable keys instead of just numbers):

export const insertComponentsIntoText = (
    str: string,
    replacements: {
        [key: string]: React.ReactNode
    }
) => {
    const splitRegex = new RegExp(/\[\[(\w*)\]\]/g);
    const parts = str.split(splitRegex);
    return parts.map(part => {
        if (replacements.hasOwnProperty(part)) {
            return replacements[part];
        }
        return part;
    });
};

Example:

insertComponentsIntoText(`"Please accept our [[privacyPolicyLink]] and [[termsLink].", {
    "privacyPolicyLink": <a key="privacyPolicyLink" href="/privacy">Privacy Policy</a>,
    "termsLink": <a key="termsLink" href="/terms">Terms and Conditions</a>
})

...becomes...

Please accept our <a key="privacyPolicyLink" href="/privacy">Privacy Policy</a> and <a key="termsLink" href="/terms">Terms and Conditions</a>.

(The keys are necessary because it actually becomes an array, and react doesn't like array elements without keys.)

@Anu-Ujin
Copy link

Anu-Ujin commented Oct 7, 2020

Nothing worked for me except this package https://www.npmjs.com/package/regexify-string

Me too. Tried all of them only this package works. Thank you!

@facebook facebook locked as resolved and limited conversation to collaborators Oct 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests