1

I have a quiz with a few questions/answers using an Accordion, AccordionItem, and Link. Everything works great except for the Link, it renders perfectly when outside the accordion component line 50 in the fiddle, but does not work when it is nested in sampleQuestions > question1 > answer, line 59 in the fiddle.

It renders the answer as: Ottawa baby!! Check [object Object] for more details.

Instead of the desired link: Ottawa baby!! Check wikipedia link b for more details.

Here is the code for reference, but I recommend skipping directly to the fiddle below the code, clicking the first question, and seeing the issue first hand.

class Link extends React.Component {
  render() {
    return (
        <span onClick={this.props.onClick} className="link">{this.props.linkTitle}</span>
    );
  }
}

class AccordionItem extends React.Component {
  constructor() {
    super();
    this.state = {
      active: false
    };
    this.toggle = this.toggle.bind(this);
  }
  toggle() {
    this.setState({
      active: !this.state.active,
      className: "active"
    });
  }
  render() {
    const activeClass = this.state.active ? "active" : "inactive";
    const question = this.props.details;
    return (
            <div className={activeClass} onClick={this.toggle}>
              <span className="summary">{question.summary}</span>
              <span className="folding-pannel answer">{question.answer}</span>
            </div>
    );
  }
}

class Accordion extends React.Component {
  constructor() {
    super();
    this.state = {
      questions: sampleQuestions,
    };
    this.renderQuestion = this.renderQuestion.bind(this);
  }
  renderQuestion(key) {
    return <AccordionItem key={key} index={key} details={this.state.questions[key]} />
  }
  render() {
    return(
      <div className="mainbody">
        <h1>What is...</h1>
        <Link onClick={() => alert('outside link works')} linkTitle={'wikipedia link a'} />
        <div className="accordion-container">
          {Object.keys(this.state.questions).map(this.renderQuestion)}
        </div>
      </div>    
    )
  }
}
const sampleQuestions = {
  question1: {summary:'the capital of Canada?', answer:'Ottawa baby!! Check ' + <Link onClick={() => alert('trying to get this nested link to show')} linkTitle={'wikipedia link b'} /> + ' for more details.'},
  question2: {summary:'the life span of a bowhead whale?', answer:'Over 200 years!!'},
  question3: {summary:'the most visited city in the world?', answer:'London, groovy baby!!'},
  question4: {summary:'the warmest ocean?', answer:'Indian Ocean, it\'s a hottie!'},
  question5: {summary:'the one thing ron swanson hates more than lying?', answer:'Skim milk, which is water that\'s lying about being milk'}
};
ReactDOM.render(
  <Accordion />,
  document.getElementById('accordion')
);

Here is the fiddle

Any idea how to get the [object Object] to render the desired Link component for the first question's answer?

1
  • 1
    Rather than have answer be a string change it to JSX. wrap it in a div and have the text in the div. Commented Sep 20, 2018 at 2:52

3 Answers 3

1

Here's one way of doing it: Utilizing React Fragment.

working example: https://codesandbox.io/s/92r12m7zp

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
</head>

<body>
    <noscript>
        You need to enable JavaScript to run this app.
    </noscript>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
        <pattern id="pattern" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
            <rect fill="rgba(159, 188, 191, 0.15)" x="0" width="20" height="20" y="0" />
            <rect fill="rgba(159, 188, 191, 0.15)" x="20" width="20" height="20" y="20" />
        </pattern>
        <rect fill="url(#pattern)" x="0" y="0" width="100%" height="100%" />
    </svg>
    <div id="accordion"></div>
</body>

</html>

index.js

import React from "react";
import { render } from "react-dom";
import Accordian from "./Accordian";
import "./styles.css";

render(<Accordian />, document.getElementById("accordion"));

Accordian.js

import map from "lodash/map";
import React, { Component } from "react";
import AccordionItem from "./AccordianItem";
import SampleQuestions from "./sampleQuestions";

export default class Accordion extends Component {
  state = { questions: SampleQuestions };

  render = () => (
    <div className="mainbody">
      <h1>What is...</h1>
      <div className="accordion-container">
        {map(this.state.questions, ({ key, ...rest }) => (
          <AccordionItem key={key} {...rest} />
        ))}
      </div>
    </div>
  );
}

AccordianItem.js

import React, { Component } from "react";

export default class AccordionItem extends Component {
  state = { isActive: false };

  toggle = () => this.setState(prevState => ({ isActive: !this.state.isActive }));

  render = () => (
    <div
      className={`${this.state.isActive ? "active" : "inactive"}`}
      onClick={this.toggle}
    >
      <span className="summary">&#62; {this.props.summary}</span>
      <span className="folding-pannel answer">
        {this.props.answer}
      </span>
    </div>
  );
}

sampleQuestions.js

import React, { Fragment } from "react";

const Link = url => (
  <a href={url} target="_blank">
    here
  </a>
);

export default [
  {
    key: "capital-of-canada",
    summary: "the capital of Canada?",
    answer: (
      <Fragment>
        Ottawa baby!! Click {Link("https://en.wikipedia.org/wiki/Ottawa")} for
        more details
      </Fragment>
    )
  },
  {
    key: "whale-lifespan",
    summary: "the life span of a bowhead whale?",
    answer: "Over 200 years!!"
  },
  {
    key: "most-popular-city",
    summary: "the most visited city in the world?",
    answer: "London, groovy baby!!"
  },
  {
    key: "warmest-ocean",
    summary: "the warmest ocean?",
    answer: "Indian Ocean, it's a hottie!"
  },
  {
    key: "swanson",
    summary: "the one thing ron swanson hates more than lying?",
    answer: "Skim milk, which is water that's lying about being milk"
  }
];

Here's another way of doing it: Utilizing arrays with mixed content.

Working example: https://codesandbox.io/s/1v1xmq1kmq

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
</head>

<body>
    <noscript>
        You need to enable JavaScript to run this app.
    </noscript>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
        <pattern id="pattern" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
            <rect fill="rgba(159, 188, 191, 0.15)" x="0" width="20" height="20" y="0" />
            <rect fill="rgba(159, 188, 191, 0.15)" x="20" width="20" height="20" y="20" />
        </pattern>
        <rect fill="url(#pattern)" x="0" y="0" width="100%" height="100%" />
    </svg>
    <div id="accordion"></div>
</body>

</html>

index.js

import React from "react";
import { render } from "react-dom";
import Accordian from "./Accordian";
import "./styles.css";

render(<Accordian />, document.getElementById("accordion"));

Accordian.js

import map from "lodash/map";
import React, { Component } from "react";
import AccordionItem from "./AccordianItem";
import SampleQuestions from "./sampleQuestions";

export default class Accordion extends Component {
  state = { questions: SampleQuestions };

  render = () => (
    <div className="mainbody">
      <h1>What is...</h1>
      <div className="accordion-container">
        {map(this.state.questions, ({ key, ...rest }) => (
          <AccordionItem key={key} {...rest} />
        ))}
      </div>
    </div>
  );
}

AccordianItem.js

import each from "lodash/each";
import React, { Component, Fragment } from "react";
import uuid from "uuid/v5";

export default class AccordionItem extends Component {
  state = { isActive: false };

  toggle = () => this.setState(prevState => ({ isActive: !this.state.isActive }));

  render = () => (
    <div
      className={`${this.state.isActive ? "active" : "inactive"}`}
      onClick={this.toggle}
    >
      <span className="summary">&#62; {this.props.summary}</span>
      <span className="folding-pannel answer">
        {each(this.props.answer, prop => <Fragment key={uuid}>{prop}</Fragment>)}
      </span>
    </div>
  );
}

sampleQuestions.js

import React from "react";

const Link = url => (
  <a href={url} target="_blank">
    here
  </a>
);

export default [
  {
    key: "capital-of-canada",
    summary: "the capital of Canada?",
    answer: [
      "Ottawa baby!! Click ",
      Link("https://en.wikipedia.org/wiki/Ottawa"),
      " for more details"
    ]
  },
  {
    key: "whale-lifespan",
    summary: "the life span of a bowhead whale?",
    answer: ["Over 200 years!!"]
  },
  {
    key: "most-popular-city",
    summary: "the most visited city in the world?",
    answer: ["London, groovy baby!!"]
  },
  {
    key: "warmest-ocean",
    summary: "the warmest ocean?",
    answer: ["Indian Ocean, it's a hottie!"]
  },
  {
    key: "swanson",
    summary: "the one thing ron swanson hates more than lying?",
    answer: ["Skim milk, which is water that's lying about being milk"]
  }
];

Here's yet another way of doing it: Utilizing dangerouslySetInnerHTML with sanitize-html.

Working example: https://codesandbox.io/s/0q1mv0omkw

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
</head>

<body>
    <noscript>
        You need to enable JavaScript to run this app.
    </noscript>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
        <pattern id="pattern" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
            <rect fill="rgba(159, 188, 191, 0.15)" x="0" width="20" height="20" y="0" />
            <rect fill="rgba(159, 188, 191, 0.15)" x="20" width="20" height="20" y="20" />
        </pattern>
        <rect fill="url(#pattern)" x="0" y="0" width="100%" height="100%" />
    </svg>
    <div id="accordion"></div>
</body>

</html>

index.js

import React from "react";
import { render } from "react-dom";
import Accordian from "./Accordian";
import "./styles.css";

render(<Accordian />, document.getElementById("accordion"));

Accordian.js

import map from "lodash/map";
import React, { Component } from "react";
import AccordionItem from "./AccordianItem";
import SampleQuestions from "./sampleQuestions";

export default class Accordion extends Component {
  state = { questions: SampleQuestions };

  render = () => (
    <div className="mainbody">
      <h1>What is...</h1>
      <div className="accordion-container">
        {map(this.state.questions, ({ key, ...rest }) => (
          <AccordionItem key={key} {...rest} />
        ))}
      </div>
    </div>
  );
}

AccordianItem.js

import React, { Component } from "react";
import sanitizeHtml from "sanitize-html";

export default class AccordionItem extends Component {
  state = { isActive: false };

  toggle = () => this.setState(prevState => ({ isActive: !this.state.isActive }));

  sanitize = ans =>
    sanitizeHtml(ans, {
      allowedTags: ["a"],
      allowedAttributes: {
        a: ["href", "target"]
      }
    });

  render = () => (
    <div
      className={`${this.state.isActive ? "active" : "inactive"}`}
      onClick={this.toggle}
    >
      <span className="summary">&#62; {this.props.summary}</span>
      <span
        className="folding-pannel answer"
        dangerouslySetInnerHTML={{
          __html: this.sanitize(this.props.answer)
        }}
      />
    </div>
  );
}

sampleQuestions.js

const Link = url => `<a href=${url} target="_blank">here</a>`;

export default [
  {
    key: "capital-of-canada",
    summary: "the capital of Canada?",
    answer: `Ottawa baby!! Click ${Link("https://en.wikipedia.org/wiki/Ottawa")} for more details`
  },
  {
    key: "whale-lifespan",
    summary: "the life span of a bowhead whale?",
    answer: "Over 200 years!!"
  },
  {
    key: "most-popular-city",
    summary: "the most visited city in the world?",
    answer: "London, groovy baby!!"
  },
  {
    key: "warmest-ocean",
    summary: "the warmest ocean?",
    answer: "Indian Ocean, it's a hottie!"
  },
  {
    key: "swanson",
    summary: "the one thing ron swanson hates more than lying?",
    answer: "Skim milk, which is water that's lying about being milk"
  }
];
Sign up to request clarification or add additional context in comments.

3 Comments

This approach seems like it's going down the wrong path. A better solution would be to intervene earlier to have it render the right thing.
Updated answer to include 3 different ways to handle strings with some html elements.
Thanks but I found a much cleaner solution and posted it.
1

Thank you for all those who answered, but I found a much less obtrusive way of doing it. As patrick suggested in the comments, simply turning the answer to a jsx instead of a string by wrapping it in a div works perfectly.

This is what it looks like now:

answer: <div>Ottawa baby!! Check <Link onClick={() => alert('trying to get this nested link to show')} linkTitle={'wikipedia link b'} /> for more details.</div>

This is the simplest solution and gets the link to render as expected.

1 Comment

You won't need to use divs if you replace it with React's <Fragment>. Same concept, cleaner DOM. See first example in my answer.
0

You can't really put a react component inside of a string generally. There are ways to do it, namely by using react-jsx-parser, but we won't go into that.

One possibly solution is doing the following: Setting up a dumb component that renders an array of children.

const AnswerWithLink = (children) => {
  return (
    <span>{[...children]}</span>
  )
}

And then on answer 1, you call that as a function and pass the parts of the string as elements of the array:

question1: {
  summary:'the capital of Canada?', 
  answer: AnswerWithLink(['Ottawa baby!! Check ', Link({onClick: () => alert('trying to get this nested link to show'), linkTitle: 'wikipedia link b'}), ' for more details'])
}

I'm sure there are more efficient ways of doing this though.

Edit: I've also edited the codepen to make it work. Also ended up changing <Link /> to be a dumb component, as it is stateless.

1 Comment

Thanks but I found a much cleaner solution and posted it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.