1
votes

I am attempting to have a "flashcard" show no background color when it is unanswered, green when it is correctly answered, and yellow when it is incorrectly answered. The properties for each card are stored in a nested object. I am having issues using conditional rendering to properly show my content.

Code: Here is what I want to accomplish, but the JSX conditional statement is only registering the last statement in the className.

<div className="row">
    {Object.keys(this.state.selections).map( (lang, index) => (
        <>
            {Object.keys(this.state.selections[lang]).map( (cat, index) => (
                <>
                {Object.keys(this.state.selections[lang][cat]).map( (sect, index)=> (
                    <>
                    {Object.keys(this.state.selections[lang][cat][sect]).map( (letter, index)=> (
                        <>
                        {this.state.selections[lang][cat][sect][letter].show &&
                            <div className={
                                (this.state.selections[lang][cat][sect][letter].correct ? correct: unanswered),
                                (this.state.selections[lang][cat][sect][letter].incorrect ? incorrect : unanswered)
                            } key= {index}>
                            </div>
                        }
                        </>
                    ))}
                    </>
                ))}
                </>
            ))}
        </>
    ))}
</div>

Essentially, I want to place a conditional statement in the className to change the background based off the object property values being true/false.

Obj model:

{
    "Hiragana": {
        "Main Kana": {
            "Vowels": {
                "a": {
                    "characterName":  "A",
                    "character":  "あ",
                    "unicode":  "\u0026‌#12354;",
                    "hexEncoding":  "\u0026‌#x3042",
                    "englishTranslation":  "a",
                    "alternateEnglishTranslation":  "",
                    "isLetter":  "1",
                    "alphabet":  "hir",
                    "show": false,
                    "answer": "",
                    "correct": false,
                    "incorrect": false,
                    "unanswered": true
                    },

Bonus question: Having to loop through each object layer seems a bit messy and tedious... Would there be any good way to shorten this up? The lang and cat are selected by input so they cannot be hard-coded. While my current solution "works" I want to improve myself when possible.

Update: Working code from the solution offered by @CertainPerformance

<div className="row">
    {Object.keys(this.state.selections).map(lang => 
        Object.values(this.state.selections).map( (cat) =>
            Object.values(cat).map((sect, indexCat) =>
                Object.values(sect).map((sectLetters, indexSect) =>
                    Object.values(sectLetters).map((letter, indexSectL) =>
                        letter.show &&
                        <div
                            className={getBgClass(letter)}
                            key={indexSectL}>
                            <div>
                                <h5 className="card-title" >{letter.character}</h5>
                                <input data-lang={lang} data-cat={Object.getOwnPropertyNames(cat)[indexCat]} data-sect={Object.getOwnPropertyNames(sect)[indexSect]} type="text" name={letter.characterName} value={letter.answer} onChange={this.handleChange.bind(this)} />
                            </div>
                        </div>
                    )
                )
            )
        )
    )}
</div>

For those confused by my explanation of accessing the array generated by Object.values, here is an example with simple console.log to better explain.

Object.values(this.state.selections).map((cat) =>{
    Object.values(cat).map((sect, indexCat) => {
        console.log("Cat: ", Object.getOwnPropertyNames(cat)[indexCat]);
    });
});
1

1 Answers

1
votes

I'm not sure if your current code works as you're expecting it to. Here:

className={
  (this.state.selections[lang][cat][sect][letter].correct ? correct : unanswered),
  (this.state.selections[lang][cat][sect][letter].incorrect ? incorrect : unanswered)
}

you're invoking the comma operator, which evaluates to only the final expression in the comma-separated list. It's equivalent to:

className={
  (this.state.selections[lang][cat][sect][letter].incorrect ? incorrect : unanswered)
}

To fix it, either use a nested conditional operator, or pass it into a function.

To make your code more concise, instead of iterating over the keys of the object, iterate over the values of the object. You also don't need a fragment <></> when returning an array.

<div className="row">
    {Object.values(this.state.selections).map(selection =>
        Object.values(selection).map(cat =>
            Object.values(cat).map(sect =>
                Object.values(sect).map((letter, index) =>
                    letter.show &&
                    <div
                        className={
                            letter.correct
                                ? correct
                                : letter.incorrect
                                    ? incorrect
                                    :
                                    unanswered
                        }
                        key={index}
                    >
                    </div>
                )
            )
        )
    )}
</div>

To avoid the nested conditional, if that's your preference, do something like:

<div
    className={getClass(letter)}
    key={index}
></div>
const getClass = (letter) => {
  if (letter.correct) return correct;
  if (letter.incorrect) return incorrect;
  return unanswered;
};

You could also consider changing your model so that the question state (correct/incorrect/unanswered) is a single property, rather than multiple, eg, instead of

"correct": false,
"incorrect": false,
"unanswered": true

have

answerState: 2

where 2 corresponds to unanswered, 1 corresponds to incorrect, and 0 corresponds to correct - or something like that. Then you can use an array to look up the appropriate class name:

className={classNames[letter.answerState]}
const classNames = [correct, incorrect, unanswered];