0

I'm using a Map to take in JSON data and create a list item for each value. Then I'm replacing specific parts of the strings with state values.

const content = {
  "content": [
    {
      "text" : "#number# Tips to Get #goal#",
      "types" : [ "email"]
    },
    {
      "text" : "Your Search For #goal# Ends Here",
      "types" : [ "ad", "email" ]
    }
  ]
}

...

const listItems = content.map((result, index) => (
    <li key={index}>
        {Object.keys(parameters).reduce((res, key) => {
            return res.replace(`#${key}#`, parameters[key]);
        }, result.text)}
    </li>
));

The problem occurs when I try to wrap the replaced text in a <span>.


{Object.keys(parameters).reduce((res, key) => {
            return res.replace(`#${key}#`, <span>parameters[key]</span>);
        }, result.text)}

The above however just replaces those parts of the string with [object Object]. I assume this is because I'm calling replace on an object rather than a string, but I'm not sure how to fix that.

How would I wrap the replaced items in a new element?

PaulVO
  • 309
  • 3
  • 13

1 Answers1

0

The problem as you pointed out is with String.prototype.replace. It expects the second parameter to be either a string, or a function that returns a string. It will always take an object (like a jsx) and simply convert it, so that it ends up being [object Object].

I believe the only way you could continue using .replace would be to use the react dangerouslySetInnerHTML escape hatch. This is almost always a really bad idea, but if you feel you really must...look at your own peril.

// This code snippet won't actually run; I only used a snippet so I could hide it
const listItems = content.content.map((result, index) => (
  <li key={index} dangerouslySetInnerHTML={{
    __html:
      Object.keys(parameters).reduce((res, key) => {
        return res.replace(`#${key}#`, `<span>${parameters[key]}</span>`);
      }, result.text)
  }}>
  </li>
));

A way that wouldn't require using something with dangerous in the name:

function safeReplace(string) {
  // split the string on #word# patterns, keeping the patterns
  return string.split(/(#\w+#)/g) 
    // filter out any empty strings
    .filter(x => x.length > 0)
    .map(x => {
      const m = /#(\w+)#/.exec(x);
      // if it's not of the form #word#, just return the string fragment
      if (!m) return x;
      const repl = parameters[m[1]];
      // if the pattern actually matches a parameter, wrap with span
      if(repl) return <span>{repl}</span>;
      // if pattern doesn't match a paraemter, return string fragment.
      return x;
    });
}

const listItems = content.content.map((result, index) => (
  <li key={index}>
    {safeReplace(result.text)}
  </li>
));

I incorporated this method into a fiddle to show it in action.

*Edit: @Thomas suggests a really cool, more efficient, and more succinct method.

With split, every odd indexed array member will be a #word# pattern. You can take advantage of that knowledge to have a much simpler map statement that doesn't need to do a second regex match:

function safeReplace(string) {
  return string.split(/#(\w+)#/g) 
    .map((x,i) => (i&1) ? (<span>{parameters[x]}</span>) : x)
    .filter(x => x.length > 0);
}
  • He's using a bitwise operator to determine if i is odd.
  • Note, we also have to move the capture parentheses in the split so that the leading and trailing # characters are not included.
  • Also, if there are any occurrences of #word# that don't correspond to a parameter replacement, this wouldn't work. You'd have to use the more verbose method above, (or use a nested ternary expression to check if parameters[x] is defined)
David784
  • 7,031
  • 2
  • 22
  • 29
  • 1
    if you `filter()` after the `map()`, then you can determine by the index wether this is a placeholder or part of the template and don't need to pattern-match a second time: `string.split(/#(\w+)#/g).map((x, i) => (i&1) ? {parameters[x]} : x).filter(x => x)` – Thomas Aug 24 '20 at 06:31