0

I'm building a quiz/survey builder, kind of CMS interface which will allow users to add as many questions as they want by clicking the type of question they want.

To start with, my state is set up in the App component as follow:

state = {
  components: API.components,
  comps: []
}

The admin screen has a selected number of buttons which will activate a question onClick. The question comes from the API.components. For example, we have: - Welcome Message - Thank You Message - Yes or No

/* 1. Generate a list of buttons with onClick props.addQuestion passing a fixed id via 'i' parameter */
const QuestionTypes = props => {  
  return (
    <div data-name='typequestion'>
      {props.details.map((el, i) => <div key={i}><button className="sm" onClick={() => props.addQuestion(i)}>{el.label}</button></div>)}
    </div>
  )
}

See Menu Questions screenshot: https://www.awesomescreenshot.com/image/3982311/8964261115690780c3f67d390ce08665

onClick, each of these buttons will trigger the 'addQuestion' method, which will pass a fixed ID (key) to the 'selectComponent' function to add the selected component to the comps[] array:

/* onClick, a method 'addQuestion' is called */
/* This method will setState and call a function selectComponent passing a key (id) in order to select the correct component */
addQuestion = key => {
  this.setState({
    comps: [...this.state.comps, this.selectComponent(key)]
  });
}

The selectComponent function has a switch to pass the correct component:

selectComponent = (key) => {
    switch(key) {
      case 0:
        return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      case 1:
        return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      case 2:
        return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      default:
        return 'Please select a component from the left side menu'
    }
  }

This will add an element to comps[] array:

[
  0: {element here ..}
  1: {element here ..} etc.
]

Here an example of the code for the component:

const ThankYouMessage = props => (

  <section className="ui-component-view" data-name="thankyou">
    <img src={ThankYouImage} alt="x" />
    <h3>Thanks for completing our form!</h3>

    <div>
      <DeleteButton deleteQuestion={props.deleteQuestion} />
    </div>
  </section>

);

See Selected Welcome Message Component screenshot: https://www.awesomescreenshot.com/image/3982315/f59e1bf79a31194aa3ee3ad2467658a0

PROBLEM:

As you can see, each component will have a delete button. While each component is added to the array without issues, I can't find a way to delete ONLY the selected component when I click the delete button.

I've tried to use .filter(), splice() but I don't have the right index for the newly created or updated array list. I want to use the React way to do it, not jQuery or Javascript-ish.

Example of Delete Button. Please note that the props.index is passing the original clicked button id (key), which will not match the newly comps[] array index:

const DeleteButton = props => (

  <span className="deleteButton" onClick={() => props.deleteQuestion(props.index)}>&times;<small>Delete</small></span>

);

export default DeleteButton;

Here the Delete method:

deleteQuestion = e => {
    const comps = [...this.state.comps]

    // 2. here I need to add something that will DELETE ONLY the clicked button index for that component

    // 3. Update state
    this.setState({ comps });
}

Please see the full code for the App component: class App extends React.Component {

  state = {
    components: API.components,
    comps: [],
    multichoice: {}
  }

  selectComponent = (key) => {
    switch(key) {
      case 0:
        return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      case 1:
        return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      case 2:
        return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
        break;
      default:
        return 'Please select a component from the left side menu'
    }
  }

  addQuestion = key => {

    this.setState({
      comps: [...this.state.comps, this.selectComponent(key)]
    });
  }

  deleteQuestion = e => {
    const comps = [...this.state.comps]

    // 2. here I need to add something that will DELETE ONLY the component related to the delete button

    // 3. Update state
    this.setState({ comps });
  }

  render() {
    return (
      <Container>
        <Row>
          <Col>
            <h1>Survey Builder </h1>
          </Col>
        </Row>
        <Row>
          <Col lg={3} className="border-right">
          <QuestionTypes addQuestion={this.addQuestion} details={this.state.components} />
          </Col>
          <Col lg={9}>
            <Row>
              <Col lg={12}>
                <QuestionEdit comps={this.state.comps} details={this.state.components} />
              </Col>
            </Row>
          </Col>
        </Row>
      </Container>
    )
  }
}

export default App;

2 Answers 2

1

You should not keep the components inside the state (cause that breaks the components lifecycle and it is hard to compare them). Instead, just keep the keys:

 addQuestion = key => {
  this.setState({
   comps: [...this.state.comps, key]
  });
 }

Then inside render(), map the keys to the components:

 {this.state.comps.map((key, index) => <SomeComponent    remove={() => this.removeQuestion(index)} />)}

Now removeQuestion can simply be:

 removeQuestion(index) {
   this.setState(({ comps }) => ({ comps: comps.filter((_, i) => i !== index) }));
 }
Sign up to request clarification or add additional context in comments.

8 Comments

Hi JonasWilms, 'e' is passing an id which is the fixed id number from the button. They do not match with the index array. For example, while e is passing value of 1 (because I have clicked the WelcomeMessage button), the component might be in index 3 of the comps[] array because I have added other questions. So, by using e as it stands, nothing would work, not even .filter. I have tried them all. I need to be able to pass to e the value of the index related to the delete button I've clicked. Does it make sense?
@joe oh, I totally missed that: You should not have components in the state at all ...
can you please elaborate? What do you mean with "You should not have components in the state at all "? Sorry, I'm quite new to this language. Can you please offer some examples? Thanks
Sorry buddy, didn't see the change above. I'll have a look. Thanks
Hi Jonas, thanks for the code. Now every button shows always the same component because I've added {<WelcomeMessage remove={() => this.removeQuestion(index)} />)}. What would be the best practice to handle all the other components? If statement within the function map? (similar to the switch). My JS way would had been to use concatenation to create the component name. OnClick, the button would pass a const with the name 'WelcomeMessage' and I would do something like '<' + constName + '>' but I know it doesn't work. Thanks for your help so far. We are getting somewhere
|
0

EDITED: No components should be held on the state, just objects representing the questions.

The comps state should be immutable, that means that each time a question is added or deleted you should create a new Array out of the old one, currently on the state.

Since you're not using any advanced state managers (like Redux, etc.), and nor should you at this point, I would suggest having a data atribute on each question with the question ID on it. Once clicked you can fetch the question ID from the target the click event is carrying and use it to figure out where is the question item reside on the comps state. Once you have it, create a new comps state, by constructing a new Array which does not have that question you've just deleted.

I would also like to recommend not using a switch/case here, since it defies the open/close principle. I think you will find a dictionary approach, where you map the type of the question to the corresponding component, much more scaleable and maintainable.

Hope this helps :)

2 Comments

Hi @Matti-Bar-Zeev, Re: switch/case, originally I wanted to create a generic component with a dynamic injection of the component name, as I would do in JS: '<' + nameCompenentCalled + ' />'. Couldn't find the way to do in React. Is there a way by any chance? I am not familiar with the dictionary thing, but I will investigate. Re: ID in the data-*, If I have 10 Welcome Messages, all will have the same ID originated from the clicked button. I would like to delete by using the comps[] index related to the delete button I've just clicked.
@Joe each component should have a distinct ID, even if it renders the same thing (take mapping over an Array to create a list of items, each must have its own key which is derived from the item ID in order for React to render as expected).

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.